JSFiddle - React, Tailwind, and code Playground

by Jiaming0324

HTML

<div id="spookitonMaze">
  <section id="gamePanel" class="game-panel">
    <header class="game-header">
      <div>
        <span class="small-label">SPOOKITON VILLAGE</span>
        <h1>Candy Street Maze</h1>
      </div>
      <button id="newGameBtn" class="new-game-btn" type="button">↻ New Game</button>
    </header>

    <div class="status-bar">
      <div><small>STAGE</small><strong id="stageText">1 / 3</strong></div>
      <div><small>SCORE</small><strong id="scoreText">0</strong></div>
      <div><small>COLLECTED</small><strong id="collectedText">0</strong></div>
    </div>

    <p id="stageMessage" class="stage-message">Find the glowing village gate.</p>

    <div class="maze-frame">
      <div id="mazeBoard" class="maze-board" aria-label="Spookiton Village maze"></div>
    </div>

    <div class="controls" aria-label="Direction controls">
      <button type="button" class="move-btn up" data-direction="up" aria-label="Move up">▲</button>
      <div class="control-row">
        <button type="button" class="move-btn" data-direction="left" aria-label="Move left">◀</button>
        <button type="button" class="move-btn" data-direction="down" aria-label="Move down">▼</button>
        <button type="button" class="move-btn" data-direction="right" aria-label="Move right">▶</button>
      </div>
    </div>

    <p class="control-help"><b>PHONE:</b> Tap the buttons · <b>COMPUTER:</b> Arrow Keys or W A S D</p>
    <p class="score-help">🍬 +3 🎃 +1 👻 −2</p>
  </section>

  <section id="finishPanel" class="finish-panel" aria-live="polite">
    <div class="finish-stars">✦ 🎃 ✦</div>
    <span class="small-label">ALL THREE STAGES COMPLETE</span>
    <h2>You Found Your Way Through Spookiton!</h2>
    <div class="result-box">
      <small>FINAL SCORE</small>
      <strong id="finalScore">0</strong>
      <code>VILLAGE-FOUND · 3/3 MAZES</code>
    </div>
    <button id="playAgainBtn" class="play-again-btn" type="button">Play...

CSS

* {
  box-sizing: border-box;
}

html,
body {
  margin: 0;
  min-height: 100%;
  background: #f7ead7;
  overscroll-behavior: contain;
}

body {
  font-family: Arial, Helvetica, sans-serif;
  color: #39243f;
}

button {
  font: inherit;
  touch-action: manipulation;
  -webkit-tap-highlight-color: transparent;
}

#spookitonMaze {
  width: 100%;
  min-height: 100vh;
  padding: 12px;
  background:
    radial-gradient(circle at 8% 8%, rgba(255, 188, 88, .28), transparent 22%),
    radial-gradient(circle at 92% 18%, rgba(108, 62, 128, .15), transparent 25%),
    #f7ead7;
}

.game-panel {
  width: min(100%, 650px);
  margin: 0 auto;
}

.game-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  margin-bottom: 8px;
}

.small-label {
  color: #8d4a69;
  font-size: 10px;
  font-weight: 800;
  letter-spacing: .2em;
}

h1 {
  margin: 2px 0 0;
  color: #5d2671;
  font-family: Georgia, serif;
  font-size: clamp(25px, 6vw, 38px);
  line-height: 1;
}

.new-game-btn,
.play-again-btn {
  border: 0;
  border-radius: 13px;
  padding: 10px 15px;
  background: #e46d2f;
  box-shadow: 0 4px 0 #a94320;
  color: #fff;
  font-weight: 800;
  cursor: pointer;
}

.new-game-btn:active,
.play-again-btn:active,
.move-btn:active {
  transform: translateY(3px);
  box-shadow: 0 1px 0 #4a205a;
}

.status-bar {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  overflow: hidden;
  border: 3px solid #5d2671;
  border-radius: 15px;
  background: #fff9ef;
}

.status-bar div {
  padding: 7px 5px;
  text-align: center;
}

.status-bar div + div {
  border-left: 2px solid #d7bfd8;
}

.status-bar small {
  display: block;
  color: #936779;
  font-size: 9px;
  font-weight: 800;
  letter-spacing: .12em;
}

.status-bar strong {
  display: block;
  margin-top: 1px;
  color: #5d2671;
  font-size: 20px;
}

.stage-message {
  min-height: 20px;
 ...

JavaScript

(function () {
  "use strict";

  function startMazeGame() {
    const SIZE = 21;
    const TOTAL_STAGES = 3;
    const DIRECTIONS = {
      up: [0, -1],
      down: [0, 1],
      left: [-1, 0],
      right: [1, 0]
    };
    const KEY_MAP = {
      ArrowUp: "up",
      ArrowDown: "down",
      ArrowLeft: "left",
      ArrowRight: "right",
      w: "up",
      a: "left",
      s: "down",
      d: "right"
    };

    const app = document.getElementById("spookitonMaze");
    const board = document.getElementById("mazeBoard");
    const stageText = document.getElementById("stageText");
    const scoreText = document.getElementById("scoreText");
    const collectedText = document.getElementById("collectedText");
    const stageMessage = document.getElementById("stageMessage");
    const finalScore = document.getElementById("finalScore");

    if (!app || !board) return;

    let grid = [];
    let player = { x: 1, y: 1 };
    let goal = { x: SIZE - 2, y: SIZE - 2 };
    let items = new Map();
    let stage = 1;
    let score = 0;
    let collected = 0;
    let changingStage = false;
    let finished = false;

    function shuffledDirections() {
      return [[2, 0], [-2, 0], [0, 2], [0, -2]]
        .sort(() => Math.random() - 0.5);
    }

    function generateMaze() {
      grid = Array.from({ length: SIZE }, () => Array(SIZE).fill(1));
      const stack = [{ x: 1, y: 1 }];
      grid[1][1] = 0;

      while (stack.length) {
        const current = stack[stack.length - 1];
        const choices = shuffledDirections()
          .map(([dx, dy]) => ({
            x: current.x + dx,
            y: current.y + dy,
            wallX: current.x + dx / 2,
            wallY: current.y + dy / 2
          }))
          .filter((next) =>
            next.x > 0 && next.y > 0 &&
            next.x < SIZE - 1 && next.y < SIZE - 1 &&
            grid[next.y][next.x] === 1
          );

        if (!choices.length)...