二人零和有限確定完全情報ゲーム

タイトル、のようなゲームには必勝法が存在する。
そのこと自体は直感的にも理解できるし、証明も簡単だろう。
さらには最善手も非常に簡単なアルゴリズムで計算できる。


が、計算できるものと現実的に計算できるものの間には
大きな開きがあり、現実的に計算できるもの、というのは
きわめて大雑把にPといわれる計算複雑性クラスに入るもの
に限られてしまう、ということも周知の事実であろう。


そういうことなのであるが、あえてそれを無視してみる。
要するに上記で非常に簡単なアルゴリズム、と書いたものを
抽象的なゲームに対して定義しようかとそんなわけである。
非常に規模の小さいゲームについては利用できるかもしれない。


ここでは○×ゲームを題材にすることにする。
また、アルゴリズムは単にand-or木を全探索するだけとする。
and-or木にはalpha-beta探索というきわめて重要な
枝狩りテクニックがあるのだが、ひとまずそれは無視して。
さらに、先ほど抽象的な…と書いたが、いきなりそれは私には少々
荷が重いので、まずは具体的なプログラムを作ってみる。

import Data.List
import Data.Char

data Elem  = Maru | Batsu | Blank deriving (Eq,Show)
type Board = (Bool,Elem)
type Hand  = (Int,Int)

bestHand :: Board -> Hand
bestHand bd = snd $ maximumBy (\x y -> andOr bd (fst x) (fst y))
                      [(f nb,h) | (nb,h) <- successor bd] where
  f bd | null sbd  = evalBoard bd
       | otherwise = maximumBy (andOr bd) [f b| (b,_) <- sbd]
    where sbd = successor bd

evalBoard :: Board -> Int
evalBoard (teban,bd)
  | or [and [bd!!y!!x == Maru  | (x,y) <- l] | l <- line] = 1
  | or [and [bd!!y!!x == Batsu | (x,y) <- l] | l <- line] = -1
  | otherwise = 0
  where
    line =
      [[(0,0),(1,0),(2,0)],[(0,1),(1,1),(2,1)],[(0,2),(1,2),(2,2)]
      ,[(0,0),(0,1),(0,2)],[(1,0),(1,1),(1,2)],[(2,0),(2,1),(2,2)]
      ,[(0,0),(1,1),(2,2)],[(2,0),(1,1),(0,2)]]

successor :: Board -> [(Board,Hand)]
successor b@(teban,bd)
  | evalBoard b /= 0 = []
  | otherwise =
    [ ( (not teban,updt x y nel bd),(x,y))
    | (l,y) <- zip bd [0..], (d,x) <- zip  l [0..], d==Blank]
  where nel = if teban then Maru else Batsu

updt x y v bd = f y (f x v (bd!!y)) bd where
  f 0 v (_:xs) = v:xs
  f n v (x:xs) = x:f (n-1) v xs

endGame = null . successor

andOr :: Board -> Int -> Int -> Ordering
andOr (True ,_) = compare
andOr (False,_) = flip compare

initBoard = (True,replicate 3 $ replicate 3 Blank)

move :: Hand -> Board -> Board
move (x,y) (teban,bd) = (not teban,updt x y (if teban then Maru else Batsu) bd)

printBoard :: Board -> IO ()
printBoard (teban,bd) = do
  putStrLn $ (if teban then "maru" else "batsu") ++ " no teban"
  mapM_ putStrLn dat
  putStrLn ""
  where
    dat = intersperse "-----" [intersperse '|' $ map showElem l | l <- bd]
    showElem c
      | c == Maru  = 'o'
      | c == Batsu = 'x'
      | c == Blank = ' '

 ------------------

main = playGame initBoard

playGame bd@(teban,dat)
  | endGame bd = do
    printBoard bd
    case compare 0 (evalBoard bd) of
      EQ -> putStrLn "Draw!"
      LT -> putStrLn "Maru win!"
      GT -> putStrLn "Batsu win!"
  | otherwise  = do
    printBoard bd
    if teban
      then do
        te <- getInput dat
        playGame $ move te bd
      else do
        playGame $ move (bestHand bd) bd

getInput bd = do
  putStrLn "input coordinate:"
  s <- getLine
  let ns = words s
  if length ns /= 2 || not (all (all isDigit) ns)
    then getInput bd
    else
      let [x,y] = map read ns in
      if 1<=x&&x<=3&&1<=y&&y<=3&&bd!!(y-1)!!(x-1)==Blank
        then return (x-1,y-1)
        else getInput bd

どうにもあんまりすっきりしないコードになってしまった。
前半がゲームの処理、後半が入出力処理。
bestHandの部分がゲーム独立である。
非常に短いけど…というか、この部分が非常に短く書けるから
全体ももっと短く書けると思っていたのだが。
しかしまぁ、唯一アルゴリズムらしいところなので、
それが独立することはやはり良いのかもしれない。
これがもっと複雑なアルゴリズムになったら効果は大きくなるだろう。