chessIO-0.3.0.0: src/Game/Chess/PGN.hs
{-# 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