16
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

WEBブラウザで動く六角形オセロを作ってみた

16
Last updated at Posted at 2026-03-24

はじめに

最近ハニカム構造にハマっており、Webブラウザで遊べる 六角形盤面のオセロ を作りました。

525622590-f2711ec6-44b0-423f-8a5f-0ec8bc818fc0.png

6角形特有の座標系やアルゴリズムを説明できればと思います。

六角形グリッドの座標系の種類

六角形グリッドの座標系にはいくつかの表現方法があります。
今回は、Cube座標(Cube Coordinates)を採用しました。

方式 説明 特徴
Offset座標 行ごとにずらして2軸(x, y)で表現 二次元配列に直接マッピングしやすい 方向ごとに計算式が変わり、実装が煩雑になりやすい
Axial座標 2軸(q, r)で表現(sは省略) データ量が少ない 方向計算時に暗黙的にsを復元する必要がある
Cube座標 3軸(q, r, s)で表現 全方向の計算がベクトル加算に統一され、距離・回転・対称性で計算

Cube座標(Cube Coordinates)

今回採用したCube座標は3つの軸 (q, r, s) を使い、常に以下の制約を満たします。

q + r + s = 0

画像1.png

この制約があることで、グリッド上のあらゆる隣接マスへの移動が「どれかを+1、どれかを-1する」という対称的な操作で表現でき、回転も容易です。

6方向をベクトルで定義する

六角形グリッドの6方向は、Cube座標では以下のように表せます。

const directions = [
  { q: 1, r: -1, s: 0 },  // 右上
  { q: 1, r: 0, s: -1 },  // 右
  { q: 0, r: 1, s: -1 },  // 右下
  { q: -1, r: 1, s: 0 },  // 左下
  { q: -1, r: 0, s: 1 },  // 左
  { q: 0, r: -1, s: 1 },  // 左上
];

どのベクトルも q + r + s = 0 が成り立っているのが分かりますね。そして隣のセルへの移動は単純な加算で済みます。

const cubeAdd = (a, b) => ({
  q: a.q + b.q,
  r: a.r + b.r,
  s: a.s + b.s,
});

ボードの「端」の判定

ボードの中心から半径 BOARD_RADIUS 以内のセルがボード上のセルです。

六角形グリッドでは、通常の座標系とは異なる距離計算が必要になります。
Cube座標における中心からの距離は、3軸の絶対値の最大値で求められます。

const BOARD_RADIUS = 4;

const cubeDistance = (coord) => {
  return Math.max(Math.abs(coord.q), Math.abs(coord.r), Math.abs(coord.s));
};

const isInBounds = (coord) => {
  return cubeDistance(coord) <= BOARD_RADIUS;
};

配列で管理

ボードの状態は Map<string, 'black' | 'white'> で管理しています。キーは "q,r,s" 形式の文字列です。

// Mapにエントリがなければ = 空のマス
// Mapに 'black' または 'white' があれば = その色の石

const createInitialBoard = () => {
  const board = new Map();
  //  中心(0,0,0)に白を置き、周囲6マスに交互配置
  board.set('0,0,0', 'white');
  board.set('1,-1,0', 'black');
  board.set('0,1,-1', 'black');
  board.set('-1,0,1', 'black');
  board.set('-1,1,0', 'white');
  board.set('1,0,-1', 'white');
  board.set('0,-1,1', 'white');
  return board;
};

通常の二次元配列 board[x][y] に比べて、座標が存在するかどうかを配列の範囲チェックなしに isInBounds() で判定できるのがポイントです。

六角形ボードを二次元配列で表現しようとすると「使われないセル」が大量に生まれますが、Mapを使うことでスパースな構造を自然に扱えます。

画面への描画は、Cube座標をピクセル座標へ変換することで行います。

const cubeToPixel = (q, r, size) => {
  const x = size * (3 / 2 * q);
  const y = size * (Math.sqrt(3) / 2 * q + Math.sqrt(3) * r);
  return { x, y };
};

これはいわゆる「フラットトップ(上辺が平らな)」六角形の変換式です。この x, y をSVGの <polygon> の中心座標として使います。

挟む処理

オセロの核心は「挟む」処理です。通常のオセロでは4方向(縦・横・斜め)ですが、六角形オセロでは6方向に対して同じ処理を行います。

ある方向に何個挟めるか調べる

const findFlipsInDirection = (coord, direction, player, currentBoard) => {
  const flips = [];
  let current = cubeAdd(coord, direction); // 1マス進む

  while (isInBounds(current)) {
    const key = `${current.q},${current.r},${current.s}`;
    const piece = currentBoard.get(key);

    if (!piece) break;           // 空マスに当たったら終了(挟めない)
    if (piece === player) return flips; // 自分の石に当たったら確定(挟める!)

    flips.push(key);             // 相手の石を記録
    current = cubeAdd(current, direction); // さらに進む
  }

  return []; // ボード外に出たら挟めない
};

ボード外に出たら空配列を返しています。

全6方向を調べてまとめる

const getFlips = (coord, player, currentBoard) => {
  const key = `${coord.q},${coord.r},${coord.s}`;
  if (currentBoard.has(key)) return []; // すでに石がある

  let allFlips = [];
  for (const dir of directions) {
    const flips = findFlipsInDirection(coord, dir, player, currentBoard);
    if (flips.length > 0) {
      allFlips = [...allFlips, ...flips];
    }
  }
  return allFlips;
};

getFlips が空配列を返すなら「その場所に石は置けない」、1つ以上返すなら「置ける石のリスト」になります。

有効な手を求める

全マスに対して getFlips を実行し、置ける場所を列挙します。

const calculateValidMoves = (player, currentBoard) => {
  const moves = new Set();
  for (let q = -BOARD_RADIUS; q <= BOARD_RADIUS; q++) {
    for (let r = -BOARD_RADIUS; r <= BOARD_RADIUS; r++) {
      const s = -q - r;
      if (!isInBounds({ q, r, s })) continue;

      const coord = { q, r, s };
      if (getFlips(coord, player, currentBoard).length > 0) {
        moves.add(`${q},${r},${s}`);
      }
    }
  }
  return moves;
};

完成したもの

Animation1.gif

ページ下のURLからプレイ可能です。

おまけ

初期で真ん中を置かない場合絶対取れないマスがあります。
真ん中も取れなければ角も取れないという、とてもつまらないゲームになります。

Animation1.gif

初期配置は、下記のように7駒置いてある状態で調整しました。

image.png

おわりに

今回は、六角形オセロをReactで作成しました。

六角形グリッドというと難しそうに聞こえますが、Cube座標(Cube Coordinates) を使うと意外と簡単に表現できました。

ただ、六角形の配置や向きなど画面出力する時は、四角形より圧倒的に難しかったです。

六角形グリッドでオセロ以外にも色々と作ってみたので、今後紹介出来たらなと思います。

ここまで読んで頂きありがとうございました。

リンク

  • 6角形オセロゲーム

対戦相手としてCPUも実装しています。ぜひ遊んでみてください!!

  • Githubリポジトリ

今回作成したオセロは、こちらのリポジトリで管理しています。

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

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?