Files
Alex Cube 4ac392a9ab Add favicon; fix two server bugs found deploying to socoban.08h.ru
- Favicon: 🚀 emoji via inline SVG data URI, both sokoban.html and php/index.php
- api.php: mb_substr() -> substr() (mbstring not enabled on the server)
- api.php: MariaDB rejects LIMIT directly inside NOT IN(...) (error 1235) —
  wrap the top-5 subquery in an extra derived-table SELECT
Verified live against the deployed MySQL/MariaDB DB: whoami, set_name,
submit (insert + dedup + trim-to-5), top all round-trip correctly.
2026-08-10 15:29:14 +03:00

2019 lines
49 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Sokoban — 60 Levels</title>
<link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>🚀</text></svg>">
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { background: #0f0f1a; display: flex; justify-content: center; align-items: center; min-height: 100vh; font-family: 'Courier New', monospace; }
#container { display: flex; flex-direction: column; align-items: center; gap: 10px; }
canvas { border: 3px solid #e94560; border-radius: 12px; box-shadow: 0 0 20px rgba(233,69,96,0.4); }
#hud { display: flex; gap: 20px; color: #e94560; font-size: 16px; font-weight: bold; }
#hud span { background: rgba(233,69,96,0.1); padding: 8px 16px; border-radius: 20px; border: 1px solid rgba(233,69,96,0.3); }
#controls { color: #aaa; font-size: 13px; }
kbd { background: #1a1a2e; border: 1px solid #e94560; border-radius: 4px; padding: 2px 6px; color: #e94560; }
#solve-overlay { position: fixed; inset: 0; background: rgba(10,10,21,0.75); display: flex; justify-content: center; align-items: center; z-index: 10; }
#solve-overlay.hidden { display: none; }
#solve-box { background: #1a1a2e; border: 2px solid #e94560; border-radius: 12px; padding: 24px 32px; text-align: center; box-shadow: 0 0 30px rgba(233,69,96,0.5); }
#solve-box p { color: #e94560; font-size: 16px; font-weight: bold; margin-bottom: 8px; }
#solve-timer { color: #48cae4; font-size: 22px; margin-bottom: 16px; }
#solve-cancel { background: #e94560; color: #fff; border: none; border-radius: 8px; padding: 8px 20px; font: inherit; font-weight: bold; cursor: pointer; }
#solve-cancel:hover { background: #ff6b8a; }
#solve-cancel.solve-found { background: #7CFF6B; color: #0a0a15; }
#solve-cancel.solve-found:hover { background: #9dff8f; }
</style>
</head>
<body>
<div id="container">
<canvas id="game"></canvas>
<div id="hud">
<span id="level-name">Уровень 1</span>
<span id="box-count">📦 0/0</span>
<span id="moves-count">👣 0</span>
</div>
<div id="controls">
<kbd>← → ↑ ↓</kbd> или <kbd>WASD</kbd> — движение &nbsp;
<kbd>N</kbd> — след. &nbsp;
<kbd>P</kbd> — пред. &nbsp;
<kbd>R</kbd> — рестарт &nbsp;
<kbd>F</kbd> — автопоиск (<kbd>Esc</kbd> — отмена) &nbsp;
<kbd>G</kbd> — лучшее сохранённое решение
</div>
</div>
<div id="solve-overlay" class="hidden">
<div id="solve-box">
<p>🤖 ИИ ищет решение… Ожидайте.</p>
<div id="solve-timer">0.0с</div>
<button id="solve-cancel">Отмена</button>
</div>
</div>
<!-- Вставь ВЕСЬ дамп между тегами -->
<script type="text/plain" id="levels-data">
*************************************
Maze: 1
File offset: 148C, DS:00FC, table offset: 0000
Size X: 22
Size Y: 11
End: 14BD
Length: 50
XXXXX
X X
X* X
XXX *XXX
X * * X
XXX X XXX X XXXXXX
X X XXX XXXXXXX ..X
X * * ..X
XXXXX XXXX X@XXXX ..X
X XXX XXXXXX
XXXXXXXX
*************************************
Maze: 2
File offset: 14BE, DS:012E, table offset: 0032
Size X: 14
Size Y: 10
End: 14E8
Length: 43
XXXXXXXXXXXX
X.. X XXX
X.. X * * X
X.. X*XXXX X
X.. @ XX X
X.. X X * XX
XXXXXX XX* * X
X * * * * X
X X X
XXXXXXXXXXXX
*************************************
Maze: 3
File offset: 14E9, DS:0159, table offset: 005D
Size X: 17
Size Y: 10
End: 150D
Length: 37
XXXXXXXX
X @X
X *X* XX
X * *X
XX* * X
XXXXXXXXX * X XXX
X.... XX * * X
XX... * * X
X.... XXXXXXXXXX
XXXXXXXX
*************************************
Maze: 4
File offset: 150E, DS:017E, table offset: 0082
Size X: 22
Size Y: 13
End: 154D
Length: 64
XXXXXXXX
X ....X
XXXXXXXXXXXX ....X
X X * * ....X
X ***X* * X ....X
X * * X ....X
X ** X* * *XXXXXXXX
XXXX * X X
X X XXXXXXXXX
X * XX
X **X** @X
X X XX
XXXXXXXXX
*************************************
Maze: 5
File offset: 154E, DS:01BE, table offset: 00C2
Size X: 17
Size Y: 13
End: 1580
Length: 51
XXXXX
X XXXXX
X X*XX X
X * X
XXXXXXXXX XXX X
X.... XX * *XXX
X.... * ** XX
X.... XX* * @X
XXXXXXXXX * XX
X * * X
XXX XX X
X X
XXXXXX
*************************************
Maze: 6
File offset: 1581, DS:01F1, table offset: 00F5
Size X: 12
Size Y: 11
End: 15AC
Length: 44
XXXXXX XXX
X.. X XX@XX
X.. XXX X
X.. ** X
X.. X X * X
X..XXX X * X
XXXX * X* X
X *X * X
X * * X
X XX X
XXXXXXXXX
*************************************
Maze: 7
File offset: 15AD, DS:021D, table offset: 0121
Size X: 13
Size Y: 12
End: 15D8
Length: 44
XXXXX
XXXXXXX XX
XX X @XX ** X
X * X
X * XXX X
XXX XXXXX*XXX
X * XXX ..X
X * * * ...X
X XXX...X
X ** X X...X
X XXX XXXXX
XXXX
*************************************
Maze: 8
File offset: 15D9, DS:0249, table offset: 014D
Size X: 16
Size Y: 17
End: 161A
Length: 66
XXXX
X XXXXXXXXXXX
X * * * X
X *X * X * X
X * * X X
XXX *X X XXXX X
X@X* * * XX X
X * X*X X X
XX * * * * X
XXXX XXXXXXXXX
XXX XXX
X X
X X
X......X
X......X
X......X
XXXXXXXX
*************************************
Maze: 9
File offset: 161B, DS:028B, table offset: 018F
Size X: 17
Size Y: 18
End: 165D
Length: 67
XXXXXXX
X ...X
XXXXX ...X
X ...X
X XX ...X
XX XX ...X
XXX XXXXXXXX
X *** XX
XXXXX * * XXXXX
XX X* * X X
X@ * * * * X
XXXXXX ** * XXXXX
X * X
XXXX XXX
X X
X X
X X
XXXX
*************************************
Maze: 10
File offset: 165E, DS:02CE, table offset: 01D2
Size X: 21
Size Y: 20
End: 16BE
Length: 97
XXXX
XXXXXX X
X X
X XXXX XXX
XXX XXXXX XXX X
XX@XXXX *** X X
X ** ** * X....XX
X ***X * X.....X
X * X ** ** X.....X
XXX X * X.....X
X X * * * X.....X
X XXXXXXX XXX.....X
X X * * X.....X
XXX X ** * *XXXXXXX
X X * X
X X *** *** X
X X X X
X XXXXXXXXX X
X X
XXXXXXXXXXXXX
*************************************
Maze: 11
File offset: 16BF, DS:032F, table offset: 0233
Size X: 19
Size Y: 15
End: 16FF
Length: 65
XXXX
XXXX X X
XXX XXX* X
XX @ * X
XX * **XX XX
X X*XX X
X X * ** X XXX
X * X X * XXXXX
XXXX X ** X X
XXXX XX * X
X. XXX XXXXXXXX
X.. ..X XXXX
X...X.X
X.....X
XXXXXXX
*************************************
Maze: 12
File offset: 1700, DS:0370, table offset: 0274
Size X: 13
Size Y: 16
End: 1741
Length: 66
XXXXXXXXX
X&.&X&.&X
X.&.&.&.X
X&.&.&.&X
X.&.&.&.X
X&.&.&.&X
XXX XXX
X X
XXXXXX XXXXXX
X X
X * * * * * X
XX * * * * XX
X* * * * *X
X *@* X
X XXXXX X
XXXX XXXX
*************************************
Maze: 13
File offset: 1742, DS:03B2, table offset: 02B6
Size X: 20
Size Y: 13
End: 178D
Length: 76
XXXXXXXXX
XXX XX XXXXX
XXX X X XXXX
X ** X* X X ... X
X X *X@*XX X X.X. X
X XX X* X ... X
X *X * X X X.X. X
X XX XX* * ... X
X * XX X X*X.X. X
XX ** * * *... X
X* XXXXXX XX X
X X XXXXXXXXXX
XXXXX
*************************************
Maze: 14
File offset: 178E, DS:03FE, table offset: 0302
Size X: 17
Size Y: 13
End: 17C7
Length: 58
XXXXXXXXXXXXXXXX
X X
X X XXXXXX X
X X * * * *X X
X X *@* XX XX
X X X* * *XXX...X
X X * * XX...X
X XXX*** * XX...X
X X XX XX...X
XXXXX XX XX...X
XXXXX XXX
X X
XXXXXXX
*************************************
Maze: 15
File offset: 17C8, DS:0438, table offset: 033C
Size X: 17
Size Y: 17
End: 180F
Length: 72
XXXX
XXXX X
XX X X
X * * X
XXX X* XXXX
X * XX* X
X X @ * X *X
X X * XXXX
XX XXXX*XX X
X *X.....X X X
X *...&. *X XXX
XX X.....X X
X XXX XXXXXXX
X ** X X
X X X
XXXXXX X
XXXXX
*************************************
Maze: 16
File offset: 1810, DS:0480, table offset: 0384
Size X: 14
Size Y: 15
End: 184E
Length: 63
XXXXX
X XX
X X XXXX
X * XXXX X
X ** * *X
XXX@ X* XX
X XX * * XX
X * XX XX .X
X X*XX* X.X
XXX *..XX.X
X X.&...X
X ** X.....X
X XXXXXXXXX
X X
XXXX
*************************************
Maze: 17
File offset: 184F, DS:04BF, table offset: 03C3
Size X: 18
Size Y: 16
End: 1895
Length: 71
XXXXXXX
XXXXXXX X
X X *@* X
X** X XXXXXXXXX
X XXX......XX X
X *......XX X X
X XXX...... X
XX XXXX XXX X*XX
X X* X * X X
X * *** X *XX X
X * * XXX** X X
XXXXX * X X
XXX XXX X X
X X X
XXXXXXXX X
XXXX
*************************************
Maze: 18
File offset: 1896, DS:0506, table offset: 040A
Size X: 22
Size Y: 13
End: 18DC
Length: 71
XXXXXXXXXXXX
X . XX X
X X. @ X
XXXXXX XX...X XXXX
XX XX...XXXX XXXX
X * XX... * X * X
X .. XX X XX XX X
XXXX*XXX*X * X X XX
XXX X XX* ** X X
X ** X X * X *XX X
X X
XXXXXXXXXXXXXXXXX X
XXXX
*************************************
Maze: 19
File offset: 18DD, DS:054D, table offset: 0451
Size X: 28
Size Y: 20
End: 1951
Length: 117
XXXXXX
X @XXXX
XXXXX * X
X XX XXXX
X *XX XX X
X X XXXXX X
X X** * X X
X * * XXX X X
X X * X X X
X X X*X X X
XX XXXX X X X
X * XXXXX X X XXXX
XX * * XXX XXXX
XXXXX XXX * *X * X .....X
X XX X XX X.....X
X **** XXXXXX*XX X.XX.X
XX XX X....X
XX XXXXXXXXXXXXXXX ....X
X X XXXXX XX
XXXX XXXX
*************************************
Maze: 20
File offset: 1952, DS:05C2, table offset: 04C6
Size X: 20
Size Y: 20
End: 19B7
Length: 102
XXXXXXXXXXXX
X..........X
XXX.X.X.X.X..X
X .........X
X@ * * * &.&.X
XXXXXXX XXXXXXX
XXXX X XX X
XX * X X * XX
X X*X XXX XXX* XX
X * * * X * * * X
X X * XX X* X
X *XXXX*XXXX*XX X
XXXX XX X X X
X* XX X X ** X
X X * X * X
XXX X ** X * XXX
X X X * XX
X XXXXXXXX X
X X
XXXXXXXXXXXX
*************************************
Maze: 21
File offset: 19B8, DS:0628, table offset: 052C
Size X: 16
Size Y: 14
End: 19F1
Length: 58
XXXXXXXXXX
X.. X X
X.. X
X.. X XXXX
XXXXXXX X XX
X X
X X XX X X
XXXX XX XXXX XX
X * XXXXX X X
X X * * X * X
X @* * X XX
XXXX XX XXXXXXX
X X
XXXXXX
*************************************
Maze: 22
File offset: 19F2, DS:0662, table offset: 0566
Size X: 22
Size Y: 20
End: 1A66
Length: 117
XXXX
XXXXXXXXXXXX XXXXX
X X X * X XX
X * * * * X * * X
XX* * X @X * * X
XXX XXXXXXXXXXXX XX
X * *X X......X *X
X X X X......XX X
X XX XX X .....X X
X X *...... * X
X X * XX X......X X
X * *X X......X *X
X * X XX*XXXXX X
X * * XXXX * * * *X
XX X * * * * XXX
X XXXXXX * * X
X X XXXXXXX X
XXXXXXX X* X
X XXXXXXXXXXX
XXXXX
*************************************
Maze: 23
File offset: 1A67, DS:06D7, table offset: 05DB
Size X: 25
Size Y: 14
End: 1AB7
Length: 81
XXXXXXX
X X XXXX
X *X* X XX
XXXXXXXX X X XXXXXXXX
X.... X *X* X *X X X
X....X X X* X X
X..X. *X X * X* X
X... @XX X* X* X X X
X.... XX *X *XXXXXXXX
XXXXXXXX X**X* X
X *X X *X
X X X X
XXXX XXXXX
XXXX
*************************************
Maze: 24
File offset: 1AB8, DS:0728, table offset: 062C
Size X: 21
Size Y: 19
End: 1B1F
Length: 104
XXXXXXXXXX
X........XXXX
X.X.X....X X
X........** X
X .XXX XXXX
XXXXXXXXX * X X
X * * * * X
X X X * *X X
XX XXXXX X X X
X * X XXXX X
XX *X X XX X X
X XX*XXX X XX
X * * X X X X
XXXXX X XX X XX XX
X*X X * * * X
X@X *X*** X X
XXX * XXXXX
XX X X X
XXXXXXXXXX
*************************************
Maze: 25
File offset: 1B20, DS:0790, table offset: 0694
Size X: 23
Size Y: 17
End: 1B7C
Length: 93
XXXX
XXXXXX XXXXX
XXXXXXX X X
X * * XX X X X
X XXXX * X .X
X * X X XX.X.X
XX*XXXX* * * XX.X.X
X X XXXX.XXX
X * XXXXXX X.X.X
XXXXXX***XX @X.X.X
X X X*X*XXX. .X
X XXXX X***** X ...X
X X * X X ...X
X X XX XX XXX...X
X XXXXXX*XXXXXX XXXXXX
X X X X
XXXXXXXXXX XXXX
*************************************
Maze: 26
File offset: 1B7D, DS:07ED, table offset: 06F1
Size X: 15
Size Y: 15
End: 1BBB
Length: 63
XXXXXXXXX
X X
X XXXX
XX XXXX X X
XX X@XX X
X *** * **X
X X XX * X
X X XX * XXXX
XXXX *** *X X
X XX ....X
X X X X.. .X
X X X XX...X
XXXXX * X...X
XX XXXXX
XXXXX
*************************************
Maze: 27
File offset: 1BBC, DS:082C, table offset: 0730
Size X: 23
Size Y: 13
End: 1C0B
Length: 80
XXXXXXXXXXXXXXXXX
X... X X XXX
XX..... *XX X X * X
X......X * X * X
X......X X X X X XX
XXXXXXXXX * * X X XXX
X X*XX* XX XX X
XX * X * * X X
X XX XXX X XXXXX*X X
X * ** * * X
X * *XX* XXXXXXXX X
XXXXXXX @ XX XXX
XXXXXX
*************************************
Maze: 28
File offset: 1C0C, DS:087C, table offset: 0780
Size X: 15
Size Y: 17
End: 1C54
Length: 73
XXXXXXX
X@ X X
X * X
XXX XX X
XXXX * X XX
X X XX
X * *XXXX * X
X ** X X *X
X* * X* X
XX **X ** XX
X ** X X * X
X XXXX * X
X X*XX..XX X
XXX .X....XXXXX
X .......XX
X.... ..X
XXXXXXXXXXX
*************************************
Maze: 29
File offset: 1C55, DS:08C5, table offset: 07C9
Size X: 24
Size Y: 11
End: 1C96
Length: 66
XXXXX
XXXXXX XXX XXXX
XXXXX XXX * * * X
XXXX XX X* * * X X
X.... ** * * * X*XX
X.. X XX X XXX*XX X X
X.... X XXX X X
X.... X XX * XXX* X
X..XXXXXX * X XXXX XX
XXXX X XXX @ X
XXXXXXXXXXXXXXX
*************************************
Maze: 30
File offset: 1C97, DS:0907, table offset: 080B
Size X: 14
Size Y: 20
End: 1CE3
Length: 77
XXXXX
X XXXXXXX
X * XXX X
X * ** X
XX XXXX X
XXX X X XXX
X X X@XX
X ** * X
X X X * XXXX
XXXXX X X X
X *XXXX X
X * * X
XX XXXXX XX
XXXXXXXXXX X
XX....X * * X
X.....X **X X
X.. ..X * * X
X.....* X X
XX XXXXXXXXXX
XXXX
*************************************
Maze: 31
File offset: 1CE4, DS:0954, table offset: 0858
Size X: 15
Size Y: 12
End: 1D16
Length: 51
XXXXXXX
X X XXXXX
XX X X...XXX
X *X X... X
X * X** ... X
X *X X... .X
X X *XXXXXXXX
XX* * * X
XX X ** X X
XXXXXX XX**@X
X XX
XXXXXXXX
*************************************
Maze: 32
File offset: 1D17, DS:0987, table offset: 088B
Size X: 18
Size Y: 16
End: 1D60
Length: 74
XXXX
X XXXXXXXXX
XX XX @X X
X *X * * XXXX
X* * X * *X XX
XX *XX X* * X
X X X X *** X
X * * *XX XXXX
X * * X*X X X
XX XXX XXX* X
X X.... X
XXXX......XXXX
X....XXXX
X...XX
X...X
XXXXX
*************************************
Maze: 33
File offset: 1D61, DS:09D1, table offset: 08D5
Size X: 13
Size Y: 15
End: 1D97
Length: 55
XXXX
XXXXX X
XX *X
XX * XX XXX
X@* * X * X
XXXX XX *X
X....X* * X
X....X *X
X.... ** XX
X... X * X
XXXXXX* * X
X XXX
X* XXX
X X
XXXX
*************************************
Maze: 34
File offset: 1D98, DS:0A08, table offset: 090C
Size X: 12
Size Y: 15
End: 1DCB
Length: 52
XXXXXXXXXXXX
XX XX X
XX * * X
XXXX XX ** X
X * X X
X *** X XXXX
X X X * XX
X X X * X
X *X *X X
X ..X XXXX
XXXX.. * X@X
X.....X *X X
XX....X * X
XXX..XX X
XXXXXXXXXXXX
*************************************
Maze: 35
File offset: 1DCC, DS:0A3C, table offset: 0940
Size X: 20
Size Y: 16
End: 1E20
Length: 85
XXXXXXXXXXXX XXXXXX
X X X@XXXX....X
X **X .....X
X X XXX XX ....X
XX XX XXX X ....X
X * * X XX XXXX
X * *XX X X
XXXX X XXXX XX XX X
X X X* XX XX X
X * * X XX XXXXXXX
X X * * X X
X * XX XX X X
X ** ** X
XX XX XXX * X
X X X X
XXXXXX XXXXXX
*************************************
Maze: 36
File offset: 1E21, DS:0A91, table offset: 0995
Size X: 18
Size Y: 19
End: 1E7E
Length: 94
XXXX
XXX XX
XXXX * X
X * * XXXX
X * X * X XXXX
X X X * X X..X
XX*X* XXXX*XXXX..X
X XXXXX XX ...X
X*X XX@XX XX ..X
X X * ...X
X XXXX XXX ..X
XXX XX X XX ...X
XX* XXXX* XXX..X
X XX X X..X
XX **XX * X XXXX
X **** X
X * XXX X
X X XXXXXX
XXXXX
*************************************
Maze: 37
File offset: 1E7F, DS:0AEF, table offset: 09F3
Size X: 21
Size Y: 15
End: 1ED0
Length: 82
XXXXXXXXXXX
X...... XXXXXXXXX
X...... X XX X
X..XXX * * X
X... * * X XXX X
X...X*XXXXX X X
XXX X X* X *XXX
X ** * * *XX * X
X * X*X XX X
XXX XX X * XXXXXXX
X * * XX XX
X * * X
XX X X X
XXXXX@XXXXX
XXX
*************************************
Maze: 38
File offset: 1ED1, DS:0B41, table offset: 0A45
Size X: 14
Size Y: 15
End: 1F11
Length: 65
XXXXXXXXX
X.... XX
X.X.X * XX
XX....X X @XX
X ....X X XX
X X* XX* X
XX XXX * X
X* * * *X X
X X * * XX X
X XXX XX X
X XX XX XX
X * X * X
XXX* * XXX
X XXXXX
XXXX
*************************************
Maze: 39
File offset: 1F12, DS:0B82, table offset: 0A86
Size X: 23
Size Y: 18
End: 1F73
Length: 98
XXX
XX.XXX
X....X
XXXXXXXXXXXXX....X
XX XX XX....XXXXX
X **XX * @XX.... X
X ** *X ....X X
X * XX ** X X....X XX
X * XX * X XX XXX X
XX XXXXX XXX X
XX * * XXXXX XXX X
X *XXX X XXXXX X XXXX
X * X X
X * X* * *XXX X
X ***X * X XXXX
X X ** X
XXXXXX XXX
XXXXX
*************************************
Maze: 40
File offset: 1F74, DS:0BE4, table offset: 0AE8
Size X: 11
Size Y: 11
End: 1F9A
Length: 39
XXXX
XXXXXXX @X
X * X
X *XX *X
XX*X...X X
X *... X
X X. .X XX
X X X* X
X* * X
X XXXXXXX
XXXX
*************************************
Maze: 41
File offset: 1F9B, DS:0C0B, table offset: 0B0F
Size X: 20
Size Y: 15
End: 1FDE
Length: 68
XXXXX
XX XX
XX X
XX ** X
XX ** * X
X * * X
XXXX X ** XXXXX
X XXXXXXXX XX X
X.. ***@X
X.X XXXXXXX XX XX
X.X XXXXXXX. X* *XXX
X........... X * X
XXXXXXXXXXXXXX * X
XX XXX
XXXX
*************************************
Maze: 42
File offset: 1FDF, DS:0C4F, table offset: 0B53
Size X: 13
Size Y: 18
End: 2022
Length: 68
XXXXXXXX
X@XX XXXX
X * * X
X * * ***X
X **X X X
XX* * X
X * *****XX
X *XXXX X X
X *....X X
X XX....X** X
X XX.... XX
X ....X X
XX X....X**X
X X....X X
X X
XXXX XX*XXX
X X
XXXXXX
*************************************
Maze: 43
File offset: 2023, DS:0C93, table offset: 0B97
Size X: 17
Size Y: 16
End: 206B
Length: 73
XXXXXXXXXXXX
X XX
X X X** * X
X* X*X XX @X
XX XX X * X XX
X * X* X X
X X * X X
XX * * XX X
X X XX * X
X XX **X X
XXXXXX** X X
X....X XXXXXXXX
X.X... XX
X.... X
X.... X
XXXXXXXXX
*************************************
Maze: 44
File offset: 206C, DS:0CDC, table offset: 0BE0
Size X: 25
Size Y: 19
End: 20DD
Length: 114
XXXXXX
XXXXX X
X X X XXXXX
X * X * XXXXXX
XX* XXX XX X
XXX ** * * X XX XXXXX
X * XXXXXX XX X
X XXXXXXXX X@ X X X X
XX XXX XXXX X*X X X
X XXX XXXX XX.. X * XX
X * * X*XX.. X*XX XX
X X X X ..XX XX * X
XXXX X XX X..X * X
XXXXX X..X X X XX
XXXXXX..X X XX
X..XXXXX X
X.. X
XX XXX XX
XXXXXXXXX
*************************************
Maze: 45
File offset: 20DE, DS:0D4E, table offset: 0C52
Size X: 19
Size Y: 11
End: 2114
Length: 55
XXXXXXX
XXXXX X XXXX
X X * X
XXXX X** XX XX X
XX X X XX XXX
X XXX *X* * * X
X... X XX X X
X...X @ X XXX XX
X...X XXX * * X
XXXXXXXX XX X X
XXXXXXXXX
*************************************
Maze: 46
File offset: 2115, DS:0D85, table offset: 0C89
Size X: 22
Size Y: 17
End: 2173
Length: 95
XXXXXXXXX XXXX
X XX XXXX X
X * X * X
X X XX X XXXX
XX * * **X X X
XXXX X X * * X
XXXXX XXXX XXX...X
X X* X X XXXX.....X
X X X X XX.....X
XXXXXX X X* XXX...X
X XX X *X X...X
XX * *X XXXXX
XX ***XX X * X
X X X XXX XXX
X * X* @XXXX
XXXXX X X
XXXXXXXX
*************************************
Maze: 47
File offset: 2174, DS:0DE4, table offset: 0CE8
Size X: 19
Size Y: 15
End: 21B5
Length: 66
XXXXX
X X
X X XXXXXX
X *@XXXXXX
X * XX* XXX X
X XXXX * * X
X XXXXX X X* XXXX
XX XXXX XX* X
X *X * X XX XX X
X X X...X X
XXXXXX XXX ... X
XXXX X X...X X
X XXX X X
X X
XXXXXXXXX
*************************************
Maze: 48
File offset: 21B6, DS:0E26, table offset: 0D2A
Size X: 16
Size Y: 15
End: 21F4
Length: 63
XXXX
X XX
X XX
X ** XX
XXX* * XX
XXXX * X
XXX X XXXXX X
X X X....* X
X X * ....X X
X * X X.&..X X
XXX XXXX XXX X
XXXX @* XX*XX
XXX * X
X XX X
XXXXXXXXX
*************************************
Maze: 49
File offset: 21F5, DS:0E65, table offset: 0D69
Size X: 19
Size Y: 16
End: 224C
Length: 88
XXXXXXXXXXXX
XX.. X X
XX..& * * X
XX..&.X X X* XX
X..&.X X X * X
XXXX...X X X X
X XX X X
X @* * XXX X X XX
X * * X X X
XXX** X X X X X
X * X X XXXXX
X *X XXXXX X
X* X X X X
X XXX XX X
X X X XX
XXXX XXXXXX
*************************************
Maze: 50
File offset: 224D, DS:0EBD, table offset: 0DC1
Size X: 21
Size Y: 16
End: 22A7
Length: 91
XXXXXXXXXXXXX
X XXX X
X * * XXXX
XXXX X * * X
XX * X*XXXX * * X
XXX X X XXX * X
X * * X * X XXXX
X XX*XXXX X*X * XXX
X XX XXX X X X * X
X @* * X * X X
XXXXX X XX X *X X
X... XXXXX* X X X
X.......X ** X* X X
X.......X X
X.......XXXXXXX XX
XXXXXXXXX XXXX
*************************************
Maze: 51
File offset: 22A8, DS:0F18, table offset: 0E1C
Size X: 16
Size Y: 14
End: 22EA
Length: 67
XXXXX XXXX
X...X X XXXX
X...XXX * X
X....XX * *XXX
XX....XX * X
XXX... XX * * X
X XX X * X
X XX X XXX XXXX
X * X X* * X
X * @ * * X
X X * ** * XXX
X XXXXXX XXX
X XX XXXX
XXX
*************************************
Maze: 52
File offset: 22EB, DS:0F5B, table offset: 0E5F
Size X: 21
Size Y: 14
End: 2332
Length: 72
XXXX
XX XXXXX
X X XXXXX
X *XXX XXX X
X..X *X X X X
X..X **X XXX
X.&X X X* * XXXXX
X..X XX XX*X X
X.&* * X XX * X
X..XX * X XXXXXX
X.&XX*XX XXXXX
X.. * XXXXX
X X @ X
XXXXXXXX
*************************************
Maze: 53
File offset: 2333, DS:0FA3, table offset: 0EA7
Size X: 13
Size Y: 19
End: 2382
Length: 80
XXXXXXXXXX
X XXX X
X * * X
X XXXX*XX
XX X X X
XX X.& X
X XX..X X
X @ X.&X XX
X X*X..X* X
X * X..X X
X X X&&X X
X * X..X*XX
X .&X X
XXX X X X
XX XXXX X
X XXXXXXX*XX
X * * X
X XX X X
XXXXXXXXXXXXX
*************************************
Maze: 54
File offset: 2383, DS:0FF3, table offset: 0EF7
Size X: 23
Size Y: 20
End: 23F8
Length: 118
XXXXXXXXXXXXXXXXXXXXX
X XX X X X X
X * * * * XX
XXXXX X X XXX XX*XXX
X X XX*XXXXXX X X
X * X ......X X * X
XX X X ......XXXXX X
XX XXXXXXXXX..X X XXX
X X..X * X
X XX XXX XXX..XX X XXX
X X X XX..XX XXX X
X @ *..X X
X X X XX X XX X
XXXXX XXXXXXXXXXXXXX XX
X X X * X
X * X * * * X X X
X X*XX *X XX XX X X
X * ** XXXX * * X X X
X X X X
XXXXXXXXXXXXXXXXXXXXXXX
*************************************
Maze: 55
File offset: 23F9, DS:1069, table offset: 0F6D
Size X: 22
Size Y: 15
End: 2450
Length: 88
XXXXXXXXXXXXXXXXXXXXX
XX X
X * X XX X X
X XXXXXX XXX X*XX XX
XX*X XX*X.... X X
X X * X....XX X X
X * X X X X....XX X
X * X** X....XX*X X
X X *@*XX*X....XX X
X *** X....X X
X *X X XXXXXX *XXX
XX X XXX** * * X
XX X * * XX X
XXXXX X XXXXXXX
XXXXXXXXX
*************************************
Maze: 56
File offset: 2451, DS:10C1, table offset: 0FC5
Size X: 14
Size Y: 16
End: 248B
Length: 59
XXXXXXXXXX
X XXXX
X XXXXXX X XX
X X * * * * X
X X* X
XXX* **X XXX
X XX X *XX
XX*X * @X
X * * XXX
X X * X
X XX X X
XX XXXXX X
X X
X.......XXX
X.......X
XXXXXXXXX
*************************************
Maze: 57
File offset: 248C, DS:10FC, table offset: 1000
Size X: 18
Size Y: 11
End: 24BD
Length: 50
XXXX
XXXXXXXXX XX
XX * * XXXXX
X XX XX XX...X
X X** * **X*XX...X
X X @ X ...X
X *X XXX** ...X
X * ** * XX....X
XXX* XXXXXXX
X XXXXXXX
XXXX
*************************************
Maze: 58
File offset: 24BE, DS:112E, table offset: 1032
Size X: 27
Size Y: 20
End: 2542
Length: 133
XXXXXX
XXXXX X
X XX X XXXXX
X &.X..X X
XXXXX XXXX *X.X... X
X XXX XX X&....XX XX
X * XX X..X..XX X
XXXXXX X X X&.XXXXX X
X X *X*X X X..XXXXX X
X * * X X&. X X
XX XX * XXX X XX X X
X * * XXX XXXXX XX X
XXX*XXX*XXX XXXX XX X
XXXX X XXX X X
X * X *XXXX XXX**X@XXXXX
X * X X XXXX X*X X
XXXX X *X X X
X * X XX XX XXXXXXXX
XX XXX XXXXXXXX
XXXX
*************************************
Maze: 59
File offset: 2543, DS:11B3, table offset: 10B7
Size X: 29
Size Y: 20
End: 25DA
Length: 152
XXXX
X X
X XXXXXXXX
XXXXXXX X X
X X X X X X XX
X * * XX * X
XXX *X X X X XXXXXXXXX
X * X *X X ** X X X X
XX X X XXX * X X X
X X* X XXX X X **X X X
X *XX * X XX * X X XX
XXXX* * X XX X * ..X
X X XXX X * * XXX XXX.&X
X XX ** @ * XX....X
X XX XX * X*X XX....&.X
XX X * X X *XX XX....&.XXX
XX XX * X * X X....&.XXX
X * XXXX X ....&.XXX
X X X X X ..&.XXX
XXXXXXXX XXXXXXXXXXX
*************************************
Maze: 60
File offset: 25DB, DS:124B, table offset: 114F
Size X: 26
Size Y: 16
End: 2646
Length: 108
XXXXX
X XXXX
X * XXXX XXXX
X X *X XXXX X
XXXXXXXXXXX X * X X
X.. X * XXXX X X X
X..* X * X * X * .XX
X.&X X * * XX XX X.X
X..X* @ X XX ** X.X
X..X * * * * XX XX .X
X.&** X XX * X*X * X.X
X..X XX X X.X
X..XXXXXXX XXX XXXXXX.XX
X ** &.XX
X XXXXXXXXXXXXXXXXXX ..X
XXXX XXXXXX
*************************************
</script>
<script src="levels.js"></script>
<script src="solver.js"></script>
<script>
// ============ КОНСТАНТЫ ============
const canvas = document.getElementById('game');
const ctx = canvas.getContext('2d');
const TILE = 32;
const WALL = 1;
const TARGET = 2;
const MOVE_SPEED = 0.15;
let currentLevel = 0;
let COLS, ROWS, level, player, boxes;
let playerTargetX, playerTargetY, playerMoving = false, moveProgress = 0;
let movesCount = 0;
let levelComplete = false; // игрок дошёл до открытого выхода — уровень завершён
let allBoxesPlaced = false; // все ящики на месте — выход открыт, но ещё не пройден
let entranceX, entranceY, entranceOpen; // дверь входа — гаснет, как только игрок с неё сошёл
let exitX, exitY; // дверь выхода — появляется, когда все ящики на местах
let autoSolveActive = false;
let autoPlayMoves = null, autoPlayIndex = 0; // плоский список [dx,dy] — либо из солвера (развёрнутый), либо из сохранённого решения
let moveLog = []; // все реально сделанные ходы этой попытки уровня — пишем в топ решений при победе
let usedAutoSolve = false; // хоть раз в этой попытке играл автосолвер/G — решение не "ручное"
let pendingSolution = null; // солвер нашёл ходы, ждём клика "Решение найдено!" чтобы проиграть
// ============ ТОП РЕШЕНИЙ (localStorage, JSON) ============
// Ключ — имя уровня, значение — список решений, отсортированный по числу
// ходов (меньше — лучше). Одинаковые (по точной последовательности ходов)
// не дублируем. Пишутся и находки солвера, и ручные победы игрока.
const SOLUTIONS_KEY = 'sokoban-solutions-v1';
function loadSolutions() {
try { return JSON.parse(localStorage.getItem(SOLUTIONS_KEY)) || {}; } catch { return {}; }
}
function saveSolutions(all) {
try { localStorage.setItem(SOLUTIONS_KEY, JSON.stringify(all)); } catch {}
}
function recordSolution(levelName, moves, source) {
if (!moves.length) return;
const all = loadSolutions();
const list = all[levelName] || [];
const key = moves.map(m => m.join('')).join('|');
if (list.some(s => s.key === key)) return; // та же последовательность уже есть
list.push({ key, moves, count: moves.length, source, date: Date.now() });
list.sort((a, b) => a.count - b.count);
all[levelName] = list;
saveSolutions(all);
}
function bestSolution(levelName) {
const list = loadSolutions()[levelName];
return list && list.length ? list[0] : null;
}
// Разворачивает ходы солвера (только толчки + откуда толкать) в плоский
// список отдельных шагов игрока (ходьба + толчки), как если бы игрок жал
// стрелки сам — так и проигрывание, и запись в топ используют один формат.
function expandSolverMoves(pushMoves, fromX, fromY, levelGrid, startBoxes) {
const flat = [];
let px = fromX, py = fromY;
const simBoxes = startBoxes.map(b => ({ x: b.x, y: b.y }));
for (const move of pushMoves) {
const [dx, dy] = move.dir;
const bx = move.boxFrom.x, by = move.boxFrom.y;
const standX = bx - dx, standY = by - dy; // клетка ЗА ящиком, откуда толкать
while (px !== standX || py !== standY) {
const boxesForPath = simBoxes.map(b => ({ targetX: b.x, targetY: b.y }));
const path = findPath(px, py, standX, standY, levelGrid, boxesForPath);
if (!path || !path.length) return null; // раскладка успела измениться — не сходится
const [sx, sy] = path[0];
flat.push([sx, sy]);
px += sx; py += sy;
}
flat.push([dx, dy]);
const idx = simBoxes.findIndex(b => b.x === bx && b.y === by);
if (idx === -1) return null;
simBoxes[idx] = { x: bx + dx, y: by + dy };
px = bx; py = by; // после толчка игрок оказывается там, где был ящик
}
return flat;
}
function startFlatPlayback(moves) {
autoPlayMoves = moves;
autoPlayIndex = 0;
autoSolveActive = true;
usedAutoSolve = true;
updateHUD();
}
function autoPlayStep() {
if (playerMoving || boxes.some(b => b.moving)) return;
if (levelComplete || !autoPlayMoves || autoPlayIndex >= autoPlayMoves.length) {
autoPlayMoves = null;
autoSolveActive = false;
updateHUD();
return;
}
const [dx, dy] = autoPlayMoves[autoPlayIndex++];
tryMove(dx, dy);
}
function playBestSolution() {
if (autoSolveActive || levelComplete || solveState || playerMoving || boxes.some(b => b.moving)) return;
const best = bestSolution(LEVELS[currentLevel].name);
if (!best) { alert('Нет сохранённых решений для этого уровня — сначала пройди его (F или руками)'); return; }
startFlatPlayback(best.moves);
}
// Клетка под выход: не стена, не цель, не вход, обязательно достижима от
// старта. Считается ОДИН раз на уровень (см. вызов ниже, сразу после
// загрузки LEVELS) и кладётся прямо в данные уровня — не пересчитывается
// при каждом loadLevel (рестарт/переход/G), иначе место прыгало бы и
// записанные решения переставали сходиться с картой.
function pickExitCell(levelGrid, cols, rows, startX, startY) {
const visited = new Set([`${startX},${startY}`]);
const queue = [[startX, startY]];
const candidates = [];
while (queue.length) {
const [x, y] = queue.shift();
if (!(x === startX && y === startY) && levelGrid[y][x] !== TARGET) candidates.push([x, y]);
for (const [dx, dy] of [[0,-1],[0,1],[-1,0],[1,0]]) {
const nx = x+dx, ny = y+dy;
const key = `${nx},${ny}`;
if (nx>=0 && nx<cols && ny>=0 && ny<rows && levelGrid[ny][nx] !== WALL && !visited.has(key)) {
visited.add(key);
queue.push([nx, ny]);
}
}
}
if (!candidates.length) return [startX, startY]; // ponytail: вырожденный уровень без свободного пола — сажаем на вход, не должно случаться на реальных 60 уровнях
candidates.sort((a, b) => a[0]-b[0] || a[1]-b[1]);
return candidates[Math.floor(candidates.length / 2)]; // клетка из середины разброса, не первая попавшаяся у самого входа
}
// Считаем выход для каждого уровня один раз, сразу как LEVELS готов.
LEVELS.forEach(lvl => { lvl.exit = pickExitCell(lvl.map, lvl.cols, lvl.rows, lvl.player[0], lvl.player[1]); });
// ============ ЗАГРУЗКА ============
function loadLevel(index) {
const lvl = LEVELS[index];
COLS = lvl.cols;
ROWS = lvl.rows;
level = lvl.map.map(row => [...row]);
player = { x: lvl.player[0], y: lvl.player[1] };
playerTargetX = player.x;
playerTargetY = player.y;
playerMoving = false;
moveProgress = 0;
boxes = lvl.boxes.map(b => ({
x: b[0], y: b[1],
targetX: b[0], targetY: b[1],
moving: false, progress: 0
}));
movesCount = 0;
levelComplete = false;
allBoxesPlaced = false;
entranceX = player.x; entranceY = player.y; entranceOpen = true;
[exitX, exitY] = lvl.exit;
autoSolveActive = false;
autoPlayMoves = null;
moveLog = [];
usedAutoSolve = false;
pendingSolution = null;
resetSolveButton();
canvas.width = COLS * TILE;
canvas.height = ROWS * TILE;
updateHUD();
}
// ============ ЛОГИКА ============
function isWall(x, y) {
if (x < 0 || x >= COLS || y < 0 || y >= ROWS) return true;
return level[y][x] === WALL;
}
function getBoxAt(x, y) {
return boxes.find(b => b.targetX === x && b.targetY === y && !b.moving);
}
function tryMove(dx, dy) {
if (playerMoving || levelComplete) return;
if (boxes.some(b => b.moving)) return;
const nx = player.x + dx, ny = player.y + dy;
if (isWall(nx, ny)) return;
const box = getBoxAt(nx, ny);
if (box) {
const bx = nx + dx, by = ny + dy;
if (isWall(bx, by) || getBoxAt(bx, by)) return;
box.targetX = bx; box.targetY = by;
box.moving = true; box.progress = 0;
}
playerTargetX = nx; playerTargetY = ny;
playerMoving = true; moveProgress = 0;
movesCount++;
moveLog.push([dx, dy]);
updateHUD();
}
function checkWin() {
return boxes.every(b => {
const bx = b.moving ? b.targetX : b.x;
const by = b.moving ? b.targetY : b.y;
return level[by]?.[bx] === TARGET;
});
}
function updateHUD() {
const onTarget = boxes.filter(b => {
const bx = b.moving ? b.targetX : b.x;
const by = b.moving ? b.targetY : b.y;
return level[by]?.[bx] === TARGET;
}).length;
document.getElementById('level-name').textContent =
`${currentLevel+1}/${LEVELS.length} ${LEVELS[currentLevel].name}${autoSolveActive ? ' 🤖' : ''}${allBoxesPlaced && !levelComplete ? ' 🚪 выход открыт' : ''}`;
document.getElementById('box-count').textContent = `📦 ${onTarget}/${boxes.length}`;
document.getElementById('moves-count').textContent = `👣 ${movesCount}`;
}
// ============ АВТОСОЛВЕР ============
// Поиск гоняется кусками через setTimeout(…,0), а не одним блокирующим
// вызовом — так браузер успевает перерисовать таймер и обработать клик по
// "Отмена" между кусками. Web Worker сюда не годится: игра открывается как
// file://, а Chrome с file:// воркеры не грузит (CORS).
let solveState = null; // { solver, startTime, timerInterval, cancelled }
function startAutoSolve() {
console.log('F нажата, autoSolveActive:', autoSolveActive, 'levelComplete:', levelComplete);
if (autoSolveActive || levelComplete || solveState) {
console.log('Выход: уже активно, уровень пройден или уже ищем');
return;
}
if (playerMoving || boxes.some(b => b.moving)) {
console.log('Выход: ещё движется');
return;
}
console.log('🔍 Ищем решение для уровня', currentLevel);
const lvl = LEVELS[currentLevel];
// Солвер должен планировать от ТЕКУЩЕГО состояния ящиков, а не от
// исходного расклада уровня — иначе после ручных ходов решение не сойдётся
// с реальной доской.
const liveLvl = { ...lvl, boxes: boxes.map(b => [b.x, b.y]) };
const solver = createSolver(liveLvl, player.x, player.y);
const startTime = Date.now();
const timerEl = document.getElementById('solve-timer');
timerEl.textContent = '0.0с';
document.getElementById('solve-overlay').classList.remove('hidden');
const timerInterval = setInterval(() => {
timerEl.textContent = ((Date.now()-startTime)/1000).toFixed(1) + 'с';
}, 100);
solveState = { solver, startTime, timerInterval, cancelled: false };
setTimeout(runSolveChunk, 0);
}
function runSolveChunk() {
if (!solveState || solveState.cancelled) return;
const result = solveState.solver.step(50); // 50мс работаем, потом отдаём управление браузеру
if (!result.done) {
setTimeout(runSolveChunk, 0);
return;
}
finishAutoSolve(result);
}
function finishAutoSolve(result) {
clearInterval(solveState.timerInterval);
solveState = null;
if (!result.solved) {
document.getElementById('solve-overlay').classList.add('hidden');
alert(`Решение не найдено (${result.explored} состояний)`);
return;
}
console.log(`✅ Найдено: ${result.moves.length} толчков, состояний: ${result.explored}`);
// не проигрываем сразу — ждём клика по кнопке "Решение найдено!"
pendingSolution = result;
const btn = document.getElementById('solve-cancel');
btn.textContent = 'Решение найдено!';
btn.classList.add('solve-found');
document.getElementById('solve-timer').textContent = `${result.moves.length} толчков`;
}
function resetSolveButton() {
const btn = document.getElementById('solve-cancel');
btn.textContent = 'Отмена';
btn.classList.remove('solve-found');
}
function cancelAutoSolve() {
pendingSolution = null;
resetSolveButton();
if (!solveState) { document.getElementById('solve-overlay').classList.add('hidden'); return; }
solveState.cancelled = true;
clearInterval(solveState.timerInterval);
document.getElementById('solve-overlay').classList.add('hidden');
solveState = null;
}
function onSolveButtonClick() {
if (!pendingSolution) { cancelAutoSolve(); return; }
const solved = pendingSolution;
pendingSolution = null;
resetSolveButton();
document.getElementById('solve-overlay').classList.add('hidden');
const flat = expandSolverMoves(solved.moves, player.x, player.y, level, boxes);
if (!flat) { alert('Раскладка успела измениться, решение больше не подходит'); return; }
startFlatPlayback(flat);
}
document.getElementById('solve-cancel').addEventListener('click', onSolveButtonClick);
// Дошёл ли игрок до открытого выхода — если да, запускаем переход.
function checkExitStep() {
if (!levelComplete && allBoxesPlaced && player.x === exitX && player.y === exitY) {
levelComplete = true;
autoSolveActive = false;
recordSolution(LEVELS[currentLevel].name, moveLog, usedAutoSolve ? 'solver' : 'player');
updateHUD();
setTimeout(goToNextLevelOrFinish, 300);
}
}
function goToNextLevelOrFinish() {
if (currentLevel + 1 < LEVELS.length) {
currentLevel++;
loadLevel(currentLevel);
} else {
alert('🚀 Станция полностью запитана. Игра пройдена!');
loadLevel(currentLevel); // даём побродить по последнему уровню ещё раз
}
}
// ============ ОБНОВЛЕНИЕ ============
function update() {
if (autoSolveActive) autoPlayStep();
if (playerMoving) {
moveProgress += MOVE_SPEED;
if (moveProgress >= 1) {
player.x = playerTargetX; player.y = playerTargetY;
playerMoving = false; moveProgress = 0;
if (entranceOpen && (player.x !== entranceX || player.y !== entranceY)) entranceOpen = false;
allBoxesPlaced = checkWin();
checkExitStep();
}
}
boxes.forEach(box => {
if (box.moving) {
box.progress += MOVE_SPEED;
if (box.progress >= 1) {
box.x = box.targetX; box.y = box.targetY;
box.moving = false; box.progress = 0;
allBoxesPlaced = checkWin();
checkExitStep();
updateHUD();
}
}
});
}
// Дверь (вход/выход) — пульсирующее кольцо поверх клетки пола.
function drawDoor(px, py, color) {
const pulse = 0.5 + 0.5 * Math.sin(Date.now() / 250);
ctx.save();
ctx.shadowColor = color;
ctx.shadowBlur = 8 + pulse * 6;
ctx.strokeStyle = color;
ctx.lineWidth = 3;
ctx.beginPath();
ctx.arc(px+TILE/2, py+TILE/2, TILE/2.6, 0, Math.PI*2);
ctx.stroke();
ctx.restore();
}
// ============ ОТРИСОВКА ============
function draw() {
ctx.fillStyle = '#0a0a15';
ctx.fillRect(0, 0, canvas.width, canvas.height);
for (let y = 0; y < ROWS; y++) {
for (let x = 0; x < COLS; x++) {
const px = x * TILE, py = y * TILE;
const tile = level[y][x];
if (tile === WALL) {
if (allBoxesPlaced) {
// станция запитана — стены светятся
ctx.save();
ctx.shadowColor = '#7CFF6B';
ctx.shadowBlur = 6;
ctx.fillStyle = '#5d7a5a';
ctx.fillRect(px, py, TILE, TILE);
ctx.restore();
} else {
ctx.fillStyle = '#4a4e69';
ctx.fillRect(px, py, TILE, TILE);
}
} else {
ctx.fillStyle = (x+y)%2 ? '#252540' : '#2d2d44';
ctx.fillRect(px, py, TILE, TILE);
if (tile === TARGET) {
ctx.fillStyle = 'rgba(233,69,96,0.5)';
ctx.beginPath();
ctx.arc(px+TILE/2, py+TILE/2, TILE/5, 0, Math.PI*2);
ctx.fill();
}
if (entranceOpen && x === entranceX && y === entranceY) drawDoor(px, py, '#48cae4');
if (allBoxesPlaced && x === exitX && y === exitY) drawDoor(px, py, '#7CFF6B');
}
}
}
boxes.forEach(box => {
const bx = box.x + (box.targetX-box.x)*box.progress;
const by = box.y + (box.targetY-box.y)*box.progress;
const onT = level[Math.round(box.targetY)]?.[Math.round(box.targetX)] === TARGET;
const px = bx*TILE, py = by*TILE;
ctx.fillStyle = onT ? '#e94560' : '#d4a373';
ctx.fillRect(px+3, py+3, TILE-6, TILE-6);
ctx.strokeStyle = onT ? '#ff6b8a' : '#f0c27a';
ctx.lineWidth = 2;
ctx.strokeRect(px+3, py+3, TILE-6, TILE-6);
});
const ppx = (player.x+(playerTargetX-player.x)*moveProgress)*TILE;
const ppy = (player.y+(playerTargetY-player.y)*moveProgress)*TILE;
ctx.fillStyle = '#48cae4';
ctx.beginPath();
ctx.arc(ppx+TILE/2, ppy+TILE/2, TILE/3, 0, Math.PI*2);
ctx.fill();
}
// ============ ВВОД ============
const keys = {};
window.addEventListener('keydown', e => {
const key = e.key.toLowerCase();
if (key === 'escape' && (solveState || pendingSolution)) { cancelAutoSolve(); e.preventDefault(); return; }
if (solveState || pendingSolution) { e.preventDefault(); return; } // поиск идёт или ждём клика по найденному — левел не трогаем
// Системные клавиши — обрабатываем сразу
if (key === 'r') { loadLevel(currentLevel); e.preventDefault(); return; }
if (key === 'n') { currentLevel=(currentLevel+1)%LEVELS.length; loadLevel(currentLevel); e.preventDefault(); return; }
if (key === 'p') { currentLevel=(currentLevel-1+LEVELS.length)%LEVELS.length; loadLevel(currentLevel); e.preventDefault(); return; }
if (key === 'f') { startAutoSolve(); e.preventDefault(); return; }
if (key === 'g') { playBestSolution(); e.preventDefault(); return; }
// Стрелки и WASD
if (['arrowup','arrowdown','arrowleft','arrowright','w','a','s','d'].includes(key)) {
keys[key] = true;
e.preventDefault();
}
});
window.addEventListener('keyup', e => {
keys[e.key.toLowerCase()] = false;
});
function processInput() {
if (autoSolveActive) return;
if (playerMoving||boxes.some(b=>b.moving)||levelComplete) return;
if (keys['arrowup']||keys['w']) tryMove(0,-1);
else if (keys['arrowdown']||keys['s']) tryMove(0,1);
else if (keys['arrowleft']||keys['a']) tryMove(-1,0);
else if (keys['arrowright']||keys['d']) tryMove(1,0);
}
// ============ ЦИКЛ ============
function gameLoop() { processInput(); update(); draw(); requestAnimationFrame(gameLoop); }
// Ждём загрузки LEVELS
setTimeout(() => {
if (typeof LEVELS !== 'undefined' && LEVELS.length > 0) {
loadLevel(0);
gameLoop();
} else {
console.error('LEVELS не загружены!');
}
}, 100);
</script>
</body>
</html>