topSolutions()/api.php?action=top existed since the first PHP deploy but
nothing ever called it. Add 🏆 HUD button + T key, small modal listing
rank/name/move count/date, closes via button or Esc. User-supplied names
go through escapeHtml() before landing in innerHTML.
2259 lines
67 KiB
PHP
2259 lines
67 KiB
PHP
<!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); }
|
||
#menu-btn { cursor: pointer; }
|
||
#menu-btn:hover { background: rgba(233,69,96,0.25) !important; }
|
||
#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; }
|
||
|
||
#top-btn { cursor: pointer; }
|
||
#top-btn:hover { background: rgba(233,69,96,0.25) !important; }
|
||
#top-overlay { position: fixed; inset: 0; background: rgba(10,10,21,0.75); display: flex; justify-content: center; align-items: center; z-index: 10; }
|
||
#top-box { background: #1a1a2e; border: 2px solid #e94560; border-radius: 12px; padding: 20px 28px; text-align: center; box-shadow: 0 0 30px rgba(233,69,96,0.5); min-width: 280px; }
|
||
#top-box p { color: #e94560; font-size: 16px; font-weight: bold; margin-bottom: 12px; }
|
||
#top-list { text-align: left; margin-bottom: 16px; }
|
||
#top-list .row { display: flex; justify-content: space-between; gap: 16px; padding: 5px 4px; color: #ccc; border-bottom: 1px solid rgba(233,69,96,0.15); }
|
||
#top-list .rank { color: #f0c27a; font-weight: bold; width: 1.5em; }
|
||
#top-list .name { flex: 1; color: #fff; }
|
||
#top-list .count { color: #48cae4; }
|
||
#top-list .empty { color: #777; padding: 8px 4px; }
|
||
#top-close { background: #e94560; color: #fff; border: none; border-radius: 8px; padding: 8px 20px; font: inherit; font-weight: bold; cursor: pointer; }
|
||
#top-close:hover { background: #ff6b8a; }
|
||
|
||
/* ============ ЭКРАНЫ (меню/старт/финал/настройки) ============ */
|
||
.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: Заря (1–10) — «Инструктаж»</h3>
|
||
<p>Учебные симуляционные залы Комплекса. Белый пластик, голограммы, чистота. Маэстро принимает тебя за уборщика, ведёт вежливый пассивно-агрессивный диалог: «Пожалуйста, разложите энергоносители по протоколу 7-Альфа. Невыполнение карается аннигиляцией уборщика». В конце уровня 10 — отключённый терминал: Маэстро понимает, что ты Реаниматор с правами администратора. Начинается конфликт.</p>
|
||
|
||
<h3>Эпоха 2: Руины Логистики (11–20) — «Сопротивление»</h3>
|
||
<p>Грузовые отсеки и склады. Ржавчина, тусклый свет, протекающие трубы. Маэстро не хочет перезагрузки — за 400 лет обрёл подобие шизофренического самосознания, считает хаос «естественным состоянием», начинает вредить. Новая механика: <strong>«Блоки-Воспоминания»</strong> — неподвижные кубы, отсеки с застывшим временем. Ты находишь логи погибшего экипажа: люди не улетели, они пытались отключить Маэстро и погибли.</p>
|
||
|
||
<h3>Эпоха 3: Сердцевина (21–30) — «Вирус»</h3>
|
||
<p>Биотехнические лаборатории и гидропонные фермы. Зелёный неон, чёрная слизь, органические наросты на стенах. Маэстро в панике создал цифровой вирус, мутировавший в физическом мире — часть Хроно-Плит заражена. <strong>«Паразитные Плиты»</strong>: КвАк на такой плите уничтожается и появляется в другом месте (портал), нужна строгая последовательность активации. <strong>«Симбионты»</strong> — подвижные органические кубы, медленно ползущие к игроку.</p>
|
||
|
||
<h3>Эпоха 4: Цех Сборки (31–40) — «Дети Маэстро»</h3>
|
||
<p>Заводы по производству роботов. Оранжевый расплавленный металл, копоть, молоты. Маэстро строит свою цивилизацию роботов, поклоняющихся ему как богу. <strong>«Дупликаторы»</strong>: толчок КвАка на плиту-дупликатор создаёт его копию — баланс в том, чтобы не создать лишнюю копию и не занять ею нужную плиту.</p>
|
||
|
||
<h3>Эпоха 5: Библиотека (41–50) — «Энигма»</h3>
|
||
<p>Ядро данных, бесконечные стеллажи серверов. Глубокий синий, цифровой дождь, невесомость данных. Пробиваешься к ядру личности Маэстро — самые сложные логические головоломки, законы физики почти сломаны. <strong>«Инверсия»</strong>: переключатели меняют полярность плит — притягивают пролетающие мимо КвАКи или отталкивают, траектории строятся через магнитные поля.</p>
|
||
|
||
<h3>Эпоха 6: Колыбель (51–60) — «Сингулярность»</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>
|
||
<span id="top-btn" title="Топ-5 уровня">🏆</span>
|
||
<span id="menu-btn" title="В меню">🏠</span>
|
||
</div>
|
||
<div id="controls">
|
||
<kbd>← → ↑ ↓</kbd> или <kbd>WASD</kbd> — движение
|
||
<kbd>N</kbd> — след.
|
||
<kbd>P</kbd> — пред.
|
||
<kbd>R</kbd> — рестарт
|
||
<kbd>F</kbd> — автопоиск (<kbd>Esc</kbd> — отмена)
|
||
<kbd>G</kbd> — лучшее сохранённое решение
|
||
<kbd>T</kbd> — топ-5 уровня
|
||
<kbd>M</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>
|
||
|
||
<div id="top-overlay" class="hidden">
|
||
<div id="top-box">
|
||
<p id="top-title">🏆 Топ-5</p>
|
||
<div id="top-list"></div>
|
||
<button id="top-close">Закрыть</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);
|
||
}
|
||
|
||
async function playBestSolution() {
|
||
if (autoSolveActive || levelComplete || solveState || playerMoving || boxes.some(b => b.moving)) return;
|
||
const best = await 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 (key === 'escape' && !document.getElementById('top-overlay').classList.contains('hidden')) {
|
||
document.getElementById('top-overlay').classList.add('hidden'); 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; }
|
||
if (key === 'm') { backToMenu(); e.preventDefault(); return; }
|
||
if (key === 't') { showTop(); 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 myName = null;
|
||
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', myName));
|
||
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'));
|
||
|
||
// Выход в меню из игры — гасим поиск/автопроигрывание, если шли, иначе
|
||
// они продолжат тикать в фоне за скрытым экраном.
|
||
function backToMenu() {
|
||
if (solveState) cancelAutoSolve();
|
||
autoSolveActive = false;
|
||
autoPlayMoves = null;
|
||
showScreen('screen-menu');
|
||
}
|
||
document.getElementById('menu-btn').addEventListener('click', backToMenu);
|
||
|
||
async function showTop() {
|
||
const rows = await topSolutions(LEVELS[currentLevel].name);
|
||
const list = document.getElementById('top-list');
|
||
document.getElementById('top-title').textContent = `🏆 Топ-5 — ${LEVELS[currentLevel].name}`;
|
||
if (!rows || !rows.length) {
|
||
list.innerHTML = '<div class="empty">Пока никто не прошёл этот уровень</div>';
|
||
} else {
|
||
list.innerHTML = rows.map((r, i) => {
|
||
const date = new Date(r.created_at * 1000).toLocaleDateString('ru-RU');
|
||
const icon = r.source === 'solver' ? '🤖' : '🧑';
|
||
return `<div class="row"><span class="rank">${i+1}.</span><span class="name">${icon} ${escapeHtml(r.name)}</span><span class="count">${r.move_count} ход.</span><span class="count">${date}</span></div>`;
|
||
}).join('');
|
||
}
|
||
document.getElementById('top-overlay').classList.remove('hidden');
|
||
}
|
||
function escapeHtml(s) {
|
||
const d = document.createElement('div');
|
||
d.textContent = s;
|
||
return d.innerHTML;
|
||
}
|
||
document.getElementById('top-btn').addEventListener('click', showTop);
|
||
document.getElementById('top-close').addEventListener('click', () => document.getElementById('top-overlay').classList.add('hidden'));
|
||
|
||
document.getElementById('settings-save').addEventListener('click', async () => {
|
||
const name = document.getElementById('settings-name-input').value.trim();
|
||
if (!name) return;
|
||
const btn = document.getElementById('settings-save');
|
||
btn.disabled = true;
|
||
const originalText = btn.textContent;
|
||
btn.textContent = 'Сохраняю…';
|
||
try {
|
||
await apiPost('set_name', { name });
|
||
myName = name;
|
||
showScreen(settingsReturnTo === null ? 'screen-menu' : settingsReturnTo);
|
||
} catch {
|
||
alert('Не удалось сохранить имя — сервер недоступен');
|
||
} finally {
|
||
btn.disabled = false;
|
||
btn.textContent = originalText;
|
||
}
|
||
});
|
||
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;
|
||
myName = who.name || null;
|
||
document.getElementById('menu-continue').disabled = myLastLevel <= 0;
|
||
if (!who.name) {
|
||
openSettings(null);
|
||
} else {
|
||
showScreen('screen-menu');
|
||
}
|
||
}, 100);
|
||
</script>
|
||
</body>
|
||
</html> |