LoginSignup
1
2

More than 3 years have passed since last update.

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

Last updated at Posted at 2021-02-10

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

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]);
1
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
2