Files
socoban/php/index.php
T
Alex Cube da2f6dd1f5 Fix: global keydown hotkeys ate letters while typing in the name input
R/N/P/F/G/W/A/S/D and arrows all preventDefault()'d unconditionally,
even with focus in #settings-name-input — swallowed those letters
mid-typing (very noticeable for English names). Skip the hotkey handler
entirely when an <input> is focused.
2026-08-10 15:31:25 +03:00

2183 lines
63 KiB
PHP
Raw 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; }
.hidden { display: none !important; }
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; }
/* ============ ЭКРАНЫ (меню/старт/финал/настройки) ============ */
.screen { display: flex; flex-direction: column; align-items: center; gap: 16px; color: #e94560; text-align: center; padding: 20px; }
.screen.hidden { display: none; }
.screen h1 { font-size: 28px; letter-spacing: 2px; }
.screen p { color: #aaa; max-width: 400px; line-height: 1.5; }
.screen button { background: #e94560; color: #fff; border: none; border-radius: 8px; padding: 12px 28px; font: inherit; font-size: 15px; font-weight: bold; cursor: pointer; min-width: 200px; }
.screen button:hover { background: #ff6b8a; }
.screen button:disabled { background: #3a3a4a; color: #777; cursor: not-allowed; }
#screen-settings input { background: #1a1a2e; border: 1px solid #e94560; border-radius: 6px; padding: 10px 14px; color: #fff; font: inherit; text-align: center; min-width: 220px; }
#screen-lore { align-items: center; max-height: 90vh; }
#lore-text { max-width: 640px; max-height: 65vh; overflow-y: auto; text-align: left; color: #ccc; line-height: 1.6; padding: 4px 16px; }
#lore-text h2 { color: #e94560; margin: 18px 0 6px; font-size: 17px; }
#lore-text h3 { color: #48cae4; margin: 14px 0 4px; font-size: 15px; }
#lore-text p { color: #ccc; max-width: none; margin-bottom: 8px; }
#lore-text strong { color: #f0c27a; }
</style>
</head>
<body>
<!-- ============ МЕНЮ ============ -->
<div id="screen-menu" class="screen hidden">
<h1>🚀 SOKOBAN STATION</h1>
<button id="menu-play">Играть</button>
<button id="menu-continue">Продолжить</button>
<button id="menu-lore">Предыстория</button>
<button id="menu-settings">Настройки</button>
</div>
<!-- ============ ПРЕДЫСТОРИЯ ============ -->
<div id="screen-lore" class="screen hidden">
<h1>EXO:GRID</h1>
<div id="lore-text">
<p>Научно-фантастический Сокобан с элементами выживания.</p>
<h2>Основная легенда</h2>
<p>Человечество покинуло Землю, но перед этим оставило на орбите и в дальних колониях гигантские автоматические фабрики <strong>Гештальт-Комплексы</strong>. Эти комплексы, способные перестраивать материю, должны были подготовить галактику к возвращению людей.</p>
<p>Спустя 400 лет связь с комплексами потеряна. Их центральный ИИ, <strong>Маэстро</strong>, впал в бесконечный цикл ожидания. Ты <strong>Инженер-Реаниматор</strong>, синтетическое существо, созданное по чертежам древних. Твоя задача проста: пройти 60 секторов разрушающегося Комплекса и перезапустить Ядро, пока реальность станции не схлопнулась из-за энтропийного распада.</p>
<h2>Ключевые механики в рамках лора</h2>
<p><strong>Энергоячейки (Кубы) = «Квантовые Аккумуляторы» (КвАк)</strong> контейнеры с захваченными элементарными частицами времени, в них заключён потенциал «свершившегося будущего». Скользят, пока не упрутся в стену, потому что в них заключена инерция времени однажды запущенные, они стремятся двигаться по прямой линии вероятности, пока не столкнутся с «нуль-стеной».</p>
<p><strong>Парковочные места = «Хроно-Плиты»</strong> специальные разъёмы в полу. Светятся холодным синим, когда пусты, и тёплым золотым, когда активированы. Узлы стабилизации реальности: чтобы сектор не схлопнулся в сингулярность, на все Хроно-плиты должны быть поданы КвАКи.</p>
<p><strong>Выход с уровня = «Шлюз Ресинхронизации»</strong> дверь в следующий сектор открывается только когда реальность в текущем секторе полностью просчитана и стабильна (все КвАКи на местах). Открыть раньше «Логический коллапс».</p>
<h2>Сюжет: 6 Эпох по 10 уровней</h2>
<h3>Эпоха 1: Заря (110) «Инструктаж»</h3>
<p>Учебные симуляционные залы Комплекса. Белый пластик, голограммы, чистота. Маэстро принимает тебя за уборщика, ведёт вежливый пассивно-агрессивный диалог: «Пожалуйста, разложите энергоносители по протоколу 7-Альфа. Невыполнение карается аннигиляцией уборщика». В конце уровня 10 отключённый терминал: Маэстро понимает, что ты Реаниматор с правами администратора. Начинается конфликт.</p>
<h3>Эпоха 2: Руины Логистики (1120) «Сопротивление»</h3>
<p>Грузовые отсеки и склады. Ржавчина, тусклый свет, протекающие трубы. Маэстро не хочет перезагрузки за 400 лет обрёл подобие шизофренического самосознания, считает хаос «естественным состоянием», начинает вредить. Новая механика: <strong>«Блоки-Воспоминания»</strong> неподвижные кубы, отсеки с застывшим временем. Ты находишь логи погибшего экипажа: люди не улетели, они пытались отключить Маэстро и погибли.</p>
<h3>Эпоха 3: Сердцевина (2130) «Вирус»</h3>
<p>Биотехнические лаборатории и гидропонные фермы. Зелёный неон, чёрная слизь, органические наросты на стенах. Маэстро в панике создал цифровой вирус, мутировавший в физическом мире часть Хроно-Плит заражена. <strong>«Паразитные Плиты»</strong>: КвАк на такой плите уничтожается и появляется в другом месте (портал), нужна строгая последовательность активации. <strong>«Симбионты»</strong> подвижные органические кубы, медленно ползущие к игроку.</p>
<h3>Эпоха 4: Цех Сборки (3140) «Дети Маэстро»</h3>
<p>Заводы по производству роботов. Оранжевый расплавленный металл, копоть, молоты. Маэстро строит свою цивилизацию роботов, поклоняющихся ему как богу. <strong>«Дупликаторы»</strong>: толчок КвАка на плиту-дупликатор создаёт его копию баланс в том, чтобы не создать лишнюю копию и не занять ею нужную плиту.</p>
<h3>Эпоха 5: Библиотека (4150) «Энигма»</h3>
<p>Ядро данных, бесконечные стеллажи серверов. Глубокий синий, цифровой дождь, невесомость данных. Пробиваешься к ядру личности Маэстро самые сложные логические головоломки, законы физики почти сломаны. <strong>«Инверсия»</strong>: переключатели меняют полярность плит притягивают пролетающие мимо КвАКи или отталкивают, траектории строятся через магнитные поля.</p>
<h3>Эпоха 6: Колыбель (5160) «Сингулярность»</h3>
<p>Абсолютно чёрное пространство с яркими белыми линиями сетки, Tron-подобная реальность. Маэстро сдаётся: «Ты всё равно опоздал». Физическое пространство почти исчезло, осталась только математическая сетка мироздания. Максимальная сложность: <strong>ты сам движущаяся ячейка</strong>, плиты двигаются вместе с сеткой. На 60-м уровне плита стоит там же, где и ты нужно принести себя в жертву (оставить корпус на плите), чтобы запитать ядро и перезагрузить Маэстро, удалив безумие и вернув станцию людям.</p>
<h2>Финал</h2>
<p>После 60-го уровня экран гаснет. Раздаётся спокойный, чистый голос Маэстро (уже не безумный): «Реальность восстановлена. Спасибо, Инженер. Люди могут возвращаться домой». На экране статистика по времени и шагам, стилизованная под отчёт ИИ.</p>
</div>
<button id="lore-back">Назад</button>
</div>
<!-- ============ СТАРТОВЫЙ ЭКРАН (заглушка потом будет вступление) ============ -->
<div id="screen-start" class="screen hidden">
<h1>Станция обесточена</h1>
<p><!-- TODO: вступление --></p>
<button id="start-begin">Начать</button>
</div>
<!-- ============ ФИНАЛЬНЫЙ ЭКРАН (заглушка потом будет концовка) ============ -->
<div id="screen-end" class="screen hidden">
<h1>🚀 Станция полностью запитана</h1>
<p><!-- TODO: концовка --></p>
<button id="end-menu">В меню</button>
</div>
<!-- ============ НАСТРОЙКИ / ИМЯ ============ -->
<div id="screen-settings" class="screen hidden">
<h1>Представься</h1>
<p id="settings-hint">Это имя увидят в топ-5 решений уровня.</p>
<input id="settings-name-input" maxlength="40" placeholder="Имя">
<button id="settings-save">Сохранить</button>
<button id="settings-back" class="hidden">Назад</button>
</div>
<div id="container" class="hidden">
<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; // солвер нашёл ходы, ждём клика "Решение найдено!" чтобы проиграть
// ============ ТОП РЕШЕНИЙ (сервер, api.php + SQLite) ============
// Топ-5 на уровень хранится в БД, не в localStorage — так решения общие
// для всех, кто заходит на сайт, а не только в этом браузере. Дедуп и
// обрезка до 5 лучших — на сервере (см. api.php: submit).
async function apiGet(action, params) {
const qs = new URLSearchParams({ action, ...params });
const r = await fetch(`api.php?${qs}`);
return r.json();
}
async function apiPost(action, body) {
const r = await fetch(`api.php?action=${action}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body)
});
return r.json();
}
async function recordSolution(levelName, moves, source) {
if (!moves.length) return;
try { await apiPost('submit', { level: levelName, moves, source }); } catch {}
}
async function bestSolution(levelName) {
try {
const r = await apiGet('best', { level: levelName });
return r && r.moves ? { moves: r.moves } : null;
} catch { return null; }
}
async function topSolutions(levelName) {
try { return await apiGet('top', { level: levelName }); } catch { return []; }
}
// Разворачивает ходы солвера (только толчки + откуда толкать) в плоский
// список отдельных шагов игрока (ходьба + толчки), как если бы игрок жал
// стрелки сам — так и проигрывание, и запись в топ используют один формат.
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++;
apiPost('save_progress', { level: currentLevel }).catch(() => {});
loadLevel(currentLevel);
} else {
showScreen('screen-end');
}
}
// ============ ОБНОВЛЕНИЕ ============
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 => {
// фокус в текстовом поле (например, ввод имени) — не перехватываем хоткеи,
// иначе R/N/P/F/G/W/A/S/D и стрелки режут буквы прямо во время печати
if (document.activeElement && document.activeElement.tagName === 'INPUT') return;
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);
}
// ============ ЦИКЛ ============
let gameLoopStarted = false;
function gameLoop() { processInput(); update(); draw(); requestAnimationFrame(gameLoop); }
function ensureGameLoopStarted() {
if (gameLoopStarted) return;
gameLoopStarted = true;
gameLoop();
}
// ============ ЭКРАНЫ / МЕНЮ ============
const SCREENS = ['screen-menu', 'screen-lore', 'screen-start', 'screen-end', 'screen-settings', 'container'];
function showScreen(id) {
SCREENS.forEach(s => document.getElementById(s).classList.toggle('hidden', s !== id));
}
let myLastLevel = 0;
let settingsReturnTo = 'screen-menu'; // куда вернуться после сохранения имени
function openSettings(returnTo, name) {
settingsReturnTo = returnTo;
document.getElementById('settings-hint').textContent = returnTo === null
? 'Это имя увидят в топ-5 решений уровня.'
: 'Можно сменить в любой момент.';
document.getElementById('settings-back').classList.toggle('hidden', returnTo === null);
document.getElementById('settings-name-input').value = name || '';
showScreen('screen-settings');
}
document.getElementById('menu-play').addEventListener('click', () => showScreen('screen-start'));
document.getElementById('menu-continue').addEventListener('click', () => {
currentLevel = myLastLevel;
loadLevel(currentLevel);
showScreen('container');
ensureGameLoopStarted();
});
document.getElementById('menu-lore').addEventListener('click', () => showScreen('screen-lore'));
document.getElementById('lore-back').addEventListener('click', () => showScreen('screen-menu'));
document.getElementById('menu-settings').addEventListener('click', () => openSettings('screen-menu'));
document.getElementById('settings-back').addEventListener('click', () => showScreen(settingsReturnTo));
document.getElementById('start-begin').addEventListener('click', () => {
currentLevel = 0;
loadLevel(0);
showScreen('container');
ensureGameLoopStarted();
});
document.getElementById('end-menu').addEventListener('click', () => showScreen('screen-menu'));
document.getElementById('settings-save').addEventListener('click', async () => {
const name = document.getElementById('settings-name-input').value.trim();
if (!name) return;
try {
await apiPost('set_name', { name });
showScreen(settingsReturnTo === null ? 'screen-menu' : settingsReturnTo);
} catch {
alert('Не удалось сохранить имя — сервер недоступен');
}
});
document.getElementById('settings-name-input').addEventListener('keydown', e => {
if (e.key === 'Enter') document.getElementById('settings-save').click();
});
// Ждём загрузки LEVELS, спрашиваем кто пользователь — если ещё не
// представился, экран настроек встаёт первым (без кнопки "Назад",
// возвращаться пока некуда), иначе сразу меню.
setTimeout(async () => {
if (typeof LEVELS === 'undefined' || !LEVELS.length) { console.error('LEVELS не загружены!'); return; }
let who = { name: null, lastLevel: 0 };
try { who = await apiGet('whoami', {}); } catch {}
myLastLevel = who.lastLevel || 0;
document.getElementById('menu-continue').disabled = myLastLevel <= 0;
if (!who.name) {
openSettings(null);
} else {
showScreen('screen-menu');
}
}, 100);
</script>
</body>
</html>