4
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

オセロアプリ開発: DockerとReactで実現するリアルタイムAI対戦

Last updated at Posted at 2024-11-02

はじめに

本記事では、Docker環境でReactを使用してオセロ(リバーシ)アプリを作成する手順を説明します。このアプリは、6x6のボードでプレイヤーとAIが対戦できるように設計しています。

Reactを用いたフロントエンド開発と、Dockerによるアプリケーションのコンテナ化により、効率的な開発環境を管理します。

現在、通っているITスクールの課題の一環としてアウトプット学習を実施しており、今後はフロントエンドおよびバックエンドに関する記事が多くなる予定です。

必要なツール

私の環境では、M1チップ搭載のMacOSを使用していますが、今回のハンズオンに必要なツールは以下の通りです。
Docker
Docker Compose
Node.js(バージョン14以上推奨)
npm(Node Package Manager)

作成したオセロアプリのデモ動画

今回のReactアプリには、AIとの対戦機能が追加されています。実際の対戦デモ動画になります。

タイトルなし.gif

初めてQiitaのプラットフォームに動画を埋め込んでみましたが、フロントエンドの開発の際には非常に便利ですね!

追記
CSSの動きが想定通りではありませんが、原因を特定して今後修正していきたいと考えています。 現時点では、具体的にどのように記述すればよいか分からなかったため、とりあえず今のまま動かしています。

ディレクトリ構成

以下のようなディレクトリ構成を持つReactアプリを想定しています。この構成により、オセロアプリのファイルを適切に配置し、Docker環境で動作させることができます。

othello/
│
├── node_modules/         # Node.jsのモジュールが格納されるディレクトリ
│
├── public/               # 公開用のファイル
│   └── index.html        # アプリケーションのエントリーポイントとなるHTMLファイル
│
├── src/                  # ソースコードのディレクトリ
│   ├── othello.js        # オセロのゲームロジック
│   ├── othello.css       # オセロのスタイルシート
│   ├── index.js          # アプリケーションのエントリーポイント
│   └── index.css         # アプリケーションのスタイルシート
│
├── .dockerignore          # Dockerビルド時に無視するファイル
├── Dockerfile             # Dockerイメージを作成するための設定ファイル
├── docker-compose.yml     # Docker Composeの設定ファイル
├── package.json           # プロジェクトの依存関係やスクリプトを管理するファイル
└── README.md              # プロジェクトの説明や使い方を記載したファイル(任意)

このディレクトリ構成を作成するために、事前に以下のコマンドを実行し、不要なファイルを削除して整理します。

npx create-react-app othello

上記のコマンドを実行すると、デフォルトで様々なファイルが作成されますが、ここでは不要なファイルを削除して進めます。

ステップ1: プロジェクトの作成

Dockerfileの作成

まず、プロジェクトのルートディレクトリに以下の内容で Dockerfile を作成します。

# Dockerfile
FROM node:14

# 作業ディレクトリの設定
WORKDIR /app

# 依存関係のインストール
COPY package*.json ./
RUN npm install

# アプリケーションのソースをコピー
COPY . .

# アプリケーションをビルド
RUN npm run build

# アプリケーションを提供する
CMD ["npm", "start"]

この Dockerfile は、Dockerイメージを作成するための設定ファイルです。Node.js 環境を構築し、アプリケーションの依存関係をインストールし、最終的にアプリをビルドします。

docker-compose.ymlの作成

次に、プロジェクトのルートディレクトリに以下の内容で docker-compose.yml を作成します。

docker-compose.yml
version: '3'
services:
  app:
    build: .
    ports:
      - "3000:3000"
    volumes:
      - .:/app
    environment:
      - CHOKIDAR_USEPOLLING=true

このファイルは、Docker Composeを使用してアプリケーションのサービス構成を定義するものです。アプリケーションのビルド、ポートの公開、ボリュームの設定などを管理します。

ステップ2: Reactアプリの作成

次に、以下のファイルをプロジェクトに追加します。

othello.js

このファイルでは、オセロゲームのロジックを実装しています。ボードの初期化、プレイヤーの手の管理、AIの動き、盤面の描画を担当しています。

src/othello.js
import React, { useState, useEffect } from 'react';
import './othello.css'; // CSSファイルをインポートしてスタイリングを適用

const BOARD_SIZE = 6; // オセロの6x6のボードサイズ

// ボードの初期化関数
const initializeBoard = () => {
  const board = Array(BOARD_SIZE).fill(null).map(() => Array(BOARD_SIZE).fill(null));
  // 初期配置
  board[2][2] = 'O';
  board[2][3] = 'X';
  board[3][2] = 'X';
  board[3][3] = 'O';
  return board; // 初期ボードを返す
};

// Reversiコンポーネント
const Reversi = () => {
  const [board, setBoard] = useState(initializeBoard); // ボードの状態を管理
  const [isPlayerTurn, setIsPlayerTurn] = useState(true); // プレイヤーのターンを管理

  // 駒をひっくり返すロジック
  const flipDiscs = (board, row, col, player) => {
    const opponent = player === 'X' ? 'O' : 'X'; // 相手の駒を決定
    const directions = [
      [0, 1], [1, 0], [0, -1], [-1, 0], 
      [1, 1], [1, -1], [-1, 1], [-1, -1] // ひっくり返す方向
    ];

    let flipped = []; // ひっくり返した位置を保持
    directions.forEach(([dx, dy]) => {
      let tempFlips = []; // 一時的にひっくり返す位置を保持
      let x = row + dx;
      let y = col + dy;

      // ひっくり返せる駒があるか確認
      while (x >= 0 && y >= 0 && x < BOARD_SIZE && y < BOARD_SIZE) {
        if (board[x][y] === opponent) {
          tempFlips.push([x, y]);
        } else if (board[x][y] === player && tempFlips.length > 0) {
          flipped = flipped.concat(tempFlips); // ひっくり返す位置を更新
          break;
        } else {
          break; // 無効な位置または自分の駒に遭遇した場合
        }
        x += dx;
        y += dy;
      }
    });

    // 駒をひっくり返す
    flipped.forEach(([x, y]) => {
      board[x][y] = player;
    });
    return flipped.length > 0 ? [...flipped, [row, col]] : []; // ひっくり返した位置を返す
  };

  // 駒を置く処理
  const makeMove = (row, col, player) => {
    const newBoard = board.map(row => [...row]); // ボードのコピーを作成
    const flipped = flipDiscs(newBoard, row, col, player); // 駒をひっくり返す

    // 駒をひっくり返せた場合、ボードを更新
    if (flipped.length) {
      flipped.forEach(([x, y]) => {
        newBoard[x][y] = player;
      });
      setBoard(newBoard); // 新しいボードを設定
      setIsPlayerTurn(!isPlayerTurn); // ターンを交代
    }
  };

  // AIの手を決定する処理
  const aiMove = () => {
    const availableMoves = []; // AIの利用可能な手を保持
    for (let i = 0; i < BOARD_SIZE; i++) {
      for (let j = 0; j < BOARD_SIZE; j++) {
        const newBoard = board.map(row => [...row]);
        if (newBoard[i][j] === null && flipDiscs(newBoard, i, j, 'O').length) {
          availableMoves.push([i, j]); // ひっくり返せる手を追加
        }
      }
    }
    // ランダムに手を選択して実行
    if (availableMoves.length) {
      const [row, col] = availableMoves[Math.floor(Math.random() * availableMoves.length)];
      setTimeout(() => {
        makeMove(row, col, 'O'); // 3秒後にAIの手を実行
      }, 3000);
    }
  };

  // プレイヤーのターンが終了したときにAIの手を実行
  useEffect(() => {
    if (!isPlayerTurn) {
      aiMove();
    }
  }, [isPlayerTurn]);

  // セルがクリックされたときの処理
  const handleClick = (row, col) => {
    if (board[row][col] === null && isPlayerTurn) {
      makeMove(row, col, 'X'); // プレイヤーの手を実行
    }
  };

  // ボードのレンダリング
  const renderBoard = () => {
    return (
      <div className="board">
        {board.map((row, i) => (
          <div key={i} className="row">
            {row.map((cell, j) => (
              <div
                key={j}
                onClick={() => handleClick(i, j)} // セルがクリックされたときの処理
                className={`cell ${cell ? cell : ''}`} // セルのクラスを設定
              >
                {cell} {/* 駒を表示 */}
              </div>
            ))}
          </div>
        ))}
      </div>
    );
  };

  return (
    <div className="container">
      <h1 className="App">オセロ (6x6)</h1>
      <p>プレイヤー: X | 対戦相手: O</p>
      {renderBoard()} {/* ボードを描画 */}
    </div>
  );
};

export default Reversi; // Reversiコンポーネントをエクスポート

othello.css

このファイルでは、オセロのゲームボードとプレイヤーの駒(XとO)のスタイルを定義しています。全体のレイアウトや色使いを設定し、視覚的な美しさを提供します。

src/othello.css
.App {
  text-align: center;
  font-family: 'Arial', sans-serif;
  background-color: #4CAF50; /* 背景色をオセロらしい緑色に */
  padding: 20px;
  border-radius: 10px;
  box-shadow: 0 0 10px rgba(0, 0, 0, 0.2); /* 影を少し強調 */
}

.container {
  margin: 30px auto;
  width: 360px; /* ボードサイズに合わせる */
}

.board {
  width: 100%; /* ボードの幅を親に合わせる */
  height: 100%; /* ボードの高さを親に合わせる */
  border: 5px solid #3f3f3f; /* ボードの境界線 */
  background-color: #008000; /* ボードの背景色 */
  display: flex;
  flex-direction: column; /* 列方向に配置 */
  box-sizing: border-box; /* ボードのサイズ計算をボーダーを含める */
  padding: 0; /* 内側の余白を無くす */
}

.row {
  display: flex;
  flex: 1; /* 各行が均等にスペースを取る */
}

.cell {
  width: 60px; /* セルの幅を固定 */
  height: 60px; /* セルの高さを固定 */
  display: flex;
  justify-content: center;
  align-items: center;
  border: 1px solid #3f3f3f; /* セルの境界線 */
  background-color: #008000; /* ボードのセル背景色をボードと統一 */
  cursor: pointer; /* クリック可能に */
  box-sizing: border-box; /* ボックスサイズをボーダーを含めたものに */
}

.cell.X {
  background-color: #333; /* プレイヤーXの色 */
  border-radius: 50%; /* 円形に */
  width: 50px; /* アイコンのサイズを固定 */
  height: 50px; /* アイコンのサイズを固定 */
}

.cell.O {
  background-color: #ddd; /* AIプレイヤーOの色 */
  border-radius: 50%; /* 円形に */
  width: 50px; /* アイコンのサイズを固定 */
  height: 50px; /* アイコンのサイズを固定 */
}

index.js

このファイルは、Reactアプリケーションのエントリーポイントです。Reversi コンポーネントをレンダリングします。

src/index.js
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import Reversi from './othello'; // 'App'を'othello'に変更

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <React.StrictMode>
    <Reversi />
  </React.StrictMode>
);

index.css

このファイルは、Reactアプリケーションのアプリケーションのスタイルシートになります。

index.css
/* src/index.css */
body {
  margin: 0;
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  background-color: #f5f5f5;
}

ステップ3: アプリの起動

最後に、以下のコマンドでアプリケーションを起動します。

# Docker Composeを使ってアプリケーションを起動
docker-compose up

ブラウザで、http://localhost:3000/を入力すると、以下のページが表示されていれば成功です。

スクリーンショット 2024-11-02 8.41.01.png

ここでは、プレイヤー: X | 対戦相手: O と表示されています。私がプレイヤーの黒色担当になりますので、駒をひっくり返してみます。

オセロデモ.gif

想定通り、私が選択した手に対してAIが相手をしていることが確認できました。

まとめ

本記事では、Dockerを利用してReactでオセロアプリを構築する方法を紹介しました。

アプリはプレイヤーとAIが対戦できる機能を持っており、シンプルなインターフェースでプレイできます。

今後はさらなる機能追加やデザインの改善を行なっていきますが、まずは自分自身が楽しんで開発を続けていきたいと思います。

参考記事

4
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?