おもしろそうなのでやってみました。
見た目がしょぼいのはご容赦。プレーできて履歴が見れるってのを目指します。
See the Pen TicTacToe by ttatsf (@ttatsf) on CodePen.
基本的にはこの記事 とやってることは一緒です。
クロージャを使ってElmっぽい感じで状態を管理しよう、ってことです。
ブロックごとに見ていきます。
model: 状態
// model
const Empty = ""
const markX = "X"
const markO = "O"
const playerA = "A"
const playerB = "B"
const Draw = "Draw"
const init =
{tiles: [Empty, Empty, Empty
, Empty, Empty, Empty
, Empty, Empty, Empty
]
, turn: playerA
, won: undefined
, history: []
}
このゲームの状態を定義しています。これだけだと何のことやらわからないですが、
- tiles の中には tile が 9個あって
- tile はそれぞれ、 Empty / markX / markO のうちのどれか
- turn は playerA / playerB のどちらか
- won は undefined / playerA / playerB / Draw のどれか
- history は 各ターンの tiles が蓄積されていく
というようなつもりです。どこにも書いてないですけどね。
この、つもり、っていうか 自分で決めたルール に基いて、次の update で状態を更新していきます。
それぞれ、簡単に表示できるように 文字列 ということにしてます。
update:状態を更新する
// update
const Start = Symbol("Start")
const Click = Symbol("Click")
const History = Symbol("History")
const Reset = Symbol("Reset")
const update =
msg => model =>
msg[0] === Start ?
init
: msg[0] === Click ?
model.won !== undefined ?
model
: model.tiles[ msg[1] ] !== Empty ?
model
: checkEnd({...model
, tiles: changeElem(msg[1])(
model.turn === playerA ?
markX
: markO
)(model.tiles)
})
: msg[0] === History ?
{...model
, tiles: model.history[ msg[1] ]
}
: msg[0] === Reset ?
init
: model
update は msg メッセージ と model 状態 を受け取って 新しい model 状態 を返します。
メッセージの種類は:
- Start : ゲームを初期化します。あとで出てくる main ブロックの中で使われます。
- Click : タイルをクリックしたときに送られます。
- History : 履歴のボタンをクリックしたときに送られます。
- Reset : リセットボタンをクリックしたときに送られます。
Click と History には さらに何番目を押したか?の情報も必要なので、[メッセージの種類, 値] という形の メッセージになります。
Start と Reset には 追加の値はいりませんが、簡単にするため、[メッセージの種類, undefined] というように、ダミーの値を渡しています。
以下、update内で使われる補助的な関数です。
const checkEnd =
model =>
[0, 3, 6].some( e =>
model.tiles[e] !== Empty
&& model.tiles[e] === model.tiles[e + 1]
&& model.tiles[e] === model.tiles[e + 2]
)
|| [0, 1, 2].some( e =>
model.tiles[e] !== Empty
&& model.tiles[e] === model.tiles[e + 3]
&& model.tiles[e] === model.tiles[e + 6]
)
|| [2, 4].some( e =>
model.tiles[4] !== Empty
&& model.tiles[4] === model.tiles[4 - e]
&& model.tiles[4] === model.tiles[4 + e]
) ?
{...model
, won: model.turn
, history: [...model.history, model.tiles]
}
: model.history.length === init.tiles.length - 1 ?
{...model
, won: Draw
, history: [...model.history, model.tiles]
}
: {...model
, turn: model.turn === playerA ? playerB : playerA
, history: [...model.history, model.tiles]
}
const changeElem =
index => value => xs =>
[...xs.slice(0, index), value, ...xs.slice(index + 1)]
checkEndは、model 状態 をとって、
- 三つそろったら、
- 勝者は今のターンの人で、
- 今の盤面を履歴に加えて
- じゃなければ、もし履歴がもう8個になってるなら
- 勝者なし、引き分けということで、
- 今の盤面を履歴に加えて
- じゃなければ、ゲーム続行なので
- ターンを別の人に変えて、
- 今の盤面を履歴に加えて
新しい状態を返します。
changeElem は、インデックス と 値 と 配列をとって、配列[インデックス] が 値 になっている新しい配列を返します。
view: 表示する
viewブロックはふたつの部分からなります。
1. htmlの構築
// view
document.body.insertAdjacentHTML(
'beforeend'
,
`
<div class="game">
<div >TicTacToe</div>
<div><span>next player:</span><span id="t0"></span></div>
<div class="board"></div>
<div class="history"></div>
<button class="reset"type="button"id="b2">reset</button>
<div id="hist"></div>
</div>
`
);
[...init.tiles.keys()].forEach(
i => {
document.querySelector(".board").insertAdjacentHTML(
'beforeend'
,
`
<div class="tile-wrapper">
<div class="tile tile-${i}"></div>
</div>
`
)
document.querySelector(".history").insertAdjacentHTML(
'beforeend'
, `<button class="hb hb-${i}"" >${i+1}</button>`
)
}
)
このあと動的にいじっていく要素をhtmlに書きこんでいます。
タイルと履歴ボタンは繰り返しになるので forEach を使って楽してみました。
2. view 関数: 状態を表示する
view 関数は model 状態 を受け取って それに基いて表示をします。
const view = model => {
[...model.tiles.keys()].forEach(
e => {
document.querySelector(".tile-" + e).textContent =
model.tiles[e]
document.querySelector(".hb-" + e).style.visibility =
model.won === undefined ? "hidden"
: e >= model.history.length ? "hidden"
: "visible"
}
)
document.querySelector("#t0").textContent =
model.won !== undefined ?
""
: model.turn
document.querySelector(".reset").textContent =
model.won === undefined ?
"reset"
: model.won === Draw ?
"Draw..."
: model.won + " win!"
}
main: しかけを作る
1. 状態管理のしかけを作る
// main
const sandBox =
model => msg => {
model = update( msg )( model )
view( model )
}
const sendMsg =
sandBox(init)
sendMsg([Start, undefined]);
sandBox 関数は、
- 状態とメッセージを受け取って、
- update 関数で 状態を更新し、
- view 関数で状態を表示する
というものです。
それに init を部分適用したものを sendMsg として使うと、あら不思議、メッセージを食べさせるだけで自動的に状態を更新して表示してくれる、というわけです。
ゲームを始めるために sendMsg に [Start, undefined] というメッセージを送り、初期状態を表示させます。
2. イベントハンドラーの登録
[...init.tiles.keys()].forEach(
e => {
document.querySelector(".tile-" + e).onclick =
_ => sendMsg([Click, e])
document.querySelector(".hb-" + e ).onclick =
_ => sendMsg([History, e])
}
)
document.querySelector(".reset").onclick =
_ => sendMsg([Reset, undefined]);