JavaScript
Node.js
オブジェクト指向
es6
ボードゲーム

ボードゲーム実装における思考フレームワーク 〜3.プレイヤーの代行〜

関連記事

ブログ

ゲームとは何か
ボードゲームはプログラミングしやすい

趣味的な考察です。長めです。
この記事との関連箇所はちゃんと記事内で抜粋しているので、無理して読まなくて大丈夫です。
興味ある方だけどうぞ。

Qiita

ボードゲーム実装における思考フレームワーク 〜1.ゲームの状態の管理〜
ボードゲーム実装における思考フレームワーク 〜2-1.ルールの施行(思考編)〜
ボードゲーム実装における思考フレームワーク 〜2-2.ルールの施行(実装編)〜
ボードゲーム実装における思考フレームワーク 〜3.プレイヤーの代行〜(本記事)

前回の訂正

CPUプレイヤーとUI

前回、以下のように書きました。

CPUプレイヤーは思考アルゴリズム毎に新しいクラスを作るイメージ(例:easyCpuPlayer, hardCpuPlayer)で、人間プレイヤーはUIの種類毎に新しいクラスを作るイメージ(例:CliPlayer, GuiPlayer)です。

CPUプレイヤーにUIは関係ない...そんなふうに考えていた時期が私にもありました。

確かにCPUプレイヤークラスは人間プレイヤークラスと違い、ユーザーからインプットを受け取る必要はありません。
しかし、ユーザーにアウトプットを示す場面は依然としてあります。
特にCPUプレイヤー同士を戦わせてユーザーが観戦するようなケースを実装しようとすると、どうしてもCPUプレイヤークラスでアウトプットをする必要が出てきます。

ゲームマスターが代わりにアウトプットする案も無くは無いですが、人間プレイヤークラスがユーザー相手にインタラクティブなUIを表示することが確定である以上、ゲームマスターにUIの責務を全部持っていくことはできません。
ゲームマスターとプレイヤーが協力してUIを担当するか、それともプレイヤーに責務を集めてしまうかなら、後者の方が良いと考えます。

というわけで、CPUプレイヤーもUIを熟知する必要があります。
結果、以下の分だけ作ることになります。

人間プレイヤークラス : UIの数
CPUプレイヤークラス : UIの数 × 思考アルゴリズムの数

UIの数だけ作る?

この想定、よく考えたらおかしいことにも気付きました。
複数UIを同時に提供するアプリってどんなでしょうね。
あんま想定する必要の無い謎シチュエーションな気がします。

というわけで、人間プレイヤークラスとCPUプレイヤークラスは、CLIを前提に書いてしまいます。
UIを変えたい時は、クラスを新しく作るというよりは該当クラスを書き換えるようなイメージで。

結果、以下の分だけ作ることになります。

人間プレイヤークラス : 1
CPUプレイヤークラス : 思考アルゴリズムの数

また、名前は以下のようになります。

人間プレイヤークラス : HumanPlayer
CPUプレイヤークラス : CpuPlayerEasy, CpuPlayerHard, etc.

まあはっきり言って前回の命名(CliPlayer)はセンスなかったです。
あと、EasyCpuPlayerCpuPlayerEasyにしてますが、これはソートのしやすさとかを意識してのことです。
英語的には前者の方が自然ですが、プログラム的には後者のように「大分類 -> 小分類」な順番で単語を並べた方が分かりやすい印象です。

CPUプレイヤーの責務分解

さて、CPUプレイヤーがUIも担当するようになった今の状況を再び整理しましょう。
"プレイヤー"という概念は現在、以下の2つの責務を担うものとなりました。

  • UIを通じてユーザーに必要な情報を表示
  • 次に指す手を思考

人間プレイヤーは特に変わりありません。
思考の部分をユーザーに委譲することでこれを果たします。
human-player.png

問題は責務が増えたCPUプレイヤーですが、人間プレイヤーの責務分解の仕方はそのまま参考になりそうです。
思考の部分を外部に切り出して委譲してみましょう。
cpu-player.png
対応関係がはっきりして、すっきり整理できました。

図中では「CPU brain???」と仮に書いています。
CPUプレイヤーにおける、次の手を考えるアルゴリズム部分。
コードに起こす段階で分かりやすくなりますが、これはGoFデザインパターンで言うところのStrategyパターンにあたる気がします。
デザパタは名前を借りるためにあります。
今回はPlayerStrategyとしておきましょう。

訂正まとめ

訂正前:

CPUプレイヤーは思考アルゴリズム毎に新しいクラスを作るイメージ(例:easyCpuPlayer, hardCpuPlayer)で、人間プレイヤーはUIの種類毎に新しいクラスを作るイメージ(例:CliPlayer, GuiPlayer)です。

訂正後:(引用を装っていますが引用じゃないです)

作成するアプリのUIを考慮しながらCPUプレイヤークラス(CpuPlayer)と人間プレイヤークラス(HumanPlayer)を作り、CPUプレイヤー用に思考アルゴリズム毎のPlayerStrategy(例:PlayerStrategyEasy, PlayerStrategyHard)を作るイメージです。

ルール3(可能/禁止アクション)の再検討

今までこのルールは「あるゲームの状態について可能なアクションであるかどうかをBooleanで返す」という方針で表現してきました。
しかし、「あるゲームの状態に対し可能なアクションの配列を返す」という方針も実は考えられます。

片方の方針があればもう片方の方針はそこからでも実装できます。
前者をアクションの「判定式定義」、後者を「列挙式定義」と仮に名付けましょう。
実装イメージは下記です。(細かいところは適当です。)

判定式定義を列挙式定義で表す.js
function isValid (action, state) {
  // Booleanを返す
  return getValidActions(state).includes(action)
}
列挙式定義を判定式定義で表す.js
function getValidActions (state) {
  // アクションの配列を返す
  return getAllActions(state).filter(action =>
    isValid(action)
  )
}

実装はできますが、しかしそれぞれ回り道をしておりパフォーマンスはよくありません。
どの定義を実装するかについて、以下の3種類の方針が考えられます。
availablility_definitions.png
両方実装すると、どっちの定義が必要な時もパフォーマンスが良いですが、代わりに多重定義となります。
つまり、ルールを追加したり変更したりする時は、必ず両方の定義を整合的に弄らなければいけません。
一方で、片方の定義しか実装しないと、もう片方の定義が必要な時にパフォーマンスが悪いです。

さて、今回はどっちの定義が必要でしょうか。

  • 判定式定義が必要な状況
    • CLIでユーザーが入力したアクションに対し、可能なアクションであるかどうかをチェックする時。不可能なアクションである場合には、不可能な理由まで表示する。
  • 列挙式定義が必要な状況
    • GUIでユーザーが入力できるアクションをあらかじめ制限する時。
    • CPUプレイヤーがアクションを考える時。

どっちの定義も必要になりそうです。
パフォーマンスを重視したいので、多重定義問題には目をつぶり、両方のやり方でルール3を実装したいと思います。

なお、今回の○×ゲームでは列挙式定義が可能ですが、例えばアクションに実数値の属性があるようなボードゲームがあったとすると(駒を2cm進める、2.1cm進める、2.01cm進める...みたいな)、アクションの列挙は不可能です。
その場合、代わりに「アクションの範囲」を表現するオブジェクトを作る必要が出てくるかもしれませんね。
とりあえず今回は考えないでおきます。

実装:機能3. ルールの施行

訂正の反映

CliPlayerHumanPlayerにリネームします。
コードは略しますが、一応こちらのコミットを見ていただければ差分は確認できます。

displayBoardの括り出し

util.js
const util = {
  // TODO: 適切な所属クラスを作る
  displayBoard: board => {
    const sq = board.squaresString
    const getSq = i => sq[i] === '-' ? ' ' : sq[i]
    const text =
`    1   2   3
  ┏━━━┳━━━┳━━━┓
a ┃ ${getSq(0)}${getSq(1)}${getSq(2)} ┃
  ┣━━━╋━━━╋━━━┫
b ┃ ${getSq(3)}${getSq(4)}${getSq(5)} ┃
  ┣━━━╋━━━╋━━━┫
c ┃ ${getSq(6)}${getSq(7)}${getSq(8)} ┃
  ┗━━━┻━━━┻━━━┛`
    console.log(text)
  }
}

module.exports = util

今までの記事で何回かコピペしてきたdisplayBoard関数。
また使う必要が出てきたので、いいかげんコピペも限界かなと思い括り出しました。
とりあえずutilという適当なユーティリティ関数集を作ってぶち込んでおきます。
CLI関係の関数が増えたきたらいい感じにクラスとして切り出したいところですが、仕様が出揃っていない時は最適化を急がずに適当な保留状態にしておくのも大事、と「オブジェクト指向設計実践ガイド」が言っていました。
なのでTODOメモだけ残して放置します。

こちらもただのリファクタリングなので細かいコード差分は省略。
確認したい方はこちら

ルール3(可能/禁止アクション)の列挙式定義の実装

fill-action.js
class FillAction {

  // (略)

  // 可能な記入アクションを全て生成
  static createValidOnes (state) {
    const playerChar = state.nextPlayerChar
    const squares = Array.from(state.board.squaresString)

    // まだ"o"や"x"が記入されていないマスを探す
    const unfilledSquareIndexes = []
    squares.forEach((char, i) => {
      if (!['o', 'x'].includes(char)) {
        unfilledSquareIndexes.push(i)
      }
    })

    // 未記入マス毎に記入アクションを生成
    return unfilledSquareIndexes.map(i =>
      new this(playerChar, i)
    )
  }
}

module.exports = FillAction
resign-action.js
class ResignAction {

  // (略)

  // 可能な降参アクションを全て生成
  static createValidOnes (state) {
    // 一応、手番じゃない人も降参可能としています。
    // 手番じゃない時はGameMasterがplayさせてくれないので、
    // 実際は無意味。
    return [
      new this('o'),
      new this('x')
    ]
  }
}

module.exports = ResignAction
abort-action.js
const GameState = require('../game-states/game-state')

class AbortAction {

  // (略)

  // 降参アクションと同様
  static createValidOnes (state) {
    return [
      new this('o'),
      new this('x')
    ]
  }
}

module.exports = AbortAction

なるべくcheckValidationメソッド(=判定式定義)の近くで実装し、両方同時にメンテする必要があることを分かりやすくしておきたいところ。

CPUPlayerの実装

cpu-player.js
const util = require('../util')

class CpuPlayer {
  constructor (char, strategy) {
    this._char = char
    this._strategy = strategy
  }
  get char () { return this._char }

  // 人間プレイヤーに揃えてasyncにしているが、
  // 実際のところ普通の同期処理しかやっていない
  async play (stateInfo) {
    util.displayBoard(stateInfo.board)
    const action = this._strategy.play(stateInfo)
    console.log(action.toNotation())
    console.log()
    return action
  }
}

module.exports = CpuPlayer

CLIへの情報表示をするだけで、あとは「CPUプレイヤーの責務分解」で述べたようにstrategyに委譲します。

PlayerStrategyの実装

ボードゲームにおけるCPUプレイヤーの思考アルゴリズムは、凝れば凝るほど深い沼がありそうです。
行き着く先はAlphaGoとかですからね。
まあ、幸い○×ゲームは盤面のパターンが非常に少ないので、初めから全ての手を全部読んでしまうような無茶も普通に通ってしまいそうです。

ということで、今回はあまり深入りせず、以下の2パターンを実装するに留めます。

  • クソ雑魚アルゴリズム。完全にランダムに○×を記入します。
  • 神の目アルゴリズム。全ての未来を完全に見通した上で最善を尽くします。負けません。1

クソ雑魚アルゴリズムの実装

util.js
const util = {

  // (略)

  // 例えばmax=2の場合、0, 1, 2のどれかをランダムで返します。
  getRandomInt: max => {
    return Math.floor(Math.random() * (max + 1))
  },

  // 配列の中からランダムで1個取り出すやつです。
  getRandomItem: array => {
    return array.length <= 0 ? null
      : array[util.getRandomInt(array.length - 1)]
  }
}

module.exports = util
player-strategy-fool.js
const FillAction = require('../actions/fill-action')
const ResignAction = require('../actions/resign-action')
const util = require('../util')

// クソ雑魚アルゴリズム
class PlayerStrategyFool {
  constructor (playerChar) {
    this._playerChar = playerChar
  }

  play (stateInfo) {
    // 可能なアクションの取得
    const actions = []
    // 分かりづらいですが、定数USE_ACTION_CLASSESは下の方(クラス定義外)で定義しています。
    // 定義されているのは「アクションクラスの配列」です。
    for (const actionClass of this.constructor.USE_ACTION_CLASSES) {
      actionClass.createValidOnes(stateInfo)
        .filter(action => action.playerChar === this._playerChar) // 念のため。
        .forEach(action => actions.push(action))
    }
    // こちらも念のためのロジック。
    // 可能な手が無いとき、それ即ちバグなんですが、もしそうなったら適当に降参とかさせときます。
    if (actions.length <= 0) {
      return ResignAction(this._char)
    }

    // クソ雑魚なので、可能なアクションから何も考えずランダムに選んで返します。
    return util.getRandomItem(actions)
  }
}
// 使用するアクションの種類を定義しています。
// つまり、基本は降参や強制終了をせずに記入アクションのみをし続けるってことです。
PlayerStrategyFool.USE_ACTION_CLASSES = [FillAction]

module.exports = PlayerStrategyFool

ルール3(可能/禁止アクション)の列挙式定義を実装したのは、ここで使うためです。

なお、実は以下をまだ実装していなかったので追加しておいてください。

fill-action.js
class FillAction {

  // (略)

  get playerChar () { return this._playerChar }
}

ResignAction, AbortActionも同様。
必要になってから作るYAGNIスタイルです。

神の目アルゴリズムの実装

ちょっと長いです。

player-strategy-god.js
const resultType = require('../consts/result-type')
const FillAction = require('../actions/fill-action')
const ResignAction = require('../actions/resign-action')

// 盤面に対する評価値
// 良い結果ほど高い値にしています。
const assessments = {
  WIN: 2,
  DRAW: 1,
  UNKNOWN: 0, // 試合続行でまだ結果が出ない盤面に対し、評価を保留にするのに使います。
  LOSE: -1
}

// 神の目アルゴリズム
class PlayerStrategyGod {
  constructor (playerChar) {
    this._playerChar = playerChar
  }

  play (stateInfo) {
    // 内部メソッドを呼び、返し値の中からアクションだけを抽出して返す感じです。
    return this._playAndPredict(stateInfo).action
  }

  // こちらがアルゴリズムの本体。
  // 次に実行するアクションだけでなく、「その後、お互いが最善を尽くし続けたら
  // どういう結果になるのか」という評価値も返します。
  // この評価値は、再帰呼び出しをする中でこのメソッド自身が利用します。
  _playAndPredict (stateInfo) {
    // 可能なアクションの取得。この辺はクソ雑魚とほぼ変わらないです。
    const actions = []
    for (const actionClass of this.constructor.USE_ACTION_CLASSES) {
      actionClass.createValidOnes(stateInfo)
        .filter(action => action.playerChar === this._playerChar)
        .forEach(action => actions.push(action))
    }
    if (actions.length <= 0) {
      return {
        assessment: assessments.LOSE,
        action: ResignAction(this._char)
      }
    }

    // 可能なアクションの実行結果を見ていきます。とりあえず1手先しか見ないで1巡するイメージ。
    // 即勝利なアクションを探すのが目的です。見つけたら即返却and終了。
    // 1手先で試合が決まらない場合、とりあえず評価値をUNKNOWN(不明)にしておきます。
    // この時の実行結果を色々詰めて、gameNodeと名付けてキャッシュしておきます。
    const gameNodes = []
    for (const action of actions) {
      const appliedState = action.applyTo(stateInfo)
      const assessment = this._assess(appliedState.checkResult())
      if (assessment === assessments.WIN) {
        return {
          assessment: assessments.WIN,
          action: action
        }
      }
      gameNodes.push({
        assessment: assessment,
        action: action,
        gameState: appliedState
      })
    }

    // UNKNOWNな試合展開について、もっと先まで読んで評価を確定します。
    // ここから先は2手以上先を読む必要があります。つまり敵の思考を読まなければいけません。
    // 尚、相変わらず勝利なアクションさえ見つければ即終了です。
    const enemyChar = this._enemyChar()
    for (const node of gameNodes) {
      if (node.assessment !== assessments.UNKNOWN) {
        continue
      }

      // 脳内仮想敵を作ります。敵も同じ神の目アルゴリズムだと想定します。
      // 仮想敵のPlayerStrategyさえあれば十分で、Playerを作る必要はありません。
      const virtualEnemyStrategy = new PlayerStrategyGod(enemyChar)

      // 1手先の状態を仮想敵に渡し、同じ思考をさせます。変則的な再帰呼び出しです。
      // 再帰呼び出しする度にもう1手先を読むことになるので、○×ゲームのルール上、
      // 最遅でも9手目には全ての結果がはっきりしてUNKNOWNな盤面がなくなります。
      // この再帰呼び出しはUNKNOWNな盤面にしか行わないので、そこで再帰が止まり、
      // 結果が返ってくるはずです。
      const stateInfoOfEnemy = node.gameState.perceivedBy(enemyChar)
      const enemyAssessment =
        virtualEnemyStrategy._playAndPredict(stateInfoOfEnemy).assessment

      // 勝利を見つけたパターン。
      // 仮想敵「ワイの負けや!」 -> 自分「つまり、ワイの勝ちやな!」
      if (enemyAssessment === assessments.LOSE) {
        return {
          assessment: assessments.WIN,
          action: node.action
        }
      }

      // 上と似たようなノリで、仮想敵の評価を参考に自分の評価を決めます。
      // 手抜きっぽいですが、一度UNKNOWNを入れたところをもう一回上書きします。
      node.assessment = (enemyAssessment => {
        switch (enemyAssessment) {
          case assessments.DRAW:
            return assessments.DRAW
          case assessments.WIN:
            return assessments.LOSE
          default:
            throw new Error(
              'Now the assessment of the virtual enemy must be DRAW or WIN.'
            )
        }
      })(enemyAssessment)
    }

    // ここまで来たということは、一度も勝利パターンが見つからないまま
    // 全展開を読み終わってしまったということ。
    // もはや負け戦です。
    // アクション毎の評価は「引き分け」か「負け」かしかないですが、
    // せめて「引き分け」なアクションを選ぶために評価値が大きいgameNodeを選んで返却します。
    gameNodes.sort((l, r) => r.assessment - l.assessment) // 降順
    const node = gameNodes[0]
    return {
      assessment: node.assessment,
      action: node.action
    }
  }

  // GameResultは客観的な値(例:「勝者は×です。」)なので、
  // 自分にとっての主観的な評価値(例:「ワイの勝ちや!」)に変換します。
  _assess (result) {
    switch (result.resultType) {
      case null:
        return assessments.UNKNOWN
      case resultType.DRAW:
        return assessments.DRAW
      case resultType.WIN_OR_LOSE:
        if (result.winner === this._playerChar) {
          return assessments.WIN
        } else {
          return assessments.LOSE
        }
      default:
        throw new Error('Given result is unusual.')
    }
  }

  // "o", "x"の反転
  _enemyChar () {
    switch (this._playerChar) {
      case 'o': return 'x'
      case 'x': return 'o'
      default: throw new Error('An irregular player character.')
    }
  }
}
PlayerStrategyGod.USE_ACTION_CLASSES = [FillAction]

module.exports = PlayerStrategyGod

ちゃんとチェックしてないですが、いわゆるミニマックス法になってるんじゃないかと思います。

先読み結果をキャッシュし、CPUプレイヤー同士で使い回したりしたらもっと速くなるはずです。
まあこれでもあまり酷い計算量ではなかったみたいなので、今回はこのまま放置します。

試す

as-a-player.js
const readline = require('readline')
const resultType = require('../lib/consts/result-type')
const CpuPlayer = require('../lib/players/cpu-player')
const HumanPlayer = require('../lib/players/human-player')
const PlayerStrategyFool = require('../lib/players/player-strategy-fool')
const PlayerStrategyGod = require('../lib/players/player-strategy-god')
const GameMaster = require('../lib/game-master')
const util = require('../lib/util')

const rl = readline.createInterface({
  input: process.stdin,
  output: process.stdout
})

// 最初にCLIで各プレイヤーの種類を指定できるようにする為、その準備。
// ユーザーが入力する文字列に対応するプレイヤー生成ロジックを用意しています。
const playerGetters = new Map()
playerGetters.set('human', (playerChar, rl) => {
  return new HumanPlayer(playerChar, rl)
})
playerGetters.set('cpu1', (playerChar, rl) => {
  const strategy = new PlayerStrategyFool(playerChar)
  return new CpuPlayer(playerChar, strategy)
})
playerGetters.set('cpu2', (playerChar, rl) => {
  const strategy = new PlayerStrategyGod(playerChar)
  return new CpuPlayer(playerChar, strategy)
})

// ユーザーにプレイヤーの種類を聞いて、入力通りに生成する一連の流れです。
const getPlayer = (playerChar, args) => {
  return new Promise((resolve, reject) => {
    const ask = () => {
      rl.question(`Enter the kind of "${playerChar}" player. > `, answer => {
        if (answer === 'quit') {
          // "quit"が入れられれば即終了です。
          // Promiseチェーンをすっ飛ばして一気に終了できるようにrejectしていますが、
          // 本当はエラーが出た時とかに使うやつなので、多分良くない使い方です。
          reject()
        } else if (playerGetters.has(answer)) {
          // "human", "cpu1", "cpu2"に対して、対応するプレイヤーを生成します。
          const player = playerGetters.get(answer)(playerChar, rl)
          args.players.push(player)
          resolve(args)
        } else {
          // 解釈不能な入力に対しては、外人風に聞き返してもう一回入力させます。
          console.log('What?')
          ask()
        }
      })
    }
    ask()
  })
}

// CLIで入力を受け付ける度に非同期処理になるので、Promiseチェーンを繋ぎまくることになります。
Promise.resolve({ players: [] })
  .then(args => getPlayer('o', args)) // "o"プレイヤーを生成してargs.playersに入れます。
  .then(args => getPlayer('x', args)) // "x"プレイヤーを以下略
  .then(args => {
    const gm = new GameMaster(args.players[0], args.players[1])

    gm.on('gameend', (result, state) => {
      util.displayBoard(state.board)
      switch (result.resultType) {
        case resultType.WIN_OR_LOSE:
          console.log(`"${result.winner}" win !`)
          break

        case resultType.DRAW:
          console.log('Draw !')
          break

        case resultType.ABORTED:
          console.log('The game is end.')
          break
      }
      console.log(result.description)
      rl.close()
    })

    gm.startGame()
  })
  .catch(() => { rl.close() }) // "quit"と入れるとここに飛びます。

プレイヤー選択中の終了は"quit"、試合の終了は"abort"と統一できていませんが気のせいです。
そんなことよりさっさと遊んでみましょう。

とりあえずクソ雑魚vs神。

Enter the kind of "o" player. > cpu1
Enter the kind of "x" player. > cpu2
    1   2   3
  ┏━━━┳━━━┳━━━┓
a ┃   ┃   ┃   ┃
  ┣━━━╋━━━╋━━━┫
b ┃   ┃   ┃   ┃
  ┣━━━╋━━━╋━━━┫
c ┃   ┃   ┃   ┃
  ┗━━━┻━━━┻━━━┛
o:c3

    1   2   3
  ┏━━━┳━━━┳━━━┓
a ┃   ┃   ┃   ┃
  ┣━━━╋━━━╋━━━┫
b ┃   ┃   ┃   ┃
  ┣━━━╋━━━╋━━━┫
c ┃   ┃   ┃ o ┃
  ┗━━━┻━━━┻━━━┛
x:b2

    1   2   3
  ┏━━━┳━━━┳━━━┓
a ┃   ┃   ┃   ┃
  ┣━━━╋━━━╋━━━┫
b ┃   ┃ x ┃   ┃
  ┣━━━╋━━━╋━━━┫
c ┃   ┃   ┃ o ┃
  ┗━━━┻━━━┻━━━┛
o:c2

    1   2   3
  ┏━━━┳━━━┳━━━┓
a ┃   ┃   ┃   ┃
  ┣━━━╋━━━╋━━━┫
b ┃   ┃ x ┃   ┃
  ┣━━━╋━━━╋━━━┫
c ┃   ┃ o ┃ o ┃
  ┗━━━┻━━━┻━━━┛
x:c1

    1   2   3
  ┏━━━┳━━━┳━━━┓
a ┃   ┃   ┃   ┃
  ┣━━━╋━━━╋━━━┫
b ┃   ┃ x ┃   ┃
  ┣━━━╋━━━╋━━━┫
c ┃ x ┃ o ┃ o ┃
  ┗━━━┻━━━┻━━━┛
o:b1

    1   2   3
  ┏━━━┳━━━┳━━━┓
a ┃   ┃   ┃   ┃
  ┣━━━╋━━━╋━━━┫
b ┃ o ┃ x ┃   ┃
  ┣━━━╋━━━╋━━━┫
c ┃ x ┃ o ┃ o ┃
  ┗━━━┻━━━┻━━━┛
x:a3

    1   2   3
  ┏━━━┳━━━┳━━━┓
a ┃   ┃   ┃ x ┃
  ┣━━━╋━━━╋━━━┫
b ┃ o ┃ x ┃   ┃
  ┣━━━╋━━━╋━━━┫
c ┃ x ┃ o ┃ o ┃
  ┗━━━┻━━━┻━━━┛
"x" win !
A diagonal line is completed !

神がちゃんとクソ雑魚の勝ち筋を潰しましたね。

次は神vs神。ドラ○ンボールか遊○王で出てきそうなフレーズ。

Enter the kind of "o" player. > cpu2
Enter the kind of "x" player. > cpu2
    1   2   3
  ┏━━━┳━━━┳━━━┓
a ┃   ┃   ┃   ┃
  ┣━━━╋━━━╋━━━┫
b ┃   ┃   ┃   ┃
  ┣━━━╋━━━╋━━━┫
c ┃   ┃   ┃   ┃
  ┗━━━┻━━━┻━━━┛
o:a1

    1   2   3
  ┏━━━┳━━━┳━━━┓
a ┃ o ┃   ┃   ┃
  ┣━━━╋━━━╋━━━┫
b ┃   ┃   ┃   ┃
  ┣━━━╋━━━╋━━━┫
c ┃   ┃   ┃   ┃
  ┗━━━┻━━━┻━━━┛
x:b2

    1   2   3
  ┏━━━┳━━━┳━━━┓
a ┃ o ┃   ┃   ┃
  ┣━━━╋━━━╋━━━┫
b ┃   ┃ x ┃   ┃
  ┣━━━╋━━━╋━━━┫
c ┃   ┃   ┃   ┃
  ┗━━━┻━━━┻━━━┛
o:a2

    1   2   3
  ┏━━━┳━━━┳━━━┓
a ┃ o ┃ o ┃   ┃
  ┣━━━╋━━━╋━━━┫
b ┃   ┃ x ┃   ┃
  ┣━━━╋━━━╋━━━┫
c ┃   ┃   ┃   ┃
  ┗━━━┻━━━┻━━━┛
x:a3

    1   2   3
  ┏━━━┳━━━┳━━━┓
a ┃ o ┃ o ┃ x ┃
  ┣━━━╋━━━╋━━━┫
b ┃   ┃ x ┃   ┃
  ┣━━━╋━━━╋━━━┫
c ┃   ┃   ┃   ┃
  ┗━━━┻━━━┻━━━┛
o:c1

    1   2   3
  ┏━━━┳━━━┳━━━┓
a ┃ o ┃ o ┃ x ┃
  ┣━━━╋━━━╋━━━┫
b ┃   ┃ x ┃   ┃
  ┣━━━╋━━━╋━━━┫
c ┃ o ┃   ┃   ┃
  ┗━━━┻━━━┻━━━┛
x:b1

    1   2   3
  ┏━━━┳━━━┳━━━┓
a ┃ o ┃ o ┃ x ┃
  ┣━━━╋━━━╋━━━┫
b ┃ x ┃ x ┃   ┃
  ┣━━━╋━━━╋━━━┫
c ┃ o ┃   ┃   ┃
  ┗━━━┻━━━┻━━━┛
o:b3

    1   2   3
  ┏━━━┳━━━┳━━━┓
a ┃ o ┃ o ┃ x ┃
  ┣━━━╋━━━╋━━━┫
b ┃ x ┃ x ┃ o ┃
  ┣━━━╋━━━╋━━━┫
c ┃ o ┃   ┃   ┃
  ┗━━━┻━━━┻━━━┛
x:c2

    1   2   3
  ┏━━━┳━━━┳━━━┓
a ┃ o ┃ o ┃ x ┃
  ┣━━━╋━━━╋━━━┫
b ┃ x ┃ x ┃ o ┃
  ┣━━━╋━━━╋━━━┫
c ┃ o ┃ x ┃   ┃
  ┗━━━┻━━━┻━━━┛
o:c3

    1   2   3
  ┏━━━┳━━━┳━━━┓
a ┃ o ┃ o ┃ x ┃
  ┣━━━╋━━━╋━━━┫
b ┃ x ┃ x ┃ o ┃
  ┣━━━╋━━━╋━━━┫
c ┃ o ┃ x ┃ o ┃
  ┗━━━┻━━━┻━━━┛
Draw !
No line is completed.

毎回この手順で引き分けになります。
同じ評価の手の中ではランダムに手を選ぶようにすれば、いろんな引き分けが見れるかもしれません。

神に挑んでみます。

Enter the kind of "o" player. > human
Enter the kind of "x" player. > cpu2
    1   2   3
  ┏━━━┳━━━┳━━━┓
a ┃   ┃   ┃   ┃
  ┣━━━╋━━━╋━━━┫
b ┃   ┃   ┃   ┃
  ┣━━━╋━━━╋━━━┫
c ┃   ┃   ┃   ┃
  ┗━━━┻━━━┻━━━┛
"o" to play. > b2

    1   2   3
  ┏━━━┳━━━┳━━━┓
a ┃   ┃   ┃   ┃
  ┣━━━╋━━━╋━━━┫
b ┃   ┃ o ┃   ┃
  ┣━━━╋━━━╋━━━┫
c ┃   ┃   ┃   ┃
  ┗━━━┻━━━┻━━━┛
x:a1

    1   2   3
  ┏━━━┳━━━┳━━━┓
a ┃ x ┃   ┃   ┃
  ┣━━━╋━━━╋━━━┫
b ┃   ┃ o ┃   ┃
  ┣━━━╋━━━╋━━━┫
c ┃   ┃   ┃   ┃
  ┗━━━┻━━━┻━━━┛
"o" to play. > c1

    1   2   3
  ┏━━━┳━━━┳━━━┓
a ┃ x ┃   ┃   ┃
  ┣━━━╋━━━╋━━━┫
b ┃   ┃ o ┃   ┃
  ┣━━━╋━━━╋━━━┫
c ┃ o ┃   ┃   ┃
  ┗━━━┻━━━┻━━━┛
x:a3

    1   2   3
  ┏━━━┳━━━┳━━━┓
a ┃ x ┃   ┃ x ┃
  ┣━━━╋━━━╋━━━┫
b ┃   ┃ o ┃   ┃
  ┣━━━╋━━━╋━━━┫
c ┃ o ┃   ┃   ┃
  ┗━━━┻━━━┻━━━┛
"o" to play. > a2

    1   2   3
  ┏━━━┳━━━┳━━━┓
a ┃ x ┃ o ┃ x ┃
  ┣━━━╋━━━╋━━━┫
b ┃   ┃ o ┃   ┃
  ┣━━━╋━━━╋━━━┫
c ┃ o ┃   ┃   ┃
  ┗━━━┻━━━┻━━━┛
x:c2

    1   2   3
  ┏━━━┳━━━┳━━━┓
a ┃ x ┃ o ┃ x ┃
  ┣━━━╋━━━╋━━━┫
b ┃   ┃ o ┃   ┃
  ┣━━━╋━━━╋━━━┫
c ┃ o ┃ x ┃   ┃
  ┗━━━┻━━━┻━━━┛
"o" to play. > b3

    1   2   3
  ┏━━━┳━━━┳━━━┓
a ┃ x ┃ o ┃ x ┃
  ┣━━━╋━━━╋━━━┫
b ┃   ┃ o ┃ o ┃
  ┣━━━╋━━━╋━━━┫
c ┃ o ┃ x ┃   ┃
  ┗━━━┻━━━┻━━━┛
x:b1

    1   2   3
  ┏━━━┳━━━┳━━━┓
a ┃ x ┃ o ┃ x ┃
  ┣━━━╋━━━╋━━━┫
b ┃ x ┃ o ┃ o ┃
  ┣━━━╋━━━╋━━━┫
c ┃ o ┃ x ┃   ┃
  ┗━━━┻━━━┻━━━┛
"o" to play. > c3

    1   2   3
  ┏━━━┳━━━┳━━━┓
a ┃ x ┃ o ┃ x ┃
  ┣━━━╋━━━╋━━━┫
b ┃ x ┃ o ┃ o ┃
  ┣━━━╋━━━╋━━━┫
c ┃ o ┃ x ┃ o ┃
  ┗━━━┻━━━┻━━━┛
Draw !
No line is completed.

勝てないですね。
凡ミスで何度か負けたので、腹が立ってcpu1の方をボコボコにしたりしました。

これでPCさえ持ち歩いていれば、いつでも一人寂しくCPUと○×ゲームができるようになりました。

締め

最後にちょっとポエムを添えます。

将棋の中級者とプロ棋士の思考を比べると、

  • 中級者は色々な候補手に対していちいち先を読んでいる
  • プロ棋士は候補手を3つぐらいにすぐ絞り、その3つについて深く深く先まで読む

みたいな感じらしいです。2
この「3つぐらいにすぐ絞る」というのは、膨大な経験と研究によって醸成された超高精度な"勘"によって可能になるんでしょうね。
そしてその勘の醸成には、中級者時代に色々な手について考えることを何度も何度も繰り返すことがきっと大事なんだろうなって気がします。

そんでもって、プログラミングとかも多分同じなんじゃないかと思います。
色々な設計方針を毎回毎回頭使ってちゃんと検討・比較する。
これを繰り返すことで、最終的には「3つぐらいの有力な設計方針」が瞬時に勘で思いつくようになる。

素人の勘と、プロの経験や知識に裏打ちされた勘は精度の次元が違います。
超高速かつ超高精度な思考ができるプロの勘を養う為に、今は時間かかってでもちゃんと頭を使い続けたいなぁと思う日々です。

今後の展望

気が向いたら以下をやるかもしれません。

  • リファクタリング
  • GUI化
  • ルール5(情報のアクセシビリティ)の実装が必要な他ゲームにチャレンジ

多分やりません。

それでは。


  1. Wikipedia曰く、お互いが最善を尽くすと○×ゲームは引き分けになるそうです。 

  2. ググったら例えば「将棋プレーヤーの棋力の違いによる読みの広さと深さ」という論文が出てきました。