index.html 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324
  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>Cocoman</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%; background: #000; 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">COCOMAN</span>
  29. </div>
  30. <div id="ui">
  31. <span>SCORE: <b id="scoreEl">0</b></span>
  32. <span>LIVES: <b id="livesEl">3</b></span>
  33. <span>BEST: <b id="bestEl">-</b></span>
  34. </div>
  35. <canvas id="c"></canvas>
  36. <div id="msg">Press SPACE or tap arrow to start</div>
  37. <div id="dpad">
  38. <div id="dpad-row"><button id="btn-up">&#8593;</button></div>
  39. <div id="dpad-row"><button id="btn-left">&#8592;</button><button id="btn-down">&#8595;</button><button id="btn-right">&#8594;</button></div>
  40. </div>
  41. <div id="controls">Arrow keys / WASD &nbsp;|&nbsp; SPACE — new game</div>
  42. <div id="scoreSubmit" style="display:none">
  43. <form method="POST" action="/games/submit-score" target="_top">
  44. <input type="hidden" name="game" value="cocoman">
  45. <input type="hidden" id="scoreInput" name="score" value="0">
  46. <button type="submit">Submit Score to Hall of Fame</button>
  47. </form>
  48. </div>
  49. <script>
  50. const canvas = document.getElementById('c');
  51. const ctx = canvas.getContext('2d');
  52. const CELL = 20;
  53. const MAP = [
  54. '############################',
  55. '#............##............#',
  56. '#.####.#####.##.#####.####.#',
  57. '#o# #.# #.##.# #.# #o#',
  58. '#.####.#####.##.#####.####.#',
  59. '#..........................#',
  60. '#.####.##.########.##.####.#',
  61. '#.####.##.########.##.####.#',
  62. '#......##....##....##......#',
  63. '######.##### ## #####.######',
  64. ' #.##### ## #####.# ',
  65. ' #.## G ##.# ',
  66. ' #.## ###=### ##.# ',
  67. '######.## # # ##.######',
  68. ' . # GGG # . ',
  69. '######.## # # ##.######',
  70. ' #.## ####### ##.# ',
  71. ' #.## ##.# ',
  72. ' #.## ######## ##.# ',
  73. '######.## ######## ##.######',
  74. '#............##............#',
  75. '#.####.#####.##.#####.####.#',
  76. '#.####.#####.##.#####.####.#',
  77. '#o..##....... .......##..o#',
  78. '###.##.##.########.##.##.###',
  79. '###.##.##.########.##.##.###',
  80. '#......##....##....##......#',
  81. '#.##########.##.##########.#',
  82. '#.##########.##.##########.#',
  83. '#..........................#',
  84. '############################'
  85. ];
  86. const COLS = MAP[0].length, ROWS = MAP.length;
  87. canvas.width = COLS * CELL;
  88. canvas.height = ROWS * CELL;
  89. const GHOST_COLORS = ['#f00', '#f9a', '#0ff', '#fa0'];
  90. let score = 0, lives = 3, gameState = 'idle', comboGhost = 0;
  91. let best = parseInt(localStorage.getItem('cocoman_best') || '-1');
  92. let dots = [], powers = [], player, ghosts, nextDir = [1, 0], frightTimer = 0;
  93. let tickInterval = null;
  94. function initLevel() {
  95. dots = []; powers = [];
  96. for (let r = 0; r < ROWS; r++) {
  97. for (let c = 0; c < COLS; c++) {
  98. if (MAP[r][c] === '.') dots.push([c, r]);
  99. if (MAP[r][c] === 'o') powers.push([c, r]);
  100. }
  101. }
  102. player = { x: 14, y: 23, dir: [1, 0] };
  103. nextDir = [1, 0];
  104. ghosts = [
  105. { x: 13, y: 14, dir: [0, -1], color: GHOST_COLORS[0] },
  106. { x: 14, y: 14, dir: [0, 1], color: GHOST_COLORS[1] },
  107. { x: 13, y: 15, dir: [1, 0], color: GHOST_COLORS[2] },
  108. { x: 14, y: 15, dir: [-1, 0], color: GHOST_COLORS[3] }
  109. ];
  110. frightTimer = 0; comboGhost = 0;
  111. }
  112. function wrapX(x) {
  113. if (x < 0) return COLS - 1;
  114. if (x >= COLS) return 0;
  115. return x;
  116. }
  117. function cellAt(x, y) {
  118. if (y < 0 || y >= ROWS) return '#';
  119. const row = MAP[Math.floor(y)];
  120. const ch = row[Math.floor(x)];
  121. return ch === undefined ? ' ' : ch;
  122. }
  123. function canMovePlayer(x, y, dx, dy) {
  124. const nx = wrapX(x + dx), ny = y + dy;
  125. const ch = cellAt(nx, ny);
  126. return ch !== '#' && ch !== '=';
  127. }
  128. function canGhostMove(x, y, dx, dy) {
  129. const nx = wrapX(x + dx), ny = y + dy;
  130. return cellAt(nx, ny) !== '#';
  131. }
  132. function moveGhost(g) {
  133. const dirs = [[0,-1],[0,1],[-1,0],[1,0]].filter(([dx,dy]) => {
  134. if (!canGhostMove(g.x, g.y, dx, dy)) return false;
  135. if (dx === -g.dir[0] && dy === -g.dir[1]) return false;
  136. return true;
  137. });
  138. if (!dirs.length) return;
  139. if (frightTimer > 0) {
  140. g.dir = dirs[Math.floor(Math.random() * dirs.length)];
  141. } else {
  142. dirs.sort((a, b) =>
  143. Math.hypot(g.x+a[0]-player.x, g.y+a[1]-player.y) -
  144. Math.hypot(g.x+b[0]-player.x, g.y+b[1]-player.y)
  145. );
  146. g.dir = dirs[0];
  147. }
  148. g.x = wrapX(g.x + g.dir[0]);
  149. g.y += g.dir[1];
  150. }
  151. function tick() {
  152. if (gameState !== 'play') return;
  153. if (canMovePlayer(player.x, player.y, ...nextDir)) player.dir = [...nextDir];
  154. if (canMovePlayer(player.x, player.y, ...player.dir)) {
  155. player.x = wrapX(player.x + player.dir[0]);
  156. player.y += player.dir[1];
  157. }
  158. const di = dots.findIndex(d => d[0] === player.x && d[1] === player.y);
  159. if (di >= 0) { dots.splice(di, 1); score += 10; document.getElementById('scoreEl').textContent = score; }
  160. const pi = powers.findIndex(p => p[0] === player.x && p[1] === player.y);
  161. if (pi >= 0) { powers.splice(pi, 1); score += 50; frightTimer = 22; comboGhost = 0; document.getElementById('scoreEl').textContent = score; }
  162. if (frightTimer > 0) frightTimer--;
  163. ghosts.forEach(moveGhost);
  164. for (const g of ghosts) {
  165. if (g.x === player.x && g.y === player.y) {
  166. if (frightTimer > 0) {
  167. comboGhost++;
  168. score += 200 * Math.pow(2, comboGhost - 1);
  169. document.getElementById('scoreEl').textContent = score;
  170. g.x = 13; g.y = 14; g.dir = [0, -1];
  171. } else {
  172. loseLife();
  173. return;
  174. }
  175. }
  176. }
  177. if (!dots.length && !powers.length) winGame();
  178. }
  179. function loseLife() {
  180. lives--;
  181. document.getElementById('livesEl').textContent = lives;
  182. if (lives <= 0) {
  183. endGame();
  184. } else {
  185. player = { x: 14, y: 23, dir: [1, 0] };
  186. nextDir = [1, 0]; frightTimer = 0;
  187. document.getElementById('msg').textContent = 'Caught! Keep going...';
  188. }
  189. }
  190. function winGame() {
  191. clearInterval(tickInterval); tickInterval = null;
  192. gameState = 'over';
  193. score += lives * 100;
  194. document.getElementById('scoreEl').textContent = score;
  195. document.getElementById('msg').textContent = `You won! Score: ${score}. SPACE = new game`;
  196. saveAndShow();
  197. }
  198. function endGame() {
  199. clearInterval(tickInterval); tickInterval = null;
  200. gameState = 'over';
  201. document.getElementById('msg').textContent = `GAME OVER! Score: ${score}. SPACE = new game`;
  202. saveAndShow();
  203. }
  204. function saveAndShow() {
  205. if (best < 0 || score > best) {
  206. best = score;
  207. localStorage.setItem('cocoman_best', best);
  208. document.getElementById('bestEl').textContent = best;
  209. }
  210. document.getElementById('scoreInput').value = score;
  211. document.getElementById('scoreSubmit').style.display = 'block';
  212. }
  213. function startGame() {
  214. score = 0; lives = 3; gameState = 'play';
  215. document.getElementById('scoreEl').textContent = '0';
  216. document.getElementById('livesEl').textContent = '3';
  217. document.getElementById('msg').textContent = '';
  218. document.getElementById('scoreSubmit').style.display = 'none';
  219. initLevel();
  220. if (tickInterval) clearInterval(tickInterval);
  221. tickInterval = setInterval(tick, 155);
  222. }
  223. document.addEventListener('keydown', e => {
  224. 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] };
  225. if (e.code === 'Space') { e.preventDefault(); startGame(); return; }
  226. if (map[e.code]) { e.preventDefault(); if (gameState === 'idle') startGame(); nextDir = map[e.code]; }
  227. });
  228. function dpadInput(dx, dy) { if (gameState === 'idle') startGame(); nextDir = [dx, dy]; }
  229. document.getElementById('btn-up').addEventListener('click', () => dpadInput(0, -1));
  230. document.getElementById('btn-down').addEventListener('click', () => dpadInput(0, 1));
  231. document.getElementById('btn-left').addEventListener('click', () => dpadInput(-1, 0));
  232. document.getElementById('btn-right').addEventListener('click', () => dpadInput(1, 0));
  233. if (best >= 0) document.getElementById('bestEl').textContent = best;
  234. initLevel();
  235. let animFrame = 0;
  236. function draw() {
  237. ctx.fillStyle = '#000';
  238. ctx.fillRect(0, 0, canvas.width, canvas.height);
  239. for (let r = 0; r < ROWS; r++) {
  240. for (let c = 0; c < COLS; c++) {
  241. if (MAP[r][c] === '#') {
  242. ctx.fillStyle = '#00c';
  243. ctx.fillRect(c * CELL, r * CELL, CELL, CELL);
  244. ctx.strokeStyle = '#44f';
  245. ctx.strokeRect(c * CELL + 1, r * CELL + 1, CELL - 2, CELL - 2);
  246. }
  247. }
  248. }
  249. ctx.fillStyle = '#fff';
  250. for (const [dc, dr] of dots) {
  251. ctx.beginPath(); ctx.arc(dc * CELL + CELL/2, dr * CELL + CELL/2, 2.5, 0, Math.PI * 2); ctx.fill();
  252. }
  253. if (Math.floor(animFrame / 15) % 2) {
  254. ctx.fillStyle = '#FFA500';
  255. for (const [dc, dr] of powers) {
  256. ctx.beginPath(); ctx.arc(dc * CELL + CELL/2, dr * CELL + CELL/2, 5, 0, Math.PI * 2); ctx.fill();
  257. }
  258. }
  259. for (const g of ghosts) {
  260. ctx.fillStyle = frightTimer > 0 ? '#006' : g.color;
  261. const gx = g.x * CELL + CELL/2, gy = g.y * CELL + CELL/2;
  262. ctx.beginPath();
  263. ctx.arc(gx, gy - 2, CELL/2 - 2, Math.PI, 0);
  264. ctx.lineTo(gx + CELL/2 - 2, gy + CELL/2 - 2);
  265. for (let w = 0; w < 4; w++) {
  266. ctx.lineTo(gx + CELL/2 - 2 - w * (CELL - 4) / 4, gy + CELL/2 - 2 - (w % 2 ? 3 : 0));
  267. }
  268. ctx.lineTo(gx - CELL/2 + 2, gy + CELL/2 - 2); ctx.closePath(); ctx.fill();
  269. ctx.fillStyle = '#fff';
  270. ctx.beginPath(); ctx.arc(gx - 3, gy - 3, 3, 0, Math.PI * 2); ctx.fill();
  271. ctx.beginPath(); ctx.arc(gx + 3, gy - 3, 3, 0, Math.PI * 2); ctx.fill();
  272. if (frightTimer <= 0) {
  273. ctx.fillStyle = '#00c';
  274. ctx.beginPath(); ctx.arc(gx - 3, gy - 3, 1.5, 0, Math.PI * 2); ctx.fill();
  275. ctx.beginPath(); ctx.arc(gx + 3, gy - 3, 1.5, 0, Math.PI * 2); ctx.fill();
  276. }
  277. }
  278. const mouth = Math.abs(Math.sin(animFrame * 0.15)) * 0.4;
  279. const pa = player.dir[0] !== 0 ? Math.atan2(player.dir[1], player.dir[0]) : -Math.PI / 2 * Math.sign(player.dir[1] || 1);
  280. ctx.fillStyle = '#ff0';
  281. ctx.beginPath();
  282. ctx.moveTo(player.x * CELL + CELL/2, player.y * CELL + CELL/2);
  283. ctx.arc(player.x * CELL + CELL/2, player.y * CELL + CELL/2, CELL/2 - 1, pa + mouth, pa + Math.PI * 2 - mouth);
  284. ctx.closePath(); ctx.fill();
  285. animFrame++;
  286. requestAnimationFrame(draw);
  287. }
  288. draw();
  289. </script>
  290. </body>
  291. </html>