Haskellでオセロ
とりあえず「ハスケリ本」と「ふつケル本」は読んで、例題の数行ぐらいなら何とか書けるようになったぐらいです。
年末からオセロを移植していたのだけれど、やっとマトモに動くレベルまでこぎつけました。添削歓迎
{------------------------------------------------------------------------------ Reversi Haskellの習得のため、Schemeで書かれたリバーシ(オセロ)をHaskellに移植してみる。 参考: http://www.stdio.h.kyoto-u.ac.jp/~hioki/gairon-enshuu/kadai2005/7.html 盤面上のマスは(x,y)で指定する(1<= x,y <= 8). -------------------------------------------------------------------------------} data Piece = Empty | Black | White | Wall deriving (Eq, Show) type Board = [[Piece]] type Pt = (Int,Int) ------------------------------------------------------------------------------- -- 盤面サイズ ------------------------------------------------------------------------------- boardSize :: Int boardSize = 8 ------------------------------------------------------------------------------- -- 8方向(dx,dy)のリスト ------------------------------------------------------------------------------- directions :: [Pt] directions = [(0, 1),(-1, 1),(-1, 0),(-1, -1),(0, -1),(1,-1),(1, 0),(1, 1)] ------------------------------------------------------------------------------- -- 盤面の初期化 -- 境界に「壁」をダミーとして置いてある. ------------------------------------------------------------------------------- initBoard:: Board initBoard = [[ (f x y) | x <- [0..9]] | y <- [0..9]] where f x y | x == 0 || y == 0 || x == 9 || y == 9 = Wall | x == 4 && y == 4 = White | x == 5 && y == 5 = White | x == 4 && y == 5 = Black | x == 5 && y == 4 = Black | otherwise = Empty ------------------------------------------------------------------------------- -- 盤面の指定された場所のコマを返す。 ------------------------------------------------------------------------------- getBoard:: Board -> Pt -> Piece getBoard board (x,y) = board !! y !! x ------------------------------------------------------------------------------- -- 盤面の指定された場所にコマを置いて、新しい盤面を返す。 ------------------------------------------------------------------------------- setBoard:: Board -> Pt -> Piece -> Board setBoard board (x,y) val = (take y board) ++ ((take x b) ++ val : (drop (x+1) b)) : (drop (y+1) board) where b = board !! y ------------------------------------------------------------------------------- -- 盤面の内容を表示する ------------------------------------------------------------------------------- showBoard:: Board -> IO () showBoard board = putStr (concat (map showLine board)) where showLine line = concat (map getSymbol line) ++ "\n" getSymbol val = case val of Empty -> "- " Black -> "B " White -> "W " Wall -> "X " ------------------------------------------------------------------------------- -- 相手の色 ------------------------------------------------------------------------------- opponent:: Piece -> Piece opponent bw = if bw==White then Black else White ------------------------------------------------------------------------------- -- 盤面上の白黒いずれかの個数を返す. ------------------------------------------------------------------------------- pcount:: Board -> Piece -> Int pcount board bw = length (filter (\val -> val == bw) (concat board)) {- ------------------------------------------------------------------------------- -- 盤面上の白黒の個数を返す. -- (黒の個数 白の個数)というタプルを返す ------------------------------------------------------------------------------- bw_count:: Board -> (Int,Int) bw_count board = (pcount board Black, pcount board White) -} ------------------------------------------------------------------------------- -- 与えられた点のある方向の位置 neighbor (5,3) (-1,1) のとき (4,4) を返す ------------------------------------------------------------------------------- neighbor:: Pt -> Pt -> Pt neighbor (x,y) (dx,dy) = (x+dx, y+dy) ------------------------------------------------------------------------------- -- 同一色のrunの終点を調べる. -- pointのdir方向の隣接点から延びるbwのrunの終点を調べる. -- そのようなrunがなければ,pointそのものを返す. -- pt 調べ始める場所 -- dir 方向 -- bw 調べる色 -- board 局面 ------------------------------------------------------------------------------- run_end:: Pt -> Pt -> Piece -> Board -> Pt run_end pt dir bw board = if getBoard board p == bw then (run_end p dir bw board) else pt where p = neighbor pt dir ------------------------------------------------------------------------------- -- p1 と p2 は同一地点か?? ------------------------------------------------------------------------------- the_same_point:: Pt -> Pt -> Bool the_same_point (x1,y1) (x2,y2) = x1==x2 && y1==y2 ------------------------------------------------------------------------------- -- dirは有効な方向の指定か? ------------------------------------------------------------------------------- valid_direction:: Pt -> Bool valid_direction (dx,dy) = ((abs dx <= 1) && (abs dy <= 1)) && (not ((dx==0)&&(dy==0))) ------------------------------------------------------------------------------- -- pointが盤面の境界を越えているか?? ------------------------------------------------------------------------------- out_of_bounds:: Pt -> Bool out_of_bounds (x,y) = (y<0) || (x<0) || (y>=boardSize) || (x>=boardSize) ------------------------------------------------------------------------------- -- flippable-range+-dir: 1方向に関して石の挟むことのできる範囲を返す. -- bwの手番でpointのdir方向で挟むことのできる石の範囲を返す. -- 挟める石がなければ[]を返す. -- 範囲は,(反転できる始点 反転できる終点の隣)を返す -- bw 手番 -- point 石を置く場所 -- dir 方向 -- board 局面 -- 例: -- flippable_range_dir Black (1,1) (0,1) board => [(1,2),(1,6)] -- (1,2) (1,3) (1,4) (1,5)が反転できることを意味する. -- つまり,(1,2)--(1,5)は相手の石,(1,6)はbwの石である. ------------------------------------------------------------------------------- flippable_range_dir:: Piece -> Pt -> Pt -> Board -> [Pt] flippable_range_dir bw point dir board = if (getBoard board point == Empty) && (valid_direction dir) then if (the_same_point s_end point) then [] else if (getBoard board s_end_next) == bw then [(neighbor point dir), s_end_next] else [] else [] where s_end = (run_end point dir (opponent bw) board) s_end_next = (neighbor s_end dir) -- flippable_range_dir black [3,4] [1,0] initBoard => [[4,4],[5,4]] ------------------------------------------------------------------------------- -- run-dirs: 石を挟める方向を列挙する -- bwの手番でpointで石を挟める方向を列挙する -- bw 手番 -- point 石を置く場所 -- board 局面 ------------------------------------------------------------------------------- run_dirs:: Piece -> Pt -> Board -> [Pt] run_dirs bw point board = aux [] directions where aux p dirs | (null dirs) = p | null (flippable_range_dir bw point (head dirs) board) = aux p (tail dirs) | otherwise = aux (p ++ [head dirs]) (tail dirs) -- run_dirs black [3,4] initBoard => [(1,0)] ------------------------------------------------------------------------------- -- do-flip-dir: 1方向に関して石の反転処理を行う. -- pointのdir方向にbwへの反転を実行する. -- dir方向の隣接点から反転することに注意. -- (point自体については操作しない) -- bw 手番 -- point 石を置く場所 -- dir 方向 -- board 局面 ------------------------------------------------------------------------------- do_flip_dir:: Piece -> Pt -> Pt -> Board -> Board do_flip_dir bw point dir board = flip board (neighbor point dir) where seq = (flippable_range_dir bw point dir board) seq_end = head(tail seq) flip board p = if the_same_point p seq_end then board else (flip (setBoard board p bw) (neighbor p dir)) -- do_flip_dir black [3,4] [1,0] initBoard => [[1,0]] -- setBoard initBoard [3,4] black ------------------------------------------------------------------------------- -- do-move: 石を置く -- 引数 -- bw 手番 -- point 石を置く場所 (y x) -- board 局面 -- bwの手番で,pointに石を置く. -- 手が有効であれば,石を置く処理を行って#tを返す. -- そうでなければ,#fを返す. ------------------------------------------------------------------------------- do_move:: Piece -> Pt -> Board -> (Bool,Board) do_move bw point board = let d = (run_dirs bw point board) in if (null d) then (False, board) else (True, (do_move_aux board d)) where -- do-move-aux: 石を置く処理を行う -- 引数 -- dirs 反転処理を行う方向のリスト -- dirsが空であれば,石をpointに置く. -- そうでなければ,dirsに含まれる方向について石の反転処理を行う do_move_aux board dirs = if null dirs then (setBoard board point bw) else (do_move_aux (do_flip_dir bw point (head dirs) board) (tail dirs)) -- do_move black [3,4] initBoard ------------------------------------------------------------------------------- -- 候補手のリストを生成する. -- bw 手番 -- board 局面 ------------------------------------------------------------------------------- moves:: Piece -> Board -> [Pt] moves bw board = filter (\point -> not $ null (run_dirs bw point board)) [(x,y) | x<-[1..8], y<-[1..8]] -- moves black initBoard => [[3,4],[4,3],[5,6],[6,5]] -- moves white initBoard => [[3,5],[4,6],[5,3],[6,4]] ------------------------------------------------------------------------------- -- ゲームの補助関数 -- 引数 -- bw 手番 -- board 現在の局面データ -- count 盤面上の石の個数 -- passed 相手が直前にパスしたか(True/False) ------------------------------------------------------------------------------- game_aux:: (Piece->Board->IO(Bool,Board)) -> (Piece->Board->IO(Bool,Board)) -> Piece -> Board -> Int -> Bool -> IO() game_aux black_player white_player bw board count passed = do -- 局面の表示 showBoard board if count == boardSize * boardSize || pcount board bw == 0 then -- 盤面が全て埋めつくされているか, -- 手番のプレイヤbwの石がないなら終了 game_end_message board else -- 一手進める if null (moves bw board) then -- bwの打つ手がない場合(PASS) if passed -- 直前に相手もPASSをしていた場合 -- お互いに打つところがない→終了 then game_end_message board -- そうでない場合は,相手の手番に移る else do putStrLn (show bw ++ " PASS") -- 次の手番(bwはPASSした) game_aux black_player white_player (opponent bw) board count True else -- bwに手を決めて石を置いてもらう do (done,newBoard) <- (if bw == Black then black_player else white_player) bw board -- 次の手番(*-player-moveが True を返した場合) if done then game_aux black_player white_player (opponent bw) newBoard (1 + count) False -- 投了(*-player-moveが False を返した場合) else game_end_message board where game_end_message board = do putStrLn "*** GAME OVER ***" putStrLn ("Black vs White " ++ show (pcount board Black) ++ " " ++ show (pcount board White)) ------------------------------------------------------------------------------- -- reversiの対戦を行う -- 引数 -- black_player 黒番の操作を行う関数 -- white_player 白番の操作を行う関数 -- これらの関数は,手を決めて石を配置する処理を適宜行う -- ものとする.また次のように真偽値を返すものとする. -- 1. ゲームを続行する場合は True -- 2. 投了する場合は False -- man_moveを使うとユーザが操作を行うことになる. ------------------------------------------------------------------------------- game:: (Piece->Board->IO(Bool,Board)) -> (Piece->Board->IO(Bool,Board)) -> IO() game black_player white_player = -- gameの本体(game-auxの呼びだし) -- 現在の状態が与えられない場合は,初期状態からスタート game_aux black_player white_player Black initBoard 4 False ------------------------------------------------------------------------------- -- man-move: MANプレイヤ操作関数 -- 引数 -- bw 手番 -- board 局面 -- 手を(y x)で入力して 局面を進めて,#tを返す. -- 入力が^Dであった場合は,投了であるとして,#fを返す. ------------------------------------------------------------------------------- man_move:: Piece -> Board -> IO (Bool,Board) man_move bw board = do -- プロンプト putStrLn (show bw ++ ">> ") -- 手を読み込む x <- getLine >>= return . read y <- getLine >>= return . read if x == -1 then return (False, board) -- ^Dなら#fを返して終了 else if out_of_bounds (x,y) then do putStrLn "illegal position" man_move bw board -- 手が無効(盤の外) else let (done,newBoard) = do_move bw (x,y) board -- 指定された石を置く in if done then return (done,newBoard) else do putStrLn "illegal position" man_move bw board ------------------------------------------------------------------------------- -- メイン ------------------------------------------------------------------------------- main:: IO () main = game man_move man_move {------------------------------------------------------------------------------ 使用例: C:\user\lisp\Haskell>ghc Reversi.hs -o Reversi.exe C:\user\lisp\Haskell>Reversi X X X X X X X X X X X - - - - - - - - X X - - - - - - - - X X - - - - - - - - X X - - - W B - - - X X - - - B W - - - X X - - - - - - - - X X - - - - - - - - X X - - - - - - - - X X X X X X X X X X X Black>> 3 4 X X X X X X X X X X X - - - - - - - - X X - - - - - - - - X X - - - - - - - - X X - - B B B - - - X X - - - B W - - - X X - - - - - - - - X X - - - - - - - - X X - - - - - - - - X X X X X X X X X X X White>> 1 1 illegal position White>> -1 -1 *** GAME OVER *** Black vs White 4 1 ------------------------------------------------------------------------------}