簡単なゲーム木探索

もはや裏技とか関係なく、単なるPythonネタのお蔵出し(私が大昔にやった趣味のBASICプログラミングの焼き直し)になってしまいますが、簡単なゲームを作ってみることにします。
ゲームといっても、PCで完全読み切りをやっても数秒で終わるくらいの軽いものです。
とりあえずルール。

  • プレイヤーは2人
    • PC対人であれば、人が先手か後手かを選択できる。
  • 紙に数字(1桁)を書いたマスを5つ書く。ゴールの数を決める。合計は最初0。
  • 先手は5つのマスのどこかにコマを置き、合計にそのマスの数を加算。
  • 後手は先手が置いたのとは別のマスにコマを動かし、合計にそのマスの数を加算。
    • 必ず別のマスに動かし、同じマスを指定することはできない。
  • 先手も同様に別のマスにコマを動かし、合計にそのマスの数を加算
  • これを繰り返して最終的に合計がゴールの数に一致した方が勝ち、ゴールの数を超えてしまったら負け

このルールで、常に必勝手を打つプログラムを作ってみます。

このゲームはシンプルなので、自分の手が必勝かどうかを返す winning(今の合計, 次に打つ手) は

  • 合計がゴール数を超えたらFalse
  • 相手の番のwinning(...)を調べて、1つでもTrue (相手の勝ち) があったらFalse (自分の負け)
  • それ以外はTrue

というように、実は再帰的な定義が可能です。

また将棋のように複雑なゲームだと、駒得や利き筋などから割り出して評価点をつけて min/max でカットするといった感じですが、このゲームは終了条件がはっきりしているので

# any(必勝手かどうか(x) for x in 次に打つことが可能な手のリスト)

みたいにしてお手軽に枝カットができます。
ただそれでも同じ枝を何度もたどると時間がかかるので、「関数の戻り値をキャッシュする」を使って同じ枝をたどらないようにしています。

target = 37
cells = [1, 2, 3, 4, 5]

def winning(t, n, cache={}):
    key = (t, n)
    return cache.get(key) or cache.setdefault(key, _winning(t, n))

def _winning(t, n):
    if t + n > target:
        return False
    elif any(winning(t+n, x) for x in cells if x != n):
        return False
    else:
        return True

マスの数が[1,2,3,4,5]、ゴールが37である場合、先手は1手目をどこにおけばいいでしょうか?

>>> [winning(0, x) for x in cells]
[False, False, False, True, False]

つまり先手は「4」に置けば必勝ということが分かります。
 

おまけ

まあ、中学の頃ポケコン(PC-1500)のBASICで作ったときは再帰なんて使えませんでしたし、「(ゴール数+1) × マス数」の2次元配列作って0か1を埋めるとかやっていて今とは全然違うコードでした。大学でCやら再帰やらを知って、随分と綺麗な書き方が出来るんだと衝撃を受けましたね。