2022年5月13日
プログラミング
Reactでオセロゲームを作る
はじめに
今回はReactで6×6のオセロゲームを作成していきます!
ゴールとしてはマスが全て埋まる。もしくはお互いに石を置けなくなったらゲーム終了です。
最終的なものが以下となります。
オセロのロジックをクラス構文で実装
Othelloクラスではオセロに関するものを実装していきます。
Othelloクラスの責務
- オセロの盤面を生成する
- プレイヤーが選択した場所に石が置けるかの判定をする
- 選択した箇所に石が置ける場合石を置く。その時に対戦相手の石を挟んだらその石を返す。
オセロの盤面を生成する
まずは二次元配列で盤面を生成し、オセロのゲーム開始の状態として中央に石を配置します。
ここではx(プレイヤー)とo(人工無能)で指定します。
// othello.js
export class OthelloBoard {
constructor() {
this.board = Array.from(new Array(6), () =>
new Array(6).fill(0).map(() => {
return null;
})
);
this.board[2][2] = "o";
this.board[2][3] = "x";
this.board[3][2] = "x";
this.board[3][3] = "o";
}
}
const othello = new CreateBoard();
console.log(othello);
こちらを出力すると盤面が作成されていることが確認できます。
それぞれのマスで対戦相手のどの石が返るのかを判定する
引数として(高さの位置、横の位置、チェックする石、オセロボード)を受け取り、返り値として選択した箇所に石を置いた場合に帰る相手の位置を配列で返します。
directionsという変数には石を置いた箇所からの8方向のチェックを行います。
// othello.js
//別ファイルで定義しているのでimportする
import { crossCheck } from "./crossCheck";
checkStone(yIndex, xIndex, player, board = this.board) {
//チェックする方向
const directions = [
[0, 1], // 右
[0, -1], // 左
[-1, 0], // 上
[1, 0], // 下
[-1, -1], // 左上
[1, 1], // 左下
[-1, 1], // 右上
[1, -1], // 右下
];
const change = [];
directions.forEach((el) => {
// 選択した箇所で相手の石を返せそうならその位置を配列に入れる
const result = crossCheck(board, { yIndex, xIndex }, el[0], el[1], player);
change.push(...result);
});
return change;
}
次に選択した箇所でどの石が返るのかを取得します。
引数として(オセロボード, 選択した石のY軸とX軸, Y軸の進む方向, X軸の進む方向, どの石か)を受け取り、返り値として石を置いた場合に変える相手の石の位置を返します。
//crossCheck.js
export const crossCheck = (board, currentPosition, yAxis, xAxis, player) => {
const change = [];
// 石を置いた箇所からチェックを進めていく時にboardの端までチェックし終えたらチェックする処理を終了する
if (
currentPosition.yIndex + yAxis > 5 ||
currentPosition.yIndex + yAxis < 0 ||
currentPosition.xIndex + xAxis > 5 ||
currentPosition.xIndex + xAxis < 0
) {
return change;
}
// 石が次に進んでいく方向
const nextPosition =
board[currentPosition.yIndex + yAxis][currentPosition.xIndex + xAxis];
// 石が次に進んでいく方向 にマスがない場合か、石が置いてある場合はチェックを終了する
if (!nextPosition || board[currentPosition.yIndex][currentPosition.xIndex]) {
return change;
}
// 石が次に進んでいく方向を最初は1つ隣、次のチェックで2つ隣となるので何個先をチェックするのか更新していく
let _yAxis = yAxis;
let _xAxis = xAxis;
const total = [];
// チェックする方向に置いた石と違う石があれば繰り返し処理をする
while (board[currentPosition.yIndex + _yAxis][currentPosition.xIndex + _xAxis] !== player ) {
// チェックする方向に石があるか、何も置いてない場合は石が置けないのでチェックを終了する
if (
board[currentPosition.yIndex + _yAxis][currentPosition.xIndex + _xAxis] === player ||
board[currentPosition.yIndex + _yAxis][currentPosition.xIndex + _xAxis] == null
) {
total.splice(0);
return total;
}
// // 置いた時に返る石を追加
total.push([currentPosition.yIndex + _yAxis, currentPosition.xIndex + _xAxis]);
// チェックを終えたらy軸とx軸を更新する
_yAxis += yAxis;
_xAxis += xAxis;
// 石を置いた箇所からチェックを進めていく時にboardの端までチェックし終えたらチェックする処理を終了する
if (
board.length <= currentPosition.yIndex + _yAxis || currentPosition.yIndex + _yAxis < 0 ||
board.length <= currentPosition.xIndex + _xAxis
) {
total.splice(0);
return total;
}
}
// 選択した箇所で相手の返る位置をchaneにpushする
change.push(...total);
return change;
};
石を置ける場所を受け取る
ここでは引数で石を受け取り、返り値として置ける箇所全てを配列で返します。
この処理は石が置けるマスの色をつける時に使います。
OthelloBoard内に追加していきます。
// othello.js
isPutPosition(player) {
const putList = [];
this.board.forEach((colItem, colIndex) => {
colItem.forEach((el, rowIndex) => {
const checkPosition = this.checkStone(colIndex, rowIndex, player);
// 石を置いた時に1つも返らなければreturn
if (checkPosition.length === 0) {
return;
}
// 1つ以上石を返せれば配列にその位置のY軸とX軸を入れる
putList.push([colIndex, rowIndex]);
});
});
return putList;
}
実際に石を置く
石を置く時はオセロボードのマスをクリックして選択します。
引数には(Y軸の位置、X軸の位置、どちらの石か)を受け取り、石が置けるかどうかを判定します。
// othello.js
putStone(yIndex, xIndex, player) {
// 既に石が置いてあれば処理を終了
if (this.board[yIndex][xIndex]) {
console.log("すでに石が置いてあります");
return;
}
//判定
const willBeReturned = this.checkStone(yIndex, xIndex, player);
// 1つも石を返せなければ処理を終了
if (willBeReturned.length === 0) {
console.log("石を置けません");
return;
}
// 問題なければ石を置く
this.board[yIndex][xIndex] = player;
// 置いた石との間にある石を返す
for (let i = 0, l = willBeReturned.length; i < l; i++) {
this.board[willBeReturned[i][0]][willBeReturned[i][1]] = player;
}
return true;
}
オセロの各コンポーネントの実装
現在オセロボードが二次元配列として確認することはできますが、画面上で実際のオセロのように表示されるように実装していきます。
各マス目のコンポーネント
propsはvalue(マスの状態)、isPutStone(石が置いてあるかどうか)、onClick(クリックイベント)、disabled(石が置けるかどうか)となります。
// square.js
export const Square = ({ value, isPutStone, onClick, disabled }) => {
return (
<button classname="{isPutStone" ?="" "put-square"="" :="" "square"}="" onclick="{onClick}" disabled="{disabled}">
<div classname="{value" =="=" "x"="" ?="" "player"="" :="" value="==" "o"="" "opponent"="" null}=""></div>
</button>
);
};
オセロボードのX軸のコンポーネント
propsはarray(X軸の状態)、board(オセロボード)、row(Y軸の位置)、isPutStone(石が置いてあるかどうか)、onClick(クリックイベント)、disabled(石が置けるかどうか)となります。
// row.js
// Squareをimportする
import { Square } from "./square";
export const Row = ({ array, board, row, isPutStone, onClick, disabled }) => {
return (
<div classname="row">
{array.map((index) => {
let isPut;
// 石が置けるかをチェックする
for (const el of isPutStone) {
isPut = el[0] === row && el[1] === index;
if (isPut) break;
}
return (
<square key="{index}" value="{board[row][index]}" isputstone="{isPut}" putposition="{[row," index]}="" onclick="{()" ==""> onClick(row, index)}
disabled={disabled}
/>
);
})}
</square>
</div>
);
};
オセロボードのコンポーネント
オセロボードのマスとX軸のコンポーネントを作成したらオセロボードを実際に作成します。
// board.js
import React, { useState } from "react";
import { Row } from "./row";
import { OthelloBoard } from "./othello";
import { opponentSelect } from "../utils/opponentSelect";
import { selectPosition } from "../utils/SelectPosition";
const othello = new OthelloBoard(); //OthelloBoardクラスのインスタンスを生成
export const Board = () => {
const [othelloBoard, setOthelloBoard] = useState(othello.board); // オセロボードの状態をstateで管理する
const [isDisabled, setIsDisabled] = useState(false); // 対戦相手が石を置くまで操作できないようにする
const rowArr = [0, 1, 2, 3, 4, 5]; // オセロのX軸のindex
const player = "x"; // プレイヤー
const opponent = "o"; // 対戦相手
//マスをクリックした時の処理
function clickSquare(row, index) {
// 石が置ける場所をチェックする
const putPositionArr = othello.isPutPosition(player); / /プレイヤーが石を置ける箇所を取得
const isPut = othello.putStone(row, index, player); // 実際に石を置く
const newArray = [...othelloBoard]; // newArrayに変更を代入する
setOthelloBoard(newArray); //変更された内容をsetOthelloBoardで更新する
}
return (
<div classname="container">
<div classname="board">
{rowArr.map((index) => {
return (
<row key="{index}" array="{rowArr}" board="{othelloBoard}" row="{rowArr[index]}" isputstone="{putPositionArr}" onclick="{clickSquare}" disabled="{isDisabled}">
);
})}
</row></div>
</div>
);
};
対戦相手の動作を実装
現状では1度石を置くと挟んだ石は返りますが、相手が石を置くこともなくゲームが進みません。
これから対戦相手の動作を実装していきます
石を置く位置の優先順位を決める
できるだけ対戦相手が強くなるように以下のルールで選択されるようにします。
- できるだけ角を選択する
- 角を取られたくないので角に隣接する箇所にはなるべく置かない
- 角に隣接する部分も含めたもの
// opponentSelect.js
// boardの角の座標
const angle = [[0, 0], [0, 5], [5, 0], [5, 5]];
// boardの角に隣接する位置の座標
const angleAdjacent = [[0, 1], [1, 0], [1, 1], [0, 4], [1, 4], [1, 5], [4, 0], [4, 1], [5, 1], [4, 4], [4, 5], [5, 5]];
// 一致した時
function someElement(arr, opponentPutArr) {
return core(arr, opponentPutArr, true);
}
// 一致していない時
function differentElement(arr, opponentPutArr) {
return core(arr, opponentPutArr, false);
}
// 石が置ける場所をチェックする
function core(positionArr, opponentPutArr, match) {
let _result = [];
// 一致したもの配列に入れる
const matchArr = [];
opponentPutArr.forEach((el) => {
positionArr.forEach((item) => {
const isMatch = el[0] === item[0] && el[1] === item[1];
if (isMatch) {
matchArr.push(el);
}
});
if (match) {
_result = matchArr;
} else {
const isInclude = matchArr.includes(el);
if (!isInclude) {
_result.push(el);
}
}
});
return _result;
}
// 引数にはopponent(相手の石)とopponentPutArr(全ての石を置ける箇所)を受け取ります。
export const opponentSelect = (opponent, opponentPutArr) => {
// 角に石を置けるかのチェック
const anglePutArr = someElement(angle, opponentPutArr);
// 角に石を置ける場合は処理を終了する
if (anglePutArr.length !== 0) {
return anglePutArr;
}
// 角に隣接しない箇所があるかをチェック
const notAngleAdjacent = differentElement(angleAdjacent, opponentPutArr);
// 角に隣接しない箇所ある場合は処理を終了する
if (notAngleAdjacent.length !== 0) {
return notAngleAdjacent;
}
// 角に隣接している部分も含めて返す
return opponentPutArr;
};
返せる石の数が多いものを選択する
石を置く箇所を優先順位をつけて取得した箇所が複数あった場合は、できるだけ多くの石を返せる箇所を選択します。
引数はarrPosition(石を置ける箇所), opponent(対戦相手の石), checkStone(OthelloBoardのcheckStoneメソッド), board(オセロボード)を受け取ります。
// selectPosition.js
export const selectPosition = (arrPosition, opponent, checkStone, board) => {
let _returnLength = 0; // 返す石の数
let opponentPutItem;
// 石を置ける箇所の数だけチェックする
for (let i = 0; i < arrPosition.length; i++) {
const checkPosition = checkStone(arrPosition[i][0], arrPosition[i][1], opponent, board);
// 石を置いた時に返せる石の数が多い箇所をopponentPutItemに代入する
if (checkPosition.length > _returnLength) {
_returnLength = checkPosition.length;
opponentPutItem = [arrPosition[i][0], arrPosition[i][1]];
}
}
// 石を多く返せる位置を返す
return opponentPutItem;
};
board.jsで対戦相手の動作を実装
プレイヤーが石を置いたら対戦相手が石を置けるようにします。
clickSquareを実行後、wait関数を作って石を置いた1秒後に対戦相手が石と置けるようにします。
// board.js
// 1000ms待つ処理
function wait() {
return new Promise((resolve) => {
setTimeout(function () {
resolve();
}, 1000);
});
}
// 非同期処理なのでasyncをつける
async function clickSquare(row, index) {
const isPut = othello.putStone(row, index, player);
const newArray = [...othelloBoard];
setOthelloBoard(newArray);
// 石を置ける箇所がなければ処理を終了する
if (!isPut) {
return;
}
setIsDisabled(true); // 相手が石を置くまでマスをクリックできないようにする
await wait(); // 1秒待つ
const opponentPutArr = othello.isPutPosition(opponent); // 対戦相手の石を置ける箇所を取得
const putPosition = opponentSelect(opponent, opponentPutArr); // 石を置く箇所の優先順位を決める
const select = selectPosition(putPosition, opponent, othello.checkStone, othello.board); // より多く返せる箇所を選択
othello.putStone(select[0], select[1], opponent); // 対戦相手の石を置く
setIsDisabled(false); // マスをdisabledを解除
const _newArray = [...othelloBoard];
setOthelloBoard(_newArray);
}
CSSで見た目を整える
// style.css
.App {
text-align: center;
}
.container {
margin: 30px auto;
width: 360px;
}
.board {
width: 100%;
}
.row {
display: flex;
}
.square {
width: 60px;
height: 60px;
line-height: 30px;
background-color: darkgreen;
padding: 0;
}
.put-square {
width: 60px;
height: 60px;
line-height: 30px;
background-color: green;
}
.player {
background-color: black;
width: 50px;
height: 50px;
border-radius: 50%;
margin: 0 auto;
}
.opponent {
background-color: white;
width: 50px;
height: 50px;
border-radius: 50%;
margin: 0 auto;
}
.othello-state {
text-align: left;
display: flex;
background-color: darkgray;
margin-top: 24px;
padding: 8px 16px;
font-size: 24px;
}
.result-state {
display: flex;
align-items: center;
margin-right: 24px;
}
.player-icon {
background-color: black;
width: 25px;
height: 25px;
border-radius: 50%;
margin-right: 8px;
}
.opponent-icon {
background-color: white;
width: 25px;
height: 25px;
border-radius: 50%;
margin-right: 8px;
}
まとめ
今回は実際にオセロゲームとして楽しめるレベルところまでは実装しました
実装を進めていく中でのクラス構文や非同期処理などの使い方も復習できてよかったです。
JavaScriptのお仕事に関するご相談
Bageleeの運営会社、palanではJavaScriptに関するお仕事のご相談を無料で承っております。
zoomなどのオンラインミーティング、お電話、貴社への訪問、いずれも可能です。
ぜひお気軽にご相談ください。
この記事は
参考になりましたか?
3
2
関連記事
2018年7月6日
【js】jQueryでできるハンバーガーメニューの作成
2018年6月18日
プログレスバーを簡単に実装できるprogressbar.js
2018年5月8日
画像に簡単にアニメーションをつけるcurtain.js
2018年2月14日
【js】jQueryでできるアコーディオンメニューの作成
2017年9月4日
【JS】スクロールした時にCSSanimationを発動させる「scrollMonitor」
簡単に自分で作れるWebAR
「palanAR」はオンラインで簡単に作れるWebAR作成ツールです。WebARとはアプリを使用せずに、Webサイト上でARを体験できる新しい技術です。
palanARへpalanでは一緒に働く仲間を募集しています
正社員や業務委託、アルバイトやインターンなど雇用形態にこだわらず、
ベテランの方から業界未経験の方まで様々なかたのお力をお借りしたいと考えております。
運営メンバー
Eishi Saito 総務
SIerやスタートアップ、フリーランスを経て2016年11月にpalan(旧eishis)を設立。 マーケター・ディレクター・エンジニアなど何でも屋。 COBOLからReactまで色んなことやります。
sasakki デザイナー
アメリカの大学を卒業後、日本、シンガポールでデザイナーとして活動。
やまかわたかし デザイナー
フロントエンドデザイナー。デザインからHTML / CSS、JSの実装を担当しています。最近はReactやReact Nativeをよく触っています。
Sayaka Osanai デザイナー
Sketchだいすきプロダクトデザイナー。シンプルだけどちょっとかわいいデザインが得意。 好きな食べものは生ハムとお寿司とカレーです。
はらた エンジニア
サーバーサイドエンジニア Ruby on Railsを使った開発を行なっています
こぼり ともろう エンジニア
サーバーサイドエンジニア。SIerを経て2019年7月に入社。日々学習しながらRuby on Railsを使った開発を行っています。
ささい エンジニア
フロントエンドエンジニア WebGLとReactが強みと言えるように頑張ってます。
Damien
WebAR/VRの企画・開発をやっています。森に住んでいます。
ゲスト bagelee
かっきー
まりな
suzuki
miyagi
ogawa
雑食デザイナー。UI/UXデザインやコーディング、時々フロントエンドやってます。最近はARも。
いわもと
デザイナーをしています。 好きな食べ物はラーメンです。
taishi kobari
フロントエンドの開発を主に担当してます。Blitz.js好きです。
kubota shogo
サーバーサイドエンジニア。Ruby on Railsを使った開発を行いつつ月500kmほど走っています!
nishi tomoya
aihara
グラフィックデザイナーから、フロントエンドエンジニアになりました。最近はWebAR/VRの開発や、Blender、Unityを触っています。モノづくりとワンコが好きです。
nagao
SIerを経てアプリのエンジニアに。xR業界に興味があり、unityを使って開発をしたりしています。
Kainuma
サーバーサイドエンジニア Ruby on Railsを使った開発を行なっています
sugimoto
asama
ando
iwasawa ayane
oshimo
異業界からやってきたデザイナー。 palanARのUIをメインに担当してます。 これからたくさん吸収していきます!