chessIO 0.2.0.0 → 0.3.0.0
raw patch · 11 files changed
+1654/−530 lines, 11 filesdep +MonadRandomdep +containersdep +extradep −splitdep ~basenew-component:exe:pgnionew-component:exe:polyplayPVP ok
version bump matches the API change (PVP)
Dependencies added: MonadRandom, containers, extra, file-embed, o-clock, optparse-applicative, prettyprinter, random, text
Dependencies removed: split
Dependency ranges changed: base
API changes (from Hackage documentation)
- Game.Chess: applyMove :: Position -> Move -> Position
- Game.Chess: data Move
- Game.Chess: instance GHC.Classes.Eq Game.Chess.Castling
- Game.Chess: instance GHC.Classes.Eq Game.Chess.Move
- Game.Chess: instance GHC.Classes.Ord Game.Chess.Castling
- Game.Chess: instance GHC.Show.Show Game.Chess.Castling
- Game.Chess: instance GHC.Show.Show Game.Chess.Move
- Game.Chess: moves :: Position -> [Move]
- Game.Chess: unsafeApplyMove :: Position -> Move -> Position
- Game.Chess.UCI: SANError :: String -> UCIException
- Game.Chess.UCI: Time :: Int -> Info
- Game.Chess.UCI: addMove :: Engine -> Move -> IO ()
- Game.Chess.UCI: move :: Engine -> String -> IO ()
- 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: tryReadBestMove :: Engine -> STM (Maybe (Move, Maybe Move))
- Game.Chess.UCI: tryReadInfo :: Engine -> STM (Maybe [Info])
+ Game.Chess: Kingside :: Castle
+ Game.Chess: Ply :: Word16 -> Ply
+ Game.Chess: Queenside :: Castle
+ Game.Chess: canCastleKingside :: Position -> Bool
+ Game.Chess: canCastleQueenside :: Position -> Bool
+ Game.Chess: castlingRights :: Position -> [(Color, Castle)]
+ Game.Chess: data Castle
+ Game.Chess: doPly :: Position -> Ply -> Position
+ Game.Chess: fromPolyglot :: Position -> Ply -> Ply
+ Game.Chess: halfMoveClock :: Position -> Int
+ Game.Chess: instance GHC.Arr.Ix Game.Chess.Castle
+ Game.Chess: instance GHC.Arr.Ix Game.Chess.Color
+ Game.Chess: instance GHC.Arr.Ix Game.Chess.PieceType
+ Game.Chess: instance GHC.Arr.Ix Game.Chess.Sq
+ Game.Chess: instance GHC.Classes.Eq Game.Chess.Castle
+ Game.Chess: instance GHC.Classes.Eq Game.Chess.Ply
+ Game.Chess: instance GHC.Classes.Eq Game.Chess.SANStatus
+ Game.Chess: instance GHC.Classes.Ord Game.Chess.Castle
+ Game.Chess: instance GHC.Classes.Ord Game.Chess.Color
+ Game.Chess: instance GHC.Classes.Ord Game.Chess.PieceType
+ Game.Chess: instance GHC.Classes.Ord Game.Chess.Sq
+ Game.Chess: instance GHC.Read.Read Game.Chess.SANStatus
+ Game.Chess: instance GHC.Show.Show Game.Chess.Castle
+ Game.Chess: instance GHC.Show.Show Game.Chess.Ply
+ Game.Chess: instance GHC.Show.Show Game.Chess.SANStatus
+ Game.Chess: instance Game.Chess.IsSquare GHC.Types.Int
+ Game.Chess: instance Game.Chess.IsSquare Game.Chess.Sq
+ Game.Chess: instance Game.Chess.SANToken GHC.Types.Char
+ Game.Chess: instance Game.Chess.SANToken GHC.Word.Word8
+ Game.Chess: legalPlies :: Position -> [Ply]
+ Game.Chess: moveNumber :: Position -> Int
+ Game.Chess: newtype Ply
+ Game.Chess: relaxedSAN :: (Stream s, SANToken (Token s), IsString (Tokens s)) => Position -> Parser s Ply
+ Game.Chess: strictSAN :: forall s. (Stream s, SANToken (Token s), IsString (Tokens s)) => Position -> Parser s Ply
+ Game.Chess: unsafeDoPly :: Position -> Ply -> Position
+ Game.Chess.PGN: Draw :: Outcome
+ Game.Chess.PGN: PGN :: [Game] -> PGN
+ Game.Chess.PGN: Undecided :: Outcome
+ Game.Chess.PGN: Win :: Color -> Outcome
+ Game.Chess.PGN: breadthFirst :: RAVOrder a
+ Game.Chess.PGN: data Outcome
+ Game.Chess.PGN: depthFirst :: RAVOrder a
+ Game.Chess.PGN: gameDoc :: RAVOrder (Doc ann) -> Game -> Doc ann
+ Game.Chess.PGN: gameFromForest :: [(ByteString, Text)] -> Forest Ply -> Outcome -> Game
+ Game.Chess.PGN: hPutPGN :: Handle -> RAVOrder (Doc ann) -> PGN -> IO ()
+ Game.Chess.PGN: instance Data.Text.Prettyprint.Doc.Internal.Pretty Game.Chess.PGN.Outcome
+ Game.Chess.PGN: instance GHC.Base.Monoid Game.Chess.PGN.PGN
+ Game.Chess.PGN: instance GHC.Base.Semigroup Game.Chess.PGN.PGN
+ Game.Chess.PGN: instance GHC.Classes.Eq Game.Chess.PGN.Outcome
+ Game.Chess.PGN: instance GHC.Classes.Eq Game.Chess.PGN.PGN
+ Game.Chess.PGN: instance GHC.Classes.Eq Game.Chess.PGN.PlyData
+ Game.Chess.PGN: instance GHC.Show.Show Game.Chess.PGN.Outcome
+ Game.Chess.PGN: instance GHC.Show.Show Game.Chess.PGN.PlyData
+ Game.Chess.PGN: newtype PGN
+ Game.Chess.PGN: pgnDoc :: RAVOrder (Doc ann) -> PGN -> Doc ann
+ Game.Chess.PGN: readPGNFile :: FilePath -> IO (Either String PGN)
+ Game.Chess.PGN: type Game = ([(ByteString, Text)], (Outcome, Forest PlyData))
+ Game.Chess.PGN: type RAVOrder a = (Forest PlyData -> a) -> Forest PlyData -> [a]
+ Game.Chess.Polyglot.Book: bookForest :: PolyglotBook -> Position -> Forest Ply
+ Game.Chess.Polyglot.Book: bookPlies :: PolyglotBook -> Position -> [Ply]
+ Game.Chess.Polyglot.Book: bookPly :: RandomGen g => PolyglotBook -> Position -> Maybe (Rand g Ply)
+ Game.Chess.Polyglot.Book: data PolyglotBook
+ Game.Chess.Polyglot.Book: defaultBook :: PolyglotBook
+ Game.Chess.Polyglot.Book: fromByteString :: ByteString -> PolyglotBook
+ Game.Chess.Polyglot.Book: instance Foreign.Storable.Storable Game.Chess.Polyglot.Book.BookEntry
+ Game.Chess.Polyglot.Book: instance GHC.Classes.Eq Game.Chess.Polyglot.Book.BookEntry
+ Game.Chess.Polyglot.Book: instance GHC.Show.Show Game.Chess.Polyglot.Book.BookEntry
+ Game.Chess.Polyglot.Book: readPolyglotFile :: FilePath -> IO PolyglotBook
+ Game.Chess.Polyglot.Book: twic :: PolyglotBook
+ Game.Chess.Polyglot.Hash: castleKey :: (Color, Castle) -> Word64
+ Game.Chess.Polyglot.Hash: hashPosition :: Position -> Word64
+ Game.Chess.Polyglot.Hash: pieceKey :: (PieceType, Color, Sq) -> Word64
+ Game.Chess.Polyglot.Hash: turnKey :: Word64
+ Game.Chess.UCI: CentiPawns :: Int -> Score
+ Game.Chess.UCI: Elapsed :: Time Millisecond -> Info
+ Game.Chess.UCI: MateIn :: Int -> Score
+ Game.Chess.UCI: OString :: ByteString -> Option
+ Game.Chess.UCI: addPly :: MonadIO m => Engine -> Ply -> m ()
+ Game.Chess.UCI: data Bounds
+ Game.Chess.UCI: data Score
+ Game.Chess.UCI: data SearchParam
+ Game.Chess.UCI: depth :: Natural -> SearchParam
+ Game.Chess.UCI: infinite :: SearchParam
+ Game.Chess.UCI: instance GHC.Classes.Eq Game.Chess.UCI.Bounds
+ Game.Chess.UCI: instance GHC.Classes.Eq Game.Chess.UCI.Info
+ Game.Chess.UCI: instance GHC.Classes.Eq Game.Chess.UCI.Score
+ Game.Chess.UCI: instance GHC.Classes.Eq Game.Chess.UCI.SearchParam
+ Game.Chess.UCI: instance GHC.Classes.Ord Game.Chess.UCI.Score
+ Game.Chess.UCI: instance GHC.Show.Show Game.Chess.UCI.Bounds
+ Game.Chess.UCI: instance GHC.Show.Show Game.Chess.UCI.Score
+ Game.Chess.UCI: instance GHC.Show.Show Game.Chess.UCI.SearchParam
+ Game.Chess.UCI: isready :: Engine -> IO ()
+ Game.Chess.UCI: movestogo :: Natural -> SearchParam
+ Game.Chess.UCI: movetime :: KnownDivRat unit Millisecond => Time unit -> SearchParam
+ Game.Chess.UCI: nodes :: Natural -> SearchParam
+ Game.Chess.UCI: search :: MonadIO m => Engine -> [SearchParam] -> m (TChan (Ply, Maybe Ply), TChan [Info])
+ Game.Chess.UCI: searching :: MonadIO m => Engine -> m Bool
+ Game.Chess.UCI: searchmoves :: [Ply] -> SearchParam
+ Game.Chess.UCI: setOptionString :: MonadIO m => ByteString -> ByteString -> Engine -> m Engine
+ Game.Chess.UCI: stop :: MonadIO m => Engine -> m ()
+ Game.Chess.UCI: timeincrement :: KnownDivRat unit Millisecond => Color -> Time unit -> SearchParam
+ Game.Chess.UCI: timeleft :: KnownDivRat unit Millisecond => Color -> Time unit -> SearchParam
- Game.Chess: fromSAN :: Position -> String -> Either String Move
+ Game.Chess: fromSAN :: (Stream s, SANToken (Token s), IsString (Tokens s)) => Position -> s -> Either String Ply
- Game.Chess: fromUCI :: Position -> String -> Maybe Move
+ Game.Chess: fromUCI :: Position -> String -> Maybe Ply
- Game.Chess: isDark :: Sq -> Bool
+ Game.Chess: isDark :: IsSquare sq => sq -> Bool
- Game.Chess: isLight :: Sq -> Bool
+ Game.Chess: isLight :: IsSquare sq => sq -> Bool
- Game.Chess: pieceAt :: Position -> Sq -> Maybe (Color, PieceType)
+ Game.Chess: pieceAt :: IsSquare sq => Position -> sq -> Maybe (Color, PieceType)
- Game.Chess: toSAN :: Position -> Move -> String
+ Game.Chess: toSAN :: Position -> Ply -> String
- Game.Chess: toUCI :: Move -> String
+ Game.Chess: toUCI :: Ply -> String
- Game.Chess: unsafeToSAN :: Position -> Move -> String
+ Game.Chess: unsafeToSAN :: Position -> Ply -> String
- Game.Chess.UCI: CurrMove :: ByteString -> Info
+ Game.Chess.UCI: CurrMove :: Ply -> Info
- Game.Chess.UCI: IllegalMove :: Move -> UCIException
+ Game.Chess.UCI: IllegalMove :: Ply -> UCIException
- Game.Chess.UCI: LowerBound :: Info
+ Game.Chess.UCI: LowerBound :: Bounds
- Game.Chess.UCI: PV :: [Move] -> Info
+ Game.Chess.UCI: PV :: [Ply] -> Info
- Game.Chess.UCI: Score :: Int -> Info
+ Game.Chess.UCI: Score :: Score -> Maybe Bounds -> Info
- Game.Chess.UCI: String :: ByteString -> Option
+ Game.Chess.UCI: String :: ByteString -> Info
- Game.Chess.UCI: UpperBound :: Info
+ Game.Chess.UCI: UpperBound :: Bounds
- Game.Chess.UCI: currentPosition :: Engine -> IO Position
+ Game.Chess.UCI: currentPosition :: MonadIO m => Engine -> m Position
- Game.Chess.UCI: quit :: Engine -> IO (Maybe ExitCode)
+ Game.Chess.UCI: quit :: MonadIO m => Engine -> m (Maybe ExitCode)
- Game.Chess.UCI: quit' :: Int -> Engine -> IO (Maybe ExitCode)
+ Game.Chess.UCI: quit' :: (KnownDivRat unit Microsecond, MonadIO m) => Time unit -> Engine -> m (Maybe ExitCode)
- Game.Chess.UCI: setOptionSpinButton :: ByteString -> Int -> Engine -> IO Engine
+ Game.Chess.UCI: setOptionSpinButton :: MonadIO m => ByteString -> Int -> Engine -> m Engine
- Game.Chess.UCI: setPosition :: Engine -> Position -> IO ()
+ Game.Chess.UCI: setPosition :: MonadIO m => Engine -> Position -> m (Position, [Ply])
- Game.Chess.UCI: start' :: Int -> (String -> IO ()) -> String -> [String] -> IO (Maybe Engine)
+ Game.Chess.UCI: start' :: KnownDivRat unit Microsecond => Time unit -> (String -> IO ()) -> String -> [String] -> IO (Maybe Engine)
Files
- app/cboard.hs +142/−40
- app/pgnio.hs +18/−0
- app/polyplay.hs +133/−0
- book/twic-9g.bin too large to diff
- chessIO.cabal +97/−14
- src/Game/Chess.hs +496/−324
- src/Game/Chess/PGN.hs +204/−0
- src/Game/Chess/Polyglot/Book.hs +114/−0
- src/Game/Chess/Polyglot/Hash.hs +234/−0
- src/Game/Chess/UCI.hs +208/−146
- test/Perft.hs +8/−6
app/cboard.hs view
@@ -1,25 +1,32 @@ module Main where +import Control.Arrow ((&&&)) import Control.Concurrent import Control.Concurrent.STM-import Control.Exception import Control.Monad+import Control.Monad.Extra hiding (loop) import Control.Monad.IO.Class+import Control.Monad.Random import Control.Monad.State.Strict+import qualified Data.ByteString.Char8 as BS import Data.Char import Data.IORef import Data.List-import Data.List.Split+import Data.List.Extra import Game.Chess-import qualified Game.Chess.UCI as UCI+import Game.Chess.Polyglot.Book (PolyglotBook, defaultBook, readPolyglotFile, bookPlies, bookPly)+import Game.Chess.UCI import System.Console.Haskeline hiding (catch, handle) import System.Exit import System.Environment+import System.Random+import Time.Units data S = S {- engine :: UCI.Engine+ engine :: Engine , mover :: Maybe ThreadId-, hintRef :: IORef (Maybe Move)+, book :: PolyglotBook+, hintRef :: IORef (Maybe Ply) } main :: IO ()@@ -27,20 +34,20 @@ [] -> do putStrLn "Please specify a UCI engine at the command line" exitWith $ ExitFailure 1- (cmd:args) -> UCI.start cmd args >>= \case+ (cmd:args) -> 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+ s <- S e Nothing defaultBook <$> newIORef Nothing runInputT (setComplete (completeSAN e) defaultSettings) chessIO `evalStateT` s exitSuccess -completeSAN :: MonadIO m => UCI.Engine -> CompletionFunc m+completeSAN :: MonadIO m => Engine -> CompletionFunc m completeSAN e = completeWord Nothing "" $ \w -> fmap (map mkCompletion . filter (w `isPrefixOf`)) $ do- pos <- liftIO $ UCI.currentPosition e- pure $ unsafeToSAN pos <$> moves pos+ pos <- currentPosition e+ pure $ unsafeToSAN pos <$> legalPlies pos where mkCompletion s = (simpleCompletion s) { isFinished = False } @@ -51,24 +58,36 @@ , "Enter a FEN string to set the starting position." , "To make a move, enter a SAN or UCI string." , "Type \"hint\" to ask for a suggestion."+ , "Type \"pass\" to let the engine make the next move, \"stop\" to end the search." , "Empty input will redraw the board." , "Hit Ctrl-D to quit." , "" ]- 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+ lift (gets engine) >>= void . quit +midgame :: InputT (StateT S IO) ()+midgame = do+ e <- lift $ gets engine+ b <- lift $ gets book+ pos <- currentPosition e+ case bookPly b pos of+ Just r -> do+ pl <- liftIO . evalRandIO $ r+ addPly e pl+ (bmc, _) <- search e [movetime (ms 100)]+ liftIO $ do+ (bm, _) <- atomically . readTChan $ bmc+ addPly e bm+ midgame+ Nothing -> outputBoard+ outputBoard :: InputT (StateT S IO) () outputBoard = do e <- lift $ gets engine liftIO $ do- pos <- UCI.currentPosition e+ pos <- currentPosition e printBoard putStrLn pos loop :: InputT (StateT S IO) ()@@ -79,22 +98,102 @@ Just input | null input -> outputBoard *> loop | Just position <- fromFEN input -> do- liftIO $ UCI.setPosition e position- outputBoard *> loop- | input == "hint" -> do+ void $ setPosition e position+ outputBoard+ loop+ | "hint" == input -> do lift (gets hintRef) >>= liftIO . readIORef >>= \case Just hint -> do- pos <- liftIO $ UCI.currentPosition e+ pos <- currentPosition e outputStrLn $ "Try " <> toSAN pos hint Nothing -> outputStrLn "Sorry, no hint available" loop+ | "pass" == input -> do+ unlessM (searching e) $ do+ (bmc, _) <- search e [movetime (sec 2)]+ hr <- lift $ gets hintRef+ externalPrint <- getExternalPrint+ tid <- liftIO . forkIO $ doBestMove externalPrint hr bmc e+ lift $ modify' $ \s -> s { mover = Just tid }+ loop+ | input `elem` ["analyze", "analyse"] -> do+ unlessM (searching e) $ do+ pos <- currentPosition e+ (bmc, ic) <- search e [infinite]+ externalPrint <- getExternalPrint+ itid <- liftIO . forkIO . forever $ do+ info <- atomically . readTChan $ ic+ case (find isScore &&& find isPV) info of+ (Just (Score s Nothing), Just (PV pv)) ->+ externalPrint $ show s <> ": " <> varToString pos pv+ _ -> pure ()+ tid <- liftIO . forkIO $ do+ (bm, ponder) <- atomically . readTChan $ bmc+ killThread itid+ pos <- currentPosition e+ externalPrint $ "Best move: " <> toSAN pos bm+ lift $ modify' $ \s -> s { mover = Just tid }+ loop+ | "stop" == input -> do+ stop e+ loop+ | ["polyglot", file] <- words input -> do+ b <- liftIO $ readPolyglotFile file+ lift $ modify $ \x -> x { book = b }+ loop+ | "book" == input -> do+ b <- lift $ gets book+ pos <- currentPosition e+ let plies = bookPlies b pos+ if not . null $ plies+ then do+ addPly e (head plies)+ outputBoard+ (bmc, _) <- search e [movetime (sec 1)]+ hr <- lift $ gets hintRef+ externalPrint <- getExternalPrint+ tid <- liftIO . forkIO $ doBestMove externalPrint hr bmc e+ lift $ modify' $ \s -> s { mover = Just tid }+ else pure ()+ loop+ | "midgame" == input -> do+ void $ setPosition e startpos+ midgame+ loop | otherwise -> do- liftIO $ printUCIException `handle` do- UCI.move e input- UCI.send "go movetime 1000" e- outputBoard+ pos <- currentPosition e+ case parseMove pos input of+ Left err -> outputStrLn err+ Right m -> ifM (searching e) (outputStrLn "Not your move") $ do+ addPly e m+ outputBoard+ (bmc, _) <- search e [movetime (sec 1)]+ hr <- lift $ gets hintRef+ externalPrint <- getExternalPrint+ tid <- liftIO . forkIO $ doBestMove externalPrint hr bmc e+ lift $ modify' $ \s -> s { mover = Just tid } loop +varToString :: Position -> [Ply] -> String+varToString _ [] = ""+varToString pos ms+ | color pos == Black && length ms == 1+ = show (moveNumber pos) <> "..." <> toSAN pos (head ms)+ | color pos == Black+ = show (moveNumber pos) <> "..." <> toSAN pos (head ms) <> " " <> fromWhite (doPly pos (head ms)) (tail ms)+ | otherwise+ = fromWhite pos ms+ where+ fromWhite pos = unwords . concat+ . zipWith f [moveNumber pos ..] . chunksOf 2 . snd+ . mapAccumL (curry (uncurry doPly &&& uncurry toSAN)) pos+ f n (x:xs) = (show n <> "." <> x):xs++parseMove :: Position -> String -> Either String Ply+parseMove pos s = case fromUCI pos s of+ Just m -> pure m+ Nothing -> fromSAN pos s+ printBoard :: (String -> IO ()) -> Position -> IO () printBoard externalPrint pos = externalPrint . init . unlines $ (map . map) pc (reverse $ chunksOf 8 [A1 .. H8])@@ -115,25 +214,28 @@ 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- pos <- UCI.currentPosition e+doBestMove :: (String -> IO ())+ -> IORef (Maybe Ply)+ -> TChan (Ply, Maybe Ply)+ -> Engine+ -> IO ()+doBestMove externalPrint hintRef bmc e = do+ (bm, ponder) <- atomically . readTChan $ bmc+ pos <- currentPosition e externalPrint $ "< " <> toSAN pos bm- UCI.addMove e bm- UCI.currentPosition e >>= printBoard externalPrint+ addPly e bm+ currentPosition e >>= printBoard externalPrint writeIORef hintRef ponder -printPV :: (String -> IO ()) -> UCI.Engine -> IO ()-printPV externalPrint engine = forever $ do- info <- atomically . UCI.readInfo $ engine+printPV :: (String -> IO ()) -> TChan [Info] -> IO ()+printPV externalPrint ic = forever $ do+ info <- atomically . readTChan $ ic case find isPV info of Just pv -> externalPrint $ show pv Nothing -> pure ()- where- isPV UCI.PV{} = True- isPV _ = False -printUCIException :: UCI.UCIException -> IO ()-printUCIException (UCI.SANError e) = putStrLn e-printUCIException (UCI.IllegalMove m) = putStrLn $ "Illegal move: " <> show m+isPV, isScore :: Info -> Bool+isPV PV{} = True+isPV _ = False+isScore Score{} = True+isScore _ = False
+ app/pgnio.hs view
@@ -0,0 +1,18 @@+module Main where++import Game.Chess.PGN+import System.Environment+import System.Exit+import System.IO+import Text.Megaparsec++main :: IO ()+main = getArgs >>= \case+ [fp] -> readPGNFile fp >>= \case+ Right pgn -> do+ hPutPGN stdout breadthFirst pgn+ exitSuccess+ Left err -> do+ hPutStr stderr err+ exitWith (ExitFailure 1)+ _ -> hPutStrLn stderr "Specify PGN file as argument"
+ app/polyplay.hs view
@@ -0,0 +1,133 @@+module Main where++import Control.Concurrent+import Control.Concurrent.STM+import Control.Monad+import Control.Monad.Random+import Data.IORef+import Data.List+import Data.String+import Data.Text.Prettyprint.Doc.Render.Text+import Data.Time.Clock+import Data.Tree+import Game.Chess+import Game.Chess.PGN+import Game.Chess.Polyglot.Book+import Game.Chess.UCI+import Options.Applicative+import System.Environment+import Time.Units++data Clock = Clock !Color !NominalDiffTime !NominalDiffTime !UTCTime++newClock :: Int -> IO Clock+newClock s = Clock White (fromIntegral s') (fromIntegral s') <$!> getCurrentTime+ where+ s' = s `div` 2++flipClock :: Clock -> IO Clock+flipClock clock = upd clock <$!> getCurrentTime where+ upd (Clock White w b t) t' = Clock Black (w - (t' `diffUTCTime` t)) b t'+ upd (Clock Black w b t) t' = Clock White w (b - (t' `diffUTCTime` t)) t'++clockRemaining :: Clock -> Color -> IO (Maybe (Time Millisecond))+clockRemaining (Clock c w b t) c'+ | c == c' = case c of+ White -> (\t' -> f $ w - (t' `diffUTCTime` t)) <$!> getCurrentTime+ Black -> (\t' -> f $ b - (t' `diffUTCTime` t)) <$!> getCurrentTime+ | otherwise = pure $ case c' of+ White -> f w+ Black -> f b+ where+ f x | x <= 0 = Nothing+ | otherwise = Just . ms . fromRational . toRational $ x * 1000++clockTimes :: Clock -> (Maybe (Time Millisecond), Maybe (Time Millisecond))+clockTimes (Clock _ w b _) = (f w, f b) where+ f x = if x <= 0 then Nothing else Just . ms . fromRational . toRational $ x * 1000++data Polyplay = Polyplay {+ hashSize :: Int+, threadCount :: Int+, tbPath :: Maybe FilePath+, timeControl :: Int+, bookFile :: FilePath+, engineProgram :: FilePath+, engineArgs :: [String]+}++opts :: Parser Polyplay+opts = Polyplay <$> option auto (long "hash" <> metavar "MB" <> value 1024)+ <*> option auto (long "threads" <> metavar "N" <> value 1)+ <*> optional (strOption $ long "tbpath" <> metavar "PATH")+ <*> option auto (long "time" <> metavar "SECONDS" <> value 600)+ <*> argument str (metavar "BOOK")+ <*> argument str (metavar "ENGINE")+ <*> many (argument str (metavar "ARG"))++main :: IO ()+main = run =<< execParser (info (opts <**> helper) mempty)++run :: Polyplay -> IO ()+run Polyplay{..} = do+ b <- readPolyglotFile bookFile+ start engineProgram engineArgs >>= \case+ Nothing -> putStrLn "Engine failed to start."+ Just e -> do+ _ <- setOptionSpinButton "Hash" hashSize e+ _ <- setOptionSpinButton "Threads" threadCount e+ case tbPath of+ Just fp -> void $ setOptionString "SyzygyPath" (fromString fp) e+ Nothing -> pure ()+ isready e+ (h, o) <- play b e =<< newClock timeControl+ let g = gameFromForest [ ("White", "Stockfish")+ , ("Black", "Stockfish")+ ] (toForest h) o+ putDoc (gameDoc breadthFirst g)+ pure ()++play :: PolyglotBook -> Engine -> Clock -> IO ([Ply], Outcome)+play b e !c = do+ pos <- currentPosition e+ case legalPlies pos of+ [] -> lost e+ _ -> case bookPly b pos of+ Nothing -> do+ let (Just wt, Just bt) = clockTimes c+ (bmc, ic) <- search e [timeleft White wt, timeleft Black bt]+ sc <- newIORef Nothing+ itid <- liftIO . forkIO . forever $ do+ info <- atomically . readTChan $ ic+ case find isScore info of+ Just (Score s Nothing) -> writeIORef sc (Just s)+ _ -> pure ()+ (bm, _) <- atomically . readTChan $ bmc+ killThread itid+ c' <- flipClock c+ clockRemaining c' (color pos) >>= \case+ Nothing -> lost e+ Just _ -> do+ addPly e bm+ s <- readIORef sc+ putStrLn $ toSAN pos bm <> " " <> show s+ play b e c'+ Just r -> do+ pl <- evalRandIO r+ putStrLn $ toSAN pos pl+ addPly e pl+ play b e =<< flipClock c++lost :: Engine -> IO ([Ply], Outcome)+lost e = do+ pos <- currentPosition e+ (_, h) <- setPosition e startpos+ pure (h, Win . opponent . color $ pos)++toForest :: [Ply] -> Forest Ply+toForest [] = []+toForest (x:xs) = [Node x $ toForest xs]++isScore :: Info -> Bool+isScore Score{} = True+isScore _ = False
+ book/twic-9g.bin view
file too large to diff
chessIO.cabal view
@@ -4,12 +4,12 @@ -- -- see: https://github.com/sol/hpack ----- hash: 599aad9935d6d4f50f3cc7799f7a98a7dda1a711826adbe805ab542e16f0affe+-- hash: dab466e43ec14aa8ef9e0f22f18879e7779c277908f0bf0c2acbfd77da80d0f3 name: chessIO-version: 0.2.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.+version: 0.3.0.0+synopsis: Basic chess 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, a PGN parser/pretty printer, and Polyglot opening book support. 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@@ -21,6 +21,7 @@ build-type: Simple extra-source-files: README.rst+ book/twic-9g.bin source-repository head type: git@@ -29,21 +30,31 @@ library exposed-modules: Game.Chess+ Game.Chess.PGN+ Game.Chess.Polyglot.Book+ Game.Chess.Polyglot.Hash Game.Chess.UCI other-modules: Paths_chessIO hs-source-dirs: src- default-extensions: BangPatterns BinaryLiterals BlockArguments DeriveGeneric LambdaCase NamedFieldPuns NumericUnderscores RecordWildCards OverloadedStrings TupleSections ViewPatterns+ default-extensions: BangPatterns BinaryLiterals BlockArguments DeriveGeneric FlexibleContexts GeneralizedNewtypeDeriving LambdaCase MultiParamTypeClasses NamedFieldPuns NumericUnderscores RecordWildCards OverloadedStrings TupleSections TypeApplications TypeFamilies ViewPatterns ghc-options: -Wall -O2 build-depends:- attoparsec- , base >=4.8 && <5+ MonadRandom+ , attoparsec+ , base >=4.10 && <5 , bytestring+ , containers+ , file-embed , megaparsec+ , o-clock , parser-combinators+ , prettyprinter , process+ , random , stm+ , text , unordered-containers , vector default-language: Haskell2010@@ -54,24 +65,89 @@ Paths_chessIO hs-source-dirs: app- default-extensions: BangPatterns BinaryLiterals BlockArguments DeriveGeneric LambdaCase NamedFieldPuns NumericUnderscores RecordWildCards OverloadedStrings TupleSections ViewPatterns+ default-extensions: BangPatterns BinaryLiterals BlockArguments DeriveGeneric FlexibleContexts GeneralizedNewtypeDeriving LambdaCase MultiParamTypeClasses NamedFieldPuns NumericUnderscores RecordWildCards OverloadedStrings TupleSections TypeApplications TypeFamilies ViewPatterns ghc-options: -Wall -O2 -threaded build-depends:- attoparsec- , base >=4.8 && <5+ MonadRandom+ , attoparsec+ , base >=4.10 && <5 , bytestring , chessIO+ , containers+ , extra+ , file-embed , haskeline , megaparsec , mtl+ , o-clock , parser-combinators+ , prettyprinter , process- , split+ , random , stm+ , text , unordered-containers , vector default-language: Haskell2010 +executable pgnio+ main-is: pgnio.hs+ other-modules:+ Paths_chessIO+ hs-source-dirs:+ app+ default-extensions: BangPatterns BinaryLiterals BlockArguments DeriveGeneric FlexibleContexts GeneralizedNewtypeDeriving LambdaCase MultiParamTypeClasses NamedFieldPuns NumericUnderscores RecordWildCards OverloadedStrings TupleSections TypeApplications TypeFamilies ViewPatterns+ ghc-options: -Wall -O2+ build-depends:+ MonadRandom+ , attoparsec+ , base >=4.10 && <5+ , bytestring+ , chessIO+ , containers+ , file-embed+ , megaparsec+ , o-clock+ , parser-combinators+ , prettyprinter+ , process+ , random+ , stm+ , text+ , unordered-containers+ , vector+ default-language: Haskell2010++executable polyplay+ main-is: polyplay.hs+ other-modules:+ Paths_chessIO+ hs-source-dirs:+ app+ default-extensions: BangPatterns BinaryLiterals BlockArguments DeriveGeneric FlexibleContexts GeneralizedNewtypeDeriving LambdaCase MultiParamTypeClasses NamedFieldPuns NumericUnderscores RecordWildCards OverloadedStrings TupleSections TypeApplications TypeFamilies ViewPatterns+ ghc-options: -Wall -O2+ build-depends:+ MonadRandom+ , attoparsec+ , base >=4.10 && <5+ , bytestring+ , chessIO+ , containers+ , file-embed+ , megaparsec+ , o-clock+ , optparse-applicative+ , parser-combinators+ , prettyprinter+ , process+ , random+ , stm+ , text+ , time+ , unordered-containers+ , vector+ default-language: Haskell2010+ test-suite perft type: exitcode-stdio-1.0 main-is: Perft.hs@@ -79,19 +155,26 @@ Paths_chessIO hs-source-dirs: test- default-extensions: BangPatterns BinaryLiterals BlockArguments DeriveGeneric LambdaCase NamedFieldPuns NumericUnderscores RecordWildCards OverloadedStrings TupleSections ViewPatterns+ default-extensions: BangPatterns BinaryLiterals BlockArguments DeriveGeneric FlexibleContexts GeneralizedNewtypeDeriving LambdaCase MultiParamTypeClasses NamedFieldPuns NumericUnderscores RecordWildCards OverloadedStrings TupleSections TypeApplications TypeFamilies ViewPatterns ghc-options: -Wall -O2 -threaded -rtsopts "-with-rtsopts=-N -s" build-depends:- attoparsec- , base >=4.8 && <5+ MonadRandom+ , attoparsec+ , base >=4.10 && <5 , bytestring , chessIO+ , containers , directory+ , file-embed , megaparsec+ , o-clock , parallel , parser-combinators+ , prettyprinter , process+ , random , stm+ , text , time , unordered-containers , vector
src/Game/Chess.hs view
@@ -1,3 +1,4 @@+{-# LANGUAGE PolyKinds, FlexibleInstances, TypeSynonymInstances, GADTs, ScopedTypeVariables #-} {-| Module : Game.Chess Description : Basic data types and functions related to the game of chess@@ -18,121 +19,263 @@ -- * Chess positions Color(..), opponent , Sq(..), isLight, isDark-, PieceType(..)-, Position, startpos, color, pieceAt, inCheck+, PieceType(..), Castle(..)+, Position, startpos, color, moveNumber, halfMoveClock, pieceAt, inCheck+, castlingRights, canCastleKingside, canCastleQueenside -- ** Converting from/to Forsyth-Edwards-Notation , fromFEN, toFEN -- * Chess moves-, Move+, Ply(..) -- ** Converting from/to algebraic notation-, fromSAN, toSAN, unsafeToSAN, fromUCI, toUCI+, strictSAN, relaxedSAN, fromSAN, toSAN, unsafeToSAN, fromUCI, toUCI, fromPolyglot -- ** Move generation-, moves+, legalPlies -- ** Executing moves-, applyMove, unsafeApplyMove+, doPly, unsafeDoPly ) where +import Control.Applicative import Control.Applicative.Combinators import Data.Bits+import qualified Data.ByteString as Strict (ByteString)+import qualified Data.ByteString.Lazy as Lazy (ByteString) import Data.Char import Data.Functor (($>)) import Data.Ix import Data.List import Data.Maybe+import Data.Ord (Down(..))+import Data.Proxy+import Data.String+import qualified Data.Text as Strict (Text)+import qualified Data.Text.Lazy as Lazy (Text) 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+import Text.Read (readMaybe)+import Data.Tree -type Parser = Parsec Void String+positionTree :: Position -> Tree Position+positionTree pos = Node pos $ positionForest pos+positionForest :: Position -> Forest Position+positionForest pos = positionTree . unsafeDoPly pos <$> legalPlies pos+plyForest :: Position -> Forest Ply+plyForest pos = plyTree pos <$> legalPlies pos+plyTree :: Position -> Ply -> Tree Ply+plyTree pos ply = Node ply . plyForest $ unsafeDoPly pos ply +type Parser s = Parsec Void s++castling :: (Stream s, IsString (Tokens s))+ => Position -> Parser s Ply+castling pos+ | ccks && ccqs = queenside <|> kingside+ | ccks = kingside+ | ccqs = queenside+ | otherwise = empty+ where+ ccks = canCastleKingside pos+ ccqs = canCastleQueenside pos+ kingside = chunk "O-O" $> castleMove Kingside+ queenside = chunk "O-O-O" $> castleMove Queenside+ castleMove Kingside | color pos == White = wKscm+ | otherwise = bKscm+ castleMove Queenside | color pos == White = wQscm+ | otherwise = bQscm+ 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+capturing :: Position -> Ply -> Maybe PieceType+capturing pos@Position{flags} (unpack -> (_, to, _))+ | (flags .&. epMask) `testBit` to = Just Pawn+ | otherwise = snd <$> pieceAt pos to -fromSAN :: Position -> String -> Either String Move-fromSAN Position{color = White, flags} s- | s `elem` ["O-O", "0-0"] && flags `testMask` crwKs = Right wKscm-fromSAN Position{color = Black, flags} s- | s `elem` ["O-O", "0-0"] && flags `testMask` crbKs = Right bKscm-fromSAN Position{color = White, flags} s- | s `elem` ["O-O-O", "0-0-0"] && flags `testMask` crwQs = Right wQscm-fromSAN Position{color = Black, flags} s- | s `elem` ["O-O-O", "0-0-0"] && flags `testMask` crbQs = Right bQscm-fromSAN pos s = case parse san "" s of- Right (pc, from, _, to, promo, _) ->- case ms pc from to promo of- [m] -> Right m- [] -> Left "Illegal move"- _ -> Left "Ambiguous move"- Left err -> Left $ errorBundlePretty err+isCapture :: Position -> Ply -> Bool+isCapture pos = isJust . capturing pos++data SANStatus = Check | Checkmate deriving (Eq, Read, Show)++class SANToken a where+ sanPieceToken :: a -> Maybe PieceType+ fileToken :: a -> Maybe Int+ rankToken :: a -> Maybe Int+ promotionPieceToken :: a -> Maybe PieceType+ statusToken :: a -> Maybe SANStatus++sanPiece :: (Stream s, SANToken (Token s)) => Parser s PieceType+sanPiece = token sanPieceToken mempty <?> "piece"++fileP, rankP, squareP :: (Stream s, SANToken (Token s)) => Parser s Int+fileP = token fileToken mempty <?> "file"+rankP = token rankToken mempty <?> "rank"+squareP = liftA2 (\f r -> r*8+f) fileP rankP <?> "square"++promotionPiece :: (Stream s, SANToken (Token s)) => Parser s PieceType+promotionPiece = token promotionPieceToken mempty <?> "Q, R, B, N"++sanStatus :: (Stream s, SANToken (Token s)) => Parser s SANStatus+sanStatus = token statusToken mempty <?> "+, #"++instance SANToken Char where+ sanPieceToken = \case+ 'N' -> Just Knight+ 'B' -> Just Bishop+ 'R' -> Just Rook+ 'Q' -> Just Queen+ 'K' -> Just King+ _ -> Nothing+ fileToken c | c >= 'a' && c <= 'h' = Just $ ord c - ord 'a'+ | otherwise = Nothing+ rankToken c | c >= '1' && c <= '8' = Just $ ord c - ord '1'+ | otherwise = Nothing+ promotionPieceToken = \case+ 'N' -> Just Knight+ 'B' -> Just Bishop+ 'R' -> Just Rook+ 'Q' -> Just Queen+ _ -> Nothing+ statusToken = \case+ '+' -> Just Check+ '#' -> Just Checkmate+ _ -> Nothing++instance SANToken Word8 where+ sanPieceToken = \case+ 78 -> Just Knight+ 66 -> Just Bishop+ 82 -> Just Rook+ 81 -> Just Queen+ 75 -> Just King+ _ -> Nothing+ rankToken c | c >= 49 && c <= 56 = Just . fromIntegral $ c - 49+ | otherwise = Nothing+ fileToken c | c >= 97 && c <= 104 = Just . fromIntegral $ c - 97+ | otherwise = Nothing+ promotionPieceToken = \case+ 78 -> Just Knight+ 66 -> Just Bishop+ 82 -> Just Rook+ 81 -> Just Queen+ _ -> Nothing+ statusToken = \case+ 43 -> Just Check+ 35 -> Just Checkmate+ _ -> Nothing++strictSAN :: forall s. (Stream s, SANToken (Token s), IsString (Tokens s))+ => Position -> Parser s Ply+strictSAN pos = case legalPlies pos of+ [] -> fail "No legal moves in this position"+ ms -> (castling pos <|> normal ms) >>= checkStatus where- ms pc from to prm = filter (f from) $ moves pos where- f (Just (Square from)) (unpack -> (from', to', prm')) =- pAt from' == pc && from' == from && to' == to && prm' == prm- f (Just (File ff)) (unpack -> (from', to', prm')) =- pAt from' == pc && from' `mod` 8 == ff && to == to' && prm == prm'- f (Just (Rank fr)) (unpack -> (from', to', prm')) =- pAt from' == pc && from' `div` 8 == fr && to == to' && prm == prm'- f Nothing (unpack -> (from', to', prm')) =- pAt from' == pc && to == to' && prm == prm'- pAt = snd . fromJust . pieceAt pos . toEnum+ normal ms = do+ p <- sanPiece <|> pure Pawn+ case filter (pieceFrom p) ms of+ [] -> fail $ show (color pos) <> " has no " <> show p <> " which could be moved"+ ms' -> target p ms'+ pieceFrom p (moveFrom -> from) = p == snd (fromJust (pieceAt pos from))+ moveFrom (unpack -> (from, _, _)) = from+ target p ms = coords p ms >>= \m@(unpack -> (_, to, _)) -> case p of+ Pawn | lastRank to -> promoteTo m <$> promotion+ _ -> pure m+ coords p ms = choice $ fmap (uncurry (<$) . fmap chunk) $+ sortOn (Down . chunkLength (Proxy :: Proxy s) . snd) $+ (\m -> (m, sanCoords pos (p,ms) m)) <$> ms+ promotion = chunk "=" *> promotionPiece+ lastRank i = i >= 56 || i <= 7+ checkStatus m+ | inCheck (color nextPos) nextPos && null (legalPlies nextPos)+ = chunk "#" $> m+ | inCheck (color nextPos) nextPos+ = chunk "+" $> m+ | otherwise+ = pure m+ where+ nextPos = unsafeDoPly pos m -toSAN :: Position -> Move -> String-toSAN pos m | m `elem` moves pos = unsafeToSAN pos m+relaxedSAN :: (Stream s, SANToken (Token s), IsString (Tokens s))+ => Position -> Parser s Ply+relaxedSAN pos = (castling pos <|> normal) <* optional sanStatus where+ normal = do+ pc <- sanPiece <|> pure Pawn+ (from, _, to) <- conv <$> location+ prm <- optional $ optional (chunk "=") *> promotionPiece+ case possible pc from to prm of+ [m] -> pure m+ [] -> fail "Illegal move"+ _ -> fail "Ambiguous move"+ conv (Nothing, Nothing, cap, to) = (Nothing, cap, to)+ conv (Just f, Nothing, cap, to) = (Just (File f), cap, to)+ conv (Nothing, Just r, cap, to) = (Just (Rank r), cap, to)+ conv (Just f, Just r, cap, to) = (Just (Square (r*8+f)), cap, to)+ location = try ((,Nothing,,) <$> (Just <$> fileP) <*> capture <*> squareP)+ <|> try ((Nothing,,,) <$> (Just <$> rankP) <*> capture <*> squareP)+ <|> try ((,,,) <$> (Just <$> fileP) <*> (Just <$> rankP)+ <*> capture <*> squareP)+ <|> (Nothing,Nothing,,) <$> capture <*> squareP+ capture = option False $ chunk "x" $> True+ ms = legalPlies pos+ possible pc from to prm = filter (f from) ms where+ f (Just (Square from)) (unpack -> (from', to', prm')) =+ pAt from' == pc && from' == from && to' == to && prm' == prm+ f (Just (File ff)) (unpack -> (from', to', prm')) =+ pAt from' == pc && from' `mod` 8 == ff && to == to' && prm == prm'+ f (Just (Rank fr)) (unpack -> (from', to', prm')) =+ pAt from' == pc && from' `div` 8 == fr && to == to' && prm == prm'+ f Nothing (unpack -> (from', to', prm')) =+ pAt from' == pc && to == to' && prm == prm'+ pAt = snd . fromJust . pieceAt pos++fromSAN :: (Stream s, SANToken (Token s), IsString (Tokens s))+ => Position -> s -> Either String Ply+fromSAN pos s = case parse (relaxedSAN pos) "" s of+ Right m -> Right m+ Left err -> Left $ errorBundlePretty err++toSAN :: Position -> Ply -> String+toSAN pos m | m `elem` legalPlies pos = unsafeToSAN pos m | otherwise = error "Game.Chess.toSAN: Illegal move" -unsafeToSAN :: Position -> Move -> String-unsafeToSAN pos@Position{flags} m@(unpack -> (from, to, promo)) =+sanCoords :: IsString s => Position -> (PieceType, [Ply]) -> Ply -> s+sanCoords pos (pc,lms) m@(unpack -> (from, to, _)) =+ fromString $ source <> target+ where+ capture = isCapture pos m+ source+ | pc == Pawn && capture+ = [fileChar from]+ | pc == Pawn+ = []+ | length ms == 1+ = []+ | length (filter fEq ms) == 1+ = [fileChar from]+ | length (filter rEq ms) == 1+ = [rankChar from]+ | otherwise+ = toCoord from+ target+ | capture = "x" <> toCoord to+ | otherwise = toCoord to+ ms = filter (isMoveTo to) lms+ isMoveTo to (unpack -> (_, to', _)) = to == to'+ fEq (unpack -> (from', _, _)) = from' `mod` 8 == fromFile+ rEq (unpack -> (from', _, _)) = from' `div` 8 == fromRank+ (fromRank, fromFile) = toRF from+ fileChar i = chr $ (i `mod` 8) + ord 'a'+ rankChar i = chr $ (i `div` 8) + ord '1'++unsafeToSAN :: Position -> Ply -> String+unsafeToSAN pos m@(unpack -> (from, to, promo)) = moveStr <> status where moveStr = case piece of- Pawn | isCapture -> fileChar from : target <> promotion+ Pawn | capture -> fileChar from : target <> promotion | otherwise -> target <> promotion King | color pos == White && m == wKscm -> "O-O" | color pos == White && m == wQscm -> "O-O-O"@@ -143,38 +286,37 @@ Bishop -> 'B' : source <> target Rook -> 'R' : source <> target Queen -> 'Q' : source <> target- piece = fromJust $ snd <$> pieceAt pos (toEnum from)- isCapture = isJust (pieceAt pos $ toEnum to) || (flags .&. epMask) `testBit` to+ piece = fromJust $ snd <$> pieceAt pos from+ capture = isCapture pos m source | length ms == 1 = [] | length (filter fEq ms) == 1 = [fileChar from] | length (filter rEq ms) == 1 = [rankChar from]- | otherwise = coord from+ | otherwise = toCoord from target- | isCapture = 'x' : coord to- | otherwise = coord to+ | capture = "x" <> toCoord to+ | otherwise = toCoord to promotion = case promo of Just Knight -> "N" Just Bishop -> "B" Just Rook -> "R" Just Queen -> "Q" _ -> ""- status | inCheck (color nextPos) nextPos && null (moves nextPos)+ status | inCheck (color nextPos) nextPos && null (legalPlies nextPos) = "#" | inCheck (color nextPos) nextPos = "+" | otherwise = ""- nextPos = unsafeApplyMove pos m- ms = filter movesTo $ moves pos+ nextPos = unsafeDoPly pos m+ ms = filter movesTo $ legalPlies pos movesTo (unpack -> (from', to', _)) =- fmap snd (pieceAt pos (toEnum from')) == Just piece && to' == to+ fmap snd (pieceAt pos from') == Just piece && to' == to fEq (unpack -> (from', _, _)) = from' `mod` 8 == fromFile rEq (unpack -> (from', _, _)) = from' `div` 8 == fromRank- (fromRank, fromFile) = from `divMod` 8+ (fromRank, fromFile) = toRF from fileChar i = chr $ (i `mod` 8) + ord 'a' rankChar i = chr $ (i `div` 8) + ord '1'- coord i = let (r,f) = i `divMod` 8 in chr (f + ord 'a') : [chr (r + ord '1')] -- | The starting position as given by the FEN string -- "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1".@@ -182,12 +324,12 @@ startpos = fromJust $ 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 PieceType = Pawn | Knight | Bishop | Rook | Queen | King deriving (Eq, Ix, Ord, Show) -data Color = White | Black deriving (Eq, Show)+data Color = Black | White deriving (Eq, Ix, Ord, Show) -pieceAt :: Position -> Sq -> Maybe (Color, PieceType)-pieceAt (board -> BB{wP, wN, wB, wR, wQ, wK, bP, bN, bB, bR, bQ, bK}) (fromEnum -> sq)+pieceAt :: IsSquare sq => Position -> sq -> Maybe (Color, PieceType)+pieceAt (board -> BB{wP, wN, wB, wR, wQ, wK, bP, bN, bB, bR, bQ, bK}) (toIndex -> sq) | wP `testBit` sq = Just (White, Pawn) | wN `testBit` sq = Just (White, Knight) | wB `testBit` sq = Just (White, Bishop)@@ -216,14 +358,28 @@ | A6 | B6 | C6 | D6 | E6 | F6 | G6 | H6 | A7 | B7 | C7 | D7 | E7 | F7 | G7 | H7 | A8 | B8 | C8 | D8 | E8 | F8 | G8 | H8- deriving (Bounded, Enum, Eq, Show)+ deriving (Bounded, Enum, Eq, Ix, Ord, Show) -isDark :: Sq -> Bool-isDark (fromEnum -> sq) = (0xaa55aa55aa55aa55 :: Word64) `testBit` sq+class IsSquare sq where+ toIndex :: sq -> Int -isLight :: Sq -> Bool+toRF :: IsSquare sq => sq -> (Int, Int)+toRF sq = toIndex sq `divMod` 8++toCoord :: (IsSquare sq, IsString s) => sq -> s+toCoord (toRF -> (r,f)) = fromString [chr (f + ord 'a'), chr (r + ord '1')]++instance IsSquare Sq where+ toIndex = fromEnum++instance IsSquare Int where+ toIndex = id++isDark :: IsSquare sq => sq -> Bool+isDark (toIndex -> sq) = (0xaa55aa55aa55aa55 :: Word64) `testBit` sq++isLight :: IsSquare sq => sq -> Bool isLight = not . isDark-data Castling = Kingside | Queenside deriving (Eq, Ord, Show) data BB = BB { wP, wN, wB, wR, wQ, wK :: !Word64 , bP, bN, bB, bR, bQ, bK :: !Word64@@ -232,11 +388,16 @@ data Position = Position { board :: !BB , color :: !Color+ -- ^ active color , flags :: !Word64 , halfMoveClock :: !Int , moveNumber :: !Int-} deriving (Eq)+ -- ^ number of the full move+} +instance Eq Position where+ a == b = board a == board b && color a == color b && flags a == flags b+ emptyBB :: BB emptyBB = BB 0 0 0 0 0 0 0 0 0 0 0 0 @@ -253,7 +414,7 @@ <*> readMaybe (parts !! 5) where parts = words fen- readBoard = go (sqToRF A8) emptyBB where+ readBoard = go (toRF A8) emptyBB where go rf@(r,f) bb ('r':s) = go (r, f + 1) (bb { bR = bR bb .|. rfBit rf }) s go rf@(r,f) bb ('n':s) = go (r, f + 1) (bb { bN = bN bb .|. rfBit rf }) s go rf@(r,f) bb ('b':s) = go (r, f + 1) (bb { bB = bB bb .|. rfBit rf }) s@@ -297,9 +458,6 @@ = Just $ bit ((ord r - ord '1') * 8 + (ord f - ord 'a')) readEP _ = Nothing -sqToRF :: Sq -> (Int, Int)-sqToRF sq = fromEnum sq `divMod` 8- rfBit :: Bits bits => (Int, Int) -> bits rfBit (r,f) | inRange (0,7) r && inRange (0,7) f = bit $ r*8 + f | otherwise = error $ "Out of range: " <> show r <> " " <> show f@@ -325,7 +483,7 @@ | otherwise = (v, xs) showEP 0 = "-" showEP x = chr (f + ord 'a') : [chr (r + ord '1')] where- (r, f) = bitScanForward x `divMod` 8+ (r, f) = toRF $ bitScanForward x rank r = concatMap countEmpty $ groupBy (\x y -> x == y && x == ' ') $ charAt r <$> [0..7] countEmpty xs | head xs == ' ' = show $ length xs@@ -358,33 +516,34 @@ foldBits :: (a -> Int -> a) -> a -> Word64 -> a foldBits _ a 0 = a-foldBits f !a n = foldBits f (f a lsb) (n `xor` (1 `unsafeShiftL` lsb)) where+foldBits f !a n = foldBits f (f a lsb) (n .&. (n-1)) where !lsb = countTrailingZeros n bitScanForward, bitScanReverse :: Word64 -> Int bitScanForward = countTrailingZeros bitScanReverse = (63 -) . countLeadingZeros -newtype Move = Move Word16 deriving (Eq)+newtype Ply = Ply Word16 deriving (Eq) -instance Show Move where+instance Show Ply where show = toUCI -move :: Int -> Int -> Move-move from to = Move $ fromIntegral from .|. fromIntegral to `unsafeShiftL` 6+move :: (IsSquare from, IsSquare to) => from -> to -> Ply+move (toIndex -> from) (toIndex -> to) =+ Ply $ fromIntegral to .|. fromIntegral from `unsafeShiftL` 6 -promoteTo :: Move -> PieceType -> Move-promoteTo (Move x) = Move . set where- set Knight = x .|. 0b001_000000_000000- set Bishop = x .|. 0b010_000000_000000- set Rook = x .|. 0b011_000000_000000- set Queen = x .|. 0b100_000000_000000+promoteTo :: Ply -> PieceType -> Ply+promoteTo (Ply x) = Ply . set where+ set Knight = x .&. 0xfff .|. 0x1000+ set Bishop = x .&. 0xfff .|. 0x2000+ set Rook = x .&. 0xfff .|. 0x3000+ set Queen = x .&. 0xfff .|. 0x4000 set _ = x -unpack :: Move -> (Int, Int, Maybe PieceType)-unpack (Move x) = ( fromIntegral (x .&. 0b111111)- , fromIntegral ((x `unsafeShiftR` 6) .&. 0b111111)- , piece)+unpack :: Ply -> (Int, Int, Maybe PieceType)+unpack (Ply x) = ( fromIntegral ((x `unsafeShiftR` 6) .&. 0b111111)+ , fromIntegral (x .&. 0b111111)+ , piece) where !piece = case x `unsafeShiftR` 12 of 1 -> Just Knight@@ -393,8 +552,20 @@ 4 -> Just Queen _ -> Nothing +fromPolyglot :: Position -> Ply -> Ply+fromPolyglot pos pl@(unpack -> (from, to, _)) = case color pos of+ White | from == toIndex E1 && canCastleKingside pos && to == toIndex H1+ -> from `move` G1+ | from == toIndex E1 && canCastleQueenside pos && to == toIndex A1+ -> from `move` C1+ Black | from == toIndex E8 && canCastleKingside pos && to == toIndex H8+ -> from `move` G8+ | from == toIndex E8 && canCastleQueenside pos && to == toIndex A8+ -> from `move` C8+ _ -> pl+ -- | Parse a move in the format used by the Universal Chess Interface protocol.-fromUCI :: Position -> String -> Maybe Move+fromUCI :: Position -> String -> Maybe Ply 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@@ -416,9 +587,9 @@ fromUCI _ _ = Nothing -- | Convert a move to the format used by the Universal Chess Interface protocol.-toUCI :: Move -> String+toUCI :: Ply -> String toUCI (unpack -> (from, to, promo)) = coord from <> coord to <> p where- coord x = let (r,f) = x `divMod` 8 in+ coord x = let (r,f) = toRF x in chr (f + ord 'a') : [chr (r + ord '1')] p = case promo of Just Queen -> "q"@@ -428,8 +599,8 @@ _ -> "" -- | 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 :: Position -> Ply -> Maybe Ply+relativeTo pos m | m `elem` legalPlies pos = Just m | otherwise = Nothing shiftN, shiftNNE, shiftNE, shiftENE, shiftE, shiftESE, shiftSE, shiftSSE, shiftS, shiftSSW, shiftSW, shiftWSW, shiftW, shiftWNW, shiftNW, shiftNNW :: Word64 -> Word64@@ -453,225 +624,220 @@ -- | Apply a move to the given position. -- -- This function checks if the move is actually legal and throws and error--- if it isn't. See 'unsafeApplyMove' for a version that omits the legality check.-applyMove :: Position -> Move -> Position-applyMove p m- | m `elem` moves p = unsafeApplyMove p m- | otherwise = error "Game.Chess.applyMove: Illegal move"+-- if it isn't. See 'unsafeDoPly' for a version that omits the legality check.+doPly :: Position -> Ply -> Position+doPly p m+ | m `elem` legalPlies p = unsafeDoPly p m+ | otherwise = error "Game.Chess.doPly: Illegal move" --- | An unsafe version of 'applyMove'. Only use this if you are sure the given move+-- | An unsafe version of 'doPly'. Only use this if you are sure the given move -- can be applied to the position. This is useful if the move has been generated -- by the 'moves' function.-unsafeApplyMove :: Position -> Move -> Position-unsafeApplyMove pos@Position{flags} m@(unpack -> (from, to, promo))+unsafeDoPly :: Position -> Ply -> Position+unsafeDoPly pos@Position{color = White, halfMoveClock} m =+ (unsafeDoPly' pos m) { color = Black, halfMoveClock = succ halfMoveClock }+unsafeDoPly pos@Position{color = Black, moveNumber, halfMoveClock} m =+ (unsafeDoPly' pos m) { color = White, moveNumber = succ moveNumber, halfMoveClock = succ halfMoveClock }++unsafeDoPly' :: Position -> Ply -> Position+unsafeDoPly' pos@Position{board, flags} m@(unpack -> (from, to, promo)) | m == wKscm && flags `testMask` crwKs- = pos { board = bb { wK = wK bb `xor` mask- , wR = wR bb `xor` (bit (fromEnum H1) `setBit` fromEnum F1)- }- , color = opponent (color pos)+ = pos { board = board { wK = wK board `xor` mask+ , wR = wR board `xor` (bit (toIndex H1) `setBit` toIndex F1)+ } , flags = flags `clearMask` (rank1 .|. epMask) } | m == wQscm && flags `testMask` crwQs- = pos { board = bb { wK = wK bb `xor` mask- , wR = wR bb `xor` (bit (fromEnum A1) `setBit` fromEnum D1)- }- , color = opponent (color pos)+ = pos { board = board { wK = wK board `xor` mask+ , wR = wR board `xor` (bit (toIndex A1) `setBit` toIndex D1)+ } , flags = flags `clearMask` (rank1 .|. epMask) } | m == bKscm && flags `testMask` crbKs- = pos { board = bb { bK = bK bb `xor` mask- , bR = bR bb `xor` (bit (fromEnum H8) `setBit` fromEnum F8)- }- , color = opponent (color pos)+ = pos { board = board { bK = bK board `xor` mask+ , bR = bR board `xor` (bit (toIndex H8) `setBit` toIndex F8)+ } , flags = flags `clearMask` (rank8 .|. epMask) } | m == bQscm && flags `testMask` crbQs- = pos { board = bb { bK = bK bb `xor` mask- , bR = bR bb `xor` (bit (fromEnum A8) `setBit` fromEnum D8)- }- , color = opponent (color pos)+ = pos { board = board { bK = bK board `xor` mask+ , bR = bR board `xor` (bit (toIndex A8) `setBit` toIndex D8)+ } , flags = flags `clearMask` (rank8 .|. epMask) }- | Just Queen <- promo- , color pos == White- = pos { board = clearB { wP = wP bb `clearBit` from- , wQ = wQ bb `setBit` to- }- , color = opponent (color pos)- , flags = flags `clearMask` (epMask .|. bit to)- }- | Just Rook <- promo- , color pos == White- = pos { board = clearB { wP = wP bb `clearBit` from- , wR = wR bb `setBit` to- }- , color = opponent (color pos)- , flags = flags `clearMask` (epMask .|. bit to)- }- | Just Bishop <- promo- , color pos == White- = pos { board = clearB { wP = wP bb `clearBit` from- , wB = wB bb `setBit` to- }- , color = opponent (color pos)- , flags = flags `clearMask` (epMask .|. bit to)- }- | Just Knight <- promo- , color pos == White- = pos { board = clearB { wP = wP bb `clearBit` from- , wN = wN bb `setBit` to- }- , color = opponent (color pos)- , flags = flags `clearMask` (epMask .|. bit to)- }- | Just Queen <- promo- , color pos == Black- = pos { board = clearW { bP = bP bb `clearBit` from- , bQ = bQ bb `setBit` to- }- , color = opponent (color pos)- , flags = flags `clearMask` (epMask .|. bit to)- } - | Just Rook <- promo- , color pos == Black- = pos { board = clearW { bP = bP bb `clearBit` from- , bR = bR bb `setBit` to- }- , color = opponent (color pos)- , flags = flags `clearMask` (epMask .|. bit to)- }- | Just Bishop <- promo- , color pos == Black- = pos { board = clearW { bP = bP bb `clearBit` from- , bB = bB bb `setBit` to- }- , color = opponent (color pos)- , flags = flags `clearMask` (epMask .|. bit to)- }- | Just Knight <- promo- , color pos == Black- = pos { board = clearW { bP = bP bb `clearBit` from- , bN = bN bb `setBit` to- }- , color = opponent (color pos)- , flags = flags `clearMask` (epMask .|. bit to)- }+ | Just piece <- promo+ = case color pos of+ White -> case piece of+ Queen -> pos { board = clearB { wP = wP board `clearBit` from+ , wQ = wQ board `setBit` to+ }+ , flags = flags `clearMask` (epMask .|. bit to)+ }+ Rook -> pos { board = clearB { wP = wP board `clearBit` from+ , wR = wR board `setBit` to+ }+ , flags = flags `clearMask` (epMask .|. bit to)+ }+ Bishop -> pos { board = clearB { wP = wP board `clearBit` from+ , wB = wB board `setBit` to+ }+ , flags = flags `clearMask` (epMask .|. bit to)+ }+ Knight -> pos { board = clearB { wP = wP board `clearBit` from+ , wN = wN board `setBit` to+ }+ , flags = flags `clearMask` (epMask .|. bit to)+ }+ _ -> error "Impossible: White tried to promote to Pawn"+ Black -> case piece of+ Queen -> pos { board = clearW { bP = bP board `clearBit` from+ , bQ = bQ board `setBit` to+ }+ , flags = flags `clearMask` (epMask .|. bit to)+ } + Rook -> pos { board = clearW { bP = bP board `clearBit` from+ , bR = bR board `setBit` to+ }+ , flags = flags `clearMask` (epMask .|. bit to)+ }+ Bishop -> pos { board = clearW { bP = bP board `clearBit` from+ , bB = bB board `setBit` to+ }+ , flags = flags `clearMask` (epMask .|. bit to)+ }+ Knight -> pos { board = clearW { bP = bP board `clearBit` from+ , bN = bN board `setBit` to+ }+ , flags = flags `clearMask` (epMask .|. bit to)+ }+ _ -> error "Impossible: Black tried to promote to Pawn" | otherwise = pos { board = newBoard- , color = opponent (color pos)- , flags = (flags `clearMask` (epMask .|. mask)) .|. dpp }+ , flags = (flags `clearMask` (epMask .|. mask)) .|. dpp+ } where- !bb = board pos- epBit = case color pos of- White | wP bb `testMask` fromMask -> shiftS $ flags .&. rank6 .&. toMask- Black | bP bb `testMask` fromMask -> shiftN $ flags .&. rank3 .&. toMask+ !epBit = case color pos of+ White | wP board `testMask` fromMask -> shiftS $ flags .&. rank6 .&. toMask+ Black | bP board `testMask` fromMask -> shiftN $ flags .&. rank3 .&. toMask _ -> 0- clearW = bb { wP = wP bb `clearMask` (toMask .|. epBit)- , wN = wN bb `clearMask` toMask- , wB = wB bb `clearMask` toMask- , wR = wR bb `clearMask` toMask- , wQ = wQ bb `clearMask` toMask- }- clearB = bb { bP = bP bb `clearMask` (toMask .|. epBit)- , bN = bN bb `clearMask` toMask- , bB = bB bb `clearMask` toMask- , bR = bR bb `clearMask` toMask- , bQ = bQ bb `clearMask` toMask- }+ clearW = board { wP = wP board `clearMask` (toMask .|. epBit)+ , wN = wN board `clearMask` toMask+ , wB = wB board `clearMask` toMask+ , wR = wR board `clearMask` toMask+ , wQ = wQ board `clearMask` toMask+ }+ clearB = board { bP = bP board `clearMask` (toMask .|. epBit)+ , bN = bN board `clearMask` toMask+ , bB = bB board `clearMask` toMask+ , bR = bR board `clearMask` toMask+ , bQ = bQ board `clearMask` toMask+ } !fromMask = 1 `unsafeShiftL` from !toMask = 1 `unsafeShiftL` to !mask = fromMask .|. toMask newBoard = case color pos of- White | wP bb `testMask` fromMask -> clearB { wP = wP bb `xor` mask }- | wN bb `testMask` fromMask -> clearB { wN = wN bb `xor` mask }- | wB bb `testMask` fromMask -> clearB { wB = wB bb `xor` mask }- | wR bb `testMask` fromMask -> clearB { wR = wR bb `xor` mask }- | wQ bb `testMask` fromMask -> clearB { wQ = wQ bb `xor` mask }- | otherwise -> clearB { wK = wK bb `xor` mask }- Black | bP bb `testMask` fromMask -> clearW { bP = bP bb `xor` mask }- | bN bb `testMask` fromMask -> clearW { bN = bN bb `xor` mask }- | bB bb `testMask` fromMask -> clearW { bB = bB bb `xor` mask }- | bR bb `testMask` fromMask -> clearW { bR = bR bb `xor` mask }- | bQ bb `testMask` fromMask -> clearW { bQ = bQ bb `xor` mask }- | otherwise -> clearW { bK = bK bb `xor` mask }+ White | wP board `testMask` fromMask -> clearB { wP = wP board `xor` mask }+ | wN board `testMask` fromMask -> clearB { wN = wN board `xor` mask }+ | wB board `testMask` fromMask -> clearB { wB = wB board `xor` mask }+ | wR board `testMask` fromMask -> clearB { wR = wR board `xor` mask }+ | wQ board `testMask` fromMask -> clearB { wQ = wQ board `xor` mask }+ | otherwise -> clearB { wK = wK board `xor` mask }+ Black | bP board `testMask` fromMask -> clearW { bP = bP board `xor` mask }+ | bN board `testMask` fromMask -> clearW { bN = bN board `xor` mask }+ | bB board `testMask` fromMask -> clearW { bB = bB board `xor` mask }+ | bR board `testMask` fromMask -> clearW { bR = bR board `xor` mask }+ | bQ board `testMask` fromMask -> clearW { bQ = bQ board `xor` mask }+ | otherwise -> clearW { bK = bK board `xor` mask } dpp = case color pos of- White | fromMask .&. rank2 .&. wP bb /= 0 && from + 16 == to -> bit (from + 8)- Black | fromMask .&. rank7 .&. bP bb /= 0 && from - 16 == to -> bit (from - 8)+ White | fromMask .&. rank2 .&. wP board /= 0 && from + 16 == to -> shiftN fromMask+ Black | fromMask .&. rank7 .&. bP board /= 0 && from - 16 == to -> shiftS fromMask _ -> 0 -- | Generate a list of possible moves for the given position.-moves :: Position -> [Move]-moves pos@Position{color, board, flags} =- filter (not . inCheck color . unsafeApplyMove pos) $- kingMoves pos notOurs+legalPlies :: Position -> [Ply]+legalPlies pos@Position{color, board, flags} = filter legalPly $+ kingMoves . knightMoves- . slideMoves Queen pos ours notOurs- . slideMoves Rook pos ours notOurs- . slideMoves Bishop pos ours notOurs+ . slideMoves Queen pos ours notOurs occ+ . slideMoves Rook pos ours notOurs occ+ . slideMoves Bishop pos ours notOurs occ . pawnMoves $ [] where- ours = occupiedBy color board - notOurs = complement ours- (!pawnMoves, !knightMoves) = case color of+ legalPly = not . inCheck color . unsafeDoPly' pos+ !ours = occupiedBy color board+ !them = occupiedBy (opponent color) board+ !notOurs = complement ours+ !occ = ours .|. them+ (!pawnMoves, !knightMoves, !kingMoves) = case color of White ->- ( wPawnMoves (wP board) (notOccupied board) (occupiedBy Black board .|. (flags .&. epMask))- , flip (foldBits genNMoves) (wN board))+ ( wPawnMoves (wP board) (complement occ) (them .|. (flags .&. epMask))+ , flip (foldBits genNMoves) (wN board)+ , flip (foldBits genKMoves) (wK board) . wShort . wLong) Black ->- ( bPawnMoves (bP board) (notOccupied board) (occupiedBy White board .|. (flags .&. epMask))- , flip (foldBits genNMoves) (bN board))+ ( bPawnMoves (bP board) (complement occ) (them .|. (flags .&. epMask))+ , flip (foldBits genNMoves) (bN board)+ , flip (foldBits genKMoves) (bK board) . bShort . bLong) genNMoves ms sq = foldBits (mkM sq) ms ((knightAttacks ! sq) .&. notOurs)- mkM from ms to = move from to : ms+ genKMoves ms sq = foldBits (mkM sq) ms ((kingAttacks ! sq) .&. notOurs)+ wShort ml | canCastleKingside' pos occ = wKscm : ml+ | otherwise = ml+ wLong ml | canCastleQueenside' pos occ = wQscm : ml+ | otherwise = ml+ bShort ml | canCastleKingside' pos occ = bKscm : ml+ | otherwise = ml+ bLong ml | canCastleQueenside' pos occ = bQscm : ml+ | otherwise = ml+ mkM !from ms !to = move from to : ms -- | Returns 'True' if 'Color' is in check in the given position. inCheck :: Color -> Position -> Bool-inCheck White Position{board} = attackedBy Black (bitScanForward (wK board)) board-inCheck Black Position{board} = attackedBy White (bitScanForward (bK board)) board+inCheck White Position{board} = attackedBy Black board (occupied board) (bitScanForward (wK board))+inCheck Black Position{board} = attackedBy White board (occupied board) (bitScanForward (bK board)) -wPawnMoves :: Word64 -> Word64 -> Word64 -> [Move] -> [Move]-wPawnMoves pawns emptySquares opponentPieces =- flip (foldBits $ mkMove 9) eastCaptureTargets- . flip (foldBits $ mkMove 7) westCaptureTargets- . flip (foldBits $ mkMove 8) singlePushTargets- . flip (foldBits $ mkMove 16) doublePushTargets+wPawnMoves :: Word64 -> Word64 -> Word64 -> [Ply] -> [Ply]+wPawnMoves !pawns !emptySquares !opponentPieces =+ flip (foldBits $ mkPly 9) eastCaptureTargets+ . flip (foldBits $ mkPly 7) westCaptureTargets+ . flip (foldBits $ mkPly 8) singlePushTargets+ . flip (foldBits $ mkPly 16) doublePushTargets where doublePushTargets = shiftN singlePushTargets .&. emptySquares .&. rank4 singlePushTargets = shiftN pawns .&. emptySquares eastCaptureTargets = shiftNE pawns .&. opponentPieces westCaptureTargets = shiftNW pawns .&. opponentPieces- mkMove diff ms tsq+ mkPly diff ms tsq | tsq >= 56 = (promoteTo m <$> [Queen, Rook, Bishop, Knight]) <> ms | otherwise = m : ms where m = move (tsq - diff) tsq -bPawnMoves :: Word64 -> Word64 -> Word64 -> [Move] -> [Move]-bPawnMoves pawns emptySquares opponentPieces =- flip (foldBits $ mkMove 9) westCaptureTargets- . flip (foldBits $ mkMove 7) eastCaptureTargets- . flip (foldBits $ mkMove 8) singlePushTargets- . flip (foldBits $ mkMove 16) doublePushTargets+bPawnMoves :: Word64 -> Word64 -> Word64 -> [Ply] -> [Ply]+bPawnMoves !pawns !emptySquares !opponentPieces =+ flip (foldBits $ mkPly 9) westCaptureTargets+ . flip (foldBits $ mkPly 7) eastCaptureTargets+ . flip (foldBits $ mkPly 8) singlePushTargets+ . flip (foldBits $ mkPly 16) doublePushTargets where doublePushTargets = shiftS singlePushTargets .&. emptySquares .&. rank5 singlePushTargets = shiftS pawns .&. emptySquares eastCaptureTargets = shiftSE pawns .&. opponentPieces westCaptureTargets = shiftSW pawns .&. opponentPieces- mkMove diff ms tsq+ mkPly diff ms tsq | tsq <= 7 = (promoteTo m <$> [Queen, Rook, Bishop, Knight]) <> ms | otherwise = m : ms where m = move (tsq + diff) tsq -slideMoves :: PieceType -> Position -> Word64 -> Word64 -> [Move] -> [Move]-slideMoves piece (Position bb c _ _ _) ours notOurs =+slideMoves :: PieceType -> Position -> Word64 -> Word64 -> Word64 -> [Ply] -> [Ply]+slideMoves piece (Position bb c _ _ _) !ours !notOurs !occ = flip (foldBits gen) pieces where- gen ms from = foldBits (mkMove from) ms (targets from)- mkMove from ms to = move from to : ms+ gen ms from = foldBits (mkPly from) ms (targets from)+ mkPly from ms to = move from to : ms targets sq = case piece of Rook -> rookTargets sq occ .&. notOurs Bishop -> bishopTargets sq occ .&. notOurs Queen -> queenTargets sq occ .&. notOurs _ -> error "Not a sliding piece"- occ = ours .|. occupiedBy (opponent c) bb pieces = case (c, piece) of (White, Bishop) -> wB bb (Black, Bishop) -> bB bb@@ -681,47 +847,45 @@ (Black, Queen) -> bQ bb _ -> 0 -kingMoves :: Position -> Word64 -> [Move] -> [Move]-kingMoves pos@Position{board, color} notOurs ml = case color of- White -> kMoves (wK board) . wCastleMoves pos $ ml- Black -> kMoves (bK board) . bCastleMoves pos $ ml- where- kMoves = flip (foldBits gen)- gen ms sq = foldBits (mkMove sq) ms ((kingAttacks ! sq) .&. notOurs)- mkMove from ms to = move from to : ms+data Castle = Kingside | Queenside deriving (Eq, Ix, Ord, Show) -wCastleMoves, bCastleMoves :: Position -> [Move] -> [Move]-wCastleMoves (Position board _ flags _ _) = short . long where- short ml | flags `testMask` crwKs && occupied board .&. crwKe == 0 &&- not (attackedBy Black (fromEnum E1) board) &&- not (attackedBy Black (fromEnum F1) board)- = wKscm : ml- | otherwise = ml- long ml | flags `testMask` crwQs && occupied board .&. crwQe == 0 &&- not (attackedBy Black (fromEnum E1) board) &&- not (attackedBy Black (fromEnum D1) board)- = wQscm : ml- | otherwise = ml-bCastleMoves (Position board _ flags _ _) = short . long where- short ml | flags `testMask` crbKs && occupied board .&. crbKe == 0 &&- not (attackedBy White (fromEnum E8) board) &&- not (attackedBy White (fromEnum F8) board)- = bKscm : ml- | otherwise = ml- long ml | flags `testMask` crbQs && occupied board .&. crbQe == 0 &&- not (attackedBy White (fromEnum E8) board) &&- not (attackedBy White (fromEnum D8) board)- = bQscm : ml- | otherwise = ml+castlingRights :: Position -> [(Color, Castle)]+castlingRights Position{flags} = wks . wqs . bks . bqs $ [] where+ wks xs | flags `testMask` crwKs = (White, Kingside):xs+ | otherwise = xs+ wqs xs | flags `testMask` crwQs = (White, Queenside):xs+ | otherwise = xs+ bks xs | flags `testMask` crbKs = (Black, Kingside):xs+ | otherwise = xs+ bqs xs | flags `testMask` crbQs = (Black, Queenside):xs+ | otherwise = xs -wKscm, wQscm, bKscm, bQscm :: Move-wKscm = move (fromEnum E1) (fromEnum G1)-wQscm = move (fromEnum E1) (fromEnum C1)-bKscm = move (fromEnum E8) (fromEnum G8)-bQscm = move (fromEnum E8) (fromEnum C8)+canCastleKingside, canCastleQueenside :: Position -> Bool+canCastleKingside !pos@Position{board} = canCastleKingside' pos (occupied board)+canCastleQueenside !pos@Position{board} = canCastleQueenside' pos (occupied board) -attackedBy :: Color -> Int -> BB -> Bool-attackedBy White sq bb@BB{wP, wN, wB, wR, wQ, wK}+canCastleKingside', canCastleQueenside' :: Position -> Word64 -> Bool+canCastleKingside' Position{board, color = White, flags} !occ =+ flags `testMask` crwKs && occ .&. crwKe == 0 &&+ not (any (attackedBy Black board occ) [E1, F1, G1])+canCastleKingside' Position{board, color = Black, flags} !occ = + flags `testMask` crbKs && occ .&. crbKe == 0 &&+ not (any (attackedBy White board occ) [E8, F8, G8])+canCastleQueenside' Position{board, color = White, flags} !occ =+ flags `testMask` crwQs && occ .&. crwQe == 0 &&+ not (any (attackedBy Black board occ) [E1, D1, C1])+canCastleQueenside' Position{board, color = Black, flags} !occ =+ flags `testMask` crbQs && occ .&. crbQe == 0 &&+ not (any (attackedBy White board occ) [E8, D8, C8])++wKscm, wQscm, bKscm, bQscm :: Ply+wKscm = move E1 G1+wQscm = move E1 C1+bKscm = move E8 G8+bQscm = move E8 C8++attackedBy :: IsSquare sq => Color -> BB -> Word64 -> sq -> Bool+attackedBy White BB{wP, wN, wB, wR, wQ, wK} !occ (toIndex -> sq) | (wPawnAttacks ! sq) .&. wP /= 0 = True | (knightAttacks ! sq) .&. wN /= 0 = True | bishopTargets sq occ .&. wB /= 0 = True@@ -729,8 +893,7 @@ | queenTargets sq occ .&. wQ /= 0 = True | (kingAttacks ! sq) .&. wK /= 0 = True | otherwise = False- where occ = occupied bb-attackedBy Black sq bb@BB{bP, bN, bB, bR, bQ, bK}+attackedBy Black BB{bP, bN, bB, bR, bQ, bK} !occ (toIndex -> sq) | (bPawnAttacks ! sq) .&. bP /= 0 = True | (knightAttacks ! sq) .&. bN /= 0 = True | bishopTargets sq occ .&. bB /= 0 = True@@ -738,7 +901,6 @@ | queenTargets sq occ .&. bQ /= 0 = True | (kingAttacks ! sq) .&. bK /= 0 = True | otherwise = False- where occ = occupied bb notAFile, notABFile, notGHFile, notHFile, rank1, rank2, rank3, rank4, rank5, rank6, rank7, rank8 :: Word64 notAFile = 0xfefefefefefefefe@@ -825,10 +987,20 @@ testMask a b = a .&. b == b {-# INLINE clearMask #-}+{-# INLINE testMask #-} {-# INLINE attackedBy #-}-{-# INLINE kingMoves #-} {-# INLINE slideMoves #-} {-# INLINE wPawnMoves #-} {-# INLINE bPawnMoves #-} {-# INLINE unpack #-} {-# INLINE foldBits #-}+{-# SPECIALISE relaxedSAN :: Position -> Parser Strict.ByteString Ply #-}+{-# SPECIALISE relaxedSAN :: Position -> Parser Lazy.ByteString Ply #-}+{-# SPECIALISE relaxedSAN :: Position -> Parser Strict.Text Ply #-}+{-# SPECIALISE relaxedSAN :: Position -> Parser Lazy.Text Ply #-}+{-# SPECIALISE relaxedSAN :: Position -> Parser String Ply #-}+{-# SPECIALISE strictSAN :: Position -> Parser Strict.ByteString Ply #-}+{-# SPECIALISE strictSAN :: Position -> Parser Lazy.ByteString Ply #-}+{-# SPECIALISE strictSAN :: Position -> Parser Strict.Text Ply #-}+{-# SPECIALISE strictSAN :: Position -> Parser Lazy.Text Ply #-}+{-# SPECIALISE strictSAN :: Position -> Parser String Ply #-}
+ src/Game/Chess/PGN.hs view
@@ -0,0 +1,204 @@+{-# LANGUAGE GADTs #-}+module Game.Chess.PGN (+ readPGNFile, gameFromForest, PGN(..), Game, Outcome(..)+, hPutPGN, pgnDoc, RAVOrder, breadthFirst, depthFirst, gameDoc) where++import Control.Monad+import Data.Bifunctor+import Data.ByteString.Char8 (ByteString)+import qualified Data.ByteString.Char8 as BS+import Data.Char+import Data.Foldable+import Data.Functor+import Data.Maybe+import Data.Text (Text)+import qualified Data.Text as T+import qualified Data.Text.Encoding as T+import Data.Text.Prettyprint.Doc hiding (space)+import Data.Text.Prettyprint.Doc.Render.Text+import Data.Tree+import Data.Word+import Data.Void+import Game.Chess+import System.IO+import Text.Megaparsec+import Text.Megaparsec.Byte+import qualified Text.Megaparsec.Byte.Lexer as L++gameFromForest :: [(ByteString, Text)] -> Forest Ply -> Outcome -> Game+gameFromForest tags forest o = (("Result", r):tags, (o, (fmap . fmap) f forest)) where+ f pl = PlyData [] pl []+ r = case o of+ Win White -> "1-0"+ Win Black -> "0-1"+ Draw -> "1/2-1/2"+ Undecided -> "*"++newtype PGN = PGN [Game] deriving (Eq, Monoid, Semigroup)+type Game = ([(ByteString, Text)], (Outcome, Forest PlyData))+data Outcome = Win Color+ | Draw+ | Undecided+ deriving (Eq, Show)++instance Pretty Outcome where+ pretty (Win White) = "1-0"+ pretty (Win Black) = "0-1"+ pretty Draw = "1/2-1/2"+ pretty Undecided = "*"++data PlyData = PlyData {+ prefixNAG :: ![Int]+, ply :: !Ply+, suffixNAG :: ![Int]+} deriving (Eq, Show)++readPGNFile :: FilePath -> IO (Either String PGN)+readPGNFile fp = first errorBundlePretty . parse pgn fp <$> BS.readFile fp++hPutPGN :: Handle -> RAVOrder (Doc ann) -> PGN -> IO ()+hPutPGN h ro (PGN games) = for_ games $ \game -> do+ hPutDoc h $ gameDoc ro game+ hPutStrLn h ""++type Parser = Parsec Void ByteString++spaceConsumer :: Parser ()+spaceConsumer = L.space+ space1 (L.skipLineComment ";") (L.skipBlockComment "{" "}")++lexeme :: Parser a -> Parser a+lexeme = L.lexeme spaceConsumer++eog :: Parser Outcome+eog = lexeme $ string "1-0" $> Win White+ <|> string "0-1" $> Win Black+ <|> string "1/2-1/2" $> Draw+ <|> string "*" $> Undecided++sym :: Parser ByteString+sym = lexeme . fmap fst . match $ do+ void $ alphaNumChar+ many $ alphaNumChar <|> oneOf [35,43,45,58,61,95]++lbraceChar, rbraceChar, semiChar, periodChar, quoteChar, backslashChar, dollarChar :: Word8+lbraceChar = fromIntegral $ ord '{'+rbraceChar = fromIntegral $ ord '}'+semiChar = fromIntegral $ ord ';'+periodChar = fromIntegral $ ord '.'+quoteChar = fromIntegral $ ord '"'+backslashChar = fromIntegral $ ord '\\'+dollarChar = fromIntegral $ ord '$'++lbracketP, rbracketP, lparenP, rparenP :: Parser ()+lbracketP = void . lexeme . single . fromIntegral $ ord '['+rbracketP = void . lexeme . single . fromIntegral $ ord ']'+lparenP = void . lexeme . single . fromIntegral $ ord '('+rparenP = void . lexeme . single . fromIntegral $ ord ')'++nag :: Parser Int+nag = lexeme $ single dollarChar *> L.decimal+ <|> string "!!" $> 3+ <|> string "??" $> 4+ <|> string "!?" $> 5+ <|> string "?!" $> 6+ <|> string "!" $> 1+ <|> string "?" $> 2++comment :: Parser String+comment = (fmap . fmap) (chr . fromEnum) $+ single semiChar *> manyTill anySingle (eof <|> void eol)+ <|> single lbraceChar *> many (anySingleBut rbraceChar) <* single rbraceChar++tagPair :: Parser (ByteString, Text)+tagPair = lexeme $ do+ lbracketP+ k <- sym+ v <- str+ rbracketP+ pure $ (k, v)++tagList :: Parser [(ByteString, Text)]+tagList = many tagPair++movetext :: Position -> Parser (Outcome, Forest PlyData)+movetext pos = (,[]) <$> eog <|> main pos where+ main p = ply p >>= \(m, n) -> fmap n <$> movetext (unsafeDoPly p m)+ var p = ply p >>= \(m, n) -> n <$> (rparenP $> [] <|> var (unsafeDoPly p m))+ ply p = do+ pnags <- many nag+ validateMoveNumber p+ m <- lexeme $ relaxedSAN p+ snags <- many nag+ rav <- concat <$> many (lparenP *> var p)+ pure $ (m, \xs -> Node (PlyData pnags m snags) xs:rav)+ validateMoveNumber p =+ optional (lexeme $ L.decimal <* space <* many (single periodChar)) >>= \case+ Just n | moveNumber p /= n ->+ fail $ "Invalid move number: " <> show n <> " /= " <> show (moveNumber p)+ _ -> pure ()++pgn :: Parser PGN+pgn = spaceConsumer *> fmap PGN (many game) <* spaceConsumer <* eof++game :: Parser Game+game = do+ tl <- tagList+ pos <- case lookup "FEN" tl of+ Nothing -> pure startpos+ Just fen -> case fromFEN (T.unpack fen) of+ Just p -> pure p+ Nothing -> fail "Invalid FEN"+ (tl,) <$> movetext pos+ +str :: Parser Text+str = p <?> "string" where+ p = fmap (T.pack . fmap (chr . fromEnum)) $ single quoteChar *> many ch <* single quoteChar+ ch = single backslashChar *> ( single backslashChar $> backslashChar+ <|> single quoteChar $> quoteChar+ )+ <|> anySingleBut quoteChar++type RAVOrder a = (Forest PlyData -> a) -> Forest PlyData -> [a]++breadthFirst, depthFirst :: RAVOrder a+breadthFirst _ [] = []+breadthFirst f ts = pure $ f ts+depthFirst f = fmap $ f . pure++pgnDoc :: RAVOrder (Doc ann) -> PGN -> Doc ann+pgnDoc ro (PGN games) = vsep $ gameDoc ro <$> games++gameDoc :: RAVOrder (Doc ann) -> Game -> Doc ann+gameDoc ro (tl, mt)+ | null tl = moveDoc ro pos mt+ | otherwise = tagsDoc tl <> line <> line <> moveDoc ro pos mt+ where+ pos | Just fen <- lookup "FEN" tl = fromJust $ fromFEN (T.unpack fen)+ | otherwise = startpos++tagsDoc :: [(ByteString, Text)] -> Doc ann+tagsDoc = vsep . fmap tagpair where+ tagpair (k, esc -> v) = brackets $ pretty (BS.unpack k) <+> dquotes (pretty v)+ esc = T.concatMap e where+ e '\\' = T.pack "\\\\"+ e '"' = T.pack "\\\""+ e c = T.singleton c++moveDoc :: RAVOrder (Doc ann) -> Position -> (Outcome, Forest PlyData) -> Doc ann+moveDoc ro pos (o,ts) = (fillSep $ go pos True ts <> [pretty o]) <> line where+ go _ _ [] = []+ go pos pmn (t:ts)+ | color pos == White || pmn+ = pnag <> (mn:san:snag) <> rav <> go pos' (not . null $ rav) (subForest t)+ | otherwise+ = pnag <> (san:snag) <> rav <> go pos' (not . null $ rav) (subForest t)+ where+ pl = ply . rootLabel $ t+ san = pretty $ unsafeToSAN pos pl+ pos' = unsafeDoPly pos pl+ pnag = nag <$> prefixNAG (rootLabel t)+ mn = pretty (moveNumber pos) <> if color pos == White then "." else "..."+ rav = ro (parens . fillSep . go pos True) ts+ snag = nag <$> suffixNAG (rootLabel t)+ nag n = "$" <> pretty n
+ src/Game/Chess/Polyglot/Book.hs view
@@ -0,0 +1,114 @@+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE TemplateHaskell #-}+module Game.Chess.Polyglot.Book (+ PolyglotBook+, fromByteString+, defaultBook, twic+, readPolyglotFile+, bookPly+, bookPlies+, bookForest+) where++import Control.Arrow+import Control.Monad.Random (Rand)+import qualified Control.Monad.Random as Rand+import Data.Bits+import Data.ByteString (ByteString)+import qualified Data.ByteString.Internal as BS+import qualified Data.ByteString as BS+import Data.FileEmbed+import qualified Data.Vector.Storable as VS+import Data.Tree+import Data.Word+import Foreign.ForeignPtr (plusForeignPtr)+import Foreign.Ptr (castPtr)+import Foreign.Storable+import Game.Chess+import Game.Chess.Polyglot.Hash+import GHC.Ptr+import System.Random (RandomGen)++data BookEntry = BookEntry {+ key :: {-# UNPACK #-} !Word64+, ply :: {-# UNPACK #-} !Ply+, weight :: {-# UNPACK #-} !Word16+, learn :: {-# UNPACK #-} !Word32+} deriving (Eq, Show)++instance Storable BookEntry where+ sizeOf _ = 16+ alignment _ = alignment (undefined :: Word64)+ peek ptr = BookEntry <$> peekBE (castPtr ptr)+ <*> (Ply <$> peekBE (castPtr ptr `plusPtr` 8))+ <*> peekBE (castPtr ptr `plusPtr` 10)+ <*> peekBE (castPtr ptr `plusPtr` 12)+ poke ptr (BookEntry key (Ply ply) weight learn) = do+ pokeBE (castPtr ptr) key+ pokeBE (castPtr ptr `plusPtr` 8) ply+ pokeBE (castPtr ptr `plusPtr` 10) weight+ pokeBE (castPtr ptr `plusPtr` 12) learn++peekBE :: forall a. (Bits a, Num a, Storable a) => Ptr Word8 -> IO a+peekBE ptr = go ptr 0 (sizeOf (undefined :: a)) where+ go _ !x 0 = pure x+ go !p !x !n = peek p >>= \w8 -> + go (p `plusPtr` 1) (x `shiftL` 8 .|. fromIntegral w8) (n - 1)++pokeBE :: forall a. (Bits a, Integral a, Num a, Storable a) => Ptr Word8 -> a -> IO ()+pokeBE p x = go x (sizeOf x) where+ go _ 0 = pure ()+ go !x !n = do+ pokeElemOff p (n-1) (fromIntegral x)+ go (x `shiftR` 8) (n-1)++defaultBook, twic :: PolyglotBook+defaultBook = twic+twic = fromByteString $(embedFile "book/twic-9g.bin")++pv :: PolyglotBook -> [Ply]+pv b = head . concatMap paths $ bookForest b startpos++newtype PolyglotBook = Book (VS.Vector BookEntry)++fromByteString :: ByteString -> PolyglotBook+fromByteString bs = Book v where+ v = VS.unsafeFromForeignPtr0 (plusForeignPtr fptr off) (len `div` elemSize)+ (fptr, off, len) = BS.toForeignPtr bs+ elemSize = sizeOf (undefined `asTypeOf` VS.head v)++readPolyglotFile :: FilePath -> IO PolyglotBook+readPolyglotFile = fmap fromByteString . BS.readFile++bookForest :: PolyglotBook -> Position -> Forest Ply+bookForest b p = tree <$> bookPlies b p where+ tree pl = Node pl . bookForest b $ unsafeDoPly p pl++paths :: Tree a -> [[a]]+paths = foldTree f where+ f a [] = [[a]]+ f a xs = (a :) <$> concat xs++bookPly :: RandomGen g => PolyglotBook -> Position -> Maybe (Rand g Ply)+bookPly b pos = case findPosition b pos of+ [] -> Nothing+ l -> Just . Rand.fromList $ map (ply &&& fromIntegral . weight) l++bookPlies :: PolyglotBook -> Position -> [Ply]+bookPlies b pos+ | halfMoveClock pos > 150 = []+ | otherwise = ply <$> findPosition b pos++findPosition :: PolyglotBook -> Position -> [BookEntry]+findPosition (Book v) pos = fmap conv . VS.toList .+ VS.takeWhile ((hash ==) . key) . VS.unsafeDrop (lowerBound hash) $ v+ where+ conv be@BookEntry{ply} = be { ply = fromPolyglot pos ply }+ hash = hashPosition pos+ lowerBound = bsearch (key . VS.unsafeIndex v) (0, VS.length v - 1)+ bsearch :: (Integral a, Ord b) => (a -> b) -> (a, a) -> b -> a+ bsearch f (lo, hi) x+ | lo >= hi = lo+ | x <= f mid = bsearch f (lo, mid) x+ | otherwise = bsearch f (mid + 1, hi) x+ where mid = (lo + hi) `div` 2
+ src/Game/Chess/Polyglot/Hash.hs view
@@ -0,0 +1,234 @@+module Game.Chess.Polyglot.Hash (hashPosition, pieceKey, castleKey, turnKey) where++import Data.Bits+import Data.Maybe+import qualified Data.Vector.Unboxed as Unboxed+import Data.Ix+import Data.Word+import Game.Chess++hashPosition :: Position -> Word64+hashPosition pos = piece `xor` castling `xor` ep `xor` turn where+ piece = foldr xor 0 $ mapMaybe f [A1 .. ] where+ f sq = (\(c, p) -> pieceKey (p, c, sq)) <$> pieceAt pos sq+ castling = foldr (xor . castleKey) 0 $ castlingRights pos+ ep = 0 -- TODO+ turn = case color pos of+ White -> turnKey+ Black -> 0++pieceKey :: (PieceType, Color, Sq) -> Word64+pieceKey = Unboxed.unsafeIndex pieceKeys . index ((Pawn,Black,A1),(King,White,H8))++castleKey :: (Color, Castle) -> Word64+castleKey (c, s) = Unboxed.unsafeIndex castleKeys $+ index ((Black, Kingside), (White, Queenside)) (opponent c, s)++pieceKeys, castleKeys, epKeys :: Unboxed.Vector Word64+pieceKeys = Unboxed.fromList+ [0x9D39247E33776D41,0x2AF7398005AAA5C7,0x44DB015024623547,0x9C15F73E62A76AE2+ ,0x75834465489C0C89,0x3290AC3A203001BF,0x0FBBAD1F61042279,0xE83A908FF2FB60CA+ ,0x0D7E765D58755C10,0x1A083822CEAFE02D,0x9605D5F0E25EC3B0,0xD021FF5CD13A2ED5+ ,0x40BDF15D4A672E32,0x011355146FD56395,0x5DB4832046F3D9E5,0x239F8B2D7FF719CC+ ,0x05D1A1AE85B49AA1,0x679F848F6E8FC971,0x7449BBFF801FED0B,0x7D11CDB1C3B7ADF0+ ,0x82C7709E781EB7CC,0xF3218F1C9510786C,0x331478F3AF51BBE6,0x4BB38DE5E7219443+ ,0xAA649C6EBCFD50FC,0x8DBD98A352AFD40B,0x87D2074B81D79217,0x19F3C751D3E92AE1+ ,0xB4AB30F062B19ABF,0x7B0500AC42047AC4,0xC9452CA81A09D85D,0x24AA6C514DA27500+ ,0x4C9F34427501B447,0x14A68FD73C910841,0xA71B9B83461CBD93,0x03488B95B0F1850F+ ,0x637B2B34FF93C040,0x09D1BC9A3DD90A94,0x3575668334A1DD3B,0x735E2B97A4C45A23+ ,0x18727070F1BD400B,0x1FCBACD259BF02E7,0xD310A7C2CE9B6555,0xBF983FE0FE5D8244+ ,0x9F74D14F7454A824,0x51EBDC4AB9BA3035,0x5C82C505DB9AB0FA,0xFCF7FE8A3430B241+ ,0x3253A729B9BA3DDE,0x8C74C368081B3075,0xB9BC6C87167C33E7,0x7EF48F2B83024E20+ ,0x11D505D4C351BD7F,0x6568FCA92C76A243,0x4DE0B0F40F32A7B8,0x96D693460CC37E5D+ ,0x42E240CB63689F2F,0x6D2BDCDAE2919661,0x42880B0236E4D951,0x5F0F4A5898171BB6+ ,0x39F890F579F92F88,0x93C5B5F47356388B,0x63DC359D8D231B78,0xEC16CA8AEA98AD76+ ,0x5355F900C2A82DC7,0x07FB9F855A997142,0x5093417AA8A7ED5E,0x7BCBC38DA25A7F3C+ ,0x19FC8A768CF4B6D4,0x637A7780DECFC0D9,0x8249A47AEE0E41F7,0x79AD695501E7D1E8+ ,0x14ACBAF4777D5776,0xF145B6BECCDEA195,0xDABF2AC8201752FC,0x24C3C94DF9C8D3F6+ ,0xBB6E2924F03912EA,0x0CE26C0B95C980D9,0xA49CD132BFBF7CC4,0xE99D662AF4243939+ ,0x27E6AD7891165C3F,0x8535F040B9744FF1,0x54B3F4FA5F40D873,0x72B12C32127FED2B+ ,0xEE954D3C7B411F47,0x9A85AC909A24EAA1,0x70AC4CD9F04F21F5,0xF9B89D3E99A075C2+ ,0x87B3E2B2B5C907B1,0xA366E5B8C54F48B8,0xAE4A9346CC3F7CF2,0x1920C04D47267BBD+ ,0x87BF02C6B49E2AE9,0x092237AC237F3859,0xFF07F64EF8ED14D0,0x8DE8DCA9F03CC54E+ ,0x9C1633264DB49C89,0xB3F22C3D0B0B38ED,0x390E5FB44D01144B,0x5BFEA5B4712768E9+ ,0x1E1032911FA78984,0x9A74ACB964E78CB3,0x4F80F7A035DAFB04,0x6304D09A0B3738C4+ ,0x2171E64683023A08,0x5B9B63EB9CEFF80C,0x506AACF489889342,0x1881AFC9A3A701D6+ ,0x6503080440750644,0xDFD395339CDBF4A7,0xEF927DBCF00C20F2,0x7B32F7D1E03680EC+ ,0xB9FD7620E7316243,0x05A7E8A57DB91B77,0xB5889C6E15630A75,0x4A750A09CE9573F7+ ,0xCF464CEC899A2F8A,0xF538639CE705B824,0x3C79A0FF5580EF7F,0xEDE6C87F8477609D+ ,0x799E81F05BC93F31,0x86536B8CF3428A8C,0x97D7374C60087B73,0xA246637CFF328532+ ,0x043FCAE60CC0EBA0,0x920E449535DD359E,0x70EB093B15B290CC,0x73A1921916591CBD+ ,0x56436C9FE1A1AA8D,0xEFAC4B70633B8F81,0xBB215798D45DF7AF,0x45F20042F24F1768+ ,0x930F80F4E8EB7462,0xFF6712FFCFD75EA1,0xAE623FD67468AA70,0xDD2C5BC84BC8D8FC+ ,0x7EED120D54CF2DD9,0x22FE545401165F1C,0xC91800E98FB99929,0x808BD68E6AC10365+ ,0xDEC468145B7605F6,0x1BEDE3A3AEF53302,0x43539603D6C55602,0xAA969B5C691CCB7A+ ,0xA87832D392EFEE56,0x65942C7B3C7E11AE,0xDED2D633CAD004F6,0x21F08570F420E565+ ,0xB415938D7DA94E3C,0x91B859E59ECB6350,0x10CFF333E0ED804A,0x28AED140BE0BB7DD+ ,0xC5CC1D89724FA456,0x5648F680F11A2741,0x2D255069F0B7DAB3,0x9BC5A38EF729ABD4+ ,0xEF2F054308F6A2BC,0xAF2042F5CC5C2858,0x480412BAB7F5BE2A,0xAEF3AF4A563DFE43+ ,0x19AFE59AE451497F,0x52593803DFF1E840,0xF4F076E65F2CE6F0,0x11379625747D5AF3+ ,0xBCE5D2248682C115,0x9DA4243DE836994F,0x066F70B33FE09017,0x4DC4DE189B671A1C+ ,0x51039AB7712457C3,0xC07A3F80C31FB4B4,0xB46EE9C5E64A6E7C,0xB3819A42ABE61C87+ ,0x21A007933A522A20,0x2DF16F761598AA4F,0x763C4A1371B368FD,0xF793C46702E086A0+ ,0xD7288E012AEB8D31,0xDE336A2A4BC1C44B,0x0BF692B38D079F23,0x2C604A7A177326B3+ ,0x4850E73E03EB6064,0xCFC447F1E53C8E1B,0xB05CA3F564268D99,0x9AE182C8BC9474E8+ ,0xA4FC4BD4FC5558CA,0xE755178D58FC4E76,0x69B97DB1A4C03DFE,0xF9B5B7C4ACC67C96+ ,0xFC6A82D64B8655FB,0x9C684CB6C4D24417,0x8EC97D2917456ED0,0x6703DF9D2924E97E+ ,0xC547F57E42A7444E,0x78E37644E7CAD29E,0xFE9A44E9362F05FA,0x08BD35CC38336615,+ 0x9315E5EB3A129ACE, 0x94061B871E04DF75, 0xDF1D9F9D784BA010, 0x3BBA57B68871B59D,+ 0xD2B7ADEEDED1F73F, 0xF7A255D83BC373F8, 0xD7F4F2448C0CEB81, 0xD95BE88CD210FFA7,+ 0x336F52F8FF4728E7, 0xA74049DAC312AC71, 0xA2F61BB6E437FDB5, 0x4F2A5CB07F6A35B3,+ 0x87D380BDA5BF7859, 0x16B9F7E06C453A21, 0x7BA2484C8A0FD54E, 0xF3A678CAD9A2E38C,+ 0x39B0BF7DDE437BA2, 0xFCAF55C1BF8A4424, 0x18FCF680573FA594, 0x4C0563B89F495AC3,+ 0x40E087931A00930D, 0x8CFFA9412EB642C1, 0x68CA39053261169F, 0x7A1EE967D27579E2,+ 0x9D1D60E5076F5B6F, 0x3810E399B6F65BA2, 0x32095B6D4AB5F9B1, 0x35CAB62109DD038A,+ 0xA90B24499FCFAFB1, 0x77A225A07CC2C6BD, 0x513E5E634C70E331, 0x4361C0CA3F692F12,+ 0xD941ACA44B20A45B, 0x528F7C8602C5807B, 0x52AB92BEB9613989, 0x9D1DFA2EFC557F73,+ 0x722FF175F572C348, 0x1D1260A51107FE97, 0x7A249A57EC0C9BA2, 0x04208FE9E8F7F2D6,+ 0x5A110C6058B920A0, 0x0CD9A497658A5698, 0x56FD23C8F9715A4C, 0x284C847B9D887AAE,+ 0x04FEABFBBDB619CB, 0x742E1E651C60BA83, 0x9A9632E65904AD3C, 0x881B82A13B51B9E2,+ 0x506E6744CD974924, 0xB0183DB56FFC6A79, 0x0ED9B915C66ED37E, 0x5E11E86D5873D484,+ 0xF678647E3519AC6E, 0x1B85D488D0F20CC5, 0xDAB9FE6525D89021, 0x0D151D86ADB73615,+ 0xA865A54EDCC0F019, 0x93C42566AEF98FFB, 0x99E7AFEABE000731, 0x48CBFF086DDF285A,+ 0x7F9B6AF1EBF78BAF, 0x58627E1A149BBA21, 0x2CD16E2ABD791E33, 0xD363EFF5F0977996,+ 0x0CE2A38C344A6EED, 0x1A804AADB9CFA741, 0x907F30421D78C5DE, 0x501F65EDB3034D07,+ 0x37624AE5A48FA6E9, 0x957BAF61700CFF4E, 0x3A6C27934E31188A, 0xD49503536ABCA345,+ 0x088E049589C432E0, 0xF943AEE7FEBF21B8, 0x6C3B8E3E336139D3, 0x364F6FFA464EE52E,+ 0xD60F6DCEDC314222, 0x56963B0DCA418FC0, 0x16F50EDF91E513AF, 0xEF1955914B609F93,+ 0x565601C0364E3228, 0xECB53939887E8175, 0xBAC7A9A18531294B, 0xB344C470397BBA52,+ 0x65D34954DAF3CEBD, 0xB4B81B3FA97511E2, 0xB422061193D6F6A7, 0x071582401C38434D,+ 0x7A13F18BBEDC4FF5, 0xBC4097B116C524D2, 0x59B97885E2F2EA28, 0x99170A5DC3115544,+ 0x6F423357E7C6A9F9, 0x325928EE6E6F8794, 0xD0E4366228B03343, 0x565C31F7DE89EA27,+ 0x30F5611484119414, 0xD873DB391292ED4F, 0x7BD94E1D8E17DEBC, 0xC7D9F16864A76E94,+ 0x947AE053EE56E63C, 0xC8C93882F9475F5F, 0x3A9BF55BA91F81CA, 0xD9A11FBB3D9808E4,+ 0x0FD22063EDC29FCA, 0xB3F256D8ACA0B0B9, 0xB03031A8B4516E84, 0x35DD37D5871448AF,+ 0xE9F6082B05542E4E, 0xEBFAFA33D7254B59, 0x9255ABB50D532280, 0xB9AB4CE57F2D34F3,+ 0x693501D628297551, 0xC62C58F97DD949BF, 0xCD454F8F19C5126A, 0xBBE83F4ECC2BDECB,+ 0xDC842B7E2819E230, 0xBA89142E007503B8, 0xA3BC941D0A5061CB, 0xE9F6760E32CD8021,+ 0x09C7E552BC76492F, 0x852F54934DA55CC9, 0x8107FCCF064FCF56, 0x098954D51FFF6580,+ 0x23B70EDB1955C4BF, 0xC330DE426430F69D, 0x4715ED43E8A45C0A, 0xA8D7E4DAB780A08D,+ 0x0572B974F03CE0BB, 0xB57D2E985E1419C7, 0xE8D9ECBE2CF3D73F, 0x2FE4B17170E59750,+ 0x11317BA87905E790, 0x7FBF21EC8A1F45EC, 0x1725CABFCB045B00, 0x964E915CD5E2B207,+ 0x3E2B8BCBF016D66D, 0xBE7444E39328A0AC, 0xF85B2B4FBCDE44B7, 0x49353FEA39BA63B1,+ 0x1DD01AAFCD53486A, 0x1FCA8A92FD719F85, 0xFC7C95D827357AFA, 0x18A6A990C8B35EBD,+ 0xCCCB7005C6B9C28D, 0x3BDBB92C43B17F26, 0xAA70B5B4F89695A2, 0xE94C39A54A98307F,+ 0xB7A0B174CFF6F36E, 0xD4DBA84729AF48AD, 0x2E18BC1AD9704A68, 0x2DE0966DAF2F8B1C,+ 0xB9C11D5B1E43A07E, 0x64972D68DEE33360, 0x94628D38D0C20584, 0xDBC0D2B6AB90A559,+ 0xD2733C4335C6A72F, 0x7E75D99D94A70F4D, 0x6CED1983376FA72B, 0x97FCAACBF030BC24,+ 0x7B77497B32503B12, 0x8547EDDFB81CCB94, 0x79999CDFF70902CB, 0xCFFE1939438E9B24,+ 0x829626E3892D95D7, 0x92FAE24291F2B3F1, 0x63E22C147B9C3403, 0xC678B6D860284A1C,+ 0x5873888850659AE7, 0x0981DCD296A8736D, 0x9F65789A6509A440, 0x9FF38FED72E9052F,+ 0xE479EE5B9930578C, 0xE7F28ECD2D49EECD, 0x56C074A581EA17FE, 0x5544F7D774B14AEF,+ 0x7B3F0195FC6F290F, 0x12153635B2C0CF57, 0x7F5126DBBA5E0CA7, 0x7A76956C3EAFB413,+ 0x3D5774A11D31AB39, 0x8A1B083821F40CB4, 0x7B4A38E32537DF62, 0x950113646D1D6E03,+ 0x4DA8979A0041E8A9, 0x3BC36E078F7515D7, 0x5D0A12F27AD310D1, 0x7F9D1A2E1EBE1327,+ 0xDA3A361B1C5157B1, 0xDCDD7D20903D0C25, 0x36833336D068F707, 0xCE68341F79893389,+ 0xAB9090168DD05F34, 0x43954B3252DC25E5, 0xB438C2B67F98E5E9, 0x10DCD78E3851A492,+ 0xDBC27AB5447822BF, 0x9B3CDB65F82CA382, 0xB67B7896167B4C84, 0xBFCED1B0048EAC50,+ 0xA9119B60369FFEBD, 0x1FFF7AC80904BF45, 0xAC12FB171817EEE7, 0xAF08DA9177DDA93D,+ 0x1B0CAB936E65C744, 0xB559EB1D04E5E932, 0xC37B45B3F8D6F2BA, 0xC3A9DC228CAAC9E9,+ 0xF3B8B6675A6507FF, 0x9FC477DE4ED681DA, 0x67378D8ECCEF96CB, 0x6DD856D94D259236,+ 0xA319CE15B0B4DB31, 0x073973751F12DD5E, 0x8A8E849EB32781A5, 0xE1925C71285279F5,+ 0x74C04BF1790C0EFE, 0x4DDA48153C94938A, 0x9D266D6A1CC0542C, 0x7440FB816508C4FE,+ 0x13328503DF48229F, 0xD6BF7BAEE43CAC40, 0x4838D65F6EF6748F, 0x1E152328F3318DEA,+ 0x8F8419A348F296BF, 0x72C8834A5957B511, 0xD7A023A73260B45C, 0x94EBC8ABCFB56DAE,+ 0x9FC10D0F989993E0, 0xDE68A2355B93CAE6, 0xA44CFE79AE538BBE, 0x9D1D84FCCE371425,+ 0x51D2B1AB2DDFB636, 0x2FD7E4B9E72CD38C, 0x65CA5B96B7552210, 0xDD69A0D8AB3B546D,+ 0x604D51B25FBF70E2, 0x73AA8A564FB7AC9E, 0x1A8C1E992B941148, 0xAAC40A2703D9BEA0,+ 0x764DBEAE7FA4F3A6, 0x1E99B96E70A9BE8B, 0x2C5E9DEB57EF4743, 0x3A938FEE32D29981,+ 0x26E6DB8FFDF5ADFE, 0x469356C504EC9F9D, 0xC8763C5B08D1908C, 0x3F6C6AF859D80055,+ 0x7F7CC39420A3A545, 0x9BFB227EBDF4C5CE, 0x89039D79D6FC5C5C, 0x8FE88B57305E2AB6,+ 0xA09E8C8C35AB96DE, 0xFA7E393983325753, 0xD6B6D0ECC617C699, 0xDFEA21EA9E7557E3,+ 0xB67C1FA481680AF8, 0xCA1E3785A9E724E5, 0x1CFC8BED0D681639, 0xD18D8549D140CAEA,+ 0x4ED0FE7E9DC91335, 0xE4DBF0634473F5D2, 0x1761F93A44D5AEFE, 0x53898E4C3910DA55,+ 0x734DE8181F6EC39A, 0x2680B122BAA28D97, 0x298AF231C85BAFAB, 0x7983EED3740847D5,+ 0x66C1A2A1A60CD889, 0x9E17E49642A3E4C1, 0xEDB454E7BADC0805, 0x50B704CAB602C329,+ 0x4CC317FB9CDDD023, 0x66B4835D9EAFEA22, 0x219B97E26FFC81BD, 0x261E4E4C0A333A9D,+ 0x1FE2CCA76517DB90, 0xD7504DFA8816EDBB, 0xB9571FA04DC089C8, 0x1DDC0325259B27DE,+ 0xCF3F4688801EB9AA, 0xF4F5D05C10CAB243, 0x38B6525C21A42B0E, 0x36F60E2BA4FA6800,+ 0xEB3593803173E0CE, 0x9C4CD6257C5A3603, 0xAF0C317D32ADAA8A, 0x258E5A80C7204C4B,+ 0x8B889D624D44885D, 0xF4D14597E660F855, 0xD4347F66EC8941C3, 0xE699ED85B0DFB40D,+ 0x2472F6207C2D0484, 0xC2A1E7B5B459AEB5, 0xAB4F6451CC1D45EC, 0x63767572AE3D6174,+ 0xA59E0BD101731A28, 0x116D0016CB948F09, 0x2CF9C8CA052F6E9F, 0x0B090A7560A968E3,+ 0xABEEDDB2DDE06FF1, 0x58EFC10B06A2068D, 0xC6E57A78FBD986E0, 0x2EAB8CA63CE802D7,+ 0x14A195640116F336, 0x7C0828DD624EC390, 0xD74BBE77E6116AC7, 0x804456AF10F5FB53,+ 0xEBE9EA2ADF4321C7, 0x03219A39EE587A30, 0x49787FEF17AF9924, 0xA1E9300CD8520548,+ 0x5B45E522E4B1B4EF, 0xB49C3B3995091A36, 0xD4490AD526F14431, 0x12A8F216AF9418C2,+ 0x001F837CC7350524, 0x1877B51E57A764D5, 0xA2853B80F17F58EE, 0x993E1DE72D36D310,+ 0xB3598080CE64A656, 0x252F59CF0D9F04BB, 0xD23C8E176D113600, 0x1BDA0492E7E4586E,+ 0x21E0BD5026C619BF, 0x3B097ADAF088F94E, 0x8D14DEDB30BE846E, 0xF95CFFA23AF5F6F4,+ 0x3871700761B3F743, 0xCA672B91E9E4FA16, 0x64C8E531BFF53B55, 0x241260ED4AD1E87D,+ 0x106C09B972D2E822, 0x7FBA195410E5CA30, 0x7884D9BC6CB569D8, 0x0647DFEDCD894A29,+ 0x63573FF03E224774, 0x4FC8E9560F91B123, 0x1DB956E450275779, 0xB8D91274B9E9D4FB,+ 0xA2EBEE47E2FBFCE1, 0xD9F1F30CCD97FB09, 0xEFED53D75FD64E6B, 0x2E6D02C36017F67F,+ 0xA9AA4D20DB084E9B, 0xB64BE8D8B25396C1, 0x70CB6AF7C2D5BCF0, 0x98F076A4F7A2322E,+ 0xBF84470805E69B5F, 0x94C3251F06F90CF3, 0x3E003E616A6591E9, 0xB925A6CD0421AFF3,+ 0x61BDD1307C66E300, 0xBF8D5108E27E0D48, 0x240AB57A8B888B20, 0xFC87614BAF287E07,+ 0xEF02CDD06FFDB432, 0xA1082C0466DF6C0A, 0x8215E577001332C8, 0xD39BB9C3A48DB6CF,+ 0x2738259634305C14, 0x61CF4F94C97DF93D, 0x1B6BACA2AE4E125B, 0x758F450C88572E0B,+ 0x959F587D507A8359, 0xB063E962E045F54D, 0x60E8ED72C0DFF5D1, 0x7B64978555326F9F,+ 0xFD080D236DA814BA, 0x8C90FD9B083F4558, 0x106F72FE81E2C590, 0x7976033A39F7D952,+ 0xA4EC0132764CA04B, 0x733EA705FAE4FA77, 0xB4D8F77BC3E56167, 0x9E21F4F903B33FD9,+ 0x9D765E419FB69F6D, 0xD30C088BA61EA5EF, 0x5D94337FBFAF7F5B, 0x1A4E4822EB4D7A59,+ 0x6FFE73E81B637FB3, 0xDDF957BC36D8B9CA, 0x64D0E29EEA8838B3, 0x08DD9BDFD96B9F63,+ 0x087E79E5A57D1D13, 0xE328E230E3E2B3FB, 0x1C2559E30F0946BE, 0x720BF5F26F4D2EAA,+ 0xB0774D261CC609DB, 0x443F64EC5A371195, 0x4112CF68649A260E, 0xD813F2FAB7F5C5CA,+ 0x660D3257380841EE, 0x59AC2C7873F910A3, 0xE846963877671A17, 0x93B633ABFA3469F8,+ 0xC0C0F5A60EF4CDCF, 0xCAF21ECD4377B28C, 0x57277707199B8175, 0x506C11B9D90E8B1D,+ 0xD83CC2687A19255F, 0x4A29C6465A314CD1, 0xED2DF21216235097, 0xB5635C95FF7296E2,+ 0x22AF003AB672E811, 0x52E762596BF68235, 0x9AEBA33AC6ECC6B0, 0x944F6DE09134DFB6,+ 0x6C47BEC883A7DE39, 0x6AD047C430A12104, 0xA5B1CFDBA0AB4067, 0x7C45D833AFF07862,+ 0x5092EF950A16DA0B, 0x9338E69C052B8E7B, 0x455A4B4CFE30E3F5, 0x6B02E63195AD0CF8,+ 0x6B17B224BAD6BF27, 0xD1E0CCD25BB9C169, 0xDE0C89A556B9AE70, 0x50065E535A213CF6,+ 0x9C1169FA2777B874, 0x78EDEFD694AF1EED, 0x6DC93D9526A50E68, 0xEE97F453F06791ED,+ 0x32AB0EDB696703D3, 0x3A6853C7E70757A7, 0x31865CED6120F37D, 0x67FEF95D92607890,+ 0x1F2B1D1F15F6DC9C, 0xB69E38A8965C6B65, 0xAA9119FF184CCCF4, 0xF43C732873F24C13,+ 0xFB4A3D794A9A80D2, 0x3550C2321FD6109C, 0x371F77E76BB8417E, 0x6BFA9AAE5EC05779,+ 0xCD04F3FF001A4778, 0xE3273522064480CA, 0x9F91508BFFCFC14A, 0x049A7F41061A9E60,+ 0xFCB6BE43A9F2FE9B, 0x08DE8A1C7797DA9B, 0x8F9887E6078735A1, 0xB5B4071DBFC73A66,+ 0x230E343DFBA08D33, 0x43ED7F5A0FAE657D, 0x3A88A0FBBCB05C63, 0x21874B8B4D2DBC4F,+ 0x1BDEA12E35F6A8C9, 0x53C065C6C8E63528, 0xE34A1D250E7A8D6B, 0xD6B04D3B7651DD7E,+ 0x5E90277E7CB39E2D, 0x2C046F22062DC67D, 0xB10BB459132D0A26, 0x3FA9DDFB67E2F199,+ 0x0E09B88E1914F7AF, 0x10E8B35AF3EEAB37, 0x9EEDECA8E272B933, 0xD4C718BC4AE8AE5F,+ 0x81536D601170FC20, 0x91B534F885818A06, 0xEC8177F83F900978, 0x190E714FADA5156E,+ 0xB592BF39B0364963, 0x89C350C893AE7DC1, 0xAC042E70F8B383F2, 0xB49B52E587A1EE60,+ 0xFB152FE3FF26DA89, 0x3E666E6F69AE2C15, 0x3B544EBE544C19F9, 0xE805A1E290CF2456,+ 0x24B33C9D7ED25117, 0xE74733427B72F0C1, 0x0A804D18B7097475, 0x57E3306D881EDB4F,+ 0x4AE7D6A36EB5DBCB, 0x2D8D5432157064C8, 0xD1E649DE1E7F268B, 0x8A328A1CEDFE552C,+ 0x07A3AEC79624C7DA, 0x84547DDC3E203C94, 0x990A98FD5071D263, 0x1A4FF12616EEFC89,+ 0xF6F7FD1431714200, 0x30C05B1BA332F41C, 0x8D2636B81555A786, 0x46C9FEB55D120902,+ 0xCCEC0A73B49C9921, 0x4E9D2827355FC492, 0x19EBB029435DCB0F, 0x4659D2B743848A2C,+ 0x963EF2C96B33BE31, 0x74F85198B05A2E7D, 0x5A0F544DD2B1FB18, 0x03727073C2E134B1,+ 0xC7F6AA2DE59AEA61, 0x352787BAA0D7C22F, 0x9853EAB63B5E0B35, 0xABBDCDD7ED5C0860,+ 0xCF05DAF5AC8D77B0, 0x49CAD48CEBF4A71E, 0x7A4C10EC2158C4A6, 0xD9E92AA246BF719E,+ 0x13AE978D09FE5557, 0x730499AF921549FF, 0x4E4B705B92903BA4, 0xFF577222C14F0A3A,+ 0x55B6344CF97AAFAE, 0xB862225B055B6960, 0xCAC09AFBDDD2CDB4, 0xDAF8E9829FE96B5F,+ 0xB5FDFC5D3132C498, 0x310CB380DB6F7503, 0xE87FBB46217A360E, 0x2102AE466EBB1148,+ 0xF8549E1A3AA5E00D, 0x07A69AFDCC42261A, 0xC4C118BFE78FEAAE, 0xF9F4892ED96BD438,+ 0x1AF3DBE25D8F45DA, 0xF5B4B0B0D2DEEEB4, 0x962ACEEFA82E1C84, 0x046E3ECAAF453CE9,+ 0xF05D129681949A4C, 0x964781CE734B3C84, 0x9C2ED44081CE5FBD, 0x522E23F3925E319E,+ 0x177E00F9FC32F791, 0x2BC60A63A6F3B3F2, 0x222BBFAE61725606, 0x486289DDCC3D6780,+ 0x7DC7785B8EFDFC80, 0x8AF38731C02BA980, 0x1FAB64EA29A2DDF7, 0xE4D9429322CD065A,+ 0x9DA058C67844F20C, 0x24C0E332B70019B0, 0x233003B5A6CFE6AD, 0xD586BD01C5C217F6,+ 0x5E5637885F29BC2B, 0x7EBA726D8C94094B, 0x0A56A5F0BFE39272, 0xD79476A84EE20D06,+ 0x9E4C1269BAA4BF37, 0x17EFEE45B0DEE640, 0x1D95B0A5FCF90BC6, 0x93CBE0B699C2585D,+ 0x65FA4F227A2B6D79, 0xD5F9E858292504D5, 0xC2B5A03F71471A6F, 0x59300222B4561E00,+ 0xCE2F8642CA0712DC, 0x7CA9723FBB2E8988, 0x2785338347F2BA08, 0xC61BB3A141E50E8C,+ 0x150F361DAB9DEC26, 0x9F6A419D382595F4, 0x64A53DC924FE7AC9, 0x142DE49FFF7A7C3D,+ 0x0C335248857FA9E7, 0x0A9C32D5EAE45305, 0xE6C42178C4BBB92E, 0x71F1CE2490D20B07,+ 0xF1BCC3D275AFE51A, 0xE728E8C83C334074, 0x96FBF83A12884624, 0x81A1549FD6573DA5,+ 0x5FA7867CAF35E149, 0x56986E2EF3ED091B, 0x917F1DD5F8886C61, 0xD20D8C88C8FFE65F+ ]++castleKeys = Unboxed.fromList+ [ 0x31D71DCE64B2C310, 0xF165B587DF898190+ , 0xA57E6339DD2CF3A0, 0x1EF6E6DBB1961EC9+ ]++epKeys = Unboxed.fromList+ [0x70CC73D90BC26E24,0xE21A6B35DF0C3AD7,0x003A93D8B2806962,0x1C99DED33CB890A1+ ,0xCF3145DE0ADD4289,0xD0E4427A5514FB72,0x77C621CC9FB3A483,0x67A34DAC4356550B+ ]++turnKey :: Word64+turnKey = 0xF8D626AAAF278509
src/Game/Chess/UCI.hs view
@@ -3,42 +3,50 @@ UCIException(..) -- * The Engine data type , Engine, name, author- -- * Starting and quitting a UCI engine+ -- * Starting a UCI engine , start, start'-, send-, quit, quit' -- * Engine options-, Option(..), options, getOption, setOptionSpinButton+, Option(..), options, getOption, setOptionSpinButton, setOptionString -- * Manipulating the current game information-, currentPosition, setPosition, addMove, move- -- * Reading engine output-, Info(..), readInfo, tryReadInfo-, readBestMove, tryReadBestMove+, isready+, currentPosition, setPosition, addPly+ -- * The Info data type+, Info(..), Score(..), Bounds(..)+ -- * Searching+, search, searching+, SearchParam+, searchmoves, timeleft, timeincrement, movestogo, movetime, nodes, depth, infinite+, stop+ -- * Quitting+, 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 Control.Monad.IO.Class import Data.Attoparsec.Combinator import Data.Attoparsec.ByteString.Char8+import Data.ByteString.Builder import Data.ByteString.Char8 (ByteString) import qualified Data.ByteString.Char8 as BS+import Data.Foldable 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 Data.String (IsString(..)) import Game.Chess-import System.Exit+import Numeric.Natural+import System.Exit (ExitCode) import System.IO import System.Process-import System.Timeout (timeout)+import Time.Rational+import Time.Units data Engine = Engine { inH :: Handle@@ -50,33 +58,24 @@ , author :: Maybe ByteString , options :: HashMap ByteString Option , isReady :: MVar ()+, isSearching :: IORef Bool , infoChan :: TChan [Info]-, bestMoveChan :: TChan (Move, Maybe Move)-, game :: IORef (Position, [Move])+, bestMoveChan :: TChan (Ply, Maybe Ply)+, game :: IORef (Position, [Ply]) } -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- -- | Set the starting position of the current game, also clearing any -- pre-existing history.-setPosition :: Engine -> Position -> IO ()-setPosition e@Engine{game} p = do- atomicWriteIORef game (p, [])+setPosition :: MonadIO m+ => Engine -> Position+ -> m (Position, [Ply])+ -- ^ the game previously in progress+setPosition e@Engine{game} p = liftIO $ do+ oldGame <- atomicModifyIORef' game ((p, []),) sendPosition e+ pure oldGame -data UCIException = SANError String- | IllegalMove Move- deriving Show+data UCIException = IllegalMove Ply deriving Show instance Exception UCIException @@ -86,47 +85,55 @@ | UCIOk | ReadyOK | Info [Info]- | BestMove !(Move, (Maybe Move))+ | BestMove !(Ply, Maybe Ply) deriving (Show) -data Info = PV [Move]+data Info = PV [Ply] | Depth Int | SelDepth Int- | Time Int+ | Elapsed (Time Millisecond) | MultiPV Int- | Score Int- | UpperBound- | LowerBound+ | Score Score (Maybe Bounds) | Nodes Int | NPS Int | TBHits Int | HashFull Int- | CurrMove ByteString+ | CurrMove Ply | CurrMoveNumber Int- deriving Show+ | String ByteString+ deriving (Eq, Show) +data Score = CentiPawns Int+ | MateIn Int+ deriving (Eq, Ord, Show)+ +data Bounds = UpperBound | LowerBound deriving (Eq, Show)++ data Option = CheckBox Bool | ComboBox { comboBoxValue :: ByteString, comboBoxValues :: [ByteString] } | SpinButton { spinButtonValue, spinButtonMinBound, spinButtonMaxBound :: Int }- | String ByteString+ | OString ByteString | Button deriving (Eq, Show) instance IsString Option where- fromString = String . BS.pack+ fromString = OString . BS.pack command :: Position -> Parser Command-command pos = skipSpace *> choice [- name, author, option, uciok, readyok, info, bestmove+command pos = skipSpace *> choice+ [ "id" `kv` name+ , "id" `kv` author+ , "option" `kv` option+ , "uciok" $> UCIOk+ , "readyok" $> ReadyOK+ , "info" `kv` fmap Info (sepBy1 infoItem skipSpace)+ , "bestmove" `kv` bestmove ] <* skipSpace where- name = fmap Name $- "id" *> skipSpace *> "name" *> skipSpace *> takeByteString- author = fmap Author $- "id" *> skipSpace *> "author" *> skipSpace *> takeByteString+ name = Name <$> kv "name" takeByteString+ author = Author <$> kv "author" takeByteString option = do- void "option"- skipSpace void "name" skipSpace optName <- BS.pack <$> manyTill anyChar (skipSpace *> "type")@@ -151,77 +158,77 @@ <*> takeByteString pure $ ComboBox def (map BS.pack vars <> [lastVar]) var = skipSpace *> "var" *> skipSpace- str = fmap String $+ str = fmap OString $ "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')+ infoItem = Depth <$> kv "depth" decimal+ <|> SelDepth <$> kv "seldepth" decimal+ <|> MultiPV <$> kv "multipv" decimal+ <|> kv "score" score+ <|> Nodes <$> kv "nodes" decimal+ <|> NPS <$> kv "nps" decimal+ <|> HashFull <$> kv "hashfull" decimal+ <|> TBHits <$> kv "tbhits" decimal+ <|> Elapsed . ms . fromIntegral <$> kv "time" decimal+ <|> kv "pv" pv+ <|> kv "currmove" currmove+ <|> CurrMoveNumber <$> kv "currmovenumber" decimal+ <|> String <$> kv "string" takeByteString+ score = do+ s <- kv "cp" (CentiPawns <$> signed decimal)+ <|> kv "mate" (MateIn <$> signed decimal)+ b <- optional $ skipSpace *> ( UpperBound <$ "upperbound"+ <|> LowerBound <$ "lowerbound"+ )+ pure $ Score s b+ pv = fmap (PV . snd) $ foldM toPly (pos, []) =<< sepBy mv skipSpace+ toPly (pos, xs) s = case fromUCI pos s of+ Just m -> pure (doPly pos m, xs <> [m])+ Nothing -> fail $ "Failed to parse move " <> s+ currmove = fmap (fromUCI pos) mv >>= \case+ Just m -> pure $ CurrMove m+ Nothing -> fail "Failed to parse move"++ mv = BS.unpack . fst <$> match (sq *> sq *> optional (satisfy p)) where+ sq = satisfy (inRange ('a', 'h')) *> satisfy (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)+ m <- mv+ ponder <- optional (skipSpace *> kv "ponder" 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'))+ Just p -> case fromUCI (doPly 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+ kv k v = k *> skipSpace *> v -- | Start a UCI engine with the given executable name and command line arguments. start :: String -> [String] -> IO (Maybe Engine)-start = start' 2000000 putStrLn+start = start' (sec 2) putStrLn -- | Start a UCI engine with the given timeout for initialisation. -- -- If the engine takes more then the given microseconds to answer to the -- initialisation request, 'Nothing' is returned and the external process -- will be terminated.-start' :: Int -> (String -> IO ()) -> String -> [String] -> IO (Maybe Engine)-start' usec outputStrLn cmd args = do+start' :: KnownDivRat unit Microsecond => Time unit -> (String -> IO ()) -> String -> [String] -> IO (Maybe Engine)+start' tout 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+ newEmptyMVar <*> newIORef False <*>+ newBroadcastTChanIO <*> newBroadcastTChanIO <*>+ newIORef (startpos, [])+ send e "uci"+ timeout tout (initialise e) >>= \case Just e' -> do tid <- forkIO . infoReader $ e' pure . Just $ e' { infoThread = Just tid }@@ -232,7 +239,7 @@ 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+ Left _ -> do outputStrLn . BS.unpack $ l initialise c Right (Name n) -> initialise (c { name = Just n })@@ -245,97 +252,152 @@ l <- BS.hGetLine outH pos <- currentPosition e case parseOnly (command pos <* endOfInput) l of- Left err -> do- outputStrLn $ err <> ":" <> show l+ Left err -> outputStrLn $ err <> ":" <> show l Right ReadyOK -> putMVar isReady () Right (Info i) -> atomically $ writeTChan infoChan i- Right (BestMove bm) -> atomically $ writeTChan bestMoveChan bm+ Right (BestMove bm) -> do+ writeIORef isSearching False+ atomically $ writeTChan bestMoveChan bm -- | Wait until the engine is ready to take more commands. isready :: Engine -> IO () isready e@Engine{isReady} = do- send "isready" e+ send e "isready" takeMVar isReady --- | Send a command to the engine.------ This function is likely going to be removed and replaced by more specific--- functions in the future.-send :: ByteString -> Engine -> IO ()-send s Engine{inH, procH} = do- BS.hPutStrLn inH s+send :: Engine -> Builder -> IO ()+send Engine{inH, procH} b = do+ hPutBuilder inH (b <> "\n") getProcessExitCode procH >>= \case Nothing -> pure () Just ec -> throwIO ec +data SearchParam = SearchMoves [Ply]+ -- ^ restrict search to the specified moves only+ | TimeLeft Color (Time Millisecond)+ -- ^ time (in milliseconds) left on the clock+ | TimeIncrement Color (Time Millisecond)+ -- ^ time increment per move in milliseconds+ | MovesToGo Natural+ -- ^ number of moves to the next time control+ | MoveTime (Time Millisecond)+ | MaxNodes Natural+ | MaxDepth Natural+ | Infinite+ -- ^ search until 'stop' gets called+ deriving (Eq, Show)+ +searchmoves :: [Ply] -> SearchParam+searchmoves = SearchMoves++timeleft, timeincrement :: KnownDivRat unit Millisecond+ => Color -> Time unit -> SearchParam+timeleft c = TimeLeft c . toUnit+timeincrement c = TimeIncrement c . toUnit++movestogo :: Natural -> SearchParam+movestogo = MovesToGo++movetime :: KnownDivRat unit Millisecond => Time unit -> SearchParam+movetime = MoveTime . toUnit++nodes, depth :: Natural -> SearchParam+nodes = MaxNodes+depth = MaxDepth++infinite :: SearchParam+infinite = Infinite++searching :: MonadIO m => Engine -> m Bool+searching Engine{isSearching} = liftIO $ readIORef isSearching++-- | Instruct the engine to begin searching.+search :: MonadIO m+ => Engine -> [SearchParam]+ -> m (TChan (Ply, Maybe Ply), TChan [Info])+search e@Engine{isSearching} params = liftIO $ do+ chans <- atomically $ (,) <$> dupTChan (bestMoveChan e)+ <*> dupTChan (infoChan e)+ send e . fold . intersperse " " $ "go" : foldr build mempty params+ writeIORef isSearching True+ pure chans+ where+ build (SearchMoves ms) xs = "searchmoves" : (fromString . toUCI <$> ms) <> xs+ build (TimeLeft White (floor . unTime -> x)) xs = "wtime" : integerDec x : xs+ build (TimeLeft Black (floor . unTime -> x)) xs = "btime" : integerDec x : xs+ build (TimeIncrement White (floor . unTime -> x)) xs = "winc" : integerDec x : xs+ build (TimeIncrement Black (floor . unTime -> x)) xs = "binc" : integerDec x : xs+ build (MovesToGo x) xs = "movestogo" : naturalDec x : xs+ build (MoveTime (floor . unTime -> x)) xs = "movetime" : integerDec x : xs+ build (MaxNodes x) xs = "nodes" : naturalDec x : xs+ build (MaxDepth x) xs = "depth" : naturalDec x : xs+ build Infinite xs = "infinite" : xs+ naturalDec = integerDec . toInteger++-- | Stop a search in progress.+stop :: MonadIO m => Engine -> m ()+stop e = liftIO $ send e "stop"+ getOption :: ByteString -> Engine -> Maybe Option getOption n = HashMap.lookup n . options -- | Set a spin option to a particular value. -- -- Bounds are validated. Make sure you don't set a value which is out of range.-setOptionSpinButton :: ByteString -> Int -> Engine -> IO Engine+setOptionSpinButton :: MonadIO m => ByteString -> Int -> Engine -> m 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+ = liftIO $ do+ send c $ "setoption name " <> byteString n <> " value " <> intDec v pure $ c { options = HashMap.update (set v) n $ options c } where set v opt@SpinButton{} = Just $ opt { spinButtonValue = v } +setOptionString :: MonadIO m => ByteString -> ByteString -> Engine -> m Engine+setOptionString n v e = liftIO $ do+ send e $ "setoption name " <> byteString n <> " value " <> byteString v+ pure $ e { options = HashMap.update (set v) n $ options e }+ where+ set v _ = Just $ OString v+ -- | Return the final position of the currently active game.-currentPosition :: Engine -> IO Position-currentPosition Engine{game} =- uncurry (foldl' applyMove) <$> readIORef game+currentPosition :: MonadIO m => Engine -> m Position+currentPosition Engine{game} = liftIO $+ uncurry (foldl' doPly) <$> 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 --- | Add the given move (in algebraic notation) to the current game.-move :: Engine -> String -> IO ()-move e@Engine{game} s = do- pos <- currentPosition e- case fromUCI pos s of- Just m -> do- addMove e m- sendPosition e- Nothing -> case fromSAN pos s of- Left err -> throwIO $ SANError err- Right m -> do- addMove e m- sendPosition e- -- | Add a 'Move' to the game history. -- -- This function checks if the move is actually legal, and throws a 'UCIException' -- if it isn't.-addMove :: Engine -> Move -> IO ()-addMove e@Engine{game} m = do+addPly :: MonadIO m => Engine -> Ply -> m ()+addPly e@Engine{game} m = liftIO $ do pos <- currentPosition e- if m `elem` moves pos- then atomicModifyIORef' game \g -> (fmap (<> [m]) g, ())- else throwIO $ IllegalMove m- + if m `notElem` legalPlies pos then throwIO $ IllegalMove m else do+ atomicModifyIORef' game $ \g -> (fmap (<> [m]) g, ())+ sendPosition e+ 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)+sendPosition e@Engine{game} = readIORef game >>= send e . cmd where+ cmd (p, h) = fold . intersperse " " $+ "position" : "fen" : fromString (toFEN p) : line h+ line [] = []+ line h = "moves" : (fromString . toUCI <$> h) -- | Quit the engine.-quit :: Engine -> IO (Maybe ExitCode)-quit = quit' 1000000+quit :: MonadIO m => Engine -> m (Maybe ExitCode)+quit = quit' (sec 1) -quit' :: Int -> Engine -> IO (Maybe ExitCode)-quit' usec c@Engine{procH, infoThread} = (pure . Just) `handle` do+quit' :: (KnownDivRat unit Microsecond, MonadIO m)+ => Time unit -> Engine -> m (Maybe ExitCode)+quit' t e@Engine{procH, infoThread} = liftIO $ (pure . Just) `handle` do maybe (pure ()) killThread infoThread- send "quit" c- timeout usec (waitForProcess procH) >>= \case+ send e "quit"+ timeout t (waitForProcess procH) >>= \case Just ec -> pure $ Just ec Nothing -> terminateProcess procH $> Nothing
test/Perft.hs view
@@ -3,6 +3,7 @@ import Control.Parallel.Strategies import Data.Foldable import Data.Maybe+import Data.Monoid import Data.Time.Clock import Data.Traversable import Game.Chess@@ -47,14 +48,15 @@ perft :: Int -> Position -> PerftResult perft 0 _ = PerftResult 1-perft 1 p = PerftResult . fromIntegral . length $- 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) $- unsafeApplyMove p <$> moves p+perft 1 p = PerftResult . fromIntegral . length $ legalPlies p+perft n p+ | n < 4+ = foldMap (perft (pred n) . unsafeDoPly p) $ legalPlies p+ | otherwise+ = fold . parMap rdeepseq (perft (pred n) . unsafeDoPly p) $ legalPlies p runTestSuite :: [(Position, [(Int, PerftResult)])] -> IO (Maybe PerftResult)-runTestSuite = fmap fold . traverse (uncurry (test mempty)) where+runTestSuite = fmap (getAp . foldMap Ap) . traverse (uncurry (test mempty)) where test sum pos ((depth, expected) : more) | result == expected = do