{- Author: Jeff Newbern Maintainer: Jeff Newbern Time-stamp: License: GPL -} {- DESCRIPTION Example 25 - Using the StateT monad transformer with the List monad to achieve non-deterministic stateful computations, and the Writer monad to do logging Usage: Compile the code and run it with an argument between 1 and 8. It will print a solution to the N-queens puzzle along with a log of the number of choices it had at each step. The N-queens puzzle is to place N queens on a chess board so that no queen can attack another one. Try: ./ex25 8 ./ex25 1 ./ex25 7 -} import IO import Monad import System import Maybe import List import Char (toLower) import Control.Monad.State import Control.Monad.Writer -- describe Chess pieces and positions type Rank = Int data File = A | B | C | D | E | F | G | H deriving (Eq, Show, Ord, Enum) data Position = Pos {file::File, rank::Rank} deriving Eq instance Show Position where show (Pos f r) = (map toLower (show f)) ++ (show r) instance Ord Position where compare p1 p2 = case (rank p1) `compare` (rank p2) of LT -> GT GT -> LT _ -> (file p1) `compare` (file p2) data Kind = Pawn | Knight | Bishop | Rook | Queen | King deriving (Eq, Ord, Enum) instance Show Kind where show Pawn = "P" show Knight = "N" show Bishop = "B" show Rook = "R" show Queen = "Q" show King = "K" data Color = Black | White deriving (Eq, Ord, Enum) instance Show Color where show Black = "b" show White = "w" data Piece = Piece {color::Color, kind::Kind} deriving (Eq, Ord) instance Show Piece where show (Piece c k) = ((show c) ++ (show k)) newtype Board = Board [(Piece,Position)] instance Show Board where show (Board ps) = let ordered = (sort . swap) ps ranks = map (showRank ordered) [8,7..1] board = intersperse "--+--+--+--+--+--+--+--" ranks rlabels = intersperse " " (map (\n->(show n)++" ") [8,7..1]) flabels = " a b c d e f g h" in unlines $ zipWith (++) rlabels board ++ [flabels] where swap = map (\(a,b)->(b,a)) showRank ps r = let rnk = filter (\(p,_)->(rank p)==r) ps cs = map (showPiece rnk) [A .. H] in concat (intersperse "|" cs) showPiece ps f = maybe " " (show . snd) (find (\(p,_)->(file p)==f) ps) data Diagonal = Ascending Position | Descending Position deriving (Eq, Show) -- define the diagonal according to its interesction with rank 1 or 8 -- or with file a normalize :: Diagonal -> Diagonal normalize d@(Ascending (Pos _ 1)) = d normalize d@(Ascending (Pos A _)) = d normalize (Ascending (Pos f r)) = normalize (Ascending (Pos (pred f) (r-1))) normalize d@(Descending (Pos _ 8)) = d normalize d@(Descending (Pos A _)) = d normalize (Descending (Pos f r)) = normalize (Descending (Pos (pred f) (r+1))) -- get the diagonals corresponding to a location on the board getDiags :: Position -> (Diagonal,Diagonal) getDiags p = (normalize (Ascending p), normalize (Descending p)) -- this is the type of our problem description data NQueensProblem = NQP {board::Board, ranks::[Rank], files::[File], asc::[Diagonal], desc::[Diagonal]} -- initial state = empty board, all ranks, files, and diagonals free initialState = let fileA = map (\r->Pos A r) [1..8] rank8 = map (\f->Pos f 8) [A .. H] rank1 = map (\f->Pos f 1) [A .. H] asc = map Ascending (nub (fileA ++ rank1)) desc = map Descending (nub (fileA ++ rank8)) in NQP (Board []) [1..8] [A .. H] asc desc -- this is our combined monad type for this problem type NDS a = WriterT [String] (StateT NQueensProblem []) a -- Get the first solution to the problem, by evaluating the solver computation with -- an initial problem state and then returning the first solution in the result list, -- or Nothing if there was no solution. getSolution :: NDS a -> NQueensProblem -> Maybe (a,[String]) getSolution c i = listToMaybe (evalStateT (runWriterT c) i) -- add a Queen to the board in a specific position addQueen :: Position -> NDS () addQueen p = do (Board b) <- gets board rs <- gets ranks fs <- gets files as <- gets asc ds <- gets desc let b' = (Piece Black Queen, p):b rs' = delete (rank p) rs fs' = delete (file p) fs (a,d) = getDiags p as' = delete a as ds' = delete d ds tell ["Added Queen at " ++ (show p)] put (NQP (Board b') rs' fs' as' ds') -- test if a position is in the set of allowed diagonals inDiags :: Position -> NDS Bool inDiags p = do let (a,d) = getDiags p as <- gets asc ds <- gets desc return $ (elem a as) && (elem d ds) -- add a Queen to the board in all allowed positions addQueens :: NDS () addQueens = do rs <- gets ranks fs <- gets files allowed <- filterM inDiags [Pos f r | f <- fs, r <- rs] tell [show (length allowed) ++ " possible choices"] msum (map addQueen allowed) -- Start with an empty chess board and add the requested number of queens, -- then get the board and print the solution along with the log main :: IO () main = do args <- getArgs let n = read (args!!0) cmds = replicate n addQueens sol = (`getSolution` initialState) $ do sequence_ cmds gets board case sol of Just (b,l) -> do putStr $ show b -- show the solution putStr $ unlines l -- show the log Nothing -> putStrLn "No solution" -- END OF FILE