- sokoban.html: canvas game, entrance/exit door mechanic (level completes by walking to the exit after all boxes are placed, not an alert popup), solution search UI (modal with timer, cancel, "Решение найдено!" button) - solver.js: A* with binary heap, true-distance heuristic, static deadlock cells, freeze-deadlock detection, tunnel macro-moves, chunked step() API for non-blocking search in the UI - localStorage-backed solution leaderboard per level (fewest moves wins), fed by both solver finds and manual player completions; G replays the best recorded solution - BACKLOG.md: known follow-ups (dead files, room/corral pruning, undo, etc.)
134 lines
4.5 KiB
JavaScript
134 lines
4.5 KiB
JavaScript
// levels.js — парсер оригинального дампа Сокобан
|
|
const LEVELS = (function() {
|
|
const dataEl = document.getElementById('levels-data');
|
|
if (!dataEl) throw new Error('Данные уровней не найдены. Добавь <script id="levels-data">');
|
|
const raw = dataEl.textContent;
|
|
|
|
const levels = [];
|
|
// Разбиваем по разделителю *****
|
|
const blocks = raw.split(/\*\*+\n/);
|
|
|
|
let currentMaze = null;
|
|
let headerLines = [];
|
|
|
|
for (const block of blocks) {
|
|
const lines = block.split('\n');
|
|
|
|
// Ищем заголовок Maze
|
|
for (const line of lines) {
|
|
const mazeMatch = line.match(/^Maze:\s*(\d+)/);
|
|
if (mazeMatch) {
|
|
// Сохраняем предыдущий уровень
|
|
if (currentMaze && currentMaze.mapLines.length > 0) {
|
|
finalizeLevel(currentMaze, levels);
|
|
}
|
|
|
|
currentMaze = {
|
|
num: parseInt(mazeMatch[1]),
|
|
cols: 0,
|
|
rows: 0,
|
|
headerLines: [],
|
|
mapLines: []
|
|
};
|
|
}
|
|
|
|
if (currentMaze) {
|
|
const sizeXMatch = line.match(/Size X:\s*(\d+)/);
|
|
const sizeYMatch = line.match(/Size Y:\s*(\d+)/);
|
|
if (sizeXMatch) currentMaze.cols = parseInt(sizeXMatch[1]);
|
|
if (sizeYMatch) currentMaze.rows = parseInt(sizeYMatch[1]);
|
|
|
|
// Всё после "Length:" и до следующего "Maze:" или "***" — это карта
|
|
if (line.match(/^Length:/)) {
|
|
currentMaze.inMap = true;
|
|
continue;
|
|
}
|
|
|
|
if (currentMaze.inMap && !line.match(/^Maze:/) && !line.match(/^\*+/)) {
|
|
// Проверяем, что строка содержит символы карты
|
|
if (line.match(/^[ X.*@$&+\-]+$/) || line.trim() === '') {
|
|
currentMaze.mapLines.push(line);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Последний уровень
|
|
if (currentMaze && currentMaze.mapLines.length > 0) {
|
|
finalizeLevel(currentMaze, levels);
|
|
}
|
|
|
|
function finalizeLevel(maze, levelsArr) {
|
|
// Убираем пустые строки в начале и конце
|
|
while (maze.mapLines.length > 0 && maze.mapLines[0].trim() === '') {
|
|
maze.mapLines.shift();
|
|
}
|
|
while (maze.mapLines.length > 0 && maze.mapLines[maze.mapLines.length-1].trim() === '') {
|
|
maze.mapLines.pop();
|
|
}
|
|
|
|
if (maze.mapLines.length === 0) {
|
|
console.warn(`Maze ${maze.num}: пустая карта`);
|
|
return;
|
|
}
|
|
|
|
// Дополняем строки до cols пробелами
|
|
const paddedLines = maze.mapLines.map(line => {
|
|
if (line.length < maze.cols) {
|
|
return line + ' '.repeat(maze.cols - line.length);
|
|
}
|
|
return line.substring(0, maze.cols);
|
|
});
|
|
|
|
// Если строк меньше rows — добавляем пустые
|
|
while (paddedLines.length < maze.rows) {
|
|
paddedLines.push(' '.repeat(maze.cols));
|
|
}
|
|
|
|
const map = [];
|
|
let player = null;
|
|
const boxes = [];
|
|
const targets = [];
|
|
|
|
for (let y = 0; y < maze.rows; y++) {
|
|
const row = [];
|
|
const line = paddedLines[y] || ' '.repeat(maze.cols);
|
|
|
|
for (let x = 0; x < maze.cols; x++) {
|
|
const ch = line[x] || ' ';
|
|
|
|
switch (ch) {
|
|
case 'X': row.push(1); break; // стена
|
|
case '.': row.push(2); targets.push({x, y}); break; // цель
|
|
case '*': row.push(0); boxes.push({x, y}); break; // ящик на полу
|
|
case '$': row.push(2); boxes.push({x, y}); targets.push({x, y}); break; // ящик на цели
|
|
case '@': row.push(0); player = {x, y}; break; // игрок на полу
|
|
case '+': row.push(2); player = {x, y}; targets.push({x, y}); break; // игрок на цели
|
|
case '&': row.push(0); break; // спец. пол
|
|
default: row.push(0); break; // пробел = пол
|
|
}
|
|
}
|
|
map.push(row);
|
|
}
|
|
|
|
if (!player) {
|
|
console.warn(`Maze ${maze.num}: игрок не найден`);
|
|
return;
|
|
}
|
|
|
|
boxes.sort((a, b) => a.y - b.y || a.x - b.x);
|
|
|
|
levelsArr.push({
|
|
name: `Maze ${maze.num}`,
|
|
cols: maze.cols,
|
|
rows: maze.rows,
|
|
map,
|
|
player: [player.x, player.y],
|
|
boxes: boxes.map(b => [b.x, b.y])
|
|
});
|
|
}
|
|
|
|
console.log(`✅ Загружено ${levels.length} уровней`);
|
|
return levels;
|
|
})(); |