commit 50623173bbc04ac8eb2d0ed488d6f0485fc0ecad Author: Alex Cube Date: Mon Aug 10 14:19:16 2026 +0300 Initial commit: Sokoban 60-level game with A* solver - sokoban.html: canvas game, entrance/exit door mechanic (level completes by walking to the exit after all boxes are placed, not an alert popup), solution search UI (modal with timer, cancel, "Решение найдено!" button) - solver.js: A* with binary heap, true-distance heuristic, static deadlock cells, freeze-deadlock detection, tunnel macro-moves, chunked step() API for non-blocking search in the UI - localStorage-backed solution leaderboard per level (fewest moves wins), fed by both solver finds and manual player completions; G replays the best recorded solution - BACKLOG.md: known follow-ups (dead files, room/corral pruning, undo, etc.) diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4c5f206 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.claude/ diff --git a/BACKLOG.md b/BACKLOG.md new file mode 100644 index 0000000..6828eb1 --- /dev/null +++ b/BACKLOG.md @@ -0,0 +1,19 @@ +# Backlog + +- [ ] Удалить `levels.dat` и `dynamic_prompt.txt`, если подтвердится что не нужны (мёртвые файлы, дублируют вшитый дамп / пустой файл) +- [x] Солвер: заменить BFS+`Array.shift()` на A* (бинарная куча) + баг с устаревшим состоянием ящиков при повторном запуске. Сделано, но по факту весь набор из 60 уровней — не лёгкая прогрессия: минимум 6 ящиков на уровень, до 34 на некоторых. Это тяжёлые многоящичные паковки, для которых даже правильный A* с хорошим deadlock-отсечением не укладывается в разумное время в JS (нужны техники уровня специализированных солверов — macro-moves, corral pruning, паттерн-базы deadlock'ов — отдельная большая задача). Сейчас солвер честно останавливается по таймауту (8с) вместо зависания на 30-80с как раньше — но "решает всё" не гарантирует +- [x] Добавить кнопку/клавишу отмены поиска — сделано: солвер разбит на `createSolver()`+пошаговый `.step()`, гоняется кусками через `setTimeout`, есть модалка с таймером и кнопкой "Отмена" (+ Esc). 8-секундный хардкап убран — считает сколько угодно, пока не отменишь +- [ ] Добавить отмену хода (undo, клавиша Z/U) +- [ ] Добавить выбор уровня (не только N/P по одному) +- [ ] Сохранять прогресс в localStorage (текущий уровень, счётчик ходов) +- [x] Заменить `alert('Победа!')` на экран победы — сделано иначе и лучше: механика двери-выхода (см. ниже), per-level alert исчез вовсе. Остался только финальный alert "игра пройдена" на 60-м уровне — можно тоже заменить на экран, но не горит +- [ ] Touch-управление для мобилок (canvas фиксированного размера, только клавиатура сейчас) +- [ ] Favicon + README (что за проект, откуда дамп уровней) + +## Солвер — следующий уровень (если понадобится тянуть тяжёлые 20-34-ящичные уровни) +- [ ] Разбивка карты на туннели/комнаты (макро-ходы) — режет дерево поиска на порядки сильнее, чем то, что уже сделано (deadlock-клетки + A*). Отдельная большая задача, не вписывается в лёгкую правку + +## Дизайн блоков +- [ ] Ящик на цели / не на цели различать не только цветом (бежевый `#d4a373` vs красный `#e94560` — для дальтоника оба буро-коричневые), добавить форму/иконку/паттерн +- [ ] Маркер пустой цели (`.`) — точка мелкая (radius TILE/5) и полупрозрачная, на шахматном полу может теряться издали; увеличить/сделать контрастнее. Актуально только для незанятых целей — занятую и так видно по цвету ящика + diff --git a/dynamic_prompt.txt b/dynamic_prompt.txt new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/dynamic_prompt.txt @@ -0,0 +1 @@ + diff --git a/levels.dat b/levels.dat new file mode 100644 index 0000000..d80f348 --- /dev/null +++ b/levels.dat @@ -0,0 +1,1469 @@ +************************************* +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 + +************************************* \ No newline at end of file diff --git a/levels.js b/levels.js new file mode 100644 index 0000000..e90732c --- /dev/null +++ b/levels.js @@ -0,0 +1,134 @@ +// levels.js — парсер оригинального дампа Сокобан +const LEVELS = (function() { + const dataEl = document.getElementById('levels-data'); + if (!dataEl) throw new Error('Данные уровней не найдены. Добавь + + + + + + \ No newline at end of file diff --git a/solver.js b/solver.js new file mode 100644 index 0000000..edf7a04 --- /dev/null +++ b/solver.js @@ -0,0 +1,373 @@ +// solver.js — автосолвер для Сокобана +// +// createSolver() делает всю разовую подготовку (deadlock-клетки, куча, +// стартовое состояние) и возвращает объект с .step(budgetMs) — прогоняет +// поиск максимум budgetMs миллисекунд и возвращает управление. Так UI может +// показывать таймер/крутилку и дать кнопку отмены, не подвешивая вкладку — +// без Web Worker, потому что игра открывается как file://, а в Chrome оттуда +// воркеры не грузятся (CORS). +// +// solveCurrentLevel() — старый синхронный вход, гоняет step() до конца сам; +// оставлен для обратной совместимости (тесты, консоль). +function createSolver(levelData, playerX, playerY) { + const { map, boxes: startBoxesArr } = levelData; + const cols = map[0].length; + const rows = map.length; + + // Собираем цели + const targetSet = new Set(); + for (let y = 0; y < rows; y++) { + for (let x = 0; x < cols; x++) { + if (map[y][x] === 2) targetSet.add(`${x},${y}`); + } + } + + function isWall(x, y) { + if (x < 0 || x >= cols || y < 0 || y >= rows) return true; + return map[y][x] === 1; + } + + // Статика: клетки, из которых ящик вообще может попасть на какую-то цель — + // считаем "оттягиванием" от каждой цели назад, игнорируя другие ящики. + // Ящик вне этого множества — гарантированный deadlock, отсекаем сразу. + // Это стандартный и намного более сильный фильтр, чем только угловой чек. + const liveCells = new Set(); + for (const tkey of targetSet) { + const [tx, ty] = tkey.split(',').map(Number); + const q = [[tx, ty]]; + liveCells.add(tkey); + while (q.length) { + const [x, y] = q.shift(); + for (const [dx, dy] of [[0,-1],[0,1],[-1,0],[1,0]]) { + const px = x - dx, py = y - dy; // откуда ящик мог прийти + const behind = x - 2*dx, py2 = y - 2*dy; // где стоял игрок, чтобы толкнуть + if (isWall(px, py) || isWall(behind, py2)) continue; + const key = `${px},${py}`; + if (liveCells.has(key)) continue; + liveCells.add(key); + q.push([px, py]); + } + } + } + + // Freeze-deadlock: ящик неподвижен по оси, если ХОТЯ БЫ С ОДНОЙ стороны — + // стена, или ящик, который сам неподвижен по ОБЕИМ осям (значит не + // сдвинется никогда и не освободит клетку). Стена с одной стороны глушит + // толчок в обе стороны сразу: толкнуть в неё нельзя (упрётся), толкнуть + // от неё тоже нельзя (там некуда встать игроку). Ловит не только одиночный + // ящик в углу, но и несколько ящиков, взаимно блокирующих друг друга — + // ровно тот случай "два ящика застряли рядом в коридоре", который старая + // угловая проверка не видела, потому что каждый ящик по отдельности не в + // углу. Рекурсия с защитой от циклов (stack) — ящики, упирающиеся друг в + // друга по кругу и не имеющие независимого выхода, взаимно замороженные. + function isFrozenAxis(boxList, posToIdx, x, y, axis, stack, memo) { + const key = `${x},${y}|${axis}`; + if (memo.has(key)) return memo.get(key); + if (stack.has(key)) return true; + stack.add(key); + + function sideBlocks(sx, sy) { + if (isWall(sx, sy)) return true; + const idx = posToIdx.get(`${sx},${sy}`); + if (idx === undefined) return false; + const nb = boxList[idx]; + return isFrozenAxis(boxList, posToIdx, nb.x, nb.y, 'x', stack, memo) && + isFrozenAxis(boxList, posToIdx, nb.x, nb.y, 'y', stack, memo); + } + + const result = axis === 'x' + ? sideBlocks(x-1, y) || sideBlocks(x+1, y) + : sideBlocks(x, y-1) || sideBlocks(x, y+1); + + stack.delete(key); + memo.set(key, result); + return result; + } + + function isDeadlock(boxList) { + const posToIdx = new Map(); + boxList.forEach((b, i) => posToIdx.set(`${b.x},${b.y}`, i)); + const memo = new Map(); + + for (const b of boxList) { + const key = `${b.x},${b.y}`; + if (targetSet.has(key)) continue; + if (!liveCells.has(key)) return true; + if (isFrozenAxis(boxList, posToIdx, b.x, b.y, 'x', new Set(), memo) && + isFrozenAxis(boxList, posToIdx, b.x, b.y, 'y', new Set(), memo)) return true; + } + return false; + } + + function getReachable(px, py, boxSet) { + const visited = new Set(); + const queue = [{x: px, y: py}]; + visited.add(`${px},${py}`); + const reachable = []; + + while (queue.length) { + const {x, y} = queue.shift(); + reachable.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 (!isWall(nx, ny) && !boxSet.has(key) && !visited.has(key)) { + visited.add(key); + queue.push({x: nx, y: ny}); + } + } + } + return new Set(reachable.map(p => `${p.x},${p.y}`)); + } + + function stateKey(px, py, boxList) { + const sorted = boxList.map(b => `${b.x},${b.y}`).sort().join('|'); + return `${px},${py}|${sorted}`; + } + + // Туннели: клетка ровно с двумя проходимыми соседями по прямой (коридор + // шириной в одну клетку, без развилок). Толкая ящик туда, у игрока нет + // выбора — либо толкать дальше в ту же сторону, либо не толкать вовсе, + // так что каждую промежуточную клетку отдельным состоянием перебирать + // бессмысленно (доказано в теории Sokoban — оставить ящик посреди такого + // коридора никогда не выгоднее, чем протолкнуть его до конца). Считаем + // геометрию один раз на уровень. + const tunnelSet = new Set(); + for (let y = 0; y < rows; y++) { + for (let x = 0; x < cols; x++) { + if (isWall(x, y)) continue; + const open = [[0,-1],[0,1],[-1,0],[1,0]].filter(([dx,dy]) => !isWall(x+dx, y+dy)); + if (open.length === 2 && open[0][0] === -open[1][0] && open[0][1] === -open[1][1]) { + tunnelSet.add(`${x},${y}`); + } + } + } + + // Толкаем ящик b в направлении (dx,dy). Молча едем дальше БЕЗ отдельной + // точки остановки только если клетка впереди — тоже чистое туннельное + // продолжение (не развилка, не стена/ящик, не цель). Останавливаться + // ПЕРЕД развилкой и заезжать НА развилку — разные вещи: встав на + // развилке, ящик перекрывает и боковой отвод тоже, а остановившись перед + // ней — нет. Схлопывать можно только строго внутренние клетки прямого + // туннеля, где по обе стороны — тот же туннель; последнюю клетку перед + // любым "особым местом" всегда фиксируем как отдельный кандидат, а вход + // в само особое место — это уже следующий, отдельный толчок обычного + // перебора. moveChain — полная цепочка одиночных толчков для + // проигрывания в UI, ничего там менять не пришлось. + function generatePushChain(b, dx, dy, boxSet) { + const results = []; + const moveChain = []; + let curX = b.x, curY = b.y; + while (true) { + const nx = curX + dx, ny = curY + dy; + if (isWall(nx, ny)) break; + if (boxSet.has(`${nx},${ny}`)) break; + moveChain.push({dir: [dx, dy], boxFrom: {x: curX, y: curY}}); + curX = nx; curY = ny; + + const isTarget = targetSet.has(`${curX},${curY}`); + const aheadX = curX + dx, aheadY = curY + dy; + const safeToContinueSilently = !isTarget && tunnelSet.has(`${curX},${curY}`) && + !isWall(aheadX, aheadY) && !boxSet.has(`${aheadX},${aheadY}`) && + tunnelSet.has(`${aheadX},${aheadY}`) && !targetSet.has(`${aheadX},${aheadY}`); + + if (!safeToContinueSilently) { + results.push({finalX: curX, finalY: curY, moveChain: moveChain.slice()}); + break; + } + } + return results; + } + + // Эвристика: для каждой цели — расстояние ПО ПРОХОДИМЫМ КЛЕТКАМ (не по + // прямой), обратным BFS от цели, посчитано один раз на весь солвер. Прямая + // (Манхэттен) в лабиринте откровенно врёт: клетка через стену выглядит + // "рядом", хотя реально до неё идти в обход через весь коридор — именно + // это и сбивало поиск с толку на уровнях с длинными ходами. BFS игнорирует + // текущие позиции ящиков (это статическая оценка по геометрии уровня) — + // стандартное допущение, ящики двигаются, стены нет. + const targets = [...targetSet].map(s => { const [x,y] = s.split(',').map(Number); return {x,y}; }); + const targetDistMaps = targets.map(t => { + const dist = Array.from({length: rows}, () => new Array(cols).fill(Infinity)); + dist[t.y][t.x] = 0; + const q = [[t.x, t.y]]; + while (q.length) { + const [x, y] = q.shift(); + for (const [dx, dy] of [[0,-1],[0,1],[-1,0],[1,0]]) { + const nx = x+dx, ny = y+dy; + if (nx < 0 || nx >= cols || ny < 0 || ny >= rows || isWall(nx, ny)) continue; + if (dist[ny][nx] > dist[y][x] + 1) { + dist[ny][nx] = dist[y][x] + 1; + q.push([nx, ny]); + } + } + } + return dist; + }); + // ponytail: "ящик к ближайшей цели независимо" — не венгерский алгоритм, + // при нескольких ящиках рядом может занижать точность (толкаются в одну + // и ту же цель на бумаге). Разница на порядки дешевле полного + // паросочетания на каждый кандидат-пуш, см. предыдущую попытку в истории. + function heuristic(boxList) { + let sum = 0; + for (const b of boxList) { + let best = Infinity; + for (const dm of targetDistMaps) { + const d = dm[b.y]?.[b.x]; + if (d !== undefined && d < best) best = d; + } + if (best !== Infinity) sum += best; + } + return sum; + } + + // ponytail: простая бинарная куча вместо готовой либы — стандартной + // priority queue в JS нет, а это 15 строк. + class MinHeap { + constructor() { this.items = []; } + get size() { return this.items.length; } + push(item) { + const a = this.items; + a.push(item); + let i = a.length - 1; + while (i > 0) { + const p = (i-1)>>1; + if (a[p].f <= a[i].f) break; + [a[p], a[i]] = [a[i], a[p]]; + i = p; + } + } + pop() { + const a = this.items; + const top = a[0]; + const last = a.pop(); + if (a.length) { + a[0] = last; + let i = 0; + while (true) { + const l = i*2+1, r = i*2+2; + let smallest = i; + if (l < a.length && a[l].f < a[smallest].f) smallest = l; + if (r < a.length && a[r].f < a[smallest].f) smallest = r; + if (smallest === i) break; + [a[smallest], a[i]] = [a[i], a[smallest]]; + i = smallest; + } + } + return top; + } + } + + const startBoxes = startBoxesArr.map(b => ({x: b[0], y: b[1]})); + const startKey = stateKey(playerX, playerY, startBoxes); + const visited = new Set(); + visited.add(startKey); + + const heap = new MinHeap(); + heap.push({ + px: playerX, py: playerY, + boxes: startBoxes, + moves: [], + g: 0, + f: heuristic(startBoxes) + }); + + // ponytail: без времени/кнопки отмены раньше это было единственной защитой + // от зависшей вкладки. Теперь отмена — дело кнопки в UI, а этот потолок — + // просто страховка от переполнения памяти на совсем патологических уровнях. + const MAX_STATES = 5000000; + let explored = 0; + + return { + get explored() { return explored; }, + // Прогоняет поиск максимум budgetMs миллисекунд и отдаёт управление — + // { done:false } значит "зови step() ещё раз", { done:true, solved, moves, explored } — готово. + step(budgetMs) { + const stepStart = Date.now(); + while (heap.size && explored < MAX_STATES) { + if (Date.now() - stepStart > budgetMs) return { done: false }; + const cur = heap.pop(); + explored++; + + if (cur.boxes.every(b => targetSet.has(`${b.x},${b.y}`))) { + return { done: true, solved: true, moves: cur.moves, explored }; + } + + const boxSet = new Set(cur.boxes.map(b => `${b.x},${b.y}`)); + const reachable = getReachable(cur.px, cur.py, boxSet); + + for (const b of cur.boxes) { + for (const [dx, dy] of [[0,-1],[0,1],[-1,0],[1,0]]) { + const fromX = b.x - dx, fromY = b.y - dy; + if (!reachable.has(`${fromX},${fromY}`)) continue; + + for (const {finalX, finalY, moveChain} of generatePushChain(b, dx, dy, boxSet)) { + const newBoxes = cur.boxes.map(b2 => + b2.x === b.x && b2.y === b.y ? {x: finalX, y: finalY} : {...b2} + ); + + if (isDeadlock(newBoxes)) continue; + + // после цепочки толчков игрок стоит там, где ящик был перед + // последним шагом — на клетку позади финальной по направлению толчка + const newPx = finalX - dx, newPy = finalY - dy; + const key = stateKey(newPx, newPy, newBoxes); + if (visited.has(key)) continue; + visited.add(key); + + const g = cur.g + moveChain.length; + heap.push({ + px: newPx, py: newPy, + boxes: newBoxes, + moves: [...cur.moves, ...moveChain], + g, + f: g + heuristic(newBoxes) + }); + } + } + } + } + return { done: true, solved: false, explored }; + } + }; +} + +// Старый синхронный вход — гоняет step() до конца сам. Для тестов/консоли, +// в игре теперь используется createSolver() напрямую (см. sokoban.html). +function solveCurrentLevel(levelData, playerX, playerY) { + const solver = createSolver(levelData, playerX, playerY); + let result; + do { result = solver.step(1000); } while (!result.done); + return result; +} + +// Поиск пути для игрока +function findPath(sx, sy, ex, ey, level, boxes) { + const cols = level[0].length; + const rows = level.length; + + function isWall(x, y) { + if (x < 0 || x >= cols || y < 0 || y >= rows) return true; + return level[y][x] === 1; + } + + const visited = new Set(); + const queue = [{x: sx, y: sy, path: []}]; + visited.add(`${sx},${sy}`); + const boxSet = new Set(boxes.map(b => `${b.targetX},${b.targetY}`)); + + while (queue.length) { + const {x, y, path} = queue.shift(); + if (x === ex && y === ey) return path; + + 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 (!isWall(nx, ny) && !boxSet.has(key) && !visited.has(key)) { + visited.add(key); + queue.push({x: nx, y: ny, path: [...path, [dx, dy]]}); + } + } + } + return null; +} \ No newline at end of file