chessIO-0.8.0.0: src/Game/Chess/Polyglot.hs
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TemplateHaskell #-}
module Game.Chess.Polyglot (
-- * Data type
PolyglotBook, BookEntry(..), beKey, bePly, beWeight, beLearn
-- * Built-in books
, defaultBook, twic
-- * Load and save
, fromByteString, toByteString
, readPolyglotFile, writePolyglotFile
, makeBook, toPGN
-- * Lookup
, bookPly
, bookPlies
, bookForest
, variations
, findPosition, hashPosition
) where
import Control.Arrow (Arrow ((&&&)))
import Control.Lens (makeLenses, (%~))
import Control.Monad.ST (ST, runST)
import Control.Monad.Random (Rand)
import qualified Control.Monad.Random as Rand
import Data.Bits (Bits (shiftL, shiftR, (.|.)))
import Data.ByteString (ByteString)
import qualified Data.ByteString as BS
import qualified Data.ByteString.Internal as BS
import Data.FileEmbed (embedFile)
import Data.Foldable (fold)
import Data.Hashable (Hashable)
import Data.List (sort)
import Data.Ord (Down (Down))
import Data.String (IsString (fromString))
import Data.Tree (Tree (Node), foldTree)
import Data.Vector.Instances ()
import qualified Data.Vector.Storable as VS
import Data.Word (Word16, Word32, Word64, Word8)
import Foreign.ForeignPtr (castForeignPtr, plusForeignPtr)
import Foreign.Storable (Storable (alignment, peek, poke, pokeElemOff, sizeOf))
import GHC.Generics (Generic)
import GHC.Ptr (Ptr, castPtr, plusPtr)
import Game.Chess.Internal (Ply (..), unpack, move, Color(..), Position (color, halfMoveClock), canCastleQueenside, canCastleKingside, wKscm, bKscm, wQscm, bQscm,
doPly, startpos, toFEN,
unsafeDoPly)
import Game.Chess.Internal.Square
import Game.Chess.PGN (Outcome (Undecided), PGN (..),
gameFromForest, weightedForest)
import Game.Chess.Polyglot.Hash (hashPosition)
import System.Random (RandomGen)
data BookEntry a = BE {
_beKey :: {-# UNPACK #-} !Word64
, _bePly :: !a
, _beWeight :: {-# UNPACK #-} !Word16
, _beLearn :: {-# UNPACK #-} !Word32
} deriving (Eq, Functor, Generic, Show)
instance Hashable a => Hashable (BookEntry a)
makeLenses ''BookEntry
instance Ord a => Ord (BookEntry a) where
compare (BE k1 p1 w1 _) (BE k2 p2 w2 _) =
k1 `compare` k2 <> Down w1 `compare` Down w2 <> p1 `compare` p2
instance Storable (BookEntry Word16) where
sizeOf _ = 16
alignment _ = alignment (undefined :: Word64)
peek ptr = BE <$> peekBE (castPtr ptr)
<*> peekBE (castPtr ptr `plusPtr` 8)
<*> peekBE (castPtr ptr `plusPtr` 10)
<*> peekBE (castPtr ptr `plusPtr` 12)
poke ptr BE { .. } = do
pokeBE (castPtr ptr) _beKey
pokeBE (castPtr ptr `plusPtr` 8) _bePly
pokeBE (castPtr ptr `plusPtr` 10) _beWeight
pokeBE (castPtr ptr `plusPtr` 12) _beLearn
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 !v !n = pokeElemOff p (n-1) (fromIntegral v) *> go (v `shiftR` 8) (n-1)
defaultBook, twic :: PolyglotBook
defaultBook = twic
twic = fromByteString $(embedFile "book/twic-9g.bin")
-- | A Polyglot opening book.
newtype PolyglotBook = Book (VS.Vector (BookEntry Word16)) deriving (Eq, Hashable)
instance Semigroup PolyglotBook where
Book a <> Book b = fromList . VS.toList $ a <> b
instance Monoid PolyglotBook where
mempty = Book mempty
-- | Create a PolyglotBook from a ByteString.
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)
toByteString :: PolyglotBook -> ByteString
toByteString (Book v) = BS.fromForeignPtr (castForeignPtr fptr) off (len * elemSize)
where
(fptr, off, len) = VS.unsafeToForeignPtr v
elemSize = sizeOf (undefined `asTypeOf` VS.head v)
readPolyglotFile :: FilePath -> IO PolyglotBook
readPolyglotFile = fmap fromByteString . BS.readFile
writePolyglotFile :: FilePath -> PolyglotBook -> IO ()
writePolyglotFile fp = BS.writeFile fp . toByteString
fromList :: [BookEntry Word16] -> PolyglotBook
fromList = Book . VS.fromList . sort
--toList :: PolyglotBook -> [BookEntry Word16]
--toList (Book v) = VS.toList v
toPGN :: PolyglotBook -> Position -> PGN
toPGN b p = PGN [gameFromForest meta (bookForest b p) Undecided] where
meta | p == startpos = []
| otherwise = [("FEN", fromString $ toFEN p)]
makeBook :: PGN -> PolyglotBook
makeBook = fromList . concatMap (foldTree f . annot startpos) . weightedForest
where
annot pos (Node a ts) =
Node (pos, a) $ annot (unsafeDoPly pos (snd a)) <$> ts
f (pos, (w, pl)) xs
| w > 0
= BE (hashPosition pos) (fromPly pos pl) (floor w) 0 : concat xs
| otherwise
= concat xs
bookForest :: PolyglotBook -> Position -> [Tree Ply]
bookForest b = (fmap . fmap) (snd . head) . forest [] where
forest pls p = tree pls p <$> filter (not . seen pls) (plies p)
tree pls p (pl, p') = Node pls' $ forest pls' p' where pls' = (p, pl) : pls
plies p = f <$> bookPlies b p where f (_bePly -> pl) = (pl, doPly p pl)
seen pls (_, p') = p' `elem` map fst pls
-- | Pick a random ply from the book.
bookPly :: RandomGen g => PolyglotBook -> Position -> Maybe (Rand g Ply)
bookPly b pos = case findPosition b pos of
[] -> Nothing
l -> Just . Rand.fromList $ map (_bePly &&& fromIntegral . _beWeight) l
-- | Probe the book for all plies known for the given position.
bookPlies :: PolyglotBook -> Position -> [BookEntry Ply]
bookPlies b pos
| halfMoveClock pos > 150 = []
| otherwise = findPosition b pos
-- | Predicted Variations. Return the most popular game.
variations :: PolyglotBook -> Position -> [[Ply]]
variations b = concatMap (foldTree f) . bookForest b where
f a [] = [[a]]
f a xs = (a :) <$> fold xs
findPosition :: PolyglotBook -> Position -> [BookEntry Ply]
findPosition (Book v) pos =
(fmap . fmap) (toPly pos) .
VS.toList .
VS.takeWhile ((hash ==) . _beKey) .
VS.unsafeDrop (lowerBound hash) $ v
where
hash = hashPosition pos
lowerBound = bsearch (_beKey . 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 - lo) `div` 2)
toPly :: Position -> Word16 -> Ply
toPly pos pl@(unpack . Ply -> (src, dst, _)) = case color pos of
White | src == E1
, canCastleKingside pos
, dst == H1
-> wKscm
| src == E1
, canCastleQueenside pos
, dst == A1
-> wQscm
Black | src == E8
, canCastleKingside pos
, dst == H8
-> bKscm
| src == E8
, canCastleQueenside pos
, dst == A8
-> bQscm
_ -> Ply pl
fromPly :: Position -> Ply -> Word16
fromPly pos pl@(unpack -> (src, dst, _)) = unPly $ case color pos of
White | src == E1
, canCastleKingside pos
, dst == G1
-> src `move` H1
| src == E1
, canCastleQueenside pos
, dst == C1
-> src `move` A1
Black | src == E8
, canCastleKingside pos
, dst == G8
-> src `move` H8
| src == E8
, canCastleQueenside pos
, dst == C8
-> src `move` A8
_ -> pl