Maze Run — Full Source Code in JavaScript, HTML, and CSS

▶ Play Maze Run 📖 Learn how to build it

This page publishes the complete source code of "Maze Run", the game running on HinaTech Games. This is not a rewritten teaching version — these are the real files that power the very game you can play right now.

The coolest part of this game is that a different maze is built automatically by the computer every time you play. The mazes are not prepared ahead of time — the program assembles one on the spot each time you start a game. We'll dig into exactly how that works in game.js.

Feel free to copy the code and run it on your own computer. If you've ever wondered "how does a maze get made?", read the explanations and follow along in the code.

Maze Run is made of 3 files

Maze Run runs on the files below, each with its own job. We will walk through every one of them.

FileRole
index.htmlThe skeleton of the game screen. Lays out the board, score display and buttons in HTML
style.cssThe look and feel: colors, sizes, layout and animations
game.jsThe brain that runs the game rules: input handling, logic and scoring

This split — HTML for structure, CSS for looks, JavaScript for behavior — is how almost every web page and browser game is built.

index.html — The skeleton of the game screen

index.html is the file the browser loads first. It only lays out the "parts" of the game in HTML — the code that actually builds the maze is not here.

It breaks down into four pieces: (1) the score-area that holds your time and best time, (2) the maze-board where the maze appears, (3) two overlays shown at the start and when you clear, and (4) the dpad-container with directional buttons for phones. The maze-board starts out empty — JavaScript creates each maze cell later and drops it in.

The game itself begins inside <body>.

* The code below omits the ad and analytics tags (the first few lines of the page), which have nothing to do with how the game works.

index.html
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
  <title>迷路ゲーム|無料ブラウザゲーム|ひなテックGames</title>
  <meta name="description" content="迷路ゲームで遊ぼう!ランダム生成される迷路をゴールまで最速で駆け抜けよう。PC・スマホ対応の無料ブラウザゲーム。">
  <link rel="stylesheet" href="../../css/style.css">
  <link rel="stylesheet" href="style.css">
  <link rel="stylesheet" href="../../css/leaderboard.css">
</head>
<body>
  <div id="header"></div>

  <div class="game-page">
    <h1 class="game-page-title">迷路ゲーム</h1>
    <div class="game-page-tags">
      <a href="../../tags/puzzle/" class="tag">パズル</a>
      <a href="../../tags/adventure/" class="tag">冒険</a>
    </div>

    <div class="game-container">
      <!-- スコアエリア -->
      <div class="score-area">
        <div class="score-box">
          <span class="score-label">タイム</span>
          <span class="score-value" id="time">0.0</span>
        </div>
        <div class="score-box">
          <span class="score-label">ベスト</span>
          <span class="score-value" id="best-time">--</span>
        </div>
        <button class="btn-new-game" id="btn-new-game">New Game</button>
      </div>

      <!-- ゲームボード -->
      <div class="board-wrapper">
        <div class="maze-board" id="maze-board"></div>

        <!-- クリアオーバーレイ -->
        <div class="game-overlay" id="game-clear-overlay">
          <div class="overlay-content">
            <h2>クリア!</h2>
            <p>タイム: <span id="final-time">0.0</span>秒</p>
            <p class="best-result" id="best-result"></p>
            <button class="btn-primary" id="btn-retry">もう一度遊ぶ</button>
          </div>
        </div>

        <!-- スタートオーバーレイ -->
        <div class="game-overlay active" id="game-start-overlay">
          <div class="overlay-content">
            <h2>迷路ゲーム</h2>
            <p>ゴールを目指して迷路を進もう!</p>
            <button class="btn-primary" id="btn-start">ゲームスタート</button>
          </div>
        </div>
      </div>

      <!-- モバイル方向ボタン -->
      <div class="dpad-container" id="dpad">
        <button class="dpad-btn dpad-up" id="btn-up" aria-label="上">&#9650;</button>
        <button class="dpad-btn dpad-left" id="btn-left" aria-label="左">&#9664;</button>
        <button class="dpad-btn dpad-center" disabled></button>
        <button class="dpad-btn dpad-right" id="btn-right" aria-label="右">&#9654;</button>
        <button class="dpad-btn dpad-down" id="btn-down" aria-label="下">&#9660;</button>
      </div>
    </div>

    <!-- ゲーム枠直下の広告(hinata-game-below) -->

    <div class="game-instructions">
      <h3>遊び方</h3>
      <ul>
        <li><strong>PC</strong>: 矢印キー または WASD でプレイヤーを移動</li>
        <li><strong>スマホ</strong>: スワイプ または 画面下の方向ボタンで操作</li>
        <li>左上のスタート地点から右下のゴールを目指そう</li>
        <li>壁を通り抜けることはできません</li>
        <li>できるだけ速くゴールしてベストタイムを目指そう!</li>
      </ul>
    </div>

    <div class="game-instructions">
      <h3>迷路ゲーム よくある質問</h3>
      <ul>
        <li><strong>Q. 迷路ゲームは無料で遊べますか?</strong><br>A. はい、完全無料です。料金や課金は一切ありません。</li>
        <li><strong>Q. ダウンロードやインストールは必要ですか?</strong><br>A. 不要です。ブラウザを開くだけですぐに遊べます。</li>
        <li><strong>Q. スマホでも遊べますか?</strong><br>A. 遊べます。スマホはスワイプまたは画面下の方向ボタン、PCは矢印キーまたはWASDでプレイヤーを操作できます。</li>
        <li><strong>Q. 迷路を速くクリアするコツはありますか?</strong><br>A. この迷路は分岐はあってもループのない一本道構造なので、片方の壁に手をつけたまま進み続ければ必ずゴールにたどり着けます。通った道は色が変わって表示されるので、同じ道を戻らないよう意識するとタイムを縮められます。</li>
        <li><strong>Q. ベストスコアは保存されますか?ランキングに登録できますか?</strong><br>A. ベストタイムはブラウザに自動で保存されます。ゴールするとランキングにタイムを登録することもでき、タイムが速い順にランキングで確認できます。</li>
      </ul>
    </div>
    <div class="game-promo">
      <h3>このゲーム、自分でも作れるよ!</h3>
      <p>
        迷路ゲームは再帰バックトラッキングと<br>
        グリッド操作で作られています。<br><br>
        ひなテックでは、こうしたゲームの<br>
        作り方を楽しく学べます!
      </p>
      <div class="promo-links">
        <a href="tutorial/" class="btn-primary">このゲームの作り方を見る →</a>
        <a href="https://hinata-ya.tech/online/" class="btn-primary" data-class-link="game-promo">オンラインで習ってみる(全国OK)→</a>
        <a href="source/" class="btn-secondary">ソースコードを見る →</a>
        <a href="scratch/" class="btn-secondary">🐱 Scratch版で中を見る →</a>
      </div>
    </div>

    <div class="other-games">
      <a href="../../">&larr; 他のゲームも遊ぶ</a>
    </div>
  </div>

  <div id="footer"></div>

  <script src="../../js/common.js?v=20260914" data-base="../../"></script>
  <script src="../../js/leaderboard.js"></script>
  <script src="../../js/game-audio.js?v=20260911"></script>
  <script src="game.js?v=20260911"></script>
</body>
</html>

style.css — Looks and animation

style.css creates the maze's signature look. The part to watch is the CSS Grid used on .maze-board. It sets display: grid, and JavaScript fills in the row and column counts (31×31) later, so all the tiny cells line up in a neat square.

Each cell changes color based on its state: .wall is a brown wall, .path is a bright corridor, .visited is a square you've already walked through. game.js only has to swap a cell's class and the look updates by itself.

The player and the goal get round and square markers layered on with the ::after pseudo-element, slowly pulsing in size with @keyframes player-pulse and goal-glow. The clear-burst at the moment you finish is the little "pop" celebration effect.

style.css
/* ============================================
   迷路ゲーム - 固有スタイル
   ============================================ */

/* スコアエリア */
.score-area {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  margin-bottom: 1rem;
}

.score-box {
  background: #BBADA0;
  border-radius: 8px;
  padding: 0.5rem 1rem;
  text-align: center;
  min-width: 70px;
}

.score-label {
  display: block;
  font-size: 0.7rem;
  color: #EEE4DA;
  text-transform: uppercase;
  letter-spacing: 0.05em;
}

.score-value {
  display: block;
  font-size: 1.25rem;
  font-weight: 700;
  color: #fff;
}

.btn-new-game {
  margin-left: auto;
  background: #8F7A66;
  color: #fff;
  border: none;
  border-radius: 8px;
  padding: 0.5rem 1.25rem;
  font-size: 0.85rem;
  font-weight: 600;
  cursor: pointer;
  transition: background 0.2s;
}

.btn-new-game:hover {
  background: #7A6658;
}

/* ゲームボード */
.board-wrapper {
  position: relative;
  width: 100%;
  max-width: 500px;
  margin: 0 auto;
  aspect-ratio: 1 / 1;
  touch-action: none;
}

.maze-board {
  position: absolute;
  inset: 0;
  display: grid;
  gap: 0px;
  padding: 0;
  background: #BBADA0;
  border-radius: 8px;
  overflow: hidden;
}

/* 迷路セル共通 */
.maze-cell {
  position: relative;
  transition: background-color 0.1s;
}

/* 壁セル */
.maze-cell.wall {
  background: #6D5D4E;
}

/* 通路セル */
.maze-cell.path {
  background: #EEE4DA;
}

/* 訪問済みの通路(プレイヤーの軌跡) */
.maze-cell.visited {
  background: #E8DDD0;
}

/* プレイヤー */
.maze-cell.player {
  background: #EEE4DA;
}

.maze-cell.player::after {
  content: '';
  position: absolute;
  top: 12%;
  left: 12%;
  width: 76%;
  height: 76%;
  background: #E67E22;
  border-radius: 50%;
  box-shadow: 0 2px 6px rgba(0, 0, 0, 0.3);
  animation: player-pulse 1.2s ease-in-out infinite alternate;
}

@keyframes player-pulse {
  0% {
    transform: scale(0.9);
  }
  100% {
    transform: scale(1);
  }
}

/* ゴール */
.maze-cell.goal {
  background: #EEE4DA;
}

.maze-cell.goal::after {
  content: '';
  position: absolute;
  top: 10%;
  left: 10%;
  width: 80%;
  height: 80%;
  background: #2ECC71;
  border-radius: 4px;
  animation: goal-glow 1s ease-in-out infinite alternate;
}

@keyframes goal-glow {
  0% {
    opacity: 0.6;
    transform: scale(0.85);
  }
  100% {
    opacity: 1;
    transform: scale(1);
  }
}

/* クリア演出 */
.maze-cell.player.cleared::after {
  background: #F1C40F;
  animation: clear-burst 0.6s ease-out forwards;
}

@keyframes clear-burst {
  0% {
    transform: scale(1);
    opacity: 1;
  }
  50% {
    transform: scale(1.6);
    opacity: 0.8;
  }
  100% {
    transform: scale(1.2);
    opacity: 1;
    background: #F1C40F;
  }
}

/* ゲームオーバーレイ */
.game-overlay {
  position: absolute;
  inset: 0;
  background: rgba(238, 228, 218, 0.85);
  border-radius: 8px;
  display: none;
  align-items: center;
  justify-content: center;
  z-index: 10;
  animation: overlay-appear 0.3s ease;
}

.game-overlay.active {
  display: flex;
}

@keyframes overlay-appear {
  0% {
    opacity: 0;
  }
  100% {
    opacity: 1;
  }
}

.overlay-content {
  text-align: center;
  padding: 1.5rem;
}

.overlay-content h2 {
  font-size: 1.75rem;
  color: #776E65;
  margin-bottom: 0.5rem;
}

.overlay-content p {
  font-size: 1.1rem;
  color: #776E65;
  margin-bottom: 0.75rem;
}

.overlay-content .best-result {
  font-size: 0.95rem;
  color: #E67E22;
  font-weight: 600;
  margin-bottom: 1rem;
}

.overlay-content .btn-primary {
  margin-bottom: 0.5rem;
}

/* モバイル方向ボタン(十字パッド) */
.dpad-container {
  display: none;
  width: 160px;
  margin: 1.25rem auto 0;
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  grid-template-rows: repeat(3, 1fr);
  gap: 6px;
}

/* PCでは方向パッドを非表示 */
@media (min-width: 769px) {
  .dpad-container {
    display: none;
  }
}

/* モバイルでは方向パッドを表示 */
@media (max-width: 768px) {
  .dpad-container {
    display: grid;
  }
}

.dpad-btn {
  width: 100%;
  aspect-ratio: 1 / 1;
  border: none;
  border-radius: 12px;
  background: #8F7A66;
  color: #fff;
  font-size: 1.25rem;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  transition: background 0.15s, transform 0.1s;
  user-select: none;
  -webkit-user-select: none;
  -webkit-tap-highlight-color: transparent;
}

.dpad-btn:active {
  background: #7A6658;
  transform: scale(0.92);
}

.dpad-btn:disabled {
  background: transparent;
  cursor: default;
}

/* 十字配置 */
.dpad-up {
  grid-column: 2;
  grid-row: 1;
}

.dpad-left {
  grid-column: 1;
  grid-row: 2;
}

.dpad-center {
  grid-column: 2;
  grid-row: 2;
}

.dpad-right {
  grid-column: 3;
  grid-row: 2;
}

.dpad-down {
  grid-column: 2;
  grid-row: 3;
}

/* レスポンシブ */
@media (max-width: 500px) {
  .maze-board {
    border-radius: 6px;
  }
}

@media (max-width: 360px) {
  .score-box {
    padding: 0.4rem 0.75rem;
    min-width: 60px;
  }

  .score-value {
    font-size: 1rem;
  }

  .dpad-container {
    width: 140px;
    gap: 4px;
  }

  .dpad-btn {
    border-radius: 10px;
    font-size: 1.1rem;
  }
}

game.js — The brain that builds the maze and moves the player

game.js is the heart of this game. The whole file is wrapped in (function () { ... })(); — an "immediately invoked function" — so the game's variables don't clash with any other code.

First, the coordinate system. The maze is stored in grid, a 2D array, where 0 is a wall and 1 is a path. The logical maze is 15×15, but because wall cells sit in between, the real grid is 15 * 2 + 1 = 31 cells on each side. The rule of this code: double a logical coordinate and add 1 to get the grid coordinate.

The star of the show is generateMaze(). It uses a maze-building method called recursive backtracking. First everything is filled with walls. Then, starting from the first cell, it randomly picks a neighboring cell it hasn't visited yet and digs forward, knocking down the wall in between. When it hits a dead end, it walks back the way it came one step at a time (that's the backtracking) and looks for another route. Repeat that, and you end up with a maze where every cell is connected by exactly one route. Instead of true recursion (a function calling itself), it remembers the trail in an array called stack — because with a big maze, true recursion can go too deep and crash.

Player movement is handled by movePlayer(). It checks that the destination cell is inside the bounds and isn't a wall in grid, and only then moves the player. Cells you pass through are recorded in visited and tinted as your trail. Reaching the goal cell calls gameClear().

render() looks at grid and the player's position and reassigns the correct class to all 31×31 cells. The timer updates every 0.1 seconds with setInterval, and your best time is saved in localStorage, so the record survives even after you close the browser.

Three kinds of controls are supported — keyboard (arrow keys and WASD), swipes on phones, and the on-screen directional pad — and all of them end up calling the same movePlayer().

game.js
// ============================================
// 迷路ゲーム ロジック
// ============================================

(function () {
  'use strict';

  // 言語別テキスト(英語版ページが window.GAME_TEXT を定義して上書きする。docs/i18n.md 参照)
  var TEXT = window.GAME_TEXT || {};

  // ============================================
  // サウンド(共通モジュール。読み込みに失敗しても無音のまま動く)
  // ============================================
  var hasAudio = typeof window.GameAudio !== 'undefined';
  if (hasAudio) {
    GameAudio.init('maze');
    // game.js は英語版ページからも読まれるので BGM は絶対パス(相対だと英語版だけ404で無音になる)
    GameAudio.setBgm('/games/assets/bgm/kakekko.mp3', 0.09);
    // 既定の右上はスコア行の New Game(margin-left:auto)と重なり、盤面の角は迷路の通路なので
    // スコア行内の New Game 左隣の空きに置く
    GameAudio.attachMuteButton({ top: '8px', right: '118px' });
  }
  function sfx(name, opts) { if (hasAudio) GameAudio.sfx(name, opts); }

  // 最初の操作で音を解禁する(ブラウザの自動再生制限。どの操作でも1回だけ)
  var audioUnlocked = false;
  function unlockAudio() {
    if (audioUnlocked || !hasAudio) return;
    audioUnlocked = true;
    GameAudio.unlock();
    GameAudio.startBgm();
  }
  document.addEventListener('pointerdown', unlockAudio, { once: true });
  document.addEventListener('keydown', unlockAudio, { once: true });

  // ============================================
  // 定数
  // ============================================
  var MAZE_SIZE = 15;              // 迷路の論理サイズ(15x15)
  var GRID_SIZE = MAZE_SIZE * 2 + 1; // 描画グリッドサイズ(壁含む: 31x31)
  var WALL = 0;
  var PATH = 1;

  // 方向の定義(迷路生成用: 2セル単位で移動)
  var DIRS = [
    { dx:  0, dy: -1 },  // 上
    { dx:  0, dy:  1 },  // 下
    { dx: -1, dy:  0 },  // 左
    { dx:  1, dy:  0 }   // 右
  ];

  // プレイヤー移動方向(1セル単位)
  var DIR = {
    UP:    { x:  0, y: -1 },
    DOWN:  { x:  0, y:  1 },
    LEFT:  { x: -1, y:  0 },
    RIGHT: { x:  1, y:  0 }
  };

  // ============================================
  // 状態変数
  // ============================================
  var grid = [];           // 迷路グリッド(GRID_SIZE x GRID_SIZE)
  var playerX = 1;         // プレイヤーの現在位置(グリッド座標)
  var playerY = 1;
  var goalX = GRID_SIZE - 2;
  var goalY = GRID_SIZE - 2;
  var visited = {};        // 訪問済みセル
  var gameRunning = false;
  var gameCleared = false;
  var timerInterval = null;
  var startTime = 0;
  var elapsedTime = 0;
  var bestTime = parseFloat(localStorage.getItem('bestMaze') || '0');

  // ============================================
  // DOM要素
  // ============================================
  var board = document.getElementById('maze-board');
  var timeEl = document.getElementById('time');
  var bestTimeEl = document.getElementById('best-time');
  var gameClearOverlay = document.getElementById('game-clear-overlay');
  var gameStartOverlay = document.getElementById('game-start-overlay');
  var finalTimeEl = document.getElementById('final-time');
  var bestResultEl = document.getElementById('best-result');

  // セルの2次元配列(高速アクセス用)
  var cells = [];

  // ============================================
  // 迷路生成(再帰バックトラッキング)
  // ============================================
  function generateMaze() {
    // グリッドをすべて壁で初期化
    grid = [];
    for (var y = 0; y < GRID_SIZE; y++) {
      grid[y] = [];
      for (var x = 0; x < GRID_SIZE; x++) {
        grid[y][x] = WALL;
      }
    }

    // 再帰バックトラッキングで迷路を掘る
    // 論理座標 (cx, cy) は 0 ~ MAZE_SIZE-1
    // グリッド座標 = 論理座標 * 2 + 1
    var mazeVisited = [];
    for (var my = 0; my < MAZE_SIZE; my++) {
      mazeVisited[my] = [];
      for (var mx = 0; mx < MAZE_SIZE; mx++) {
        mazeVisited[my][mx] = false;
      }
    }

    // スタックベースの再帰バックトラッキング(スタックオーバーフロー防止)
    var stack = [];
    var startCX = 0;
    var startCY = 0;

    mazeVisited[startCY][startCX] = true;
    grid[startCY * 2 + 1][startCX * 2 + 1] = PATH;
    stack.push({ cx: startCX, cy: startCY });

    while (stack.length > 0) {
      var current = stack[stack.length - 1];
      var cx = current.cx;
      var cy = current.cy;

      // 未訪問の隣接セルを取得
      var neighbors = [];
      for (var d = 0; d < DIRS.length; d++) {
        var nx = cx + DIRS[d].dx;
        var ny = cy + DIRS[d].dy;
        if (nx >= 0 && nx < MAZE_SIZE && ny >= 0 && ny < MAZE_SIZE && !mazeVisited[ny][nx]) {
          neighbors.push({ nx: nx, ny: ny, dx: DIRS[d].dx, dy: DIRS[d].dy });
        }
      }

      if (neighbors.length === 0) {
        // 行き止まり: バックトラック
        stack.pop();
      } else {
        // ランダムに隣接セルを選択
        var chosen = neighbors[Math.floor(Math.random() * neighbors.length)];

        // 壁を壊す(現在セルと選択セルの間の壁)
        var wallGX = cx * 2 + 1 + chosen.dx;
        var wallGY = cy * 2 + 1 + chosen.dy;
        grid[wallGY][wallGX] = PATH;

        // 選択セルを通路にする
        grid[chosen.ny * 2 + 1][chosen.nx * 2 + 1] = PATH;

        // 訪問済みにしてスタックに追加
        mazeVisited[chosen.ny][chosen.nx] = true;
        stack.push({ cx: chosen.nx, cy: chosen.ny });
      }
    }
  }

  // ============================================
  // ボードの初期化(DOMセルを生成)
  // ============================================
  function createBoard() {
    board.innerHTML = '';
    board.style.gridTemplateColumns = 'repeat(' + GRID_SIZE + ', 1fr)';
    board.style.gridTemplateRows = 'repeat(' + GRID_SIZE + ', 1fr)';
    cells = [];

    for (var y = 0; y < GRID_SIZE; y++) {
      cells[y] = [];
      for (var x = 0; x < GRID_SIZE; x++) {
        var cell = document.createElement('div');
        cell.className = 'maze-cell';
        board.appendChild(cell);
        cells[y][x] = cell;
      }
    }
  }

  // ============================================
  // ボードの描画更新
  // ============================================
  function render() {
    for (var y = 0; y < GRID_SIZE; y++) {
      for (var x = 0; x < GRID_SIZE; x++) {
        var cell = cells[y][x];

        if (x === playerX && y === playerY) {
          cell.className = 'maze-cell player';
        } else if (x === goalX && y === goalY) {
          cell.className = 'maze-cell goal';
        } else if (grid[y][x] === WALL) {
          cell.className = 'maze-cell wall';
        } else if (visited[x + ',' + y]) {
          cell.className = 'maze-cell visited';
        } else {
          cell.className = 'maze-cell path';
        }
      }
    }
  }

  // ============================================
  // タイマー管理
  // ============================================
  function startTimer() {
    startTime = Date.now();
    timerInterval = setInterval(function () {
      elapsedTime = (Date.now() - startTime) / 1000;
      timeEl.textContent = elapsedTime.toFixed(1);
    }, 100);
  }

  function stopTimer() {
    if (timerInterval) {
      clearInterval(timerInterval);
      timerInterval = null;
    }
    // 最終時間を正確に計算
    elapsedTime = (Date.now() - startTime) / 1000;
    timeEl.textContent = elapsedTime.toFixed(1);
  }

  function resetTimer() {
    if (timerInterval) {
      clearInterval(timerInterval);
      timerInterval = null;
    }
    elapsedTime = 0;
    timeEl.textContent = '0.0';
  }

  // ============================================
  // ベストタイム更新
  // ============================================
  function updateBestTime() {
    if (bestTime > 0) {
      bestTimeEl.textContent = bestTime.toFixed(1);
    } else {
      bestTimeEl.textContent = '--';
    }
  }

  // ============================================
  // プレイヤー移動
  // ============================================
  function movePlayer(dir) {
    if (!gameRunning || gameCleared) return;

    var newX = playerX + dir.x;
    var newY = playerY + dir.y;

    // 範囲チェック
    if (newX < 0 || newX >= GRID_SIZE || newY < 0 || newY >= GRID_SIZE) return;

    // 壁チェック
    if (grid[newY][newX] === WALL) return;

    // 現在位置を訪問済みに
    visited[playerX + ',' + playerY] = true;

    // プレイヤーを移動
    playerX = newX;
    playerY = newY;

    // 描画更新
    render();

    // ゴール判定
    if (playerX === goalX && playerY === goalY) {
      gameClear();
    }
  }

  // ============================================
  // ゲームクリア処理
  // ============================================
  function gameClear() {
    gameRunning = false;
    gameCleared = true;
    stopTimer();
    sfx('finish');
    if (hasAudio) GameAudio.stopBgm(1.0);
    if (window.Leaderboard) Leaderboard.show('maze', Math.round(elapsedTime * 10) / 10, { order: 'asc', format: 'time' });

    // クリア演出
    cells[playerY][playerX].classList.add('cleared');

    // タイムを表示
    finalTimeEl.textContent = elapsedTime.toFixed(1);

    // ベストタイム更新チェック
    if (bestTime === 0 || elapsedTime < bestTime) {
      bestTime = elapsedTime;
      localStorage.setItem('bestMaze', bestTime.toString());
      updateBestTime();
      bestResultEl.textContent = TEXT.newBest || 'ベストタイム更新!';
    } else {
      bestResultEl.textContent = (TEXT.best || 'ベスト: ') + bestTime.toFixed(1) + (TEXT.seconds || '秒');
    }

    // 少し遅延させてオーバーレイを表示
    setTimeout(function () {
      gameClearOverlay.classList.add('active');
    }, 600);
  }

  // ============================================
  // ゲーム開始・リスタート
  // ============================================
  function startGame() {
    // タイマーをリセット
    resetTimer();

    // 状態をリセット
    gameCleared = false;
    visited = {};

    // オーバーレイを非表示
    gameClearOverlay.classList.remove('active');
    gameStartOverlay.classList.remove('active');

    // 迷路を生成
    generateMaze();

    // プレイヤーとゴールの位置を設定
    playerX = 1;
    playerY = 1;
    goalX = GRID_SIZE - 2;
    goalY = GRID_SIZE - 2;

    // 描画
    render();

    // ゲーム開始(1歩ごとの音は多すぎるので鳴らさず、開始とゴールだけに絞る)
    sfx('go');
    // 再挑戦時は BGM を鳴らし直す(初回は unlockAudio 側で開始する)
    if (audioUnlocked && hasAudio) GameAudio.startBgm();
    gameRunning = true;
    startTimer();
  }

  // ============================================
  // キーボード操作
  // ============================================
  document.addEventListener('keydown', function (e) {
    var keyMap = {
      ArrowUp: DIR.UP, ArrowDown: DIR.DOWN, ArrowLeft: DIR.LEFT, ArrowRight: DIR.RIGHT,
      w: DIR.UP, s: DIR.DOWN, a: DIR.LEFT, d: DIR.RIGHT,
      W: DIR.UP, S: DIR.DOWN, A: DIR.LEFT, D: DIR.RIGHT
    };

    var dir = keyMap[e.key];
    if (dir) {
      e.preventDefault();
      movePlayer(dir);
    }
  });

  // ============================================
  // タッチ・スワイプ操作
  // ============================================
  var touchStartX = 0;
  var touchStartY = 0;
  var touchStartTime = 0;
  var boardWrapper = board.parentElement;

  boardWrapper.addEventListener('touchstart', function (e) {
    if (e.touches.length === 1) {
      touchStartX = e.touches[0].clientX;
      touchStartY = e.touches[0].clientY;
      touchStartTime = Date.now();
    }
  }, { passive: true });

  boardWrapper.addEventListener('touchmove', function (e) {
    // ゲーム中はスクロールを防止
    if (gameRunning) {
      e.preventDefault();
    }
  }, { passive: false });

  boardWrapper.addEventListener('touchend', function (e) {
    if (e.changedTouches.length === 0) return;

    var dx = e.changedTouches[0].clientX - touchStartX;
    var dy = e.changedTouches[0].clientY - touchStartY;
    var dt = Date.now() - touchStartTime;

    // 最小スワイプ距離と最大時間
    var minDist = 20;
    var maxTime = 400;

    if (dt > maxTime) return;
    if (Math.abs(dx) < minDist && Math.abs(dy) < minDist) return;

    var dir;
    if (Math.abs(dx) > Math.abs(dy)) {
      dir = dx > 0 ? DIR.RIGHT : DIR.LEFT;
    } else {
      dir = dy > 0 ? DIR.DOWN : DIR.UP;
    }

    movePlayer(dir);
  }, { passive: true });

  // ============================================
  // 方向ボタン(モバイル用D-Pad)
  // ============================================
  function setupDpadButton(btnId, dir) {
    var btn = document.getElementById(btnId);
    if (!btn) return;

    // タッチ操作(クリックとタッチの両方に対応)
    btn.addEventListener('click', function (e) {
      e.preventDefault();
      movePlayer(dir);
    });

    // タッチで押した場合のスクロール防止
    btn.addEventListener('touchstart', function (e) {
      e.preventDefault();
      movePlayer(dir);
    }, { passive: false });
  }

  setupDpadButton('btn-up', DIR.UP);
  setupDpadButton('btn-down', DIR.DOWN);
  setupDpadButton('btn-left', DIR.LEFT);
  setupDpadButton('btn-right', DIR.RIGHT);

  // ============================================
  // ボタンイベント
  // ============================================
  document.getElementById('btn-new-game').addEventListener('click', function () {
    startGame();
  });

  document.getElementById('btn-retry').addEventListener('click', function () {
    startGame();
  });

  document.getElementById('btn-start').addEventListener('click', function () {
    startGame();
  });

  // ============================================
  // ページ離脱時にタイマーを停止
  // ============================================
  document.addEventListener('visibilitychange', function () {
    if (document.hidden && gameRunning) {
      if (timerInterval) {
        clearInterval(timerInterval);
        timerInterval = null;
      }
      // ページが再度表示されたときにタイマーを再開
    } else if (!document.hidden && gameRunning && !gameCleared) {
      // 経過時間を維持しつつタイマーを再開
      timerInterval = setInterval(function () {
        elapsedTime = (Date.now() - startTime) / 1000;
        timeEl.textContent = elapsedTime.toFixed(1);
      }, 100);
    }
  });

  // ============================================
  // 初期化
  // ============================================
  updateBestTime();
  createBoard();
  generateMaze();
  render();
})();

What you can learn from this code

Run it on your own computer

Save each code block above using its file name, put them all in the same folder, and open index.html in your browser — the game will run. The "Copy" button in each file-name bar makes this easy.

The live game also uses a few site-wide files (shared CSS and the header), so when you open just these files the header and other shared parts will be missing. The game itself works fine, which is all you need to study or tinker with it.

Once you feel comfortable, try changing the colors or tweaking the rules. After reading code, changing it is the best way to learn.

Can I use this code?

Yes — feel free to read, copy, run and modify this source code to learn programming. Using it for school projects is welcome too. Let it spark ideas about how you would build it.

Please don't republish or distribute the code as-is as your own game or service, though. It is meant for learning.

Want to learn more?

If reading the code makes you think "I want to build this myself!", check out the step-by-step guide for Maze Run. While this page shows the finished code, the guide builds the game up from nothing, one step at a time.