Hyperappで tic-tac-toe（OXゲーム）を作る。

## はじめに

hyperappでReact チュートリアルの tic-tac-toe（OXゲーム）を作った際の技術メモです。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/195024/62894525-f33a-d33e-56e5-24dce026124d.png)

- Hyperappとは（参考：[2018 年は Hyperapp の年だ](https://qiita.com/JorgeBucaran/items/c48446babe0627e25ee6))

> Web アプリのフロントエンド用 JavaScript ライブラリ。React, Preact, Vue といった代表的なものよりもずっと小さく、1 KB という超軽量サイズ。他のライブラリに依存することなく使えて、さらにスピードもある :fire:

> Elmアーキテクチャーに基づいてて、アプリケーション設計はElmやReact、Reduxと似てるけど、ボイラープレートは少ないし、TypeScriptにも対応して、とにかくシンプル。

## 環境

環境設定は[この記事](https://qiita.com/sanonosa/items/b33a1ac69a738e0750c8)と同じです。

## 制作

それでは早速作っていきます。

以下の順で作ります

1. *state*（アプリケーションにおけるデータモデル全体）
2. *actions*（*state*を更新する）
3. *components(views)*（*state*と*actions*を受け取り、DOMツリーを構築）

## *state*

ＯＸゲームの状態を定義します。

- `squares`：現在のボードの状態
- `nowPlayer`：現在のプレイヤー
- `history`：`squares`の履歴

```ts:./src/state.ts
export default interface IAppState {
  squares: Squares;
  nowPlayer: Player;
  history: Squares[];
}

export type Player = "X" | "O";
type Sign = Player | null;
export type Squares = Sign[];
```

## Actions

アプリの状態を更新する関数を定義します。
状態の更新には[`immer`](https://github.com/mweststrate/immer)を使いました。
「直接ステートを更新せずに、新たなステートを返す」というコードが直感的に書けるライブラリです。

- `mark`：盤面にＯＸをマークします。
- `jumpTo`：履歴（何手目）を指定してその状態に遷移します。

```ts:./src/actions.ts
import produce from "immer";
import IAppState, { Player, Squares } from "./state";

export interface IAppActions {
  mark: (index: number) => (state: IAppState) => IAppState;
  jumpTo: (step: number) => (state: IAppState) => IAppState;
}
export const actions: IAppActions = {
  mark: index => state => {
    if (state.squares[index] || judge(state.squares)) {
      // 既にそのマスがマークされている、もしくは決着がついている場合更新しない
      return state;
    }
    return produce(state, draft => {
      // 指定のマスにマーク
      draft.squares[index] = state.nowPlayer;
      // プレイヤー交代
      draft.nowPlayer = state.nowPlayer === "X" ? "O" : "X";
      // ボードの状態を履歴へ追加
      draft.history.push(state.squares);
    });
  },
  jumpTo: step => state =>
    produce(state, draft => {
      // 履歴を復元
      draft.squares = draft.history[step];
      // プレイヤーを復元
      draft.nowPlayer = step % 2 === 0 ? "X" : "O";
      draft.history.splice(step, draft.history.length);
    })
};

// 勝敗判定
export function judge(board: Squares): Player | null {
  const lines = [
    [0, 1, 2],
    [3, 4, 5],
    [6, 7, 8],
    [0, 3, 6],
    [1, 4, 7],
    [2, 5, 8],
    [0, 4, 8],
    [2, 4, 6]
  ];
  for (const line of lines) {
    const [a, b, c] = line;
    if (board[a] && board[a] === board[b] && board[a] === board[c]) {
      return board[a];
    }
  }
  return null;
}
```

## Components

アプリのロジックが完成したのでステートレスなビューを書けば完成です。
親コンポーネントからのpropsでなくグローバルなステートとアクションをそのまま使える`Lazy Components`がすごく便利です。
参考：[HyperappのLazy Components](https://qiita.com/sosukesuzuki/items/1015c1f256459ac76996)

- 個々のマス

```tsx:./src/components/square.tsx
import { Component, h } from "hyperapp";
import { IAppActions } from "../actions";
import IAppState from "../state";

export const Square: Component<{ index: number }, IAppState, IAppActions> = ({
  index
}) => ({ squares }, { mark }) => (
  <button class="square" onclick={() => mark(index)}>
    {squares[index]}
  </button>
);
```

- 盤面

```tsx:./src/components/board.tsx
import { h } from "hyperapp";
import { Square } from "./square";

const Board = () => (
  <div>
    <div class="board-row">
      <Square index={0} />
      <Square index={1} />
      <Square index={2} />
    </div>
    <div class="board-row">
      <Square index={3} />
      <Square index={4} />
      <Square index={5} />
    </div>
    <div class="board-row">
      <Square index={6} />
      <Square index={7} />
      <Square index={8} />
    </div>
  </div>
);
export default Board;
```

- ゲーム画面

```tsx:./src/components/game.tsx
import { h, View } from "hyperapp";
import { IAppActions, judge } from "../actions";
import IAppState, { Player } from "../state";
import Board from "./board";
import History from "./history";

const Game: View<IAppState, IAppActions> = state => {
  const status = ({ squares, nowPlayer }: IAppState) => {
    const winner: Player | null = judge(squares);
    return winner ? `Winner: ${winner}` : `Next Player: ${nowPlayer}`;
  };
  return (
    <div class="game">
      <div class="game-board">
        <Board />
      </div>
      <div class="game-info">
        <div>{status(state)}</div>
        <History />
      </div>
    </div>
  );
};
export default Game;
```

- 履歴の表示

```tsx:./src/components/history.tsx
import { Component, h } from "hyperapp";
import { IAppActions } from "../actions";
import IAppState from "../state";

const History: Component<never, IAppState, IAppActions> = () => (
  { history },
  { jumpTo }
) => (
  <ol>
    {history.map((_, move) => {
      const desc = move ? `Go to move #${move}` : `Go to game start`;
      return (
        <li>
          <button onclick={() => jumpTo(move)}>{desc}</button>
        </li>
      );
    })}
  </ol>
);
export default History;
```

## Index.ts

```ts:./src/index.ts
import { app } from "hyperapp";
import { actions } from "./actions";
import Game from "./components/game";
import IAppState from "./state";

const initialState: IAppState = {
  squares: Array(9).fill(null),
  nowPlayer: "X",
  history: []
};

app(initialState, actions, Game, document.body);
```

`index.html`は`Index.ts`を呼ぶだけなので省略します。
css は react tutorial そのまま使わせてもらいました。


Hyperapp は react + redux よりシンプルなコードでElmアーキテクチャ風の開発が可能なところが好きです。