Edited at

ボードゲーム実装における思考フレームワーク 〜2-1.ルールの施行(思考編)〜


関連記事


ブログ

ゲームとは何か

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

趣味的な考察です。長めです。

この記事との関連箇所はちゃんと記事内で抜粋しているので、無理して読まなくて大丈夫です。

興味ある方だけどうぞ。


Qiita

ボードゲーム実装における思考フレームワーク 〜1.ゲームの状態の管理〜

ボードゲーム実装における思考フレームワーク 〜2-1.ルールの施行(思考編)〜(本記事)

ボードゲーム実装における思考フレームワーク 〜2-2.ルールの施行(実装編)〜

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


ギョーム分析

「業務」っていうかただの「ゲーム」なので、間をとって「ギョーム」って書きます。


ルールは誰が施行しているか


オセロとかそのへんを思い浮かべる

0. 両プレイヤーで初期状態をセッティング(ルール1を施行)

1. 手番プレイヤーが盤面を認識(物理的制約が自動的にルール5を施行)
2. 手番プレイヤーがアクションを適用(ルール2を施行)
3. 相手プレイヤーがアクションの妥当性を確認(ルール3を施行)
4. 両プレイヤーが勝敗判定(ルール4を施行)
5. 勝敗が続行なら、手番を交代し1へ。

プレイヤーが協力してルールを施行しあっている感じがします。

プレイヤーは自分の手を考えたり相手の手を確認したり忙しいですね。

この状況をそのままプログラムに落とし込むと複雑なことになりそうです。

単一責任原則が守られないですね。

もっとこう、審判がその辺をやってくれる場面を想像してみましょう。


公式戦って感じのオセロとかを思い浮かべる

ボードゲームじゃないですがいっそテニスとかでもいいです。

テニスの緩い試合はプレイヤー2人でセルフジャッジしたりしますが、公式の大会とかでは審判が居ます。

0. 審判が初期状態をセッティング(ルール1を施行)

1. 手番プレイヤーが盤面を認識(物理的制約が自動的にルール5を施行)
2. 手番プレイヤーがアクションを適用(ルール2を施行)
3. 審判がアクションの妥当性を確認(ルール3を施行)
4. 審判が勝敗判定(ルール4を施行)
5. 勝敗が続行なら、手番を交代し1へ。

大体のルールは審判が施行してくれるようになりました。

こっちの状況の方がプログラムに落とし込む上で参考になりそうですね。

更にプログラミング向けの状況を考えることもできます。


審判しか盤を触れないようにする

0. 審判が初期状態をセッティング(ルール1を施行)

1. 審判が手番プレイヤーに盤面を通達(ルール5を施行)
2. 手番プレイヤーがアクションを宣言(ルール2を施行)
3. 審判がアクションの妥当性を確認(ルール3を施行)
4. 審判が盤にアクションを適用
4. 審判が勝敗判定(ルール4を施行)
5. 勝敗が続行なら、手番を交代し1へ。

盤を審判の中にカプセル化したイメージです。

プレイヤーが間違って盤をいじっちゃわないようにします。

盤のスコープが狭くて分かりやすい。

この状況をモデルとしてプログラムに落とし込むことにします。

execute_rules.png


ルールは誰が知っているか(誰が使用できるか)

ゲームに対してルールを施行するのは基本的に審判だとしました。

じゃあ、ルールを知っているのも審判だけか。

もちろんそんなことはありません。

プレイヤーも良い手を考えるためにルールを知る必要があります。

know_rules.png

上図のようなイメージです。

プレイヤーの脳内には盤も示しました。

「こう指すと相手はどう指すだろう...」と、脳内に無数の盤を思い浮かべているのです。

何が言いたいかというと、ルールや盤は誰もが使えるようにしておくべきだということです。

具体的な話をすると、実際の試合状況を表すBoardオブジェクトは審判が保有しつつ、CPUプレイヤーはそのBoardをコピーしたり新しくnewしたりして、試合に関係ない無数のBoardオブジェクトを脳内(自身のスコープ内)に生成し、自由にルールを適用しながら色々シミュレーションできるようにしたいです。

人間プレイヤーの方はあまり考えてませんが、まあUIを提供する場面ではやはりBoardオブジェクトとか使えると良いんじゃないでしょうか。

この辺を念頭に起きつつ、実装寄りの話に進みます。


実装のシミュレーション


Playerクラスの具体的イメージ

前回の記事で、プレイヤーは2種類いると書きました。


  • CPUプレイヤー

  • 人間プレイヤー(UIを使う)

CPUプレイヤーは、審判からゲームの状態の情報(GameStateInformation)を受け取った時に、独自のアルゴリズムで最善手を思考し、指す手(Action)を返せば良さそうですね。

問題は人間プレイヤーですが、やはりCPUプレイヤーとインターフェースを揃えてポリモーフィりたいです。

つまり、GameStateInformationを受け取ってActionを返すメソッドを持っていて欲しいです。

そこにUIを噛ませるなら、


  1. GameStateInformationを受け取る

  2. UIにGameStateInformationを表示する

  3. ユーザーの入力待機

  4. UIからActionを受け取る

  5. Actionを返す

ということをすれば、CPUプレイヤーも人間プレイヤーも同じインターフェースをもつクラスで表せそうですね。

人間プレイヤークラスの場合、最善手を考える処理はUIを通じてユーザー自身に委譲するわけです。

CPUプレイヤー

  VS
人間プレイヤー = UI + ユーザー

というわけですね。

「ロボット VS 戦闘スーツに身を包んだ人間」って感じの絵を無駄に添えようかと思いましたが、画力がないのでやめます。

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


Judgementクラスの具体的イメージ

審判は色々なルールを施行してくれますが、その中でも分かりやすい役割はルール3(可能/禁止アクション)の施行です。

ズルを取り締まるのがやっぱり審判の醍醐味(?)ですよね。

審判はプレイヤーのアクションについてvalidationチェックをし、invalid(ズル)だった場合にそのことをプレイヤーに伝え、プレイヤーにアクションを再請求するわけです。

その辺の仕組みを図で整理してみます。

judgement_execute_rule3.png

さて、この実装で行けそうでしょうか。

私は以下の2点について、実装しづらさを感じました。


  • CPUプレイヤーの取り扱い

人間プレイヤーが送るアクションはvalidかinvalidかわからないのでチェックする必要がありますが、CPUプレイヤーはvalidな手しか指しません(普通そのように作ります)。

CPUプレイヤーオブジェクトからズルの手を審判オブジェクトに送る。



審判オブジェクトにズルだと判断させ、CPUプレイヤーに考え直しを要求させる。



CPUプレイヤーオブジェクトにその結果を受け止めさせ、今度は正しい手を審判オブジェクトに送る。

なんて回りくどいプログラムは書きたくないですよね。

結果、CPUプレイヤーの手まで審判がチェックするのは2度手間になり、パフォーマンス的に良くない動きとなります。

かといって、「人間プレイヤーならチェックする。CPUプレイヤーならチェックしない。」なんてif文もあまりイケてない感じです。


  • GUIの取り扱い

よくあるボードゲームアプリみたいな感じで○×ゲームのGUIを実装したところを想像してみてください。

普通、○×が書き込めるマスだけがクリック(タップ)できる状態になっていて、他のマスはdisableになっていませんか?

つまり、ルール3(可能/禁止アクション)をUIの時点で施行したいということになってきます。

以上の2点を考慮すると、

「あれ、Playerクラスでズルを取り締まった方が良くない?」

という説が浮上してきます。


桐**、審判やめるってよ

player_execute_rule3.png

上記説を図に起こしてみました。

どうでしょうか。

前の図よりずっとスッキリして自然な感じがしないでしょうか。

ということで、こちらの設計を採用しましょう。

私のイメージでは、「絶対にズルをしないロボットと、絶対にズルができないUIヘルメットを被らされた人間しかプレイしないため、審判はプレイヤーの指す手をチェックしない」って感じです。

さて、こうなると問題なのは審判です。

こいつ、本当に必要だったんか?という話になってきます。

とはいえ、彼にやらせたい仕事はまだあります。



  • ゲームの状態の保有


    • これは実は前回のコードみたいにトップレベルに直置きでも問題ないです。




  • ルール1(初期状態)の施行


    • これもトップレベルで良さそうです。




  • ゲームフローの施行 → 各プレイヤーに適切なタイミングでアクションを請求する


    • これはゲーム毎にカスタマイズしてロジックを作り込むところです。是非とも審判クラスとして分離させたい処理。




  • ルール4(結果判定)の施行


    • 結果判定のタイミングはゲームフローの一部です。なので上記と同様。




  • ルール5(ゲームの状態のアクセシビリティ)の施行 → ゲームの状態を各プレイヤー向けの情報に変換(不可知な部分を隠す)してプレイヤーに伝える


    • プレイヤークラスがゲームの状態を自身向けの情報に変換してから使うよう気をつければ良いだけだが、審判クラスにやらせた方がプレイヤークラスでミスが起こりづらく、safe2なプログラムになる。



特にゲームフローの仕組みが大きな役目になってきそうです。

こうなってくると、彼の名前は仕切り役、司会者、ゲームマスターといった表現の方が相応しくなってきます。

というわけで、ここから彼にはゲームマスターに転向してもらうことにします。

birthday_of_game_master.png


オレオレOOPの話

ここまでオブジェクト指向的な発想で話を進めてきました。

現実世界のモノを洗い出し、それをそのままオブジェクトやクラスとしてプログラムに落とし込んでいます。

しかし、ここに来て「絶対にズルをしないプレイヤー」など、現実世界とは違うモノが現れ始めています。

これはオブジェクト指向的にどうなのか。

結論から言えば問題ないと思っています。

プログラム上のメリットを追求する上で、現実世界のモノのアナロジーではだんだん説明がつかなくなっていくのは当然のことです。

私の考えをちょっと説明します。


OOPのメリット

他の人がどうかは知りませんが、私がプログラムでオブジェクトを使うのは、「オブジェクトに役割分担させると良いことがあるから」です。

具体的には拡張性とか再利用性とか可読性とか、そういうのが諸々良くなります。

「モジュールを疎結合に保つメリット」とかで調べれば色々と出てくると思います。

ポイントは、決して「現実世界をプログラムで再現できるから」とかそういう理由では無いということです。

では、なぜ現実世界のモノに着目するんでしょうか。


現実世界とオブジェクト指向プログラム世界の関係

オブジェクトは、データと振る舞いでできています。

「データとそれに関連する振る舞い」と言っても良い。

「振る舞いとそれに関連するデータ」ってまとめ方をするより、「データとそれに関連する振る舞い」でまとめた方が色々捗るね、って発見がオブジェクト指向の一つの核なんじゃないかと思ったり思わなかったり。

そんなわけで、プログラムをどういうオブジェクトに分割するか考えるときは、まず必要なデータをどう分割するかから考える必要があります。

この時、「現実世界のどのモノについてのデータか」でデータを分類するのは、ごく自然なことです。

こうして、現実世界のモノとオブジェクトは1対1対応し始めるわけです。

しかし、振る舞いの定義は必ずしも現実世界が参考になるとは限りません。

現実世界ではプレイヤーが盤面から勝敗を決定する一方で、プログラム世界では盤面オブジェクトが自身の勝敗をプレイヤーに告げても良いのです。

そして、こういう主語目的語の逆転は、オブジェクト指向ではかなり頻繁に見られます。

なんでそういうことをするんでしょうか。

「オブジェクトが疎結合になる」のような、プログラム世界の論理で考えて設計しているからです。

上で「なぜ現実世界のモノに着目するのか」と書きました。

答えは「参考になるから」です。

そして、あくまで参考になるだけです。

最終的には、プログラム上のメリットを考え、プログラム世界の論理で説明できる最適なオブジェクト構成を考える必要があります。

「現実世界を全部再現すれば良いんだ!」というのは、より良いプログラムを作ろうとしている人たちにとっては思考放棄としか思えません。


プログラムを書くと現実も変わる

そもそも今回の場合、そして世のシステム開発における多くのケースで、システムを導入する前の業務と後の業務では、現実世界の様相は大きく変わります。

new_process.png

サイバー空間を中心とした新しい業務形態を構築しようとしている以上、物理空間に依存した古い業務形態はやはり参考程度にしかなりません。

それでは「絶対にズルをしないプレイヤーオブジェクト」の存在を許してもらったと仮定して、話を戻しましょう。

まあ実際のところ、私の設計が単に未熟だとか、「プレイヤー」という命名がそもそもおかしいんじゃないかとか、色んな説はあります。


Webシステムの場合

Web上でユーザー同士対戦できるようなアプリを実装することを考えます。

この場合、GameMasterクラスをサーバー側プログラム、Playerクラスをクライアント側プログラムとして置くことで綺麗に分離できそうな気がします。

しかしこの場合、Playerクラスを通さずにcurl等で無理やりPOSTデータを送ることで、invalidなアクションを送ることが可能になってしまいます。

web_system.png

GameMasterとPlayerの両方がvalidationチェックする必要がありそうです。

また、プレイヤーのなりすましを防ぐためにPlayerにトークンを持たせて...とか色々あると思いますが、今回は諸々省き、スタンドアローンアプリ前提で説明することにします。

長くなったので、2-2.ルールの施行(実装編)へ続く。





  1. 後の記事で方針を変更しました。 



  2. safetyとsecurityは違います。意図しない誤作動を防止するのがsafety、意図的な悪意ある攻撃を防止するのがsecurityです。Fluent Pythonに書いてありました。私はこの使い分けに準じています。