LoginSignup
8
4

More than 5 years have passed since last update.

Hyperappで tic-tac-toe(OXゲーム)を作る。

Last updated at Posted at 2018-05-21

はじめに

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

image.png

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

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

環境

環境設定はこの記事と同じです。

制作

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

以下の順で作ります

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

state

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

  • squares:現在のボードの状態
  • nowPlayer:現在のプレイヤー
  • historysquaresの履歴
./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を使いました。
「直接ステートを更新せずに、新たなステートを返す」というコードが直感的に書けるライブラリです。

  • mark:盤面にOXをマークします。
  • jumpTo:履歴(何手目)を指定してその状態に遷移します。
./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

  • 個々のマス
./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>
);
  • 盤面
./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;
  • ゲーム画面
./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;
  • 履歴の表示
./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

./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.htmlIndex.tsを呼ぶだけなので省略します。
css は react tutorial そのまま使わせてもらいました。

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

8
4
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
8
4