JavaScript: 素のJSで三目並べをやってみた

おもしろそうなのでやってみました。
見た目がしょぼいのはご容赦。プレーできて履歴が見れるってのを目指します。

<p class="codepen" data-height="265" data-theme-id="light" data-default-tab="js,result" data-user="ttatsf" data-slug-hash="BaQzONy" style="height: 265px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;" data-pen-title="TicTacToe">
  <span>See the Pen <a href="https://codepen.io/ttatsf/pen/BaQzONy">
  TicTacToe</a> by ttatsf (<a href="https://codepen.io/ttatsf">@ttatsf</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://cpwebassets.codepen.io/assets/embed/ei.js"></script>


基本的には[この記事](https://qiita.com/ttatsf/items/22a15913c30f04d718ca) とやってることは一緒です。
クロージャを使ってElmっぽい感じで状態を管理しよう、ってことです。

ブロックごとに見ていきます。

# model: 状態

```js
// 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:状態を更新する

```js
// 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内で使われる補助的な関数です。

```js
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の構築

```js
// 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 状態 を受け取って それに基いて表示をします。

```js
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. 状態管理のしかけを作る

```js
// 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. イベントハンドラーの登録


```js
[...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]);
```