ロゴシュミグラム

Javascriptによるオセロの作り方
【オブジェクト指向編】その1

このオセロの特徴

  • 🤼複数人で対戦可能
  • 🛠️マス目の形を自由に作れる
  • 🧑‍🤝‍🧑「ヒトとCOMの混合チーム vs COMチーム」のようなチーム編成もできる
  • 📝必要なのはHTML、CSS、Javascriptのみ(画像もライブラリも必要なし!)

このプログラムについて

Javascriptを使いオブジェクト指向で作成しています。なるべくシンプルでわかりやすくするためカプセル化などの技術は使用していません。

このゲームではcanvas要素に絵を描くのではなく、HTML要素とCSSを用いてオセロを表現しています。こうすることでCSSをフルに活用でき、石を裏返すアニメーションなども簡単に実装することができます。

対戦人数やマス目の形、コンピュータの戦略などを簡単に設定できるようにしているので、自分だけのオリジナルのオセロを作ることができます。

必要なプログラミング知識

HTMLとCSSの基礎
Javascriptの基礎(DOMの操作方法やasyncの使い方など)
オブジェクト指向の基礎(クラスやインスタンスの作り方など)

オセロのパーツの呼び方と対応するクラス

各パーツの呼び方は次のようにします。英語名はクラス名や変数名です。

パーツの呼び方

その他のクラスとして次のようなものがあります。

クラス名説明
Gameゲームを進行するクラス
Message「PASS」などのメッセージを表示するクラス
Playerゲームプレイヤーのクラス
Stateターン数など現在のゲームの状態を管理するクラス
Strategyコンピュータの戦略を集めたクラス

ファイルの作成

次のようにreversiフォルダを作成し、その中にHTMLファイル、CSSファイル、JSファイルを作成します。


reversi
┣━ index.html
┣━ styles.css
┣━ Board.js
┣━ Color.js
┣━ Game.js
┣━ Information.js
┣━ main.js
┣━ Message.js
┣━ Player.js
┣━ settings.js
┣━ Square.js
┣━ State.js
┣━ Stone.js
┣━ Strategy.js
┗━ utils.js

HTMLの作成

index.htmlに次のように書き込みます。

index.html

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>マルチ オセロ</title>
<link rel="stylesheet" type="text/css" href="styles.css">
<script defer src="settings.js"></script>
<script defer src="utils.js"></script>
<script defer src="Color.js"></script>
<script defer src="Board.js"></script>
<script defer src="Game.js"></script>
<script defer src="Information.js"></script>
<script defer src="Message.js"></script>
<script defer src="Player.js"></script>
<script defer src="Square.js"></script>
<script defer src="State.js"></script>
<script defer src="Stone.js"></script>
<script defer src="Strategy.js"></script>
<script defer src="main.js"></script>
</head>
<body>
<div id="reversi">
<!-- 情報板 -->
<div id="information">
<table>
<thead>
<tr>
<th></th>
<th></th>
<th></th>
<th class="num-area">石の数</th>
<th class="num-area">勝ち数</th>
</tr>
</thead>
<tbody id="information-body">
</tbody>
</table>
</div>
<!-- 盤面 -->
<div id="board">
</div>
</div>
<!-- メッセージ -->
<div id="message">
</div>
</body>
</html>

head要素内のscriptは settings.js が最初で、 main.js が最後になるようにしてください。また、scriptに defer を付けないとHTML解析前にJavascriptを実行してしまうためエラーとなります。

盤面やメッセージはJavascriptで動的に作成していくので空白のままです。

CSSの作成

次にstyles.cssに次のように書き込みます。

styles.css

body {
margin: 0;
/* 背景の模様 */
background: repeating-linear-gradient(-45deg, #E2E8F0, #E2E8F0 8px, #EDF2F7 8px, #EDF2F7 10px);
}
#reversi {
display: flex;
flex-direction: column;
align-items: center;
}

情報板と盤面を縦に並べて中央に寄せるようにしています。

styles.cssにはこの後もいくつか追加していきます。

ゲームの初期設定

settings.jsに次のようなゲームの設定を書いていきます。

変数名説明
colorList石の色とその色を操作するプレイヤーの配列
initialBoard盤面の初期状態
GAME_SPEEDメッセージの表示速度などゲーム進行の速さ
MAX_COM_WAITING_TIMEコンピュータの最大待機時間(ミリ秒単位)
PASS_MESSAGE_TIMEメッセージで「PASS」と表示し続ける時間
MESSAGE_DURATION_TIMEメッセージ表示のアニメーションにかかる時間
FLIP_INTERVAL_TIME石を裏返すタイミングの間隔
FLIP_DURATION_TIME石が完全に裏返るまでにかかる時間

色と参加プレイヤーの設定

settings.js

/**
* 石の色とその色を操作するプレイヤーを配列に設定する。
* この配列の順番で手番が回ってくる
*/
const colorList = [
{
colorCode: 'white',
members: [{ type: 'human' }]
},
{
colorCode: 'black',
members: [{ type: 'computer', level: 1 }]
},
{
colorCode: 'orange',
members: [{ type: 'computer', level: 1 }]
},
{
colorCode: 'blue',
members: [{ type: 'computer', level: 2 }]
},
{
colorCode: 'purple',
members: [{ type: 'computer', level: 2 }]
},
];

colorList には石の色を書きます。 'white''#ffffff' など、CSSで認識できる形式で書くようにしてください。

members にはこの色を操作できるプレイヤーを書きます。ヒトが操作する場合は type: 'human' にします。

コンピュータが操作する場合は type: 'computer' にして、強さを level に設定します。 level は1と2から選択可能です(あとあと独自にlevelを追加することももできます)

1つの色に複数人のプレイヤーを書くこともできるので、ヒトとコンピュータの混合チームなども作成できます。

盤面の初期配置

盤面の初期配置を二次元配列に数字で書いていきます。

数字説明
-2何もない空間
-1石の置いていないマス目
0以上石の置いてあるマス目(色をcolorListの配列番号で指定する)
settings.js

/**
* initialBoardに盤面の初期配置を数字で書いていく
* -2:何もない空間
* -1:石の置いていないマス目
* 0以上:石の置いてあるマス目(色をcolorListの配列番号で指定する)
*/
const initialBoard = [
[-1, -1, -1, -1, -1, -1, -1, -1, -1],
[-1, -1, -2, -1, -1, -1, -2, -1, -1],
[-1, -2, -1, +0, +1, +2, -1, -2, -1],
[-1, -1, -1, +1, +2, +3, -1, -1, -1],
[-1, -1, -1, +2, +3, +4, -1, -1, -1],
[-1, -1, -1, +3, +4, +0, -1, -1, -1],
[-1, -2, -1, +4, +0, +1, -1, -2, -1],
[-1, -1, -2, -1, -1, -1, -2, -1, -1],
[-1, -1, -1, -1, -1, -1, -1, -1, -1],
];

たとえば数字が「+0」の場所は、 colorList[0] の色の石が置いてあることを意味します。
0以上の数字に+の符号が付いている理由は石の場所をわかりやすくするためで、付けなくてもかまいません。

時間の設定

settings.js

/**
* メッセージの表示速度やコンピュータの待機時間などの調整をする
* 基準は1で0に近づくほど速度が速くなり、0にすると瞬時に動作するようになる
*/
const GAME_SPEED = 1;
/**
* コンピュータの最大待機時間(ミリ秒単位)
*/
const MAX_COM_WAITING_TIME = 2000 * GAME_SPEED;
/**
* メッセージで「PASS」と表示し続ける時間(ミリ秒単位)
*/
const PASS_MESSAGE_TIME = 1000 * GAME_SPEED;
/**
* メッセージ表示のアニメーションにかかる時間(ミリ秒単位)
*/
const MESSAGE_DURATION_TIME = 500 * GAME_SPEED;
/**
* 石を裏返すタイミングの間隔(ミリ秒単位)
*/
const FLIP_INTERVAL_TIME = 80 * GAME_SPEED;
/**
* 石が完全に裏返るまでにかかる時間(ミリ秒単位)
*/
const FLIP_DURATION_TIME = 400 * GAME_SPEED;

MAX_COM_WAITING_TIME はコンピュータの手番になったときに待機する最大時間です。これがないとコンピュータの手番になるとすぐに石を置いてしまいます。

0ミリ秒~MAX_COM_WAITING_TIME の間でランダムに待機させることでコンピュータが考えているような演出ができます。

また、上記のような変数名や関数名の上に「/***/」を使ってコメントを書く方法をJSDocといいます。このように書くことでVSCodeなどのエディターで変数や関数を使用するときに説明文も表示されるので便利です。

ユーティリティ関数の作成

まずはいろんな場所で使う便利な関数(ユーティリティ関数)を作っておきます。

名前説明
pickRandom(array)配列の中から1つの要素をランダムに取り出す
forEachNest(twoDimensionalArray, callback)二次元配列をforEachで回していく
sleep(milliseconds)引数で渡した時間(ミリ秒)だけ待機する

utils.jsに次のように書いていきます。

utils.js

/**
* 配列の中から1つの要素をランダムに取り出す
*/
const pickRandom = (array) => {
// ランダムな配列番号を選ぶ
const selectedIndex = Math.floor(Math.random() * array.length);
return array[selectedIndex];
};
/**
* 二次元配列をforEachで回していく
* callback関数の引数には二次元配列の要素、行番号、列番号を受け取る
*/
const forEachNest = (twoDimensionalArray, callback) => {
twoDimensionalArray.forEach((rowArray, rowIndex) => {
rowArray.forEach((item, columnIndex) => {
callback(item, rowIndex, columnIndex);
});
});
};
/**
* 引数で渡した時間(ミリ秒)だけ待機する
*/
const sleep = (milliseconds) => {
return new Promise((resolve) => {
setTimeout(resolve, milliseconds);
});
};

sleepPromise を使った非同期処理となっています。たとえば1000ミリ秒待機させたい場合は次のように使用します。

sleep関数の使用例

const someFunction = async () => {
...
await sleep(1000);
...
};

色を管理するColorクラスの作成

Colorクラスはある色の石の数や勝ち数を管理するクラスです。

Colorクラスには次のようなプロパティやメソッドがあります。

名前説明
constructor(colorCode)コンストラクターにカラーコードと配列番号を受け取る
colorCodeこの色のカラーコード
membersこの色の石を操作できるプレイヤーの配列
stoneCountこの色の石の数
winCountこの色が勝利した回数
getCurrentPlayer()membersの中で現在のプレイヤーを返す

Color.jsに次のように書き込みます。

Color.js

class Color {
constructor(colorCode) {
this.colorCode = colorCode;
this.members = [];
this.stoneCount = 0;
this.winCount = 0;
}
getCurrentPlayer () {
// 現在が何巡目かを計算する。最初の巡目は0巡目となる
// たとえば、石の色の数が3種類、ターン数が4の場合,
// 7/3=1.33から小数点を切り捨てて、現在は1巡目となる
const round = Math.trunc(state.turnCount / state.colors.length);
// 巡目数をこの色のメンバー数で割った余りが現在のプレイヤーの配列番号となる
// たとえば、メンバー数が2人で5巡目の場合、,
// 5÷2の余りは1となりthis.members[1]が現在のプレイヤーとなる
return this.members[round % this.members.length];
}
}

プレイヤーとなるPlayerクラスの作成

Playerクラスには次のようなプロパティやメソッドがあります。

名前説明
type"human"または"computer"
colorColorオブジェクト
levelコンピュータのレベル
isHuman()ヒトであるか否かを判別する
isComputer()コンピュータであるか否かを判別する
isAlive()このプレイヤーの石が盤面に残っているか否かを判別する
act()コンピュータに石を置くマス目を選択させる

Player.jsに次のように書き込みます。

Player.js

class Player {
constructor(type, color, level = 1) {
this.type = type;
this.color = color;
this.level = level;
}
isHuman () {
return this.type === 'human';
}
isComputer () {
return this.type === 'computer';
}
isAlive () {
return this.color.stoneCount >= 1;
}
async act () {
// 0~MAX_COM_WAITING_TIMEミリ秒の間でランダムに待機させる
// こうすることでコンピュータが思考しているような演出ができる
await sleep(Math.random() * MAX_COM_WAITING_TIME);
// 石を置くマス目を選ぶ
// getSquareはマス目(Squareオブジェクト)を返す
const selectedSquare = Strategy.getSquare(this.level);
// 石を置くマス目を選択する
selectedSquare.select();
}
}

ゲーム状態を管理するStateクラスの作成

Stateクラスはゲームの状態を管理するクラスです。

Stateクラスには次のようなプロパティやメソッドがあります。

名前説明
colorsColorオブジェクトの配列
turnCount現在のターン数。次のプレイヤーの番になるとターン数が増える
currentPlayer手番となったPlayerオブジェクト
selectableSquares石を置くことができるマス目(Squareオブジェクト)の配列
sandwichedStones自分の石で挟み込んだ石(Stoneオブジェクト)の配列
passCount連続してパスした回数
emptySquareCount空のマス目の個数
mostColors最も石の数が多いColorオブジェクトの配列
init()状態を初期化する
turnCountUp()ターン数を増やして現在のプレイヤーを変える
increasePassCount()一つの色で石が統一されたか否かを判別する
isUnified()ゲーム終了の状態か否かを判別する
isGameover()どの色の石が最も多いかチェックする

State.jsに次のように書き込みます。

State.js

class State {
constructor() {
this.emptySquareCount = 0;
// colorListからColorオブジェクトの配列を作る
this.colors = colorList.map(({ colorCode, members }, i) => {
const color = new Color(colorCode, i);
members.forEach(({ type, level }) => {
const player = new Player(type, color, level);
// この色のメンバーにプレイヤーを追加する
color.members.push(player);
});
return color;
});
// 状態を初期化する
this.init();
}
init () {
this.turnCount = 0;
this.currentPlayer = this.colors[0].members[0];
this.selectableSquares = [];
this.sandwichedStones = [];
this.passCount = 0;
this.mostColors = [];
}
turnCountUp () {
this.turnCount++;
// 現在のターン数を石の色の種類数で割った余りが現在の色の配列番号となる
// たとえば、ターン数が4、石の色が3種類の場合、
// 4÷3の余りが1のため、this.colors[1]が石の色となる
const currentColor = this.colors[this.turnCount % this.colors.length];
// Colorオブジェクトから現在のプレイヤーを取得する
this.currentPlayer = currentColor.getCurrentPlayer();
}
isUnified () {
// 盤面に残っている色のColorオブジェクトを配列として取得する
let existColors = this.colors.filter((color) => color.stoneCount >= 1);
// もし盤面に残っている石の色が1種類だけなら、その色で統一したことになる
return existColors.length === 1;
}
isGameover () {
// 次のような状態はゲーム終了とみなされる
// 空のマス目が無い状態、
// またはすべてのプレイヤーがパスした状態、
// または盤面に残っている石の色が1種類だけの状態
return this.emptySquareCount === 0
|| this.passCount === this.colors.length
|| this.isUnified();
}
checkMostColors () {
// 各色の石の数の配列を作る
// たとえば、[22,17,5,...]のような配列になる
const stoneCountList = this.colors.map((color) => color.stoneCount);
// 配列の中から最大値を得る
let maxStoneCount = Math.max(...stoneCountList);
// 最も石の数が多かったColorオブジェクトの配列を作る
// たとえば、黒と白の石の数が同じだった場合、二つのColorオブジェクトがmostColorsに入る
this.mostColors = this.colors.filter((color) => color.stoneCount === maxStoneCount);
// 最も多い色が1つだけなら、その色を勝ったことにする
if (this.mostColors.length === 1) {
this.mostColors[0].winCount++;
}
}
}