index.html 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223
  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="UTF-8">
  5. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  6. <title>Labyrinth</title>
  7. <style>
  8. * { margin: 0; padding: 0; box-sizing: border-box; }
  9. html, body { overflow: hidden; }
  10. body { background: #000; color: #eee; font-family: monospace; display: flex; flex-direction: column; align-items: center; height: 100vh; }
  11. #topbar { width: 100%; padding: 8px 16px; display: flex; align-items: center; gap: 16px; background: #111; border-bottom: 1px solid #333; }
  12. #topbar a { color: #FFA500; text-decoration: none; font-size: 14px; }
  13. #ui { display: flex; gap: 24px; padding: 8px; font-size: 15px; color: #FFA500; }
  14. canvas { display: block; flex: 1; align-self: stretch; min-height: 0; width: 100%; border: 1px solid #333; }
  15. #msg { font-size: 16px; color: #FFA500; margin: 4px; text-align: center; min-height: 22px; }
  16. #dpad { display: flex; flex-direction: column; align-items: center; gap: 4px; margin: 6px; }
  17. #dpad-row { display: flex; gap: 4px; }
  18. #dpad button { width: 44px; height: 44px; background: #222; border: 1px solid #555; color: #FFA500; font-size: 18px; cursor: pointer; }
  19. #dpad button:active { background: #444; }
  20. #controls { color: #888; font-size: 13px; text-align: center; margin-bottom: 4px; }
  21. #scoreSubmit { text-align: center; margin: 6px; }
  22. #scoreSubmit button { background: #1a3a1a; border: 1px solid #FFA500; color: #FFA500; padding: 6px 16px; cursor: pointer; font-family: monospace; font-size: 14px; }
  23. </style>
  24. </head>
  25. <body>
  26. <div id="topbar">
  27. <a href="/games" target="_top">&#8592; Back to Games</a>
  28. <span style="color:#FFA500;font-weight:bold">LABYRINTH</span>
  29. </div>
  30. <div id="ui">
  31. <span>LEVEL: <b id="levelEl">1</b></span>
  32. <span>MOVES: <b id="movesEl">150</b></span>
  33. <span>SCORE: <b id="scoreEl">0</b></span>
  34. <span>BEST: <b id="bestEl">-</b></span>
  35. </div>
  36. <canvas id="c"></canvas>
  37. <div id="msg">Press SPACE or tap to start</div>
  38. <div id="dpad">
  39. <div id="dpad-row"><button id="btn-up">&#8593;</button></div>
  40. <div id="dpad-row"><button id="btn-left">&#8592;</button><button id="btn-down">&#8595;</button><button id="btn-right">&#8594;</button></div>
  41. </div>
  42. <div id="controls">Arrow keys / WASD &nbsp;|&nbsp; SPACE — new game</div>
  43. <div id="scoreSubmit" style="display:none">
  44. <form method="POST" action="/games/submit-score" target="_top">
  45. <input type="hidden" name="game" value="labyrinth">
  46. <input type="hidden" id="scoreInput" name="score" value="0">
  47. <button type="submit">Submit Score to Hall of Fame</button>
  48. </form>
  49. </div>
  50. <script>
  51. const canvas = document.getElementById('c');
  52. const ctx = canvas.getContext('2d');
  53. const BASE_SIZE = 10;
  54. let CELL = 36;
  55. let level = 1, moves = 0, maxMoves = 150, score = 0, total = 0, gameState = 'idle';
  56. let cols, rows, grid, px, py;
  57. let best = parseInt(localStorage.getItem('labyrinth_best') || '-1');
  58. function seededRand(seed) {
  59. let s = seed;
  60. return () => { s = (s * 1103515245 + 12345) & 0x7fffffff; return s / 0x7fffffff; };
  61. }
  62. function generateMaze(c, r, seed) {
  63. const rng = seededRand(seed);
  64. const cells = Array.from({length: r}, () => Array.from({length: c}, () => ({ n: true, s: true, e: true, w: true, visited: false })));
  65. function shuffle(arr) {
  66. for (let i = arr.length - 1; i > 0; i--) {
  67. const j = Math.floor(rng() * (i + 1));
  68. [arr[i], arr[j]] = [arr[j], arr[i]];
  69. }
  70. return arr;
  71. }
  72. function carve(x, y) {
  73. cells[y][x].visited = true;
  74. const dirs = shuffle([['n', 0, -1], ['s', 0, 1], ['e', 1, 0], ['w', -1, 0]]);
  75. for (const [d, dx, dy] of dirs) {
  76. const nx = x + dx, ny = y + dy;
  77. if (nx >= 0 && nx < c && ny >= 0 && ny < r && !cells[ny][nx].visited) {
  78. const opp = { n: 's', s: 'n', e: 'w', w: 'e' };
  79. cells[y][x][d] = false;
  80. cells[ny][nx][opp[d]] = false;
  81. carve(nx, ny);
  82. }
  83. }
  84. }
  85. carve(0, 0);
  86. return cells;
  87. }
  88. function getAvailableSize() {
  89. const usedH = (document.getElementById('topbar').offsetHeight || 40)
  90. + (document.getElementById('ui').offsetHeight || 30)
  91. + (document.getElementById('msg').offsetHeight || 22)
  92. + (document.getElementById('dpad').offsetHeight || 100)
  93. + (document.getElementById('controls').offsetHeight || 16)
  94. + 24;
  95. return { w: window.innerWidth - 8, h: Math.max(200, window.innerHeight - usedH) };
  96. }
  97. function startLevel() {
  98. cols = BASE_SIZE + (level - 1) * 2;
  99. rows = Math.floor(cols * 0.7);
  100. maxMoves = cols * rows * 2;
  101. moves = 0;
  102. grid = generateMaze(cols, rows, level * 7919 + 42);
  103. px = Math.floor(cols / 2); py = Math.floor(rows / 2);
  104. const avail = getAvailableSize();
  105. CELL = Math.max(16, Math.floor(Math.min(avail.w / cols, avail.h / rows)));
  106. canvas.width = cols * CELL;
  107. canvas.height = rows * CELL;
  108. document.getElementById('levelEl').textContent = level;
  109. document.getElementById('movesEl').textContent = maxMoves;
  110. document.getElementById('msg').textContent = `Level ${level} — Reach the exit!`;
  111. gameState = 'play';
  112. }
  113. function tryMove(dx, dy) {
  114. if (gameState !== 'play') return;
  115. const cell = grid[py][px];
  116. const dirs = { '0,-1': 'n', '0,1': 's', '1,0': 'e', '-1,0': 'w' };
  117. const key = `${dx},${dy}`;
  118. const wall = dirs[key];
  119. if (!wall || cell[wall]) return;
  120. px += dx; py += dy;
  121. moves++;
  122. document.getElementById('movesEl').textContent = maxMoves - moves;
  123. if (px === cols - 1 && py === rows - 1) {
  124. const remaining = maxMoves - moves;
  125. const levelScore = remaining + level * 50;
  126. total += levelScore;
  127. document.getElementById('scoreEl').textContent = total;
  128. document.getElementById('msg').textContent = `EXIT! +${levelScore} pts. Next level...`;
  129. gameState = 'transit';
  130. setTimeout(() => { level++; startLevel(); }, 1000);
  131. } else if (moves >= maxMoves) {
  132. endGame();
  133. }
  134. }
  135. function endGame() {
  136. gameState = 'over';
  137. if (best < 0 || total > best) {
  138. best = total;
  139. localStorage.setItem('labyrinth_best', best);
  140. document.getElementById('bestEl').textContent = best;
  141. }
  142. document.getElementById('msg').textContent = `Out of moves! Score: ${total}. SPACE = new game`;
  143. document.getElementById('scoreInput').value = total;
  144. document.getElementById('scoreSubmit').style.display = 'block';
  145. }
  146. function newGame() {
  147. level = 1; total = 0; score = 0;
  148. document.getElementById('scoreEl').textContent = '0';
  149. document.getElementById('scoreSubmit').style.display = 'none';
  150. startLevel();
  151. }
  152. document.addEventListener('keydown', e => {
  153. if (e.code === 'Space') { e.preventDefault(); newGame(); return; }
  154. const map = { ArrowUp: [0,-1], ArrowDown: [0,1], ArrowLeft: [-1,0], ArrowRight: [1,0], KeyW: [0,-1], KeyS: [0,1], KeyA: [-1,0], KeyD: [1,0] };
  155. if (map[e.code]) { e.preventDefault(); if (gameState === 'idle') newGame(); else tryMove(...map[e.code]); }
  156. });
  157. canvas.addEventListener('click', () => { if (gameState === 'idle') newGame(); });
  158. window.addEventListener('resize', () => { if (gameState !== 'idle') startLevel(); });
  159. const dpadStart = () => { if (gameState === 'idle') newGame(); };
  160. document.getElementById('btn-up').addEventListener('click', () => { dpadStart(); tryMove(0, -1); });
  161. document.getElementById('btn-down').addEventListener('click', () => { dpadStart(); tryMove(0, 1); });
  162. document.getElementById('btn-left').addEventListener('click', () => { dpadStart(); tryMove(-1, 0); });
  163. document.getElementById('btn-right').addEventListener('click', () => { dpadStart(); tryMove(1, 0); });
  164. if (best >= 0) document.getElementById('bestEl').textContent = best;
  165. const VIEW = 6;
  166. function draw() {
  167. const W = canvas.width, H = canvas.height;
  168. ctx.fillStyle = '#0a0a0a';
  169. ctx.fillRect(0, 0, W, H);
  170. if (gameState === 'idle') { requestAnimationFrame(draw); return; }
  171. const viewCols = Math.min(cols, Math.floor(W / CELL));
  172. const viewRows = Math.min(rows, Math.floor(H / CELL));
  173. const offX = Math.max(0, Math.min(px - Math.floor(viewCols / 2), cols - viewCols));
  174. const offY = Math.max(0, Math.min(py - Math.floor(viewRows / 2), rows - viewRows));
  175. for (let r = offY; r < Math.min(offY + viewRows + 1, rows); r++) {
  176. for (let c = offX; c < Math.min(offX + viewCols + 1, cols); c++) {
  177. const x = (c - offX) * CELL, y = (r - offY) * CELL;
  178. const cell = grid[r][c];
  179. ctx.strokeStyle = '#FFA500'; ctx.lineWidth = 2;
  180. if (cell.n) { ctx.beginPath(); ctx.moveTo(x, y); ctx.lineTo(x + CELL, y); ctx.stroke(); }
  181. if (cell.s) { ctx.beginPath(); ctx.moveTo(x, y + CELL); ctx.lineTo(x + CELL, y + CELL); ctx.stroke(); }
  182. if (cell.w) { ctx.beginPath(); ctx.moveTo(x, y); ctx.lineTo(x, y + CELL); ctx.stroke(); }
  183. if (cell.e) { ctx.beginPath(); ctx.moveTo(x + CELL, y); ctx.lineTo(x + CELL, y + CELL); ctx.stroke(); }
  184. }
  185. }
  186. const ex = (cols - 1 - offX) * CELL + CELL / 2, ey = (rows - 1 - offY) * CELL + CELL / 2;
  187. if (cols - 1 >= offX && cols - 1 < offX + viewCols && rows - 1 >= offY && rows - 1 < offY + viewRows) {
  188. ctx.fillStyle = '#f44';
  189. ctx.beginPath(); ctx.arc(ex, ey, 7, 0, Math.PI * 2); ctx.fill();
  190. ctx.fillStyle = '#fff'; ctx.font = 'bold 10px monospace'; ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
  191. ctx.fillText('EXIT', ex, ey);
  192. }
  193. const ppx = (px - offX) * CELL + CELL / 2, ppy = (py - offY) * CELL + CELL / 2;
  194. ctx.fillStyle = '#00ff88';
  195. ctx.beginPath(); ctx.arc(ppx, ppy, 7, 0, Math.PI * 2); ctx.fill();
  196. requestAnimationFrame(draw);
  197. }
  198. draw();
  199. </script>
  200. </body>
  201. </html>