はじめに
本記事では、Docker環境でReactを使用してオセロ(リバーシ)アプリを作成する手順を説明します。このアプリは、6x6のボードでプレイヤーとbotが対戦できるように設計しています。
Reactを用いたフロントエンド開発と、Dockerによるアプリケーションのコンテナ化により、効率的な開発環境を管理します。
本記事はITスクールの課題としてアウトプット学習を目的に作成したもので、個人の備忘録レベルの内容ですので、ご了承ください。
必要なツール
私の環境では、M1チップ搭載のMacOSを使用していますが、今回のハンズオンに必要なツールは以下の通りです。
・Docker
・Docker Compose
・Node.js(バージョン14以上推奨)
・npm(Node Package Manager)
作成したオセロアプリのデモ動画
今回のReactアプリには、botとの対戦機能が追加されています。実際の対戦デモ動画になります。
初めて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
このファイルでは、オセロゲームのロジックを実装しています。ボードの初期化、プレイヤーの手の管理、botの動き、盤面の描画を担当しています。
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
と表示されています。私がプレイヤーの黒色担当になりますので、駒をひっくり返してみます。
想定通り、私が選択した手に対してbotが相手をしていることが確認できました。
まとめ
本記事では、Dockerを利用してReactでオセロアプリを構築する方法を紹介しました。
アプリはプレイヤーとbotが対戦できる機能を持っており、シンプルなインターフェースでプレイできます。
今後はさらなる機能追加やデザインの改善を行なっていきますが、まずは自分自身が楽しんで開発を続けていきたいと思います。
参考記事