ロゴシュミグラム

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

情報板となるInformationクラスの作成

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

名前説明
elements情報板のtd要素たちの配列
createRowElement()一行分のtr要素とtd要素たちを作成する
update()情報板の表示を更新する

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

Information.js

class Information {
constructor() {
this.elements = [];
state.colors.forEach((color) => {
// 情報板の一行ぶんの要素を作成する
this.createRowElement(color);
});
}
createRowElement (color) {
// tr要素を作成して、その中にtd要素を入れていく
const trElement = document.createElement('tr');
// 現在のプレイヤーに赤い矢印を表示するためのtd要素
const arrowTdElement = document.createElement('td');
arrowTdElement.style.color = 'red';
arrowTdElement.style.fontSize = '14px';
// 石の色を表示するためのtd要素
const stoneTdElement = document.createElement('td');
stoneTdElement.textContent = '●';
stoneTdElement.style.color = color.colorCode;
// 「あなた」や「COM」と表示するためのtd要素
const typeTdElement = document.createElement('td');
const html = color.members.map((player) => player.isHuman() ? '<div>あなた</div>' : '<div>COM</div>');
typeTdElement.innerHTML = html;
// 石の数を表示するためのtd要素
const stoneContTdElement = document.createElement('td');
stoneContTdElement.classList.add('num-area');
// 勝ち数を表示するためのtd要素
const winContTdElement = document.createElement('td');
winContTdElement.classList.add('num-area');
// winContTdElement.textContent = 0;
trElement.append(arrowTdElement, stoneTdElement, typeTdElement, stoneContTdElement, winContTdElement);
// tr要素をtbody要素に入れる
const bodyElement = document.getElementById('information-body');
bodyElement.append(trElement);
// のちのち更新する要素は保持しておく
this.elements.push({ arrowTdElement, stoneContTdElement, winContTdElement });
}
update () {
state.colors.forEach((color, i) => {
// 現在のプレイヤーがこの色であれば矢印を表示させる
this.elements[i].arrowTdElement.textContent = color === state.currentPlayer.color ? '▶︎' : '';
this.elements[i].stoneContTdElement.textContent = color.stoneCount;
this.elements[i].winContTdElement.textContent = color.winCount;
});
}
}

情報板のCSSを追加する

styles.cssに次のものを書きくわえます。

styles.css

#information {
color: white;
font-weight: bold;
margin: 10px 0;
padding: 6px 20px;
border-radius: 5px;
border: 4px ridge gray;
box-shadow: 1px 2px 2px 0px #444;
background: radial-gradient(circle farthest-side at 90% 90%, #444 0%, rgb(143, 143, 143) 100%);
}
#information th {
font-weight: normal;
font-size: 12px;
}
/* 数字の欄は左寄せにする */
#information .num-area {
width: 70px;
text-align: right;
}

メッセージを表示するMessageクラスの作成

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

名前説明
elementメッセージのdiv要素
show()メッセージを表示する
hide()メッセージを非表示にする
showPass()結果時に表示する石のdiv要素を作成する
createWinningStoneDiv()結果時の勝利メッセージを作成する
createWinMessage()結果時の引き分けメッセージを作成する
createDrawMessage()結果のメッセージを表示する

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

Message.js

class Message {
constructor() {
this.element = document.getElementById('message');
this.element.style.transitionDuration = `${MESSAGE_DURATION_TIME}ms`;
}
/**
* メッセージを表示させる
*/
async show () {
this.element.style.visibility = 'visible';
this.element.style.opacity = 1;
await sleep(MESSAGE_DURATION_TIME);
}
/**
* メッセージを非表示にする
*/
async hide () {
this.element.style.visibility = 'hidden';
this.element.style.opacity = 0;
await sleep(MESSAGE_DURATION_TIME);
}
/**
* パスのメッセージを表示させる
*/
async showPass () {
this.element.textContent = 'PASS';
await this.show();
await sleep(PASS_MESSAGE_TIME);
await this.hide();
}
/**
* 結果に表示させる石となるdiv要素を作成する
*/
createWinningStoneDiv (colorCode) {
return `<div class="winning-stone" style="color:${colorCode}">●</div>`;
}
/**
* 特定の石が勝利したときのメッセージを作成する
*/
createWinMessage () {
return `<div>${this.createWinningStoneDiv(state.mostColors[0].colorCode)}の勝ち</div>`;
}
/**
* 引き分けとなったときのメッセージを作成する
*/
createDrawMessage () {
let drawMessage = '<div>';
state.mostColors.forEach((color) => {
drawMessage += this.createWinningStoneDiv(color.colorCode);
});
drawMessage += '</div><div>引き分け</div>';
return drawMessage;
}
/**
* 結果を表示させる
*/
showResult () {
// ゲーム終了が最も多かった石の色が1つだけの場合、勝利メッセージを作成する
// 複数の色がある場合は引き分けメッセージを作成する
let resultMessage = state.mostColors.length === 1
? this.createWinMessage()
: this.createDrawMessage();
resultMessage += '<div class="small-text">クリックしてもう1回!</div>';
this.element.innerHTML = resultMessage;
this.show();
}
}

メッセージのCSSを追加

styles.cssに次のものを書きくわえます。

styles.css

#message {
/* メッセージは画面全体に表示するためfixedにする */
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
overflow: auto;
visibility: hidden;
opacity: 0;
background-color: rgba(30, 30, 30, 0.5);
font-size: 60px;
font-weight: bold;
color: white;
/* 文字の縁取りをする */
-webkit-text-stroke-width: 2px;
-webkit-text-stroke-color: #333;
}
#message .small-text {
font-size: 20px;
-webkit-text-stroke-width: 1px;
}
/* 結果のメッセージの中で表示させる石 */
.winning-stone {
display: inline-block;
/* text-align: center; */
font-size: 100px;
animation-name: hopping;
animation-duration: 500ms;
animation-iteration-count: infinite;
animation-direction: alternate;
}
/* 画面が大きいときはメッセージの文字も大きくする */
@media (min-width: 700px) {
#message {
font-size: 100px;
-webkit-text-stroke-width: 3px;
}
}

ゲームの流れ

1ターンごとのゲームの流れは次のようになります。

オセロのターンの流れ

ゲームを進行するGameクラスの作成

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

名前説明
boardBoardオブジェクト
informationInfomationオブジェクト
messageMessageオブジェクト
start()プレイヤーが行動できるようにする
pass()パスをする
flipStones()挟んだ石を裏返す
update()挟んだ石を裏返して、ゲームを進行する
gameover()ゲーム終了の処理をして結果メッセージを表示する
reset()ゲーム状態と盤面を初期状態に戻す

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

Game.js

class Game {
constructor() {
this.board = new Board();
this.information = new Information();
this.message = new Message();
}
start () {
// 情報板を更新する
this.information.update();
// 石の置けるマス目をチェックする
this.board.checkSelectableSquares();
// 石の置けるマス目が無い場合はパスする
if (state.selectableSquares.length === 0) {
this.pass();
return;
}
state.passCount = 0;
// もし現在のプレイヤーがコンピュータの場合、石を置おくように行動させる
if (state.currentPlayer.isComputer()) {
state.currentPlayer.act();
}
}
async pass () {
state.passCount++;
// 現在のプレイヤーの石が盤面に残っている場合は、PASSメッセージを表示する
if (state.currentPlayer.isAlive()) {
await this.message.showPass();
}
// すべてのプレイヤーがパスした場合は、ゲームを終了させる
if (state.isGameover()) {
this.gameover();
return;
}
// ターンを進める
state.turnCountUp();
// 次のプレイヤーが行動できるようにする
this.start();
}
async flipStones () {
// タイミングをずらして裏返していく
// 1個目は0ミリ秒後、2個目は100ミリ秒後、3個目は200ミリ秒後、・・・となる
state.sandwichedStones.forEach((stone, i) => {
setTimeout(
() => { stone.flip(state.currentPlayer.color); },
i * FLIP_INTERVAL_TIME
);
});
// 最後の石が完全に裏返るまで待機する
await sleep(state.sandwichedStones.length * FLIP_INTERVAL_TIME + FLIP_DURATION_TIME);
state.sandwichedStones = [];
}
async update () {
// 石の置けるマス目を取り消す
this.board.resetSelectableSquares();
// 挟んだ石たちを裏返す
await this.flipStones();
// ゲーム終了の状態であれば、終了させる
if (state.isGameover()) {
this.gameover();
return;
}
// ターンを進める
state.turnCountUp();
// 次のプレイヤーが行動できるようにする
this.start();
}
gameover () {
// 最も多い石の色をチェックする
state.checkMostColors();
// 情報板を更新する
this.information.update();
// 結果のメッセージを表示する
this.message.showResult();
// メッセージをクリックすると初期化するようにする
// bind(this)を付けないと、resetメソッド内のthisはクリックした要素になってしまう
// once: trueとすることで一回のみresetを実行するようにする
this.message.element.addEventListener('click', this.reset.bind(this), { once: true });
}
reset () {
state.init();
this.board.reset();
this.message.hide();
// 最初のプレイヤーが行動できるようにする
this.start();
}
}

コンピュータの戦略を作成

ここではコンピュータの強さレベルを2つ作成します。

強さレベル1

  • 石を置くマス目をランダムに選ぶ

強さレベル2

  • 角のマス目に石を置ける場合、優先的に置くようにする
  • それ以外の場合、ゲームの序盤は石をなるべく取らないようにして、終盤になったら多く取るようにする

これらの関数をStrategyクラスの静的メソッドとして作ります。

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

Strategy.js

// いずれのメソッドもSquareオブジェクトを返す
class Strategy {
static getSquare (lebel) {
return Strategy[lebel]();
}
// レベル1の戦略
static 1 () {
// 石が置けるマス目からランダムに1つ抜き出す
return pickRandom(state.selectableSquares);
}
// レベル1の戦略
static 2 () {
// 石を置けるマス目の中から角のマス目を抜き出す。無い場合は空の配列となる
const cornerStones = state.selectableSquares.filter((square) => square.isCorner);
// 角のマス目があった場合、その中から一つ選んで返す
if (cornerStones.length !== 0) {
return pickRandom(cornerStones);
}
// 挟める石の個数の配列を作る。たとえば、[2,1,2,..]のような配列になる
const sandwichStonesCountList = state.selectableSquares.map((square) => square.sandwichStones.length);
// 自分が置くことのできる残りマス目数を計算する
const remainingSquareCount = state.emptySquareCount / state.colors.length;
// 残りマス目数が8個以上あれば最小の個数、それ以外であれば最大の個数を取得する
const sandwichStonesCount = remainingSquareCount >= 8
? Math.min(...sandwichStonesCountList)
: Math.max(...sandwichStonesCountList);
// その個数となるマス目を抜き出す。複数個ある可能性もあるため配列に入れる
const selectedSquares = state.selectableSquares.filter((square) => square.sandwichStones.length === sandwichStonesCount);
// その配列の中から一つ選んで返す
return pickRandom(selectedSquares);
};
}

mainファイルの作成

main.jsで今まで作ってきたクラスのオブジェクトを生成します。

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

main.js

const state = new State();
const game = new Game();
game.start();

これでマルチオセロの完成です!
ブラウザからindex.htmlを開いてみてください。

オリジナルのオセロを作ろう

settings.jsの colorList に色を追加したり、 initialBoard を変更しすればオリジナルの盤面を作ることができます。

すべてのプレイヤーをコンピュータにしてコンピュータ同士の戦いを観戦することもできます。そのときはsettings.jsの GAME_SPEED を低めに設定するとゲーム進行が速くなるので見やすくなります。

また、Strategyクラスに新しい戦略を加えることもできます。石が少ない色を積極的に殲滅しにいく戦略などもあると面白いと思います。

ぜひ自分だけのオセロを作ってみてください。