1(function () {
2 'use strict';
3
4 var TILE = 36;
5
6
7
8 var LEVELS = [
9 {
10 name: 'Level 1',
11 rows: [
12 '#####',
13 '#@ #',
14 '# $ #',
15 '# .#',
16 '#####'
17 ]
18 },
19 {
20 name: 'Level 2',
21 rows: [
22 '#######',
23 '# #',
24 '# # #',
25 '# # #',
26 '# $# #',
27 '# .@#',
28 '#######'
29 ]
30 },
31 {
32 name: 'Level 3',
33 rows: [
34 '########',
35 '# @ #',
36 '# $ $ #',
37 '# #',
38 '# . . #',
39 '# #',
40 '########'
41 ]
42 },
43 {
44 name: 'Level 4',
45 rows: [
46 '########',
47 '# #',
48 '# ## #',
49 '# $##$ #',
50 '# #',
51 '# . . #',
52 '# @ #',
53 '########'
54 ]
55 },
56 {
57 name: 'Level 5',
58 rows: [
59 '#########',
60 '# #',
61 '# # # #',
62 '# $#$#$ #',
63 '# #',
64 '# . . . #',
65 '# @ #',
66 '#########'
67 ]
68 },
69 {
70 name: 'Level 6',
71 rows: [
72 '#########',
73 '# #',
74 '# $ $ $ #',
75 '# #',
76 '# #',
77 '# . . .#',
78 '# @ #',
79 '# #',
80 '#########'
81 ]
82 },
83 {
84 name: 'Level 7',
85 rows: [
86 '###########',
87 '# #',
88 '# $ $ $ $ #',
89 '# #',
90 '# # # #',
91 '# #',
92 '# . . . . #',
93 '# @ #',
94 '# #',
95 '###########'
96 ]
97 },
98 {
99 name: 'Level 8',
100 rows: [
101 '#############',
102 '# #',
103 '# $ $ $ $ $ #',
104 '# #',
105 '# # # # # #',
106 '# # # # # #',
107 '# . . . . . #',
108 '# @ #',
109 '# #',
110 '#############'
111 ]
112 }
113 ];
114
115 var WALL = '#';
116 var GOAL_CHARS = { '.': 1, '*': 1, '+': 1 };
117 var BOX_CHARS = { '$': 1, '*': 1 };
118 var PLAYER_CHARS = { '@': 1, '+': 1 };
119
120 var DIRS = {
121 up: { dr: -1, dc: 0 },
122 down: { dr: 1, dc: 0 },
123 left: { dr: 0, dc: -1 },
124 right: { dr: 0, dc: 1 }
125 };
126
127 var boardEl = document.getElementById('board');
128 var overlayEl = document.getElementById('overlay');
129 var overlayTitleEl = document.getElementById('overlay-title');
130 var overlayStatsEl = document.getElementById('overlay-stats');
131 var overlayNextBtn = document.getElementById('overlay-next');
132 var levelStripEl = document.getElementById('level-strip');
133 var hudLevelEl = document.getElementById('hud-level');
134 var hudMovesEl = document.getElementById('hud-moves');
135 var hudPushesEl = document.getElementById('hud-pushes');
136 var btnUndo = document.getElementById('btn-undo');
137 var btnRestart = document.getElementById('btn-restart');
138
139 var currentLevelIndex = 0;
140 var width = 0, height = 0;
141 var walls = null;
142 var goals = null;
143 var boxes = [];
144 var player = { r: 0, c: 0 };
145 var facing = 'down';
146 var moves = 0;
147 var pushes = 0;
148 var history = [];
149 var completed = {};
150 var overlayTimer = null;
151 var inputLocked = false;
152
153 var playerEl = null;
154 var boxEls = [];
155
156 function key(r, c) {
157 return r + ',' + c;
158 }
159
160 function parseLevel(idx) {
161 var rows = LEVELS[idx].rows;
162 height = rows.length;
163 width = 0;
164 for (var i = 0; i < rows.length; i++) {
165 width = Math.max(width, rows[i].length);
166 }
167
168 walls = {};
169 goals = {};
170 boxes = [];
171 player = { r: 0, c: 0 };
172
173 for (var r = 0; r < height; r++) {
174 var row = rows[r];
175 for (var c = 0; c < width; c++) {
176 var ch = c < row.length ? row[c] : ' ';
177 if (ch === WALL) walls[key(r, c)] = true;
178 if (GOAL_CHARS[ch]) goals[key(r, c)] = true;
179 if (BOX_CHARS[ch]) boxes.push({ r: r, c: c });
180 if (PLAYER_CHARS[ch]) player = { r: r, c: c };
181 }
182 }
183 }
184
185 function boxIndexAt(r, c) {
186 for (var i = 0; i < boxes.length; i++) {
187 if (boxes[i].r === r && boxes[i].c === c) return i;
188 }
189 return -1;
190 }
191
192 function isWall(r, c) {
193 return !!walls[key(r, c)];
194 }
195
196 function isGoal(r, c) {
197 return !!goals[key(r, c)];
198 }
199
200
201
202 function renderStatic() {
203 boardEl.innerHTML = '';
204 boardEl.style.setProperty('--cols', width);
205 boardEl.style.setProperty('--rows', height);
206 boardEl.style.gridTemplateColumns = 'repeat(' + width + ', ' + TILE + 'px)';
207 boardEl.style.gridTemplateRows = 'repeat(' + height + ', ' + TILE + 'px)';
208 boardEl.style.width = (width * TILE) + 'px';
209 boardEl.style.height = (height * TILE) + 'px';
210
211 for (var r = 0; r < height; r++) {
212 for (var c = 0; c < width; c++) {
213 var tile = document.createElement('div');
214 if (isWall(r, c)) {
215 tile.className = 'tile wall';
216 } else {
217 var parity = (r + c) % 2 === 0 ? 'floor-a' : 'floor-b';
218 tile.className = 'tile ' + parity + (isGoal(r, c) ? ' goal' : '');
219 }
220 boardEl.appendChild(tile);
221 }
222 }
223
224
225 boxEls = boxes.map(function () {
226 var el = document.createElement('div');
227 el.className = 'entity box';
228 var body = document.createElement('div');
229 body.className = 'box-body';
230 el.appendChild(body);
231 boardEl.appendChild(el);
232 return el;
233 });
234
235 playerEl = document.createElement('div');
236 playerEl.className = 'entity player';
237 var pbody = document.createElement('div');
238 pbody.className = 'player-body';
239 var peyes = document.createElement('div');
240 peyes.className = 'player-eyes';
241 pbody.appendChild(peyes);
242 playerEl.appendChild(pbody);
243 boardEl.appendChild(playerEl);
244
245
246 boardEl.appendChild(overlayEl);
247 }
248
249 function renderDynamic() {
250 for (var i = 0; i < boxes.length; i++) {
251 var b = boxes[i];
252 var el = boxEls[i];
253 el.style.left = (b.c * TILE) + 'px';
254 el.style.top = (b.r * TILE) + 'px';
255 el.className = 'entity box' + (isGoal(b.r, b.c) ? ' on-goal' : '');
256 }
257 playerEl.style.left = (player.c * TILE) + 'px';
258 playerEl.style.top = (player.r * TILE) + 'px';
259 playerEl.className = 'entity player facing-' + facing;
260 }
261
262 function updateHud() {
263 hudLevelEl.textContent = (currentLevelIndex + 1);
264 hudMovesEl.textContent = moves;
265 hudPushesEl.textContent = pushes;
266 }
267
268 function buildLevelStrip() {
269 levelStripEl.innerHTML = '';
270 for (var i = 0; i < LEVELS.length; i++) {
271 (function (idx) {
272 var btn = document.createElement('button');
273 btn.className = 'level-btn';
274 btn.textContent = (idx + 1);
275 btn.title = LEVELS[idx].name;
276 btn.addEventListener('click', function () {
277 loadLevel(idx);
278 });
279 levelStripEl.appendChild(btn);
280 })(i);
281 }
282 updateLevelStrip();
283 }
284
285 function updateLevelStrip() {
286 var btns = levelStripEl.querySelectorAll('.level-btn');
287 for (var i = 0; i < btns.length; i++) {
288 var classes = ['level-btn'];
289 if (i === currentLevelIndex) classes.push('current');
290 if (completed[i]) classes.push('completed');
291 btns[i].className = classes.join(' ');
292 }
293 }
294
295
296
297 function hideOverlay() {
298 overlayEl.classList.add('hidden');
299 if (overlayTimer) {
300 clearTimeout(overlayTimer);
301 overlayTimer = null;
302 }
303 }
304
305 function showOverlay() {
306 var isLast = currentLevelIndex === LEVELS.length - 1;
307 overlayTitleEl.textContent = isLast ? 'All Levels Complete!' : 'Level Complete!';
308 overlayStatsEl.textContent = 'Moves: ' + moves + ' • Pushes: ' + pushes;
309 overlayNextBtn.textContent = isLast ? 'Play Again ↻' : 'Next Level ▸';
310 overlayEl.classList.remove('hidden');
311
312 overlayNextBtn.onclick = function () {
313 if (overlayTimer) clearTimeout(overlayTimer);
314 advanceAfterWin(isLast);
315 };
316
317 if (!isLast) {
318 overlayTimer = setTimeout(function () {
319 advanceAfterWin(false);
320 }, 1600);
321 }
322 }
323
324 function advanceAfterWin(isLast) {
325 overlayTimer = null;
326 if (isLast) {
327 loadLevel(0);
328 } else {
329 loadLevel(currentLevelIndex + 1);
330 }
331 }
332
333
334
335 function loadLevel(idx) {
336 hideOverlay();
337 currentLevelIndex = idx;
338 parseLevel(idx);
339 moves = 0;
340 pushes = 0;
341 history = [];
342 facing = 'down';
343 renderStatic();
344 renderDynamic();
345 updateHud();
346 updateLevelStrip();
347 }
348
349 function restart() {
350 hideOverlay();
351 parseLevel(currentLevelIndex);
352 moves = 0;
353 pushes = 0;
354 history = [];
355 facing = 'down';
356 renderDynamic();
357 updateHud();
358 }
359
360 function snapshot() {
361 return {
362 player: { r: player.r, c: player.c },
363 boxes: boxes.map(function (b) { return { r: b.r, c: b.c }; }),
364 moves: moves,
365 pushes: pushes,
366 facing: facing
367 };
368 }
369
370 function undo() {
371 if (overlayVisible()) return;
372 if (history.length === 0) return;
373 var snap = history.pop();
374 player = snap.player;
375 boxes = snap.boxes;
376 moves = snap.moves;
377 pushes = snap.pushes;
378 facing = snap.facing;
379 renderDynamic();
380 updateHud();
381 }
382
383 function overlayVisible() {
384 return !overlayEl.classList.contains('hidden');
385 }
386
387 function checkWin() {
388 if (boxes.length === 0) return false;
389 for (var i = 0; i < boxes.length; i++) {
390 if (!isGoal(boxes[i].r, boxes[i].c)) return false;
391 }
392 return true;
393 }
394
395 function attemptMove(dir) {
396 if (overlayVisible()) return;
397 var d = DIRS[dir];
398 if (!d) return;
399 facing = dir;
400
401 var nr = player.r + d.dr;
402 var nc = player.c + d.dc;
403
404 if (isWall(nr, nc)) {
405 renderDynamic();
406 return;
407 }
408
409 var bIdx = boxIndexAt(nr, nc);
410 var willPush = false;
411
412 if (bIdx !== -1) {
413 var br = nr + d.dr;
414 var bc = nc + d.dc;
415 if (isWall(br, bc) || boxIndexAt(br, bc) !== -1) {
416 renderDynamic();
417 return;
418 }
419 willPush = true;
420 }
421
422 history.push(snapshot());
423
424 if (willPush) {
425 boxes[bIdx] = { r: nr + d.dr, c: nc + d.dc };
426 pushes++;
427 }
428 player = { r: nr, c: nc };
429 moves++;
430
431 renderDynamic();
432 updateHud();
433
434 if (checkWin()) {
435 completed[currentLevelIndex] = true;
436 updateLevelStrip();
437 showOverlay();
438 }
439 }
440
441
442
443 var KEY_TO_DIR = {
444 ArrowUp: 'up',
445 ArrowDown: 'down',
446 ArrowLeft: 'left',
447 ArrowRight: 'right'
448 };
449
450 document.addEventListener('keydown', function (e) {
451 if (KEY_TO_DIR[e.key]) {
452 e.preventDefault();
453 attemptMove(KEY_TO_DIR[e.key]);
454 return;
455 }
456 if (e.key === 'u' || e.key === 'U') {
457 undo();
458 return;
459 }
460 if (e.key === 'r' || e.key === 'R') {
461 restart();
462 return;
463 }
464 });
465
466 btnUndo.addEventListener('click', undo);
467 btnRestart.addEventListener('click', restart);
468
469 var dpadBtns = document.querySelectorAll('.dpad-btn');
470 for (var i = 0; i < dpadBtns.length; i++) {
471 dpadBtns[i].addEventListener('click', function () {
472 attemptMove(this.getAttribute('data-dir'));
473 });
474 }
475
476
477
478 buildLevelStrip();
479 loadLevel(0);
480})();
481
Discussion
No comments yet. Start the discussion. Recorded by @patrick-toulme.