はじめに
本記事では、Docker環境でReactを使用してオセロ(リバーシ)アプリを作成する手順を説明します。このアプリは、6x6のボードでプレイヤーとAIが対戦できるように設計しています。
Reactを用いたフロントエンド開発と、Dockerによるアプリケーションのコンテナ化により、効率的な開発環境を管理します。
現在、通っているITスクールの課題の一環としてアウトプット学習を実施しており、今後はフロントエンドおよびバックエンドに関する記事が多くなる予定です。
必要なツール
私の環境では、M1チップ搭載のMacOSを使用していますが、今回のハンズオンに必要なツールは以下の通りです。
・Docker
・Docker Compose
・Node.js(バージョン14以上推奨)
・npm(Node Package Manager)
作成したオセロアプリのデモ動画
今回のReactアプリには、AIとの対戦機能が追加されています。実際の対戦デモ動画になります。
初めて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
を作成します。
version: '3'
services:
app:
build: .
ports:
- "3000:3000"
volumes:
- .:/app
environment:
- CHOKIDAR_USEPOLLING=true
このファイルは、Docker Composeを使用してアプリケーションのサービス構成を定義するものです。アプリケーションのビルド、ポートの公開、ボリュームの設定などを管理します。
ステップ2: Reactアプリの作成
次に、以下のファイルをプロジェクトに追加します。
othello.js
このファイルでは、オセロゲームのロジックを実装しています。ボードの初期化、プレイヤーの手の管理、AIの動き、盤面の描画を担当しています。
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)のスタイルを定義しています。全体のレイアウトや色使いを設定し、視覚的な美しさを提供します。
.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 コンポーネントをレンダリングします。
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アプリケーションのアプリケーションのスタイルシートになります。
/* 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/
を入力すると、以下のページが表示されていれば成功です。
ここでは、プレイヤー: X | 対戦相手: O
と表示されています。私がプレイヤーの黒色担当になりますので、駒をひっくり返してみます。
想定通り、私が選択した手に対してAIが相手をしていることが確認できました。
まとめ
本記事では、Dockerを利用してReactでオセロアプリを構築する方法を紹介しました。
アプリはプレイヤーとAIが対戦できる機能を持っており、シンプルなインターフェースでプレイできます。
今後はさらなる機能追加やデザインの改善を行なっていきますが、まずは自分自身が楽しんで開発を続けていきたいと思います。
参考記事