chessIO 0.0.0.0 → 0.1.0.0
raw patch · 6 files changed
+644/−72 lines, 6 filesdep +attoparsecdep +bytestringdep +haskelinenew-component:exe:cboardPVP ok
version bump matches the API change (PVP)
Dependencies added: attoparsec, bytestring, haskeline, megaparsec, mtl, parser-combinators, process, split, stm, unordered-containers
API changes (from Hackage documentation)
- Game.Chess: instance GHC.Classes.Ord Game.Chess.Color
- Game.Chess: relativeTo :: Position -> Move -> Maybe Move
+ Game.Chess: A1 :: Sq
+ Game.Chess: A2 :: Sq
+ Game.Chess: A3 :: Sq
+ Game.Chess: A4 :: Sq
+ Game.Chess: A5 :: Sq
+ Game.Chess: A6 :: Sq
+ Game.Chess: A7 :: Sq
+ Game.Chess: A8 :: Sq
+ Game.Chess: B1 :: Sq
+ Game.Chess: B2 :: Sq
+ Game.Chess: B3 :: Sq
+ Game.Chess: B4 :: Sq
+ Game.Chess: B5 :: Sq
+ Game.Chess: B6 :: Sq
+ Game.Chess: B7 :: Sq
+ Game.Chess: B8 :: Sq
+ Game.Chess: Bishop :: PieceType
+ Game.Chess: Black :: Color
+ Game.Chess: C1 :: Sq
+ Game.Chess: C2 :: Sq
+ Game.Chess: C3 :: Sq
+ Game.Chess: C4 :: Sq
+ Game.Chess: C5 :: Sq
+ Game.Chess: C6 :: Sq
+ Game.Chess: C7 :: Sq
+ Game.Chess: C8 :: Sq
+ Game.Chess: D1 :: Sq
+ Game.Chess: D2 :: Sq
+ Game.Chess: D3 :: Sq
+ Game.Chess: D4 :: Sq
+ Game.Chess: D5 :: Sq
+ Game.Chess: D6 :: Sq
+ Game.Chess: D7 :: Sq
+ Game.Chess: D8 :: Sq
+ Game.Chess: E1 :: Sq
+ Game.Chess: E2 :: Sq
+ Game.Chess: E3 :: Sq
+ Game.Chess: E4 :: Sq
+ Game.Chess: E5 :: Sq
+ Game.Chess: E6 :: Sq
+ Game.Chess: E7 :: Sq
+ Game.Chess: E8 :: Sq
+ Game.Chess: F1 :: Sq
+ Game.Chess: F2 :: Sq
+ Game.Chess: F3 :: Sq
+ Game.Chess: F4 :: Sq
+ Game.Chess: F5 :: Sq
+ Game.Chess: F6 :: Sq
+ Game.Chess: F7 :: Sq
+ Game.Chess: F8 :: Sq
+ Game.Chess: G1 :: Sq
+ Game.Chess: G2 :: Sq
+ Game.Chess: G3 :: Sq
+ Game.Chess: G4 :: Sq
+ Game.Chess: G5 :: Sq
+ Game.Chess: G6 :: Sq
+ Game.Chess: G7 :: Sq
+ Game.Chess: G8 :: Sq
+ Game.Chess: H1 :: Sq
+ Game.Chess: H2 :: Sq
+ Game.Chess: H3 :: Sq
+ Game.Chess: H4 :: Sq
+ Game.Chess: H5 :: Sq
+ Game.Chess: H6 :: Sq
+ Game.Chess: H7 :: Sq
+ Game.Chess: H8 :: Sq
+ Game.Chess: King :: PieceType
+ Game.Chess: Knight :: PieceType
+ Game.Chess: Pawn :: PieceType
+ Game.Chess: Queen :: PieceType
+ Game.Chess: Rook :: PieceType
+ Game.Chess: White :: Color
+ Game.Chess: color :: Position -> Color
+ Game.Chess: data Color
+ Game.Chess: data PieceType
+ Game.Chess: data Sq
+ Game.Chess: fromSAN :: Position -> String -> Either String Move
+ Game.Chess: instance GHC.Show.Show Game.Chess.From
+ Game.Chess: instance GHC.Show.Show Game.Chess.Move
+ Game.Chess: isDark :: Sq -> Bool
+ Game.Chess: isLight :: Sq -> Bool
+ Game.Chess: opponent :: Color -> Color
+ Game.Chess: pieceAt :: Position -> Sq -> Maybe (Color, PieceType)
+ Game.Chess: unsafeApplyMove :: Position -> Move -> Position
+ Game.Chess.UCI: Button :: Option
+ Game.Chess.UCI: CheckBox :: Bool -> Option
+ Game.Chess.UCI: ComboBox :: ByteString -> [ByteString] -> Option
+ Game.Chess.UCI: CurrMove :: ByteString -> Info
+ Game.Chess.UCI: CurrMoveNumber :: Int -> Info
+ Game.Chess.UCI: Depth :: Int -> Info
+ Game.Chess.UCI: HashFull :: Int -> Info
+ Game.Chess.UCI: LowerBound :: Info
+ Game.Chess.UCI: MultiPV :: Int -> Info
+ Game.Chess.UCI: NPS :: Int -> Info
+ Game.Chess.UCI: Nodes :: Int -> Info
+ Game.Chess.UCI: PV :: [Move] -> Info
+ Game.Chess.UCI: Score :: Int -> Info
+ Game.Chess.UCI: SelDepth :: Int -> Info
+ Game.Chess.UCI: SpinButton :: Int -> Option
+ Game.Chess.UCI: String :: ByteString -> Option
+ Game.Chess.UCI: TBHits :: Int -> Info
+ Game.Chess.UCI: Time :: Int -> Info
+ Game.Chess.UCI: UpperBound :: Info
+ Game.Chess.UCI: [comboBoxValue] :: Option -> ByteString
+ Game.Chess.UCI: [comboBoxValues] :: Option -> [ByteString]
+ Game.Chess.UCI: [spinButtonValue, spinButtonMinBound, spinButtonMaxBound] :: Option -> Int
+ Game.Chess.UCI: addMove :: Engine -> Move -> IO ()
+ Game.Chess.UCI: author :: Engine -> Maybe ByteString
+ Game.Chess.UCI: currentPosition :: Engine -> IO Position
+ Game.Chess.UCI: data Engine
+ Game.Chess.UCI: data Info
+ Game.Chess.UCI: data Option
+ Game.Chess.UCI: data UCIException
+ Game.Chess.UCI: game :: Engine -> IORef (Position, [Move])
+ Game.Chess.UCI: getOption :: ByteString -> Engine -> Maybe Option
+ Game.Chess.UCI: instance Data.String.IsString Game.Chess.UCI.Option
+ Game.Chess.UCI: instance GHC.Classes.Eq Game.Chess.UCI.Option
+ Game.Chess.UCI: instance GHC.Exception.Type.Exception Game.Chess.UCI.UCIException
+ Game.Chess.UCI: instance GHC.Show.Show Game.Chess.UCI.Command
+ Game.Chess.UCI: instance GHC.Show.Show Game.Chess.UCI.Info
+ Game.Chess.UCI: instance GHC.Show.Show Game.Chess.UCI.Option
+ Game.Chess.UCI: instance GHC.Show.Show Game.Chess.UCI.UCIException
+ Game.Chess.UCI: isready :: Engine -> IO ()
+ Game.Chess.UCI: move :: Engine -> String -> IO ()
+ Game.Chess.UCI: name :: Engine -> Maybe ByteString
+ Game.Chess.UCI: options :: Engine -> HashMap ByteString Option
+ Game.Chess.UCI: quit :: Engine -> IO (Maybe ExitCode)
+ Game.Chess.UCI: quit' :: Int -> Engine -> IO (Maybe ExitCode)
+ Game.Chess.UCI: readBestMove :: Engine -> STM (Move, Maybe Move)
+ Game.Chess.UCI: readInfo :: Engine -> STM [Info]
+ Game.Chess.UCI: send :: ByteString -> Engine -> IO ()
+ Game.Chess.UCI: setOptionSpinButton :: ByteString -> Int -> Engine -> IO Engine
+ Game.Chess.UCI: start :: String -> [String] -> IO (Maybe Engine)
+ Game.Chess.UCI: start' :: Int -> (String -> IO ()) -> String -> [String] -> IO (Maybe Engine)
+ Game.Chess.UCI: tryReadBestMove :: Engine -> STM (Maybe (Move, Maybe Move))
+ Game.Chess.UCI: tryReadInfo :: Engine -> STM (Maybe [Info])
- Game.Chess: fromUCI :: String -> Maybe Move
+ Game.Chess: fromUCI :: Position -> String -> Maybe Move
Files
- app/cboard.hs +118/−0
- chessIO.cabal +50/−10
- src/Game/Chess.hs +160/−34
- src/Game/Chess/UCI.hs +299/−0
- src/Game/Debug.hs +0/−16
- test/Perft.hs +17/−12
+ app/cboard.hs view
@@ -0,0 +1,118 @@+module Main where++import Control.Concurrent+import Control.Concurrent.STM+import Control.Exception+import Control.Monad+import Control.Monad.IO.Class+import Control.Monad.State.Strict+import Data.Char+import Data.IORef+import Data.List+import Data.List.Split+import Game.Chess+import qualified Game.Chess.UCI as UCI+import System.Console.Haskeline hiding (catch, handle)+import System.Exit+import System.Environment++data S = S {+ engine :: UCI.Engine+, mover :: Maybe ThreadId+, hintRef :: IORef (Maybe Move)+}++main = getArgs >>= \case+ [] -> do+ putStrLn "Please specify a UCI engine at the command line"+ exitWith $ ExitFailure 1+ (cmd:args) -> do+ UCI.start cmd args >>= \case+ Nothing -> do+ putStrLn "Unable to initialise engine, maybe it doesn't speak UCI?"+ exitWith $ ExitFailure 2+ Just e -> do+ s <- S e Nothing <$> newIORef Nothing+ runInputT defaultSettings chessIO `evalStateT` s+ exitWith ExitSuccess++chessIO :: InputT (StateT S IO) ()+chessIO = do+ externalPrint <- getExternalPrint+ e <- lift $ gets engine+ hr <- lift $ gets hintRef+ tid <- liftIO . forkIO $ doBestMove externalPrint hr e+ lift $ modify' $ \s -> s { mover = Just tid }+ outputBoard+ loop+ void . liftIO $ UCI.quit e++outputBoard :: InputT (StateT S IO) ()+outputBoard = do+ e <- lift $ gets engine+ liftIO $ do+ pos <- UCI.currentPosition e+ printBoard putStrLn pos++loop :: InputT (StateT S IO) ()+loop = do+ e <- lift $ gets engine+ getInputLine "> " >>= \case+ Nothing -> pure ()+ Just input+ | null input -> loop+ | Just position <- fromFEN input -> do+ outputStrLn $ toFEN position+ loop+ | input == "hint" -> do+ lift (gets hintRef) >>= liftIO . readIORef >>= \case+ Just hint -> outputStrLn $ "Try " <> show hint+ Nothing -> outputStrLn "Sorry, no hint available"+ loop+ | otherwise -> do+ liftIO $ do+ printUCIException `handle` do+ UCI.move e input+ UCI.send "go movetime 1000" e+ outputBoard+ loop++printBoard :: (String -> IO ()) -> Position -> IO ()+printBoard externalPrint pos = externalPrint . init . unlines $+ (map . map) pc (reverse $ chunksOf 8 [A1 .. H8])+ where+ pc sq = (if isDark sq then toUpper else toLower) case pieceAt pos sq of+ Just (White, Pawn) -> 'P'+ Just (White, Knight) -> 'N'+ Just (White, Bishop) -> 'B'+ Just (White, Rook) -> 'R'+ Just (White, Queen) -> 'Q'+ Just (White, King) -> 'K'+ Just (Black, Pawn) -> 'X'+ Just (Black, Knight) -> 'S'+ Just (Black, Bishop) -> 'L'+ Just (Black, Rook) -> 'T'+ Just (Black, Queen) -> 'D'+ Just (Black, King) -> 'J'+ Nothing | isDark sq -> '.'+ | otherwise -> ' '+doBestMove :: (String -> IO ()) -> IORef (Maybe Move) -> UCI.Engine -> IO ()+doBestMove externalPrint hintRef e = forever $ do+ (bm, ponder) <- atomically . UCI.readBestMove $ e+ UCI.addMove e bm+ externalPrint $ "< " <> show bm+ UCI.currentPosition e >>= printBoard externalPrint+ writeIORef hintRef ponder++printPV :: (String -> IO ()) -> UCI.Engine -> IO ()+printPV externalPrint engine = forever $ do+ info <- atomically . UCI.readInfo $ engine+ case find isPV info of+ Just pv -> externalPrint $ show pv+ Nothing -> pure ()+ where+ isPV UCI.PV{} = True+ isPV _ = False++printUCIException :: UCI.UCIException -> IO ()+printUCIException e = print e
chessIO.cabal view
@@ -4,11 +4,13 @@ -- -- see: https://github.com/sol/hpack ----- hash: bce4c13865f7d91aaf5bc6807f962a6263a4f6ccd6ccea79504ebac4a9f53f64+-- hash: 4e2926a2dc0dbb48166ba6817910daed6e92c23dbc6e644942c9ebcfe3922702 name: chessIO-version: 0.0.0.0-description: A simple chess move generation library+version: 0.1.0.0+synopsis: Basic chess move generation and UCI client library+description: A simple library for generating legal chess moves. Also includes a module for communication with external processes that speak the UCI (Universal Chess Interface) protocol. On top of that, provides a console frontend program (cboard) that can be used to interactively play against UCI engines.+category: Game homepage: https://github.com/mlang/chessIO#readme bug-reports: https://github.com/mlang/chessIO/issues author: Mario Lang@@ -25,18 +27,49 @@ library exposed-modules: Game.Chess+ Game.Chess.UCI other-modules:- Game.Debug Paths_chessIO hs-source-dirs: src- default-extensions: BangPatterns BinaryLiterals DeriveGeneric NamedFieldPuns NumericUnderscores TupleSections ViewPatterns- ghc-options: -Wall -O2+ default-extensions: BangPatterns BinaryLiterals BlockArguments DeriveGeneric LambdaCase NamedFieldPuns NumericUnderscores RecordWildCards OverloadedStrings TupleSections ViewPatterns+ ghc-options: -Wall build-depends:- base >=4.8 && <5+ attoparsec+ , base >=4.8 && <5+ , bytestring+ , megaparsec+ , parser-combinators+ , process+ , stm+ , unordered-containers , vector default-language: Haskell2010 +executable cboard+ main-is: cboard.hs+ other-modules:+ Paths_chessIO+ hs-source-dirs:+ app+ default-extensions: BangPatterns BinaryLiterals BlockArguments DeriveGeneric LambdaCase NamedFieldPuns NumericUnderscores RecordWildCards OverloadedStrings TupleSections ViewPatterns+ ghc-options: -Wall -threaded+ build-depends:+ attoparsec+ , base >=4.8 && <5+ , bytestring+ , chessIO+ , haskeline+ , megaparsec+ , mtl+ , parser-combinators+ , process+ , split+ , stm+ , unordered-containers+ , vector+ default-language: Haskell2010+ test-suite perft type: exitcode-stdio-1.0 main-is: Perft.hs@@ -44,13 +77,20 @@ Paths_chessIO hs-source-dirs: test- default-extensions: BangPatterns BinaryLiterals DeriveGeneric NamedFieldPuns NumericUnderscores TupleSections ViewPatterns- ghc-options: -Wall -O2 -threaded -rtsopts "-with-rtsopts=-N -s"+ default-extensions: BangPatterns BinaryLiterals BlockArguments DeriveGeneric LambdaCase NamedFieldPuns NumericUnderscores RecordWildCards OverloadedStrings TupleSections ViewPatterns+ ghc-options: -Wall -threaded -rtsopts "-with-rtsopts=-N -s" build-depends:- base >=4.8 && <5+ attoparsec+ , base >=4.8 && <5+ , bytestring , chessIO , directory+ , megaparsec , parallel+ , parser-combinators+ , process+ , stm , time+ , unordered-containers , vector default-language: Haskell2010
src/Game/Chess.hs view
@@ -15,32 +15,124 @@ package name chessIO. -} module Game.Chess (- -- * Representing chess positions- Position, startpos- -- * Converting from/to Forsyth-Edwards-Notation+ -- * Chess positions+ Color(..), opponent+, Sq(..), isLight, isDark+, PieceType(..)+, Position, startpos, color, pieceAt+ -- ** Converting from/to Forsyth-Edwards-Notation , fromFEN, toFEN -- * Chess moves , Move- -- ** Converting from/to algebraic notation used by the Universal Chess Interface-, fromUCI, toUCI- -- ** Validating that a move is actually legal in a given position-, relativeTo+ -- ** Converting from/to algebraic notation+, fromSAN, fromUCI, toUCI -- ** Move generation , moves -- ** Executing moves-, applyMove+, applyMove, unsafeApplyMove ) where +import Control.Applicative.Combinators import Data.Bits import Data.Char+import Data.Functor (($>)) import Data.Ix import Data.List import Data.Maybe import Data.Vector.Unboxed (Vector, (!))+import Data.Void import qualified Data.Vector.Unboxed as Vector import Data.Word+import Text.Megaparsec+import Text.Megaparsec.Char import Text.Read +type Parser = Parsec Void String++data From = File Int+ | Rank Int+ | Square Int+ deriving (Show)++san :: Parser (PieceType, Maybe From, Bool, Int, Maybe PieceType, Maybe Char)+san = conv <$> piece+ <*> location+ <*> optional (optional (char '=') *> promo)+ <*> optional (char '+' <|> char '#') where+ conv pc (Nothing, Nothing, cap, to) = (pc, Nothing, cap, to,,)+ conv pc (Just f, Nothing, cap, to) = (pc, Just (File f), cap, to,,)+ conv pc (Nothing, Just r, cap, to) = (pc, Just (Rank r), cap, to,,)+ conv pc (Just f, Just r, cap, to) = (pc, Just (Square $ r*8+f), cap, to,,)+ piece = char 'N' $> Knight+ <|> char 'B' $> Bishop+ <|> char 'R' $> Rook+ <|> char 'Q' $> Queen+ <|> char 'K' $> King+ <|> pure Pawn+ location = try ((,,,) <$> (Just <$> file)+ <*> pure Nothing+ <*> capture+ <*> square)+ <|> try ((,,,) <$> pure Nothing+ <*> (Just <$> rank)+ <*> capture+ <*> square)+ <|> try ((,,,) <$> (Just <$> file)+ <*> (Just <$> rank)+ <*> capture+ <*> square)+ <|> (,,,) <$> pure Nothing+ <*> pure Nothing+ <*> capture+ <*> square+ promo = char 'N' $> Knight+ <|> char 'B' $> Bishop+ <|> char 'R' $> Rook+ <|> char 'Q' $> Queen+ capture = option False $ char 'x' $> True+ square = frToInt <$> file <*> rank+ file = subtract (ord 'a') . ord <$> oneOf ['a'..'h']+ rank = subtract (ord '1') . ord <$> oneOf ['1'..'8']+ frToInt f r = r*8 + f++fromSAN :: Position -> String -> Either String Move+fromSAN Position{color = White, flags} s+ | s `elem` ["O-O", "0-0"] && flags `testMask` crwKs+ = Right $ move (fromEnum E1) (fromEnum G1)+fromSAN Position{color = Black, flags} s+ | s `elem` ["O-O", "0-0"] && flags `testMask` crbKs+ = Right $ move (fromEnum E8) (fromEnum G8)+fromSAN Position{color = White, flags} s+ | s `elem` ["O-O-O", "0-0-0"] && flags `testMask` crwQs+ = Right $ move (fromEnum E1) (fromEnum C1)+fromSAN Position{color = Black, flags} s+ | s `elem` ["O-O-O", "0-0-0"] && flags `testMask` crbQs+ = Right $ move (fromEnum E8) (fromEnum C8)+fromSAN pos s = case parse san "" s of+ Right (pc, from, capture, to, promo, status) ->+ case ms pc from to promo of+ [m] -> Right m+ [] -> Left "Illegal move"+ _ -> Left "Ambiguous move"+ Left err -> Left $ errorBundlePretty err+ where+ ms pc from to prm = filter (f from) $ moves pos where+ f (Just (Square from)) (unpack -> (from', to', prm')) =+ pAt pos from' == pc && from' == from && to' == to && prm' == prm+ f (Just (File ff)) (unpack -> (from', to', prm')) =+ pAt pos from' == pc && from' `mod` 8 == ff && to == to' && prm == prm'+ f (Just (Rank fr)) (unpack -> (from', to', prm')) =+ pAt pos from' == pc && from' `div` 8 == fr && to == to' && prm == prm'+ f Nothing (unpack -> (from', to', prm')) =+ pAt pos from' == pc && to == to' && prm == prm'+ pAt (Position BB{wP, wN, wB, wR, wQ, wK, bP, bN, bB, bR, bQ, bK} _ _ _ _) sq+ | (wP .|. bP) `testBit` sq = Pawn+ | (wN .|. bN) `testBit` sq = Knight+ | (wB .|. bB) `testBit` sq = Bishop+ | (wR .|. bR) `testBit` sq = Rook+ | (wQ .|. bQ) `testBit` sq = Queen+ | otherwise = King+ -- | The starting position as given by the FEN string -- "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1". startpos :: Position@@ -48,8 +140,25 @@ fromFEN "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1" data PieceType = Pawn | Knight | Bishop | Rook | Queen | King deriving (Eq, Show)-data Color = White | Black deriving (Eq, Ord, Show) +data Color = White | Black deriving (Eq, Show)++pieceAt :: Position -> Sq -> Maybe (Color, PieceType)+pieceAt (board -> BB{wP, wN, wB, wR, wQ, wK, bP, bN, bB, bR, bQ, bK}) (fromEnum -> sq)+ | wP `testBit` sq = Just (White, Pawn)+ | wN `testBit` sq = Just (White, Knight)+ | wB `testBit` sq = Just (White, Bishop)+ | wR `testBit` sq = Just (White, Rook)+ | wQ `testBit` sq = Just (White, Queen)+ | wK `testBit` sq = Just (White, King)+ | bP `testBit` sq = Just (Black, Pawn)+ | bN `testBit` sq = Just (Black, Knight)+ | bB `testBit` sq = Just (Black, Bishop)+ | bR `testBit` sq = Just (Black, Rook)+ | bQ `testBit` sq = Just (Black, Queen)+ | bK `testBit` sq = Just (Black, King)+ | otherwise = Nothing+ opponent :: Color -> Color opponent White = Black opponent Black = White@@ -66,6 +175,11 @@ | A8 | B8 | C8 | D8 | E8 | F8 | G8 | H8 deriving (Bounded, Enum, Eq, Show) +isDark :: Sq -> Bool+isDark (fromEnum -> sq) = (0xaa55aa55aa55aa55 :: Word64) `testBit` sq++isLight :: Sq -> Bool+isLight = not . isDark data Castling = Kingside | Queenside deriving (Eq, Ord, Show) data BB = BB { wP, wN, wB, wR, wQ, wK :: !Word64@@ -149,7 +263,7 @@ -- | Convert a position to Forsyth-Edwards-Notation. toFEN :: Position -> String-toFEN (Position bb c flgs hm mn) = intercalate " " [+toFEN (Position bb c flgs hm mn) = unwords [ intercalate "/" (rank <$> [7,6..0]) , showColor c, showCst (flgs `clearMask` epMask), showEP (flgs .&. epMask), show hm, show mn ]@@ -171,7 +285,7 @@ (r, f) = bitScanForward x `divMod` 8 rank r = concatMap countEmpty $ groupBy (\x y -> x == y && x == ' ') $ charAt r <$> [0..7]- countEmpty xs | head xs == ' ' = if length xs == 8 then "" else show (length xs)+ countEmpty xs | head xs == ' ' = show $ length xs | otherwise = xs charAt r f | wP bb `testBit` b = 'P'@@ -196,8 +310,8 @@ occupied :: BB -> Word64 occupied bb = occupiedBy White bb .|. occupiedBy Black bb -empty :: BB -> Word64-empty = complement . occupied+notOccupied :: BB -> Word64+notOccupied = complement . occupied foldBits :: (a -> Int -> a) -> a -> Word64 -> a foldBits _ a 0 = a@@ -210,6 +324,9 @@ newtype Move = Move Word16 deriving (Eq) +instance Show Move where+ show = toUCI+ move :: Int -> Int -> Move move from to = Move $ fromIntegral from .|. fromIntegral to `unsafeShiftL` 6 @@ -234,14 +351,15 @@ _ -> Nothing -- | Parse a move in the format used by the Universal Chess Interface protocol.-fromUCI :: String -> Maybe Move-fromUCI (fmap (splitAt 2) . splitAt 2 -> (from, (to, promo)))- | length from == 2 && length to == 2 && length promo == 0- = move <$> readCoord from <*> readCoord to+fromUCI :: Position -> String -> Maybe Move+fromUCI pos (fmap (splitAt 2) . splitAt 2 -> (from, (to, promo)))+ | length from == 2 && length to == 2 && null promo+ = move <$> readCoord from <*> readCoord to >>= relativeTo pos | length from == 2 && length to == 2 && length promo == 1 = (\f t p -> move f t `promoteTo` p) <$> readCoord from <*> readCoord to <*> readPromo promo+ >>= relativeTo pos where readCoord [f,r] | inRange ('a','h') f && inRange ('1','8') r@@ -252,12 +370,13 @@ readPromo "b" = Just Bishop readPromo "n" = Just Knight readPromo _ = Nothing+fromUCI _ _ = Nothing -- | Convert a move to the format used by the Universal Chess Interface protocol. toUCI :: Move -> String toUCI (unpack -> (from, to, promo)) = coord from <> coord to <> p where coord x = let (r,f) = x `divMod` 8 in- chr (f + (ord 'a')) : [chr (r + (ord '1'))]+ chr (f + ord 'a') : [chr (r + ord '1')] p = case promo of Just Queen -> "q" Just Rook -> "r"@@ -267,7 +386,7 @@ -- | Validate that a certain move is legal in the given position. relativeTo :: Position -> Move -> Maybe Move-relativeTo pos m | m `elem` (moves pos) = Just m+relativeTo pos m | m `elem` moves pos = Just m | otherwise = Nothing shiftN, shiftNNE, shiftNE, shiftENE, shiftE, shiftESE, shiftSE, shiftSSE, shiftS, shiftSSW, shiftSW, shiftWSW, shiftW, shiftWNW, shiftNW, shiftNNW :: Word64 -> Word64@@ -289,31 +408,36 @@ shiftNNW w = w `unsafeShiftL` 15 .&. notHFile applyMove :: Position -> Move -> Position-applyMove pos m@(unpack -> (from, to, promo))+applyMove p m+ | m `elem` moves p = unsafeApplyMove p m+ | otherwise = error "Game.Chess.applyMove: Illegal move"++unsafeApplyMove :: Position -> Move -> Position+unsafeApplyMove pos m@(unpack -> (from, to, promo)) | m == wKscm && flags pos `testMask` crwKs = pos { board = bb { wK = wK bb `xor` mask- , wR = wR bb `xor` (bit (fromEnum H1) `setBit` (fromEnum F1))+ , wR = wR bb `xor` (bit (fromEnum H1) `setBit` fromEnum F1) } , color = opponent (color pos) , flags = flags pos `clearMask` (rank1 .|. epMask) } | m == wQscm && flags pos `testMask` crwQs = pos { board = bb { wK = wK bb `xor` mask- , wR = wR bb `xor` (bit (fromEnum A1) `setBit` (fromEnum D1))+ , wR = wR bb `xor` (bit (fromEnum A1) `setBit` fromEnum D1) } , color = opponent (color pos) , flags = flags pos `clearMask` (rank1 .|. epMask) } | m == bKscm && flags pos `testMask` crbKs = pos { board = bb { bK = bK bb `xor` mask- , bR = bR bb `xor` (bit (fromEnum H8) `setBit` (fromEnum F8))+ , bR = bR bb `xor` (bit (fromEnum H8) `setBit` fromEnum F8) } , color = opponent (color pos) , flags = flags pos `clearMask` (rank8 .|. epMask) } | m == bQscm && flags pos `testMask` crbQs = pos { board = bb { bK = bK bb `xor` mask- , bR = bR bb `xor` (bit (fromEnum A8) `setBit` (fromEnum D8))+ , bR = bR bb `xor` (bit (fromEnum A8) `setBit` fromEnum D8) } , color = opponent (color pos) , flags = flags pos `clearMask` (rank8 .|. epMask)@@ -423,8 +547,9 @@ Black | bit from .&. rank7 .&. bP bb /= 0 && from - 16 == to -> bit (from - 8) _ -> 0 +-- | Generate a list of possible moves for the given position. moves :: Position -> [Move]-moves pos@Position{color} = filter (not . legal) $+moves pos@Position{color} = filter (not . check) $ pawnMoves pos <> slideMoves Bishop pos <> slideMoves Rook pos@@ -432,7 +557,7 @@ <> knightMoves pos <> kingMoves pos where- legal m = let board' = board (applyMove pos m) in case color of+ check m = let board' = board (unsafeApplyMove pos m) in case color of White -> let kSq = bitScanForward (wK board') in attackedBy Black kSq board' Black -> let kSq = bitScanForward (bK board') in@@ -440,16 +565,16 @@ pawnMoves :: Position -> [Move] pawnMoves (Position bb White flags _ _) =- wPawnMoves (wP bb) (empty bb) (occupiedBy Black bb .|. (flags .&. epMask))+ wPawnMoves (wP bb) (notOccupied bb) (occupiedBy Black bb .|. (flags .&. epMask)) pawnMoves (Position bb Black flags _ _) =- bPawnMoves (bP bb) (empty bb) (occupiedBy White bb .|. (flags .&. epMask))+ bPawnMoves (bP bb) (notOccupied bb) (occupiedBy White bb .|. (flags .&. epMask)) wPawnMoves :: Word64 -> Word64 -> Word64 -> [Move]-wPawnMoves pawns empty opponentPieces =+wPawnMoves pawns emptySquares opponentPieces = foldBits (mkMove 9) (foldBits (mkMove 7) (foldBits (mkMove 8) (foldBits (mkMove 16) [] doublePushTargets) singlePushTargets) westCaptureTargets) eastCaptureTargets where- doublePushTargets = shiftN singlePushTargets .&. empty .&. rank4- singlePushTargets = shiftN pawns .&. empty+ doublePushTargets = shiftN singlePushTargets .&. emptySquares .&. rank4+ singlePushTargets = shiftN pawns .&. emptySquares eastCaptureTargets = shiftNE pawns .&. opponentPieces westCaptureTargets = shiftNW pawns .&. opponentPieces mkMove diff ms tsq@@ -458,11 +583,11 @@ where m = move (tsq - diff) tsq bPawnMoves :: Word64 -> Word64 -> Word64 -> [Move]-bPawnMoves pawns empty opponentPieces =+bPawnMoves pawns emptySquares opponentPieces = foldBits (mkMove 9) (foldBits (mkMove 7) (foldBits (mkMove 8) (foldBits (mkMove 16) [] doublePushTargets) singlePushTargets) eastCaptureTargets) westCaptureTargets where- doublePushTargets = shiftS singlePushTargets .&. empty .&. rank5- singlePushTargets = shiftS pawns .&. empty+ doublePushTargets = shiftS singlePushTargets .&. emptySquares .&. rank5+ singlePushTargets = shiftS pawns .&. emptySquares eastCaptureTargets = shiftSE pawns .&. opponentPieces westCaptureTargets = shiftSW pawns .&. opponentPieces mkMove diff ms tsq@@ -626,6 +751,7 @@ SW -> (bitScanReverse, attackSW) W -> (bitScanReverse, attackW) +attackDir :: (Word64 -> Word64) -> Vector Word64 attackDir s = Vector.generate 64 $ \sq -> foldr (.|.) 0 $ take 7 $ tail $ iterate s (bit sq)
+ src/Game/Chess/UCI.hs view
@@ -0,0 +1,299 @@+module Game.Chess.UCI (+ UCIException+, Engine, name, author, options, game+, currentPosition, readInfo, tryReadInfo, readBestMove, tryReadBestMove+, start, start', isready+, Option(..), getOption, setOptionSpinButton+, Info(..)+, send+, addMove, move+, quit, quit'+) where++import Control.Applicative+import Control.Concurrent+import Control.Concurrent.STM+import Control.Concurrent.STM.TChan+import Control.Exception+import Control.Monad+import Data.Attoparsec.Combinator+import Data.Attoparsec.ByteString.Char8+import Data.ByteString.Char8 (ByteString)+import qualified Data.ByteString.Char8 as BS+import Data.Functor+import Data.HashMap.Strict (HashMap)+import qualified Data.HashMap.Strict as HashMap+import Data.IORef+import Data.Ix+import Data.List+import Data.Maybe+import Data.String+import Game.Chess+import System.Exit+import System.IO+import System.Process+import System.Timeout (timeout)++data Engine = Engine {+ inH :: Handle+, outH :: Handle+, procH :: ProcessHandle+, outputStrLn :: String -> IO ()+, infoThread :: Maybe ThreadId+, name :: Maybe ByteString+, author :: Maybe ByteString+, options :: HashMap ByteString Option+, isReady :: MVar ()+, infoChan :: TChan [Info]+, bestMoveChan :: TChan (Move, Maybe Move)+, game :: IORef (Position, [Move])+}++readInfo :: Engine -> STM [Info]+readInfo = readTChan . infoChan++tryReadInfo :: Engine -> STM (Maybe [Info])+tryReadInfo = tryReadTChan . infoChan++readBestMove :: Engine -> STM (Move, Maybe Move)+readBestMove = readTChan . bestMoveChan++tryReadBestMove :: Engine -> STM (Maybe (Move, Maybe Move))+tryReadBestMove = tryReadTChan . bestMoveChan++data UCIException = SANError String deriving Show+++instance Exception UCIException++data Command = Name ByteString+ | Author ByteString+ | Option ByteString Option+ | UCIOk+ | ReadyOK+ | Info [Info]+ | BestMove !(Move, (Maybe Move))+ deriving (Show)++data Info = PV [Move]+ | Depth Int+ | SelDepth Int+ | Time Int+ | MultiPV Int+ | Score Int+ | UpperBound+ | LowerBound+ | Nodes Int+ | NPS Int+ | TBHits Int+ | HashFull Int+ | CurrMove ByteString+ | CurrMoveNumber Int+ deriving Show++data Option = CheckBox Bool+ | ComboBox { comboBoxValue :: ByteString, comboBoxValues :: [ByteString] }+ | SpinButton { spinButtonValue, spinButtonMinBound, spinButtonMaxBound :: Int }+ | String ByteString+ | Button+ deriving (Eq, Show)++instance IsString Option where+ fromString = String . BS.pack++command :: Position -> Parser Command+command pos = skipSpace *> choice [+ name, author, option, uciok, readyok, info, bestmove+ ] <* skipSpace+ where+ name = fmap Name $+ "id" *> skipSpace *> "name" *> skipSpace *> takeByteString+ author = fmap Author $+ "id" *> skipSpace *> "author" *> skipSpace *> takeByteString+ option = do+ void "option"+ skipSpace+ void "name"+ skipSpace+ optName <- BS.pack <$> manyTill anyChar (skipSpace *> "type")+ skipSpace+ optValue <- spin <|> check <|> combo <|> str <|> button+ pure $ Option optName optValue+ check =+ fmap CheckBox $ "check" *> skipSpace *> "default" *> skipSpace *>+ ("false" $> False <|> "true" $> True)+ spin = do+ void "spin"+ skipSpace+ value <- "default" *> skipSpace *> signed decimal <* skipSpace+ minValue <- "min" *> skipSpace *> signed decimal <* skipSpace+ maxValue <- "max" *> skipSpace *> signed decimal+ pure $ SpinButton value minValue maxValue+ combo = do+ void "combo"+ skipSpace+ def <- fmap BS.pack $ "default" *> skipSpace *> manyTill anyChar var+ (vars, lastVar) <- (,) <$> many (manyTill anyChar var)+ <*> takeByteString+ pure $ ComboBox def (map BS.pack vars <> [lastVar])+ var = skipSpace *> "var" *> skipSpace+ str = fmap String $+ "string" *> skipSpace *> "default" *> skipSpace *> takeByteString+ button = "button" $> Button+ uciok = "uciok" $> UCIOk+ readyok = "readyok" $> ReadyOK+ info = do+ "info"+ skipSpace+ Info <$> sepBy1 infoItem skipSpace+ infoItem = Depth <$> ("depth" *> skipSpace *> decimal)+ <|> SelDepth <$> ("seldepth" *> skipSpace *> decimal)+ <|> MultiPV <$> ("multipv" *> skipSpace *> decimal)+ <|> Score <$> ("score" *> skipSpace *> "cp" *> skipSpace *> signed decimal)+ <|> UpperBound <$ "upperbound"+ <|> LowerBound <$ "lowerbound"+ <|> Nodes <$> ("nodes" *> skipSpace *> decimal)+ <|> NPS <$> ("nps" *> skipSpace *> decimal)+ <|> HashFull <$> ("hashfull" *> skipSpace *> decimal)+ <|> TBHits <$> ("tbhits" *> skipSpace *> decimal)+ <|> Time <$> ("time" *> skipSpace *> decimal)+ <|> pv+ <|> CurrMove <$> ("currmove" *> skipSpace *> mv)+ <|> CurrMoveNumber <$> ("currmovenumber" *> skipSpace *> decimal)+ pv = do+ xs <- (fmap . fmap) BS.unpack $ "pv" *> skipSpace *> sepBy mv skipSpace+ PV . snd <$> foldM toMove (pos, []) xs+ toMove (pos, xs) s = do+ case fromUCI pos s of+ Just m -> pure (applyMove pos m, xs <> [m])+ Nothing -> fail $ "Failed to parse move " <> s+ mv = fmap fst $ match $ satisfy f *> satisfy r *> satisfy f *> satisfy r *> optional (satisfy p) where+ f = inRange ('a','h')+ r = inRange ('1', '8')+ p 'q' = True+ p 'r' = True+ p 'b' = True+ p 'n' = True+ p _ = False + bestmove = do+ void "bestmove"+ skipSpace+ m <- BS.unpack <$> mv+ ponder <- (fmap . fmap) BS.unpack $+ optional (skipSpace *> "ponder" *> skipSpace *> mv)+ case fromUCI pos m of+ Just m' -> case ponder of+ Nothing -> pure $ BestMove (m', Nothing)+ Just p -> case fromUCI (applyMove pos m') p of+ Just p' -> pure $ BestMove (m', (Just p'))+ Nothing -> fail $ "Failed to parse ponder move " <> p+ Nothing -> fail $ "Failed to parse best move " <> m++start :: String -> [String] -> IO (Maybe Engine)+start = start' 2000000 putStrLn++start' :: Int -> (String -> IO ()) -> String -> [String] -> IO (Maybe Engine)+start' usec outputStrLn cmd args = do+ (Just inH, Just outH, Nothing, procH) <- createProcess (proc cmd args) {+ std_in = CreatePipe, std_out = CreatePipe+ }+ hSetBuffering inH LineBuffering+ e <- Engine inH outH procH outputStrLn Nothing Nothing Nothing HashMap.empty <$>+ newEmptyMVar <*> newTChanIO <*> newTChanIO <*> newIORef (startpos, [])+ send "uci" e+ timeout usec (initialise e) >>= \case+ Just e' -> do+ tid <- forkIO . infoReader $ e'+ pure . Just $ e' { infoThread = Just tid }+ Nothing -> quit e $> Nothing++initialise :: Engine -> IO Engine+initialise c@Engine{outH, outputStrLn, game} = do+ l <- BS.hGetLine outH+ pos <- fst <$> readIORef game+ if BS.null l then initialise c else case parseOnly (command pos <* endOfInput) l of+ Left err -> do+ outputStrLn . BS.unpack $ l+ initialise c+ Right (Name n) -> initialise (c { name = Just n })+ Right (Author a) -> initialise (c { author = Just a })+ Right (Option name opt) -> initialise (c { options = HashMap.insert name opt $ options c })+ Right UCIOk -> pure c++infoReader :: Engine -> IO ()+infoReader e@Engine{..} = forever $ do+ l <- BS.hGetLine outH+ pos <- currentPosition e+ case parseOnly (command pos <* endOfInput) l of+ Left err -> do+ outputStrLn $ err <> ":" <> show l+ Right ReadyOK -> putMVar isReady ()+ Right (Info i) -> atomically $ writeTChan infoChan i+ Right (BestMove bm) -> atomically $ writeTChan bestMoveChan bm++isready :: Engine -> IO ()+isready e@Engine{isReady} = do+ send "isready" e+ takeMVar isReady+ +send :: ByteString -> Engine -> IO ()+send s Engine{inH, procH} = do+ BS.hPutStrLn inH s+ getProcessExitCode procH >>= \case+ Nothing -> pure ()+ Just ec -> throwIO ec++getOption :: ByteString -> Engine -> Maybe Option+getOption n = HashMap.lookup n . options++setOptionSpinButton :: ByteString -> Int -> Engine -> IO Engine+setOptionSpinButton n v c+ | Just (SpinButton _ minValue maxValue) <- getOption n c+ , inRange (minValue, maxValue) v+ = do+ send ("setoption name " <> n <> " value " <> BS.pack (show v)) c+ pure $ c { options = HashMap.update (set v) n $ options c }+ where+ set v opt@SpinButton{} = Just $ opt { spinButtonValue = v }++currentPosition :: Engine -> IO Position+currentPosition Engine{game} =+ uncurry (foldl' applyMove) <$> readIORef game++nextMove :: Engine -> IO Color+nextMove Engine{game} = do+ (initialPosition, history) <- readIORef game+ pure $ if even . length $ history then color initialPosition else opponent . color $ initialPosition++move :: Engine -> String -> IO ()+move e@Engine{game} san = do+ pos <- currentPosition e+ case fromSAN pos san of+ Left err -> throwIO $ SANError err+ Right m -> do+ addMove e m+ sendPosition e++addMove :: Engine -> Move -> IO ()+addMove e@Engine{game} m =+ atomicModifyIORef' game \g -> (fmap (<> [m]) g, ())++sendPosition :: Engine -> IO ()+sendPosition e@Engine{game} = do+ readIORef game >>= (flip send) e . cmd+ where+ cmd (p, h) = "position fen " <> BS.pack (toFEN p) <> line h+ line h+ | null h = ""+ | otherwise = " moves " <> BS.unwords (BS.pack . toUCI <$> h)++quit :: Engine -> IO (Maybe ExitCode)+quit = quit' 1000000++quit' :: Int -> Engine -> IO (Maybe ExitCode)+quit' usec c@Engine{procH, infoThread} = (pure . Just) `handle` do+ maybe (pure ()) killThread infoThread+ send "quit" c+ timeout usec (waitForProcess procH) >>= \case+ Just ec -> pure $ Just ec+ Nothing -> terminateProcess procH $> Nothing
− src/Game/Debug.hs
@@ -1,16 +0,0 @@-module Game.Debug where--import Data.Bits-import Data.List-import Numeric--newtype Hex a = Hex { bless :: a }-instance (Integral a, Show a) => Show (Hex a) where- show (Hex a) = showHex a ""--newtype BitBoard a = BitBoard a-instance (Bits a) => Show (BitBoard a) where- show (BitBoard v) = intercalate "\n" $ map row [7,6..0] where- row r = map (cell r) [0..7]- cell r f | testBit v (r*8+f) = 'o'- | otherwise = '.'
test/Perft.hs view
@@ -15,7 +15,7 @@ main = do start <- getCurrentTime exists <- doesFileExist "perftsuite.epd"- ok <- if exists+ result <- if exists then do suite <- readTestSuite "perftsuite.epd" runTestSuite suite@@ -23,10 +23,15 @@ for_ [0..6] $ \n -> do putStrLn $ showResult n (perft n startpos) hFlush stdout- pure False+ pure Nothing end <- getCurrentTime+ case result of+ Just PerftResult{nodes} -> putStrLn $+ "nps: " <>+ show (floor (realToFrac (fromIntegral nodes) / realToFrac (diffUTCTime end start)))+ _ -> pure () putStrLn $ "Time: " <> show (diffUTCTime end start)- exitWith $ if ok then ExitSuccess else ExitFailure 1+ exitWith $ if isJust result then ExitSuccess else ExitFailure 1 data PerftResult = PerftResult { nodes :: !Integer } deriving (Eq, Generic, Show) instance NFData PerftResult@@ -43,28 +48,28 @@ perft :: Int -> Position -> PerftResult perft 0 _ = PerftResult 1 perft 1 p = PerftResult . fromIntegral . length $- applyMove p <$> moves p-perft 2 p = fold . map (perft 1) $ applyMove p <$> moves p+ unsafeApplyMove p <$> moves p+perft 2 p = fold . map (perft 1) $ unsafeApplyMove p <$> moves p perft n p = fold . withStrategy (parList rdeepseq) . map (perft $ pred n) $- applyMove p <$> moves p+ unsafeApplyMove p <$> moves p -runTestSuite :: [(Position, [(Int, PerftResult)])] -> IO Bool-runTestSuite = fmap (all id) . traverse (uncurry test) where- test pos ((depth, expected) : more)+runTestSuite :: [(Position, [(Int, PerftResult)])] -> IO (Maybe PerftResult)+runTestSuite = fmap fold . traverse (uncurry (test mempty)) where+ test sum pos ((depth, expected) : more) | result == expected = do putStrLn $ "OK " <> fen <> " ;D" <> show depth <> " " <> show (nodes expected) hFlush stdout- test pos more+ test (sum <> result) pos more | otherwise = do putStrLn $ "FAIL " <> fen <> " ;D" <> show depth <> " " <> show (nodes expected) <> " /= " <> show (nodes result)- pure False+ pure Nothing where result = perft depth pos fen = toFEN pos- test _ [] = pure True+ test sum _ [] = pure (Just sum) readTestSuite :: FilePath -> IO [(Position, [(Int, PerftResult)])] readTestSuite fp = do