前回のつづきで三目並べ。
マシン側の指し手の選択を、少しだけ知的にしてみる。
- 後一手で列が完成する盤面なら、その列を完成して終了。
- ユーザが後一手で一列完成できる盤面なら、それをブロックする。
- 後一手で二方向の王手が作れる盤面なら、それを作る。
- ユーザが後一手で二方向の王手が作れる盤面なら、ブロックする。
- 中央が空いていたら、そこに置く。
- 相手が角を取っていて対角が空いていたら、そこに置く。
- 角が空いていたらそこに置く。
- 空いているマスがあったら、そこに置く。
◆ 方針
前回の暫定実装した、マシンの指し手を選択する関数をこんな風に書き換えてみる。machinesTurn :: StateT Board IO Int machinesTurn = do board <- get let idx = fromJust $ select board updateBoard Machine idx return idx where select = findWinPos <||> findBlockPos <||> findForkPos <||> findForkBlockPos <||> tryCenter <||> findAnyBlank f1 <||> f2 = mplus <$> f1 <*> f2select 以下に並べられた findWinPos から findAnyBlank までの関数は、盤面 Board を取って、指し手を Maybe Intとして返すもの。
これらの関数そのものを「(->) r」というタイプの Applicative とみなして合成する演算子を、<||> として定義した。MonadPlus としての Maybe の振る舞いを利用している。
結果として合成された select 関数は、ある関数に盤面を与えて、指し手が得られたらそれを返し、ダメなら次の関数に盤面を与えるという計算を数珠つなぎにしたものになる。 ※((->) r)を Functor や Applicative として扱う手法はこのあたりの記事に詳しい。Applicative 一般の入門としても、超絶わかりやすく解説されている。
◆ コード全体
findWinPos 〜 findAnyBlank の実装は以下のようなものになる(前回書いた部分も含めて全部晒してしまうことにした)。import Control.Monad.State import Control.Applicative import Data.List import Data.Functor.Identity import Control.Monad.List import Data.Maybe import qualified Data.List.Utils as LU import System data Mark = User|Machine|Blank deriving Eq instance Show Mark where show User = "U" show Machine = "M" show Blank = " " type Board = [Mark] main :: IO () main = do args <- getArgs let machineFirst = 0 < length args && "m"==args!!0 evalStateT (play machineFirst) initialBoard >>= putStrLn initialBoard :: Board initialBoard = take 9 $ repeat Blank printBoard :: Board -> IO () printBoard board = putStrLn $ unlines $ LU.join ["-+-+-"] $ map showRow [0..2] where showRow row = [LU.join "|" $ map show $ cells row] cells row = take 3 $ snd $ splitAt (row * 3) board play :: Bool -> StateT Board IO String play isMachinesTurn = do if isMachinesTurn then machinesTurn else usersTurn board <- get liftIO $ printBoard board case judge board of Nothing -> (play $ not isMachinesTurn) Just result -> return result judge :: Board -> Maybe String judge board | wonBy User board = return "won by user." | wonBy Machine board = return "won by machine." | Nothing == elemIndex Blank board = return "drawn." | otherwise = Nothing wonBy :: Mark -> Board -> Bool wonBy mark board = any threeInRow rows where threeInRow indices = all (mark==) $ rowStates indices rowStates indices = map (board!!) indices rows :: [[Int]] rows = [[0,3,6],[1,4,7],[2,5,8], [0,1,2],[3,4,5],[6,7,8], [0,4,8],[2,4,6]] updateBoard :: Mark -> Int -> StateT Board IO () updateBoard mark pos = get >>= put.(update mark pos) update :: Mark -> Int -> Board -> Board update mark pos board = let (f, (x:xs)) = splitAt pos board in f ++ (mark:xs) usersTurn :: StateT Board IO Int usersTurn = do position <- liftIO getDigit board <- get if Blank /= board!!position then (liftIO $putStrLn "wrong position") >> usersTurn else updateBoard User position >> return position where getDigit = do ch <- getChar putStrLn "" if elem ch ['0'..'8'] then return (read [ch]) else putStrLn "wrong input" >> getDigit machinesTurn :: StateT Board IO Int machinesTurn = do board <- get let idx = fromJust $ select board updateBoard Machine idx return idx where select = findWinPos <||> findBlockPos <||> findForkPos <||> findForkBlockPos <||> tryCenter <||> findAnyBlank f1 <||> f2 = mplus <$> f1 <*> f2 findWinPos :: Board -> Maybe Int findWinPos = findWinPos' Machine findBlockPos :: Board -> Maybe Int findBlockPos = findWinPos' User findWinPos' :: Mark -> Board -> Maybe Int findWinPos' mark board = ff ([mark, mark, Blank], 2) `mplus` ff ([mark, Blank, mark], 1) `mplus` ff ([Blank, mark, mark], 0) where ff (css, idx) = do findPattern css board >>= return.(!!idx) findForkPos :: Board -> Maybe Int findForkPos = findForkPos' Machine findForkBlockPos :: Board -> Maybe Int findForkBlockPos = findForkPos' User findForkPos':: Mark -> Board -> Maybe Int findForkPos' mark brd = do let posibles = elemIndices Blank brd idx <- findIndex (\pos->containsFork (update mark pos brd)) posibles return $ posibles!!idx where containsFork b = (2<=) $ length $ findTwoInARow b findTwoInARow b = findIndices (matchTwoInARow b) rows matchTwoInARow b row = let stts = map (b!!) row in 2 == (countState mark stts) && 1 == (countState Blank stts) findOppositeCorner :: Board -> Maybe Int findOppositeCorner brd = do let pairs = [(0,8), (8,0), (2,6), (6,2)] r' <- findIndex (\(f,s)-> brd!!f==User && brd!!s==Blank) pairs return $ snd $ pairs!!r' findBlankCorner :: Board -> Maybe Int findBlankCorner = findBlankCell [0, 2, 8, 6] findBlankCell :: [Int] -> Board -> Maybe Int findBlankCell candidate board = findIndex ((Blank==).(board!!)) candidate >>= return.(candidate!!) findAnyBlank :: Board -> Maybe Int findAnyBlank board = elemIndex Blank board tryCenter :: Board -> Maybe Int tryCenter brd = if Blank == brd!!4 then Just 4 else Nothing findPattern :: [Mark] -> Board -> Maybe [Int] findPattern pattern board = do idx <- elemIndex pattern $ map (\row->map (\idx->board!!idx) row) rows return $ rows!!idx countState :: Mark -> [Mark] -> Int countState state cells = length $elemIndices state cells
◆ まとめ
- 実は、上のアルゴリズムではユーザ先手で 0, 8, 6 と指すと、ユーザが勝ってしまう。三目並べはちゃんとやると引き分けになるんだけど、ちょっと手抜きをしている(ユーザのフォークをマシンが防ぐ辺り)。三目並べ自体が目的ではないので、割愛した。
- 少し書いては直し、また書いては直しと常時リファクタしているが、コンパイルが通るだけで、感覚的には JUnit で 6割くらいのコード・カバレッジがあるくらいの安定感がある。この 150行弱のコードだと、デグレ的な事は一回も起きなかった。
- 上で少し触れたサイト、「Learn You a Haskell for Great Good!」の、Functor 〜 Applicative 〜 Monado の解説(この順序で説明されている)が、ものすごくわかりやすくて感動。
0 件のコメント:
コメントを投稿