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="canonical" href="https://hinata-ya.tech/games/games/maze/">
  <!-- I18N:hreflang -->
  <link rel="alternate" hreflang="ja" href="https://hinata-ya.tech/games/games/maze/">
  <link rel="alternate" hreflang="en" href="https://hinata-ya.tech/games/en/games/maze/">
  <link rel="alternate" hreflang="x-default" href="https://hinata-ya.tech/games/games/maze/">
  <!-- /I18N:hreflang -->
  <!-- OGP -->
  <meta property="og:title" content="迷路ゲーム|無料ブラウザゲーム|ひなテックGames">
  <meta property="og:description" content="迷路ゲームで遊ぼう!ランダム生成される迷路をゴールまで最速で駆け抜けよう。PC・スマホ対応の無料ブラウザゲーム。">
  <meta property="og:type" content="website">
  <meta property="og:url" content="https://hinata-ya.tech/games/games/maze/">
  <meta property="og:site_name" content="ひなテックGames">
  <meta property="og:locale" content="ja_JP">
  <!-- SEO:meta-extra -->
  <meta property="og:image" content="https://hinata-ya.tech/games/images/og/maze.png">
  <meta property="og:image:width" content="1200">
  <meta property="og:image:height" content="630">
  <meta property="og:image:alt" content="迷路ゲーム">
  <meta name="twitter:card" content="summary_large_image">
  <meta name="twitter:title" content="迷路ゲーム|無料ブラウザゲーム|ひなテックGames">
  <meta name="twitter:description" content="迷路ゲームで遊ぼう!ランダム生成される迷路をゴールまで最速で駆け抜けよう。PC・スマホ対応の無料ブラウザゲーム。">
  <meta name="twitter:image" content="https://hinata-ya.tech/games/images/og/maze.png">
  <!-- /SEO:meta-extra -->
  <link rel="stylesheet" href="../../css/style.css">
  <link rel="stylesheet" href="style.css">
  <link rel="stylesheet" href="../../css/leaderboard.css">
  <!-- SEO:ld+json -->
  <script type="application/ld+json">
  {
    "@context": "https://schema.org",
    "@type": "VideoGame",
    "name": "迷路ゲーム",
    "description": "迷路ゲームで遊ぼう!ランダム生成される迷路をゴールまで最速で駆け抜けよう。PC・スマホ対応の無料ブラウザゲーム。",
    "url": "https://hinata-ya.tech/games/games/maze/",
    "image": "https://hinata-ya.tech/games/images/og/maze.png",
    "inLanguage": "ja",
    "genre": [
      "パズル",
      "冒険"
    ],
    "gamePlatform": [
      "Web Browser"
    ],
    "applicationCategory": "GameApplication",
    "operatingSystem": [
      "Windows",
      "macOS",
      "iOS",
      "Android",
      "ChromeOS"
    ],
    "browserRequirements": "Requires JavaScript. Requires HTML5.",
    "isAccessibleForFree": true,
    "offers": {
      "@type": "Offer",
      "price": "0",
      "priceCurrency": "JPY",
      "availability": "https://schema.org/InStock"
    },
    "publisher": {
      "@type": "Organization",
      "name": "ひなテック",
      "url": "https://hinata-ya.tech"
    },
    "author": {
      "@type": "Organization",
      "name": "ひなテック",
      "url": "https://hinata-ya.tech"
    }
  }
  </script>
  <script type="application/ld+json">
  {
    "@context": "https://schema.org",
    "@type": "BreadcrumbList",
    "itemListElement": [
      {
        "@type": "ListItem",
        "position": 1,
        "name": "ホーム",
        "item": "https://hinata-ya.tech/games/"
      },
      {
        "@type": "ListItem",
        "position": 2,
        "name": "迷路ゲーム",
        "item": "https://hinata-ya.tech/games/games/maze/"
      }
    ]
  }
  </script>
  <!-- /SEO:ld+json -->
</head>
<body>
  <div id="header"></div>

  <div class="game-page">
    <h1 class="game-page-title">迷路ゲーム</h1>
    <div class="game-page-tags">
      <span class="tag">パズル</span>
      <span class="tag">冒険</span>
    </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>

    <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-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/contact/" class="btn-primary">無料体験に申し込む →</a>
        <a href="source/" class="btn-secondary">ソースコードを見る →</a>
      </div>
    </div>

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

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

  <script src="../../js/common.js?v=20260613c" data-base="../../"></script>
  <script src="../../js/leaderboard.js"></script>
  <script src="game.js?v=20260610a"></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 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();
    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();

    // ゲーム開始
    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.