Loading
  • LIGHT

  • DARK

ROUTE

ルートゼロの
アクティビティ

ゲームを作りながら学ぶプログラムの動きとロジック

3

こんにちは!
本記事では、HTML5の <canvas> 要素とJavaScriptを使用して、シンプルなオセロゲームを作成する方法を解説します。
ゲームの進行やロジックの処理を学びながら、実際に動くオセロゲームを作成する過程を追っていきます。

オセロゲームのイメージ

全体のコードはこちら!

<!DOCTYPE html>
    <html lang="ja">
      <head>
        <meta charset="UTF-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>オセロゲーム</title>
        <style>
          body {
            display: flex;
            justify-content: center;
            align-items: center;
            height: 100vh;
            background-color: #2c3e50;
            margin: 0;
          }
          #game-container {
            text-align: center;
          }
          canvas {
            background-color: green;
            display: block;
            margin: 0 auto;
          }
          #controls {
            margin-top: 10px;
          }
          button {
            padding: 10px 20px;
            font-size: 16px;
            border: none;
            background-color: #3498db;
            color: #fff;
            cursor: pointer;
            border-radius: 5px;
          }
          button:hover {
            background-color: #2980b9;
          }
        </style>
      </head>
      <body>
        <div id="game-container">
          <canvas id="js-canvas"></canvas>
          <div id="js-result"></div>
          <div id="controls">
            <button id="reset-button">Reset Game</button>
          </div>
        </div>
    
        <script>
          // キャンバス(ゲーム盤)の要素を取得し、描画のためのコンテキストを取得
          const canvas = document.getElementById("js-canvas");
          const ctx = canvas.getContext("2d");
    
          // 結果表示用の要素を取得
          const result = document.getElementById("js-result");
    
          // キャンバスの幅を設定(最大480px、ウィンドウの幅に応じて調整)
          canvas.width = Math.min(480, window.innerWidth - 20);
          canvas.height = canvas.width;
    
          // ゲームボードのグリッドサイズ(8×8)
          const GRID_SIZE = 8;
    
          // 1マスの大きさを計算
          const CELL_SIZE = canvas.width / GRID_SIZE;
    
          // ゲームの状態を管理する変数
          let board = []; // 8×8のゲームボード(2次元配列)
          let currentPlayer = "black"; // 現在のプレイヤー(黒 or 白)
          let gameState = "playing"; // ゲームの状態("playing":進行中、"gameover":終了)
    
          // ゲームを初期化する関数
          function initGame() {
            // 8×8のボードを作成し、すべてのマスをnull(空)で埋める
            board = Array.from({ length: GRID_SIZE }, () => Array(GRID_SIZE).fill(null));
    
            // 初期配置(オセロの標準的な開始位置)
            board[3][3] = board[4][4] = "white";
            board[3][4] = board[4][3] = "black";
    
            // 黒からスタート
            currentPlayer = "black";
            gameState = "playing";
    
            // ボードを描画
            drawBoard();
          }
    
          // ゲームボードを描画する関数
          function drawBoard() {
            // 背景を緑色に設定
            ctx.fillStyle = "green";
            ctx.fillRect(0, 0, canvas.width, canvas.height);
    
            // グリッド(線)を描画
            for (let i = 1; i < GRID_SIZE; i++) {
              ctx.beginPath();
              ctx.moveTo(i * CELL_SIZE, 0);
              ctx.lineTo(i * CELL_SIZE, canvas.height);
              ctx.stroke();
    
              ctx.beginPath();
              ctx.moveTo(0, i * CELL_SIZE);
              ctx.lineTo(canvas.width, i * CELL_SIZE);
              ctx.stroke();
            }
    
            // 各マスに石を描画
            for (let i = 0; i < GRID_SIZE; i++) {
              for (let j = 0; j < GRID_SIZE; j++) {
                if (board[i][j]) {
                  ctx.fillStyle = board[i][j]; // 石の色を設定(黒 or 白)
                  ctx.beginPath();
                  ctx.arc(j * CELL_SIZE + CELL_SIZE / 2, i * CELL_SIZE + CELL_SIZE / 2, CELL_SIZE / 2 - 2, 0, Math.PI * 2);
                  ctx.fill();
                }
              }
            }
    
            // ゲームが終了した場合、結果を表示
            if (gameState === "gameover") {
              ctx.fillStyle = "rgba(0, 0, 0, 0.5)"; // 半透明の背景
              ctx.fillRect(0, 0, canvas.width, canvas.height);
              ctx.fillStyle = "white";
              ctx.font = "30px Arial";
              ctx.fillText(`Game Over! ${getWinner()} wins!`, canvas.width / 2 - 100, canvas.height / 2);
            }
          }
    
          // 石を置く処理
          function placeStone(row, col) {
            // すでに石が置かれている or ゲーム終了時は何もしない
            if (board[row][col] || gameState === "gameover") return;
    
            // ひっくり返せる石があるかチェック
            const flippedStones = getFlippedStones(row, col, currentPlayer);
            if (!flippedStones.length) return;
    
            // 石を置く
            board[row][col] = currentPlayer;
    
            // ひっくり返す
            flippedStones.forEach(([r, c]) => (board[r][c] = currentPlayer));
    
            // プレイヤー交代
            currentPlayer = currentPlayer === "black" ? "white" : "black";
    
            // どちらのプレイヤーも有効な手がない場合、ゲーム終了
            if (!hasValidMove()) {
              currentPlayer = currentPlayer === "black" ? "white" : "black";
              if (!hasValidMove()) gameState = "gameover";
            }
    
            // 更新後のボードを描画
            drawBoard();
          }
    
          // ひっくり返せる石を探す処理
          function getFlippedStones(row, col, player) {
            const opponent = player === "black" ? "white" : "black";
    
            // 8方向の座標変化(上、下、左、右、斜め4方向)
            const directions = [
              [-1, -1],
              [-1, 0],
              [-1, 1],
              [0, -1],
              [0, 1],
              [1, -1],
              [1, 0],
              [1, 1],
            ];
    
            let flippedStones = [];
    
            for (const [dx, dy] of directions) {
              let x = row + dx;
              let y = col + dy;
              let tempFlipped = [];
    
              while (x >= 0 && x < GRID_SIZE && y >= 0 && y < GRID_SIZE && board[x][y] === opponent) {
                tempFlipped.push([x, y]);
                x += dx;
                y += dy;
              }
    
              if (x >= 0 && x < GRID_SIZE && y >= 0 && y < GRID_SIZE && board[x][y] === player) {
                flippedStones = flippedStones.concat(tempFlipped);
              }
            }
    
            return flippedStones;
          }
    
          // 有効な手があるかチェック
          function hasValidMove() {
            return board.some((row, i) => row.some((cell, j) => !cell && getFlippedStones(i, j, currentPlayer).length));
          }
    
          // 勝者を判定する関数
          function getWinner() {
            let counts = board.flat().reduce(
              (acc, cell) => {
                if (cell) acc[cell]++;
                return acc;
              },
              { black: 0, white: 0 }
            );
    
            if (counts.black > counts.white) return "Black";
            if (counts.white > counts.black) return "White";
            return "Draw";
          }
    
          // ユーザーがキャンバスをクリックしたときの処理
          canvas.addEventListener("click", (e) => {
            const rect = canvas.getBoundingClientRect();
            const row = Math.floor((e.clientY - rect.top) / CELL_SIZE);
            const col = Math.floor((e.clientX - rect.left) / CELL_SIZE);
    
            placeStone(row, col);
          });
    
          // リセットボタンのクリックイベント
          document.getElementById("reset-button").addEventListener("click", initGame);
    
          // ゲーム開始
          initGame();
        </script>
      </body>
    </html>

コードの各部分にコメントを付け、ロジックの処理内容や注意すべき点を順を追って説明します。

1. HTML と CSSのセットアップ

まずは、ゲームのUI部分を作成します。
<canvas> 要素を使って、ゲーム盤を描画し、リセットボタンを設置します。これにより、ゲーム画面がどのように表示されるのかがわかります。

HTML部分

<!DOCTYPE html>
    <html lang="ja">
      <head>
        <meta charset="UTF-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>オセロゲーム</title>
        <style>
          body {
            display: flex;
            justify-content: center;
            align-items: center;
            height: 100vh;
            background-color: #2c3e50;
            margin: 0;
          }
          #game-container {
            text-align: center;
          }
          canvas {
            background-color: green;
            display: block;
            margin: 0 auto;
          }
          #controls {
            margin-top: 10px;
          }
          button {
            padding: 10px 20px;
            font-size: 16px;
            border: none;
            background-color: #3498db;
            color: #fff;
            cursor: pointer;
            border-radius: 5px;
          }
          button:hover {
            background-color: #2980b9;
          }
        </style>
        <script>
          // ここにJavaScriptの処理を書きます。
        </script>
      </head>

ポイント

  • <canvas> 要素を使用してオセロのゲーム盤を描画します。

  • #game -container #controls は、ゲームのビジュアルと操作を管理するためのラッパーです。

  • ボタン(リセット用) がユーザーインターフェースに含まれています。

CSS部分

CSSでは、ページ全体のレイアウトをセンター配置し、ゲーム盤(canvas)に緑色の背景を設定します。また、リセットボタンのスタイルも整えています。


2. JavaScript のロジック解説

次に、オセロゲームのロジックを実装します。
ユーザーがクリックした位置に石を置き、ターンを交代しながらゲームを進行させます。オセロのルールに基づいて、石をひっくり返す処理やゲーム終了の判定を行い、順番に解説していきます。

初期化

// キャンバス(ゲーム盤)の要素を取得し、描画のためのコンテキストを取得
    const canvas = document.getElementById("js-canvas");
    const ctx = canvas.getContext("2d");
    
    // 結果表示用の要素を取得
    const result = document.getElementById("js-result");
    
    // キャンバスの幅を設定(最大480px、ウィンドウの幅に応じて調整)
    canvas.width = Math.min(480, window.innerWidth - 20);
    canvas.height = canvas.width;
  • canvas 要素を取得し、描画コンテキスト(ctx)を取得します。ctx は、キャンバスに描画するために必要なオブジェクトです。

  • ウィンドウサイズに応じてキャンバスのサイズを動的に設定し、最大幅を480pxに制限しています。

ゲームの状態管理

// ゲームボードのグリッドサイズ(8×8)
    const GRID_SIZE = 8;
    
    // 1マスの大きさを計算
    const CELL_SIZE = canvas.width / GRID_SIZE;
    
    // ゲームの状態を管理する変数
    let board = []; // 8×8のゲームボード(2次元配列)
    let currentPlayer = "black"; // 現在のプレイヤー(黒 or 白)
    let gameState = "playing"; // ゲームの状態("playing":進行中、"gameover":終了)
  • オセロのボードは 8×8 の2次元配列 で管理されます。

  • currentPlayer には現在のターンのプレイヤー(黒または白)を保持します。

  • gameState はゲームが進行中か終了したかを示します。

ゲームの初期化

JavaScriptの関数と処理の流れについて

JavaScriptでは、関数はそのままでは実行されません。関数を呼び出すことによって初めて、その処理が実行されます。この記事のオセロゲームも同様で、ゲームの初期化やゲームの進行に関わる処理は、あらかじめ定義した関数が適切に呼ばれることによって動きます。

特に重要なのが、ページが最初に読み込まれた際に実行される処理です。この処理は、ゲームが開始するための初期設定を行う役割を持っています。そのため、最初に呼ばれるべき関数はinitGame()です。

初期化の流れ

// ゲームを初期化する関数
    function initGame() {
      // 8×8のボードを作成し、すべてのマスをnull(空)で埋める
      board = Array.from({ length: GRID_SIZE }, () => Array(GRID_SIZE).fill(null));
    
      // 初期配置(オセロの標準的な開始位置)
      board[3][3] = board[4][4] = "white";
      board[3][4] = board[4][3] = "black";
    
      // 黒からスタート
      currentPlayer = "black";
      gameState = "playing";
    
      // ボードを描画
      drawBoard();
    }
  • 初期化時にゲームボードを空(null)で埋め、オセロの標準配置に合わせて黒と白の石を配置します。

  • ゲーム開始は黒からスタートします。

initGame() は、ゲームの最初に呼ばれ、以下の処理を行います。

  1. ゲームボードを空に初期化します。

  2. オセロの標準的な初期配置(黒と白の石)をボードにセットします。

  3. ゲームの状態を「進行中」とし、黒からスタートするように設定します。

  4. drawBoard() 関数を呼び出して、画面にボードを描画します。

このように、initGame() はゲームが開始される際に最初に呼ばれる関数であり、ゲームの進行に必要な初期化をすべて行う非常に重要な役割を持っています。

ゲームボードの描画

// ゲームボードを描画する関数
    function drawBoard() {
      // 背景を緑色に設定
      ctx.fillStyle = "green";
      ctx.fillRect(0, 0, canvas.width, canvas.height);
    
      // グリッド(線)を描画
      for (let i = 1; i < GRID_SIZE; i++) {
        ctx.beginPath();
        ctx.moveTo(i * CELL_SIZE, 0);
        ctx.lineTo(i * CELL_SIZE, canvas.height);
        ctx.stroke();
    
        ctx.beginPath();
        ctx.moveTo(0, i * CELL_SIZE);
        ctx.lineTo(canvas.width, i * CELL_SIZE);
        ctx.stroke();
      }
    
      // 各マスに石を描画
      for (let i = 0; i < GRID_SIZE; i++) {
        for (let j = 0; j < GRID_SIZE; j++) {
          if (board[i][j]) {
            ctx.fillStyle = board[i][j]; // 石の色を設定(黒 or 白)
            ctx.beginPath();
            ctx.arc(j * CELL_SIZE + CELL_SIZE / 2, i * CELL_SIZE + CELL_SIZE / 2, CELL_SIZE / 2 - 2, 0, Math.PI * 2);
            ctx.fill();
          }
        }
      }
    
      // ゲームが終了した場合、結果を表示
      if (gameState === "gameover") {
        ctx.fillStyle = "rgba(0, 0, 0, 0.5)"; // 半透明の背景
        ctx.fillRect(0, 0, canvas.width, canvas.height);
        ctx.fillStyle = "white";
        ctx.font = "30px Arial";
        ctx.fillText(`Game Over! ${getWinner()} wins!`, canvas.width / 2 - 100, canvas.height / 2);
      }
    }
  • ボードの描画では、まず背景色を塗り、次にグリッド(線)を描画します。

  • 各マスに黒または白の石を描画します。

  • ゲーム終了時には、ゲームオーバーのメッセージが表示されます。

石を置く処理

// 石を置く処理
    function placeStone(row, col) {
      // すでに石が置かれている or ゲーム終了時は何もしない
      if (board[row][col] || gameState === "gameover") return;
    
      // ひっくり返せる石があるかチェック
      const flippedStones = getFlippedStones(row, col, currentPlayer);
      if (!flippedStones.length) return;
    
      // 石を置く
      board[row][col] = currentPlayer;
    
      // ひっくり返す
      flippedStones.forEach(([r, c]) => (board[r][c] = currentPlayer));
    
      // プレイヤー交代の処理(ターンを交代)
      currentPlayer = currentPlayer === "black" ? "white" : "black";
    
      // どちらのプレイヤーも有効な手がない場合、ゲーム終了
      if (!hasValidMove()) {
        currentPlayer = currentPlayer === "black" ? "white" : "black";
        if (!hasValidMove()) gameState = "gameover";
      }
    
      // 更新後のボードを描画
      drawBoard();
    }
  • クリックされた位置に石を置く処理です。

  • 置けるかどうかは、ひっくり返せる石があるかを確認することで判定します。

  • ひっくり返せる石があれば、その石をひっくり返し、次のプレイヤーにターンを移します。

ひっくり返せる石の判定

// ひっくり返せる石を探す処理
    function getFlippedStones(row, col, player) {
      const opponent = player === "black" ? "white" : "black";
    
      // 8方向の座標変化(上、下、左、右、斜め4方向)
      const directions = [
        [-1, -1], // 左上
        [-1, 0],  // 上
        [-1, 1],  // 右上
        [0, -1],  // 左
        [0, 1],   // 右
        [1, -1],  // 左下
        [1, 0],   // 下
        [1, 1],   // 右下
      ];
    
      let flippedStones = [];
    
      // 各方向に対してひっくり返せる石を探す
      for (const [dx, dy] of directions) {
        let x = row + dx;
        let y = col + dy;
        let tempFlipped = [];
    
        // 相手の石がある限りチェック
        while (x >= 0 && x < GRID_SIZE && y >= 0 && y < GRID_SIZE && board[x][y] === opponent) {
          tempFlipped.push([x, y]);
          x += dx;
          y += dy;
        }
    
        // ひっくり返せる場合のみ追加
        if (x >= 0 && x < GRID_SIZE && y >= 0 && y < GRID_SIZE && board[x][y] === player) {
          flippedStones = flippedStones.concat(tempFlipped);
        }
      }
    
      return flippedStones;
    }
    
  • ひっくり返せる石を判定するために、8方向に対して相手の石があるかを確認し、最終的に自分の石が来る位置までチェックします。


3. ゲームの終了判定とリセット

// ゲーム終了時の勝者判定
    function getWinner() {
      let counts = board.flat().reduce(
        (acc, cell) => {
          if (cell) acc[cell]++;
          return acc;
        },
        { black: 0, white: 0 }
      );
    
      if (counts.black > counts.white) return "Black";
      if (counts.white > counts.black) return "White";
      return "Draw";
    }
    
    // ゲームリセット
    document.getElementById("reset-button").addEventListener("click", initGame);

これで、オセロゲームが完成しました。ゲーム終了時には、勝者を判定し、リセットボタンでゲームを再スタートできるようになります。基本的なロジックをシンプルに実装しているので、さらに改良や機能追加も可能です。

  • ゲーム終了時には、ボードに置かれた石の数をカウントして勝者を判定します。

  • リセットボタンがクリックされると、initGame 関数が呼ばれて、ボードが初期化され、ゲームが最初から始まります。

まとめ

このオセロゲームは、基本的なゲームの流れ(石をひっくり返す、ターンの管理、ゲーム終了判定など)をシンプルに実装しています。
<canvas> を使った描画と、JavaScriptのイベント処理を活用することで、プログラムの仕組みやロジックの流れを視覚的に確認できるようにしています。

RANKINGranking-icon

LATEST POSTS

もっとルートゼロを知りたいなら

DISCOVER MORE