JavaScriptで作る変則リバーシ②

# はじめに
以前ご紹介させていただいた、下記記事からの続編(第②弾)になります。

https://qiita.com/y-tetsu/items/5ba5a5487534e47cc7ca

なお、プログラムの土台部分はこちら。

https://qiita.com/y-tetsu/items/59237213d544f14bbc9c

今回は前回作ったものをベースに、新たに**灰色の石**を追加した、**3人対戦で遊べる変則リバーシ**を作りましたので、ご紹介したいと思います。

![ash.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2653390/c1982787-8b66-d784-6ee7-0776cd688dad.png)



# できたもの
以下のような、ブラウザで遊べるものを作りました。
自身が先手(黒)で、相手(白と灰)はコンピュータ、としました。
<a href="https://y-tetsu.github.io/colorful-reversi/" target="_blank"><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2653390/23062fcb-b520-d530-2516-85c5d2f0e860.png" width=50%></a>

ソースコードなど一式は、以下に置いております。

https://github.com/y-tetsu/colorful-reversi/tree/ch02

ということで、お見せしたい結果も出揃いましたので
簡単ではありますが、作った上でのポイントを説明していきたいと思います。



# 灰色の石とは
"灰色の石"は私が思いついた、第三の石です。(といっても、同じようなものは既にいろいろな方が、作られているとは思います)

この灰色の石はもともとの黒と白の石と、扱いは同等とします。つまり、**相手の石を自分の石で挟めば、挟まれた石は自分のものになる**というルールに準拠したものとします。

以下に、石をひっくり返す時の挙動を示します。

(黒が相手を挟んだ場合)
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2653390/05acc619-9976-4edd-318e-e8172f1624da.gif" width=50%>
(白が相手を挟んだ場合)
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2653390/69e63458-f354-84d0-a273-b9457907d00d.gif" width=50%>
(灰色が相手を挟んだ場合)
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2653390/cd2a44dc-335c-d657-f3e4-349437daee4c.gif" width=50%>

こうすると、石をひっくり返す(reverse)というよりは、色を塗り替える(repaint)といった方が、あるいは自然なのかもしれませんね。ですが、ここは名のある"リバーシ"で通させてもらいたいと思います。

石の種類と盤面の初期設定は以下とします。黒、白、灰色の石を4つづつ置き、かつ、すぐには灰色が全滅してしまわないようばらしました。
```board.js
const H = 0;  // 穴
const E = 1;  // 空きマス
const B = 2;  // 黒色の石
const W = 3;  // 白色の石
const A = 4;  // 灰色の石

const BOARD = [
  H, H, H, H, H, H, H, H, H, H,
  H, H, H, E, E, E, E, H, H, H,
  H, H, E, E, E, E, E, E, H, H,
  H, E, E, E, B, A, E, E, E, H,
  H, E, E, A, W, B, W, E, E, H,
  H, E, E, W, B, W, A, E, E, H,
  H, E, E, E, A, B, E, E, E, H,
  H, H, E, E, E, E, E, E, H, H,
  H, H, H, E, E, E, E, H, H, H,
  H, H, H, H, H, H, H, H, H, H,
];
```
ブラウザに描画した結果が、以下になります。(灰色の石の描画処理は、以降の[描画関連の変更](https://qiita.com/y-tetsu/items/d1a5c7485cb34ae5c904#描画関連の変更)でまとめて示します)
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2653390/e0b8dd69-0ea7-84ab-5a6c-76e5444cfc4f.png" width=50%>

灰色の石の追加に伴い、*board.js*のひっくり返せる石を取得する処理が変更になりました。

```diff_javascript
// ひっくり返せる石を取得する処理
// (引数)
//  turn  : プレイヤーの手番(色)
//  board : 盤面情報を格納した配列
//  index : 石を置く位置(マスを示す番号)
// (戻り値)
//  flippables : ひっくり返せる石の位置(マスを示す番号)の配列
function getFlippablesAtIndex(turn, board, index) {
  let flippables = [];
  if (board[index] !== E) return flippables;  // 空きマス以外はスキップ
- const opponent = turn === B ? W : B;
+ const opponents = getOpponentColors(turn);
  for (let {x, y} of DIRECTION_XY) {
    const dir = (GAME_BOARD_SIZE * y) + x;
    let opponentDiscs = [];
    let next = index + dir;
    // 相手ディスクが連続しているものを候補とする
-   while (opponent === board[next]) {
+   while (opponents.includes(board[next])) {
      opponentDiscs.push(next);
      next += dir;
    }
```

これにともない、`getOpponentColors`という、相手ディスクを返す関数も追加しました。

```board.js
// 自身の対戦相手を返す
// (引数)
//  turn  : プレイヤーの手番(色)
// (戻り値)
//  return : 対戦相手を格納した配列
function getOpponentColors(turn) {
  return turn in FLIPPERS ? FLIPPERS[turn].opponents : [];
}
```

`FLIPPERS`はgame.jsで定義した、**石を返すプレイヤー情報**をまとめたものです。(後述)

`getOpponentColors`の戻り値の雰囲気としては、相手ディスクが連続している箇所を検索するときに

以前は、
- 自身が黒の場合は、白
- 自身が白の場合は、黒

を調べていましたが、今回はこれを
- 自身が黒の場合は、白か灰
- 自身が白の場合は、灰か黒
- 自身が灰の場合は、黒か白

となるよう変更しています。

灰色の石に関する処理としましては、以上になります。



# 3人対戦の実装
3人対戦を実現する上では、手番の回し方を決める必要があります。
今回は、**黒→白→灰→黒→白→灰**…の順番でゲームを進めるものとしました。

関連して、主に以下が変更となりました。
- 使用可能なプレイヤーを`FLIPPERS`連想配列にて管理し、灰色プレイヤーを追加
- 手番を回す順番を`ORDER`配列で管理する
- スコア算出および勝敗判定、ゲーム終了判定のパス回数を`ORDER`配列に基づいて行う
- ゲームオブジェクト作成時に、手番の配列と、プレイヤー情報の連想配列を入力とする
- 石の描画処理を`UI_PLAYER_INFO`連想配列にまとめて、灰色を追加

また、ゲーム性向上のため以下も変更しました。
- ランダムより強いコンピュータAI(原始モンテカルロ探索)を追加

プレイヤーの設定は以下としています。
- 黒は人が操作
- 白はコンピュータ(やや強い)
- 灰はコンピュータ(ランダム)

## ゲーム管理の変更
ゲーム管理を変更した結果が以下です。ターンの順番や終了判定などが、こまごまと変わっています(差分だと多いので、コードそのままを載せました)。

```game.js
const GAME_INIT = 0;
const GAME_PLAY = 1;
const GAME_STOP = 2;
const GAME_END = 3;
const GAME_TURN_END = 'End';
const DRAW = 'Draw';
const PASS = 'Pass';
const WIN = 'Win!';
const WAIT_TIME = 800;                // ウェイト時間(ms)
const ORDER = [B, W, A];              // プレイヤーの順番
const FLIPPERS = {                    // 使用可能な全プレイヤー情報(石をひっくり返せる)
  [B]: {                              // 黒
    'player'   : new Player(HUMAN),   // 人が操作
    'opponents': [W, A],              // 白と灰をひっくり返せる
    'score'    : 0,                   // 黒の石の数
  },
  [W]: {                              // 白
    'player'   : new Player(MCS),     // コンピュータが操作(原始モンテカルロ探索)
    'opponents': [B, A],              // 黒と灰をひっくり返せる
    'score'    : 0,                   // 白の石の数
  },
  [A]: {                              // 灰
    'player'   : new Player(RANDOM),  // コンピュータが操作(ランダム)
    'opponents': [B, W],              // 黒と白をひっくり返せる
    'score'    : 0,                   // 灰の石の数
  },
};

// ゲームの管理
class Game {
  constructor(board, order, flippers) {
    this.board = board.concat();
    this.order = order.concat();
    this.turnIndex = 0;
    this.turn = this.order[this.turnIndex];
    this.flippers = this.copyFlippers(flippers);
    this.player = this.flippers[this.turn].player;
    this.participants = [...new Set(order)];
    this.pass = 0;
    this.wait = this.getWaitTime();
    this.humanMove = NO_MOVE;
    this.updateScore();
    this.updatedDiscs = [];
    this.state = GAME_INIT;
  }

  // ゲームループ
  loop() {
    this.updatedDiscs = [];
    this.state = this.play();
    this.updateScore();
    updateUi();
    switch (this.state) {
      case GAME_PLAY:
        setTimeout(() => this.loop(), this.wait);
        break;
      case GAME_STOP:
        break;
      case GAME_END:
        alert(this.getWinnerMessage());
        break;
      default:
        break;
    }
  }

  // 1手プレイ
  play() {
    if (this.isEnd()) return GAME_END;
    if (this.indicatePass()) alert(this.getPassMessage());
    this.pass = 0;
    this.updatedDiscs = this.player.actMove(this);
    if (this.updatedDiscs.length === 0) return GAME_STOP;
    this.setNextPlayer();
    return GAME_PLAY;
  }

  // 次のプレイヤーを設定する
  setNextPlayer() {
    this.turnIndex++;
    if (this.turnIndex >= this.order.length) this.turnIndex = 0;
    this.turn = this.order[this.turnIndex];
    this.player = this.flippers[this.turn].player;
  }

  // スコアの更新
  updateScore() {
    for (let color of this.order) {
      this.flippers[color].score = this.board.filter(e => e === color).length;
    }
  }

  // 終了の判定
  isEnd() {
    for (let i=0; i<this.order.length; i++) {
      if (!this.isPass()) return false;
    }
    this.turn = GAME_TURN_END;
    return true;
  }

  // パスの判定
  isPass() {
    if (getLegalMoves(this.turn, this.board).length <= 0) {
      this.setNextPlayer(this.turn);
      this.pass++;
      return true;
    }
    return false;
  }

  // パス通知の有無
  indicatePass() {
    const pass = (this.pass < this.order.length && this.pass > 0);
    const human = this.participants.map(e => this.flippers[e].player.name).includes(HUMAN);
    return pass && human;
  }

  // パス通知のメッセージを取得
  getPassMessage() {
    let pre = []
    let index = this.turnIndex;
    for (let i=0; i<this.pass; i++) {
      index--;
      if (index < 0) index = this.order.length - 1;
      pre.push(getGameTurnText(this.order[index]));
    }
    return pre.reverse().join(' and ') + ' ' + PASS;
  }

  // 勝利プレイヤー通知のメッセージを取得
  getWinnerMessage() {
    const winner = this.getWinner();
    return winner === DRAW ? winner : getGameTurnText(winner) + ' ' + WIN;
  }

  // 勝利プレイヤーの石を返す
  getWinner() {
    const scores = this.participants.map(e => this.flippers[e].score);
    const max = Math.max(...scores);
    if (scores.filter(e => e === max).length === 1) return this.participants[scores.indexOf(max)];
    return DRAW;
  }

  // 待ち時間取得
  getWaitTime() {
    // ユーザー同士、コンピュータ同士の場合はウェイトなし
    const players = [...new Set(this.participants.map(e => this.flippers[e].player.name))];
    return players.includes(HUMAN) && (players.length > 1) ? WAIT_TIME : 0;
  }

  // プレイヤー情報のコピー
  copyFlippers(flippers) {
    let copy = {};
    for (let key in flippers) {
      copy[key] = {
        'player'   : new Player(flippers[key].player.name),
        'opponents': flippers[key].opponents.concat(),
        'score'    : flippers[key].score,
      };
    }
    return copy;
  }
}
```

## 描画関連の変更
描画についても、灰色の石の表示処理が追加になっているほか、クリック時のプレイヤーの判定処理などにも変更が入りました。灰色の石の画像もフォルダに追加しています。
```ui.js
const UI_BOARD = 'ui_board';
const UI_PLAYER_INFO = {
  [B]: {
    'name': 'Black',
    'img' : './image/black.png',
  },
  [W]: {
    'name': 'White',
    'img' : './image/white.png',
  },
  [A]: {
    'name': 'Ash',
    'img' : './image/ash.png',
  },
};

// 盤面のテーブル作成
function createBoardTable() {
  // ボードの子要素を一旦全削除
  const uiBoard = document.getElementById(UI_BOARD);
  removeChilds(uiBoard);
  // ヘッダ含めた盤面をテーブルで作成
  const table = document.createElement('table');
  uiBoard.appendChild(table);
  for (let y=0; y<BOARD_SIZE; y++) {
    const tr = document.createElement('tr');
    table.appendChild(tr);
    for (let x=0; x<BOARD_SIZE; x++) {
      const td = document.createElement('td');
      tr.appendChild(td);
      td.addEventListener('click', onBoardClicked);
      td.setAttribute('id', UI_BOARD + (y * BOARD_SIZE + x));
    }
  }
  // 盤面のサイズと背景色と形
  for (let i=0; i<BOARD.length; i++) {
    const square = document.getElementById(UI_BOARD + i);
    square.width = 60;
    square.height = 60;
    const boardColor = BOARD_COLOR[i];
    if (reversi.board[i] !== H && boardColor !== '*') {
      square.style.backgroundColor = COLOR_CODE[boardColor];
    }
  }
  // 盤面のヘッダー情報を追加
  for (let i=0; i<PLAYABLE_SIZE; i++) {
    // 上辺のヘッダ(アルファベット)
    const topHeader = document.getElementById(UI_BOARD + (i + 1));
    topHeader.textContent = String.fromCharCode('A'.charCodeAt(0) + i);
    // 左辺のヘッダ(数字)
    const leftHeader = document.getElementById(UI_BOARD + ((i + 1) * BOARD_SIZE));
    leftHeader.textContent = i + 1;
  }
  // 石の初期配置
  setupDiscs(PLAYABLE_INDEXS);
}

// 盤面の更新
function updateUi() {
  setupDiscs(reversi.updatedDiscs);
  const turn = document.getElementById('turn');
  turn.textContent = getGameTurnText(reversi.turn);
  const score = document.getElementById('score');
  score.textContent = reversi.participants.map(e => reversi.flippers[e].score).join(' : ');
}

// 石を並べる
// (引数)
//  indexs  : 石を置く位置(マスを示す番号)の配列
function setupDiscs(indexs) {
  for (let index of indexs) {
    const square = document.getElementById(UI_BOARD + index);
    const disc = reversi.board[index];
    if (disc in UI_PLAYER_INFO) setImg(square, UI_PLAYER_INFO[disc].img);
  }
}

// 画像を配置
// (引数)
//  element : Document要素
//  imgPath : 画像のパス
function setImg(element, imgPath) {
  removeChilds(element);                      // 一旦、子要素削除
  const img = document.createElement('img');  // 画像要素作成
  img.src = imgPath;                          // 画像パス
  img.width = 40;                             // 横サイズ（px）
  img.height = 40;                            // 縦サイズ（px）
  element.appendChild(img);                   // 画像追加
}

// 要素の子を削除
function removeChilds(element) {
  for (let i=element.childNodes.length-1; i>=0; i--) {
    element.removeChild(element.childNodes[i]);
  }
}

// 手番の文字列を取得
function getGameTurnText(turn) {
  return turn in UI_PLAYER_INFO ? UI_PLAYER_INFO[turn].name : GAME_TURN_END;
}

// マスをクリックした時の処理
function onBoardClicked(event) {
  if (reversi.state !== GAME_STOP) return;
  if (reversi.player.name === HUMAN) {
    const index = Number(this.getAttribute('id').replace(UI_BOARD, ''));
    const flippables = getFlippablesAtIndex(reversi.turn, reversi.board, index);
    if (flippables.length > 0) {
      reversi.humanMove = index;
      reversi.loop();
    }
  }
}
```

## AIの追加
ゲームを少し面白くするため、ランダムよりも強いAIの追加も行いました。

以下の理由から、今回は**原始モンテカルロ探索**を採用しました。
- 3人対戦ということで、2人対戦用のMinMax法やアルファ・ベータ法がそのまま使えなかった
- 盤面形状が様々に異なっても、強さに影響が出にくいものとしたかった
- ~~なるべく簡単に作れるものが良かった~~

また、探索数を終盤にかけて増やしていく仕組みを入れ、序盤に処理が重くならないようにしています。結果、序盤は手を抜き、終わりが近づくにつれ頑張りだすAIになりました。処理の高速化が実現できれば、もう少しAIを強くできそうですが、今後の課題としたいと思います。

```player.js
const HUMAN = 'human';
const RANDOM = 'random';
const MCS = 'mcs';
const NO_MOVE = -1;
const MCS_SCHEDULE = [
  [40, 34, 28, 22,  16,  13,  10,   6],  // 空きマス残り
  [20, 40, 60, 80, 120, 200, 400, 800],  // プレイアウト回数
];

// プレイヤー
class Player {
  constructor(name) {
    this.name = name;
  }

  // 1手打つ
  actMove(game) {
    let move = NO_MOVE;
    switch (this.name) {
      case HUMAN:
        move = getMoveByHuman(game);
        break;
      case RANDOM:
        move = getMoveByRandom(game);
        break;
      case MCS:
        move = getMoveByMonteCarloSearch(game, MCS_SCHEDULE);
        break;
      default:
        break;
    }
    return putDisc(game.turn, game.board, move);
  }
}

// 原始モンテカルロ探索で選んだ手を返す
// (引数)
//  game     : ゲーム情報
//  schedule : 残り空きマス数に応じたプレイアウト数を格納した配列
// (戻り値)
//  return : コンピュータの手(マスを表す番号)
function getMoveByMonteCarloSearch(game, schedule) {
  const num = getPlayoutNum(game.board, schedule);
  const turn = game.turn;
  const randomFlippers = getRandomFlippers(game.flippers);
  const legalMoves = getLegalMoves(turn, game.board);
  if (legalMoves.length === 0) return NO_MOVE;
  let results = [];
  for (let move of legalMoves) {
    let localBoard = game.board.concat();
    putDisc(turn, localBoard, move);
    let value = 0;
    for (let i=0; i<num; i++) {
      value += getPlayoutValue(game.turnIndex, localBoard, game.order, randomFlippers);
    }
    results.push(value);
  }
  return legalMoves[results.indexOf(Math.max.apply(null, results))];
}

// プレイアウト回数を返す
// (引数)
//  board    : 盤面情報を格納した配列
//  schedule : 残り空きマス数に応じたプレイアウト数を格納した配列
// (戻り値)
//  return   : プレイアウト回数
function getPlayoutNum(board, schedule) {
  const remain = board.filter(e => e === E).length;
  let index = schedule[0].indexOf(Math.min.apply(null, schedule[0].filter(e => e >= remain)));
  index = index === -1 ? 0 : index;
  return schedule[1][index];
}

// ランダムなプレイヤー情報を取得
// (引数)
//  flippers : プレイヤー情報を格納した連想配列
// (戻り値)
//  return   : flippersのプレイヤーをランダムに変えたもの
function getRandomFlippers(flippers) {
  let randoms = {};
  for (let key in flippers) {
    randoms[key] = Object.assign({}, flippers[key]);
    randoms[key].player = new Player(RANDOM);
  }
  return randoms;
}

// プレイアウト結果を取得
// (引数)
//  index    : プレイヤー手番の番号
//  board    : 盤面情報を格納した配列
//  order    : プレイヤーの順番を格納した配列
//  flippers : プレイヤー情報を格納した連想配列
// (戻り値)
//  return   : プレイアウト結果(勝ち=1、それ以外=0)
function getPlayoutValue(index, board, order, flippers) {
  const playout = new Game(board, order, flippers);
  playout.turnIndex = index;
  playout.setNextPlayer();
  while (playout.play() === GAME_PLAY);
  playout.updateScore();
  return playout.getWinner() === order[index] ? 1 : 0;
}
```

ずいぶん込み入ったコードになってしまいましたが、仕組みとしては以下のイメージで作成しました。

- 打てる手の候補それぞれについて、一定回数ランダムにゲーム終了まで対戦(プレイアウト)させ、勝利数を集計
- 勝利数が一番多かった手を選ぶ
- 何回ランダムに対戦させるかは、空きマスの数に応じてあらかじめ決めておく

### 対戦結果
下記表のAIの組み合わせそれぞれで、**100回**づつ対戦させてみました。
意外とはっきり強くなっていそうです。(といっても、実際に対戦してみるとまだまだ普通に勝てるレベルでしたね…)
|黒 |白 |灰　|黒の勝率 |白の勝率 |灰の勝率 |
|---|---|---|---|---|---|
|モンテカルロ | ランダム |ランダム |98% |2% |0% |
|ランダム | モンテカルロ |ランダム |1% |98% |1% |
|ランダム | ランダム |モンテカルロ |1% |2% |97% |

### 対戦シミュレータ
先ほどの対戦結果を出すにあたり、簡単なシミュレータを追加しました。

```
colorful-reversi
└─test
        simulator.html
        simulator.js
```

#### 使い方
1. *simulator.html*をブラウザで開き、開発者ツールを起動します。

2. 下図のテキストボックスに対戦回数を入力し、startボタンを押してシミュレーションを開始します。
![simulator1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2653390/a93cb55e-70c1-ab0e-9cd4-d9a351b4eeb7.png)

3. 対戦が終わると以下のように結果が表示されます。
![simulator2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2653390/44adf362-7e69-d9b1-9622-3a240035333c.png)
(処理に時間がかかるため、対戦回数は様子を見ながら増やしてください)



# 完成
以上で、今回作ろうと思っていた、**3人対戦で遊べる変則リバーシ**が完成しました。

灰色プレイヤーはどちらかというと、黒プレイヤーVS白プレイヤーの対戦を乱す**お邪魔キャラ**の想定で、今回は用意しております。遊ばれる際は、白プレイヤーを倒すことを目標にプレイしてみて下さい。

もちろん、ダウンロードしたソースコードを改変して、3プレイヤーとも"人"が操作するようにすることも、"コンピュータ"に操作させるようにすることも可能です。

完成したものをプレイしている様子が、以下になります。
(灰色プレイヤーが入ってくるタイミングが、なんとも慣れませんね)
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2653390/c64a0cf1-8e71-47dc-06d7-95bbe5e3b75a.gif" width=75%>



# おまけ
今回の変更により、ある程度手番の順序やプレイヤー追加を単純化しました。完成品には入れなかったのですが、試してみた結果を、2つ残しておきます。

## 灰色が黒と白の間にも割り込むケース
手番を回す順序を、黒→**灰**→白→**灰**→黒→…というのも試してみました。
```game.js
const ORDER = [B, A, W, A];          // プレイヤーの順番
```

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2653390/451ab42a-0f71-44ab-ad69-3e05ce72da36.gif" width=75%>

灰色プレイヤーのお邪魔率が倍！展開が早いです。

## 5人対戦
まだ見たことがなかったので、5人対戦も試してみました。灰色プレイヤーの同類として、新たに**シアン**プレイヤーと**山吹**プレイヤーを足してみます。(両プレイヤーともランダム扱い)

```board.js
const H = 0;  // 穴
const E = 1;  // 空きマス
const B = 2;  // 黒色の石
const W = 3;  // 白色の石
const A = 4;  // 灰色の石
const C = 5;  // シアン色の石
const Y = 6;  // 山吹色の石

const BOARD = [
  H, H, H, H, H, H, H, H, H, H,
  H, H, H, E, E, E, E, H, H, H,
  H, H, E, E, C, E, E, E, H, H,
  H, E, E, Y, B, A, C, E, E, H,
  H, E, E, A, W, B, W, Y, E, H,
  H, E, Y, W, B, W, A, E, E, H,
  H, E, E, C, A, B, Y, E, E, H,
  H, H, E, E, E, C, E, E, H, H,
  H, H, H, E, E, E, E, H, H, H,
  H, H, H, H, H, H, H, H, H, H,
];

```

```game.js
const ORDER = [B, W, A, C, Y];        // プレイヤーの順番
const FLIPPERS = {                    // 使用可能な全プレイヤー情報(石をひっくり返せる)
  [B]: {                              // 黒
    'player'   : new Player(HUMAN),   // 人が操作
    'opponents': [W, A, C, Y],        // 対戦相手
    'score'    : 0,                   // 黒の石の数
  },
  [W]: {                              // 白
    'player'   : new Player(MCS),     // コンピュータが操作(原始モンテカルロ探索)
    'opponents': [B, A, C, Y],        // 対戦相手
    'score'    : 0,                   // 白の石の数
  },
  [A]: {                              // 灰
    'player'   : new Player(RANDOM),  // コンピュータが操作(ランダム)
    'opponents': [B, W, C, Y],        // 対戦相手
    'score'    : 0,                   // 灰の石の数
  },
  [C]: {                              // シアン
    'player'   : new Player(RANDOM),  // コンピュータが操作(ランダム)
    'opponents': [B, W, A, Y],        // 対戦相手
    'score'    : 0,                   // シアンの石の数
  },
  [Y]: {                              // 山吹
    'player'   : new Player(RANDOM),  // コンピュータが操作(ランダム)
    'opponents': [B, W, A, C],        // 対戦相手
    'score'    : 0,                   // 山吹の石の数
  },
};
```

```ui.js
const UI_PLAYER_INFO = {
  [B]: {
    'name': 'Black',
    'img' : './image/black.png',
  },
  [W]: {
    'name': 'White',
    'img' : './image/white.png',
  },
  [A]: {
    'name': 'Ash',
    'img' : './image/ash.png',
  },
  [C]: {
    'name': 'Cyan',
    'img' : './image/cyan.png',
  },
  [Y]: {
    'name': 'Yamabuki',
    'img' : './image/yamabuki.png',
  },
};
```

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2653390/aa951e2d-7684-3981-cd3a-260014fdbe87.gif" width=75%>

自分が打った手の意味がわからなくなるくらい、目まぐるしい展開でした。



# 最後に
黒、白、灰色が入り乱れる、少し変わったリバーシを作ってみましたが、いかがでしたでしょうか。今回の3人対戦の場合、実際問題として何色の石が有利なのか、興味がわくところでもありますね。(リバーシの三体問題、あるのかないのか…、ないか…)
最後まで読んで下さり、ありがとうございました。

本記事の続編となる以下も、よかったら見てみてください。

https://qiita.com/y-tetsu/items/ab8d92969c3c1b20fc9c

それでは皆様、よきリバーシ・ライフを！