2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

お題は不問!Qiita Engineer Festa 2024で記事投稿!
Qiita Engineer Festa20242024年7月17日まで開催中!

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

Last updated at Posted at 2024-07-16

自分は過去に React-Tutorial の三目並べ実装に取り組み,その内容を記事にしました.

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

へクス五目 (Hex-Gomoku) について

へクス五目は,三目並べ(○×ゲーム),五目並べ,連珠と関連が深いゲームです.
適切な名前や定義が見つからなかったので,自分がへクス五目と名付けました.

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

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

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

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

image.png image.png image.png
三目並べ
Tic-tac-toe
五目並べ
Gomoku
へクス五目
Hex-Gomoku

へクス五目の実装方針

プログラミング言語はTypeScript,フレームワークはNext,Reactを利用しました.

盤面の実装は,react-hexgridを利用しました.
こちらは,簡単に六角形グリッド盤面を実装できるライブラリです.
プログラムの実装例と実装した画面の例があり,非常に親切です.

ゲームの勝利判定には,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://github.com/Hellenic/react-hexgrid より引用

データとしては,以下の2つのみstateで管理することにしました.

  • history:ユーザーが各ステップ(ターン)で選択したセルのid列
  • win:○か×が5回連続した,勝利判定の決め手となったセルのid列

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

メインプログラム

ゲーム情報を表示するヘッダー部や勝利判定プログラムは別ファイルに切り分けています.
メインプログラムのみ記載します.

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_headergameの2つに分けました.
HexGridLayoutは,基本設定だとサイズ感がイマイチだったので,手動で調整しました.

終わりに

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

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

2
2
1

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
2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?