簡単なボードゲーム（へクス五目）を実装してみた

自分は過去に [React-Tutorial](https://ja.react.dev/learn/tutorial-tic-tac-toe) の三目並べ実装に取り組み，その内容を記事にしました．

https://qiita.com/tomtkg/items/a489006b85f5fb9896aa

今回，発展形としてへクス五目を実装してみたので，その内容について紹介します．
まずは以下から遊んでみてください．

https://tomtkg.github.io/Hex-Gomoku/

# へクス五目 (Hex-Gomoku) について
へクス五目は，三目並べ（○×ゲーム），五目並べ，連珠と関連が深いゲームです．
適切な名前や定義が見つからなかったので，自分がへクス五目と名付けました．

まず，三目並べ（○×ゲーム）について，確認します．
$3\times3$の正方形グリッド盤面に○と×（または○と●）を交互に配置していきます．
合計9マスの盤面で，縦・横・斜めのいずれか1列に，先に3個自分のマークを並べられた方を勝ちとするゲームです．

次に，盤面を$15\times15$などの大きな正方形グリッド盤面に拡張し，先に5個自分のマークを並べられた方を勝ちとするゲームが五目並べです．
そして，競技として成立するようにルールを整えた五目並べが連珠になります．

今回，五目並べの盤面を正方形グリッドから六角形のグリッドに変更したものをへクス五目と定義しています．

画像を見るのが一番分かり易いと思います．

|![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/308259/015c86ee-d62d-9679-dbfa-78a2b23e786a.png)|![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/308259/7eff1a89-79c0-32fe-2ef7-b67987642286.png)|![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/308259/0535cf7b-ca52-1bd3-3f7d-8808fa5bc162.png)|
|:-:|:-:|:-:|
|三目並べ<br>Tic-tac-toe|五目並べ<br>Gomoku|へクス五目<br>Hex-Gomoku|

# へクス五目の実装方針
プログラミング言語はTypeScript，フレームワークはNext，Reactを利用しました．

盤面の実装は，[react-hexgrid](https://github.com/Hellenic/react-hexgrid)を利用しました．
こちらは，簡単に六角形グリッド盤面を実装できるライブラリです．
プログラムの実装例と[実装した画面の例](https://hellenic.github.io/react-hexgrid/examples/)があり，非常に親切です．

ゲームの勝利判定には，react-hexgridの座標表現法を最大限に活用しました．
react-hexgridは，座標(s,q,r)で1つのセルの位置を表現します．
s+q+r=0であり，中心のセルを(0,0,0)，外側のセルを(-9,9,0)，(9,-5,-4)のように表現します．
ユーザーが○×の選択をしたセルに注目し，そのセルと同じs, q, rを持つ各セル列について，○か×が5回連続しているか否か確認することで，ゲームの勝利を判定できます．

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/308259/f704026a-8009-8672-5b46-37c96e8f3559.png)
https://github.com/Hellenic/react-hexgrid より引用

データとしては，以下の2つのみstateで管理することにしました．
* `history`：ユーザーが各ステップ（ターン）で選択したセルのid列
* `win`：○か×が5回連続した，勝利判定の決め手となったセルのid列

どちらも`number[]`型で，必要最小限のデータと言えます．一手前に戻る「Go back ボタン」や初期状態に戻る「Reset ボタン」を簡潔に実装できる利点があります．

# メインプログラム
ゲーム情報を表示するヘッダー部や勝利判定プログラムは別ファイルに切り分けています．
メインプログラムのみ記載します．
```ts:Game.tsx
import React, { useState } from 'react';
import { HexGrid, Layout } from 'react-hexgrid';
import { Cell } from './Cell';
import { initialBoard, calculateWinner } from './utils';
import { GameStatus } from './GameStatus';
import { GameControls } from './GameControls';

export function Game(): JSX.Element {
  const [history, setHistory] = useState<number[]>([]);
  const [win, setWin] = useState<number[]>([]);
  const step = history.length;

  const board = initialBoard.map((nums, id) => {
    const hStep = history.findIndex(hId => hId === id);
    const t = hStep === -1 ? null :
      hStep % 2 === 0 ? '×' : '○';
    return { nums, t, win: win.includes(id) };
  });

  function handleClick(id: number) {
    if (board[id].t || win.length > 0) return;

    board[id].t = step % 2 === 0 ? '×' : '○';
    setWin(calculateWinner(id, board));
    setHistory([...history, id]);
  }

  return (
    <>
      <div className="game_header">
        <GameStatus step={step} win={win} />
        <GameControls step={step}
          goBack={function () { setWin([]); setHistory(history.slice(0, step - 1)) }}
          resetGame={function () { setWin([]); setHistory([]) }}
        />
      </div>
      <div className="game">
        <HexGrid width="100%" height="100%" viewBox={"-50 -42 100 84"}>
          <Layout size={{ x: 2.5, y: 2.5 }} flat={false} spacing={1} >
            {board.map((cell, id) =>
              <Cell key={id} data={cell} onClick={handleClick} />
            )}
          </Layout>
        </HexGrid>
      </div>
    </>
  );
}
````
`board`は，盤面を構成する各六角形セルに関する以下の情報が保存されています．
* idと座標(s,q,r)
* 表示文字`t (="" or "×" or "○")`
* 勝利判定の決め手となったセルであるか否か

`function handleClick`は，ボタンクリック時に呼び出されます．
既に○×が書いてあるセルをクリックした場合，勝敗がついている場合，何もしません．
それ以外の場合，セルに○か×を付けて，その後にゲームの勝利判定を実行します．

画面表示部分は，`game_header`と`game`の2つに分けました．
`HexGrid`と`Layout`は，基本設定だとサイズ感がイマイチだったので，手動で調整しました．

# 終わりに
プログラムは以下に公開しています．皆さんの参考になれば幸いです．

https://github.com/tomtkg/Hex-Gomoku

ゲーム内容に関する考察も投稿いたしました．

https://qiita.com/tomtkg/items/e5644ccf5b9a08f200bd