hcheckers-0.1.0.2: src/Formats/Pdn.hs
{-# LANGUAGE DeriveDataTypeable #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE RecordWildCards #-}
module Formats.Pdn where
import Control.Monad
import Control.Monad.State
import Data.Char
import Data.Maybe
import Data.List
import qualified Data.Map as M
import qualified Data.Text as T
import Text.Megaparsec hiding (Label, State)
import Text.Megaparsec.Char
import qualified Data.Text.IO as TIO
import Text.Printf
import Core.Types
import Core.Board
import Rules.Diagonal
import Rules.English
import Rules.International
import Rules.Canadian
import Rules.Russian
import Rules.Simple
import Rules.Spancirety
import Formats.Types
import Formats.Fen
pLabel :: SomeRules -> Parser Label
pLabel (SomeRules rules) = do
let letters = ['a' .. 'l']
word <- some $ oneOf $ letters ++ map toUpper letters ++ ['0'..'9']
case parseNotation rules (T.pack word) of
Left err -> fail err
Right label -> return label
pSemiMove :: SomeRules -> Parser SemiMoveRec
pSemiMove rules = try full <|> try short
where
short = do
from <- pLabel rules
x <- oneOf ['-', 'x']
let capture = (x == 'x')
to <- pLabel rules
return $ ShortSemiMoveRec from to capture
full = do
first <- pLabel rules
char 'x'
second <- pLabel rules
char 'x'
rest <- pLabel rules `sepBy1` char 'x'
return $ FullSemiMoveRec (first : second : rest)
whitespace :: Parser ()
whitespace = label "white space or comment" $ do
some $ try pComment <|> try pNag <|> space1
return ()
pNag :: Parser ()
pNag = label "NAG" $ do
char '$'
some digitChar
return ()
pComment :: Parser ()
pComment = between (char '{') (char '}') $ do
skipSome $ noneOf ['}']
pMove :: SomeRules -> Parser MoveRec
pMove rules = do
n <- some digitChar
char '.'
whitespace
first <- pSemiMove rules
whitespace
second <- optional $ try (pSemiMove rules)
return $ MoveRec (Just first) second
pInstruction :: SomeRules -> Parser Instruction
pInstruction rules =
(try pSetSecondMoveNr) <|> (try pSetMoveNr) <|>
(try $ SemiMove `fmap` pSemiMove rules) <|> (try $ pVariant rules)
pSetMoveNr :: Parser Instruction
pSetMoveNr = do
n <- some digitChar
char '.'
return $ SetMoveNumber (read n)
pSetSecondMoveNr :: Parser Instruction
pSetSecondMoveNr = do
n <- some digitChar
char '.'
char '.'
char '.'
return $ SetSecondMoveNumber (read n)
pVariant :: SomeRules -> Parser Instruction
pVariant rules = between (char '(') (char ')') $ do
optional whitespace
instructions <- try (pInstruction rules) `sepEndBy` whitespace
return $ Variant instructions
pResult :: Parser (Maybe GameResult)
pResult =
(try $ string "*" >> return Nothing)
<|> (try $ string "1-0" >> return (Just FirstWin))
<|> (try $ string "2-0" >> return (Just FirstWin))
<|> (try $ string "0-1" >> return (Just SecondWin))
<|> (try $ string "0-2" >> return (Just SecondWin))
<|> (try $ string "1/2-1/2" >> return (Just Draw))
<|> (try $ string "1-1" >> return (Just Draw))
pText :: Parser T.Text
pText = between (char '"') (char '"') $ do
str <- many $ noneOf ['"']
return $ T.pack str
pTag :: Parser Tag
pTag = do
tag <- choice $ map try [pEvent, pSite, pDate, pRound, pWhite, pBlack, pResultTag, pFenTag, pGameTypeTag, pOpening, pUnknown]
eol
return tag
where
pTag tag name parser = between (char '[') (char ']') $ do
string name
space1
value <- parser
return $ tag value
textTag tag name = pTag tag name pText
pEvent = textTag Event "Event"
pSite = textTag Site "Site"
pDate = textTag Date "Date"
pRound = textTag Round "Round"
pWhite = textTag White "White"
pBlack = textTag Black "Black"
pResultTag = pTag Result "Result" $ between (char '"') (char '"') pResult
pFenTag = do
mbRules <- lift get
case mbRules of
Nothing -> fail "cant apply FEN: rules are not defined"
Just rules -> pTag FEN "FEN" $ between (char '"') (char '"') (pFen rules)
pGameTypeTag = do
tag@(GameType rules) <- pTag GameType "GameType" $ between (char '"') (char '"') pGameType
lift $ put $ Just rules
return tag
pOpening = textTag Opening "Opening"
pUnknown = between (char '[') (char ']') $ do
name <- some alphaNumChar
space1
value <- pText
return $ Unknown (T.pack name) value
pGameType :: Parser SomeRules
pGameType = do
n <- some digitChar
case n of
"20" -> return $ SomeRules international
"21" -> return $ SomeRules english
"25" -> return $ SomeRules russian
"27" -> return $ SomeRules canadian
"41" -> return $ SomeRules spancirety
"42" -> return $ SomeRules diagonal
"43" -> return $ SomeRules simple
_ -> fail $ "Unsupported rules: " ++ n
titleFromTags :: [Tag] -> Maybe T.Text
titleFromTags [] = Nothing
titleFromTags (Event title:_) = Just title
titleFromTags (_: rest) = titleFromTags rest
rulesFromTags :: [Tag] -> Maybe SomeRules
rulesFromTags [] = Nothing
rulesFromTags (GameType r:_) = Just r
rulesFromTags (_:rest) = rulesFromTags rest
pGame :: Maybe SomeRules -> Parser GameRecord
pGame dfltRules = do
tags <- some (try pTag)
case rulesFromTags tags `mplus` dfltRules of
Nothing -> fail "Rules are not defined"
Just rules -> do
eol
optional whitespace
moves <- try (pInstruction rules) `sepEndBy` whitespace
result <- pResult
return $ GameRecord tags moves result
parsePdn :: Maybe SomeRules -> T.Text -> Either String GameRecord
parsePdn dfltRules text =
case evalState (runParserT (pGame dfltRules) "<PDN>" text) dfltRules of
Left err -> Left $ errorBundlePretty err
Right pdn -> Right pdn
parsePdnFile :: Maybe SomeRules -> FilePath -> IO [GameRecord]
parsePdnFile dfltRules path = do
text <- TIO.readFile path
case evalState (runParserT ((pGame dfltRules) `sepEndBy` space1) path text) dfltRules of
Left err -> fail $ errorBundlePretty err
Right pdn -> return pdn
parseMoveRec :: GameRules rules => rules -> Side -> Board -> SemiMoveRec -> Either Error Move
parseMoveRec rules side board rec =
let moves = possibleMoves rules side board
passedFields m = nonCaptureLabels rules side board (pmMove m)
suits m =
case rec of
ShortSemiMoveRec {..} ->
aLabel (pmBegin m) == smrFrom &&
aLabel (pmEnd m) == smrTo &&
(not $ null $ pmVictims m) == smrCapture
FullSemiMoveRec {..} ->
(not $ null $ pmVictims m) &&
smrLabels `isSubsequenceOf` passedFields m
in case filter suits moves of
[m] -> return $ pmMove m
[] -> Left $ NoSuchMoveExt (show rec) side (boardRep board) (map (moveRep rules side . pmMove) moves)
ms -> Left $ AmbigousPdnMove (show rec) (show ms) (boardRep board)
fenFromTags :: [Tag] -> Maybe Fen
fenFromTags [] = Nothing
fenFromTags (FEN fen:_) = Just fen
fenFromTags (_:rest) = fenFromTags rest
initBoardFromTags :: SupervisorState -> SomeRules -> [Tag] -> Board
initBoardFromTags rnd (SomeRules rules) tags =
case fenFromTags tags of
Nothing -> initBoard rnd rules
Just fen -> parseBoardRep rnd rules $ fenToBoardRep fen
resultFromTags :: [Tag] -> Maybe GameResult
resultFromTags [] = Nothing
resultFromTags (Result result : _) = result
resultFromTags (_ : rest) = resultFromTags rest
data InterpreterState = InterpreterState {
isCurrentVariant :: Int
, isLastVariant :: Int
, isCurrentMove :: Int
, isCurrentSide :: Side
, isVariants :: M.Map Int (M.Map Int MoveRec)
}
type Interpreter a = State InterpreterState a
interpret :: Instruction -> Interpreter ()
interpret (SetMoveNumber n) =
modify $ \st -> st {isCurrentMove = n, isCurrentSide = First}
interpret (SetSecondMoveNumber n) =
modify $ \st -> st {isCurrentMove = n, isCurrentSide = Second}
interpret (SemiMove rec) = do
side <- gets isCurrentSide
variant <- gets isCurrentVariant
moveNr <- gets isCurrentMove
modify $ \st ->
let updateVariant (Just moves) = Just $ M.alter setMove moveNr moves
updateVariant Nothing = Just $ M.singleton moveNr singleMove
singleMove :: MoveRec
singleMove =
case side of
First -> MoveRec (Just rec) Nothing
Second -> MoveRec Nothing (Just rec)
setMove :: Maybe MoveRec -> Maybe MoveRec
setMove Nothing = Just singleMove
setMove (Just old) =
case side of
First -> Just $ old {mrFirst = Just rec}
Second -> Just $ old {mrSecond = Just rec}
in st {isVariants = M.alter updateVariant variant (isVariants st)}
when (side == First) $
modify $ \st -> st {isCurrentSide = Second}
interpret (Variant instructions) = do
src <- gets isCurrentVariant
v <- gets isLastVariant
init <- gets (fromJust . M.lookup src . isVariants)
modify $ \st -> st {
isLastVariant = v+1,
isCurrentVariant = v+1,
isVariants = M.insert (v+1) init (isVariants st)
}
forM_ instructions interpret
modify $ \st -> st {isCurrentVariant = src}
instructionsToMoves :: [Instruction] -> [[MoveRec]]
instructionsToMoves instructions =
let initState = InterpreterState 0 0 0 First M.empty
state = execState (forM_ instructions interpret) initState
in map M.elems $ M.elems $ isVariants state
loadPdn :: SupervisorState -> GameRecord -> Either Error ([HistoryRecord], Board)
loadPdn rnd r = do
let findRules [] = Nothing
findRules (GameType rules:_) = Just rules
findRules (_:rest) = findRules rest
withRules :: SomeRules -> Either Error ([HistoryRecord], Board)
withRules some@(SomeRules rules) = do
let board0 = initBoardFromTags rnd some (grTags r)
go :: ([HistoryRecord], Board) -> [MoveRec] -> Either Error ([HistoryRecord], Board)
go (history, board) [] = return (history, board)
go (history, board0) (moveRec : rest) = do
(records1, board1) <-
case mrFirst moveRec of
Just rec -> do
move1 <- parseMoveRec rules First board0 rec
let (board1,_,_) = applyMove rules First move1 board0
record = HistoryRecord First move1 board0
return ([record], board1)
Nothing -> return ([], board0)
case mrSecond moveRec of
Nothing -> return (records1 ++ history, board1)
Just rec -> do
move2 <- parseMoveRec rules Second board1 rec
let (board2,_,_) = applyMove rules Second move2 board1
record = HistoryRecord Second move2 board1
go ([record] ++ records1 ++ history, board2) rest
case instructionsToMoves (grMoves r) of
[moves] -> go ([], board0) moves
vars -> Left $ AmbigousPdnInstruction $ show vars
case findRules (grTags r) of
Nothing -> Left $ Unhandled "rules are not specified"
Just rules -> withRules rules
moveToInstructions :: Int -> MoveRec -> [Instruction]
moveToInstructions n move =
[SetMoveNumber n]
++ case mrFirst move of
Nothing -> []
Just rec -> [SemiMove rec]
++ case mrSecond move of
Nothing -> []
Just rec -> [SemiMove rec]
movesToInstructions :: [MoveRec] -> [Instruction]
movesToInstructions moves = concat $ zipWith moveToInstructions [1..] moves
gameToPdn :: SupervisorState -> Game -> GameRecord
gameToPdn rnd game =
GameRecord {
grTags = tags
, grMoves = moves
, grResult = result
}
where
result = case gStatus game of
Ended result -> Just result
_ -> Nothing
tags = [Event "HCheckers game", GameType (gRules game)]
moves = movesToInstructions $ translate (gRules game) board0 (reverse $ gsHistory $ gState game)
board0 = case gRules game of
SomeRules rules -> initBoard rnd rules
translate :: SomeRules -> Board -> [HistoryRecord] -> [MoveRec]
translate _ _ [] = []
translate rules board [r] = [MoveRec (Just $ translateMove rules First board $ hrMove r) Nothing]
translate some@(SomeRules rules) board0 (r1:r2:rest) =
let m1 = translateMove some First board0 $ hrMove r1
(board1,_,_) = applyMove rules First (hrMove r1) board0
m2 = translateMove some Second board1 $ hrMove r2
(board2,_,_) = applyMove rules Second (hrMove r2) board1
rec = MoveRec (Just m1) (Just m2)
in rec : translate some board2 rest
translateMove :: SomeRules -> Side -> Board -> Move -> SemiMoveRec
translateMove (SomeRules rules) side board move =
ShortSemiMoveRec {
smrFrom = aLabel (moveBegin move)
, smrTo = aLabel (moveEnd rules side board move)
, smrCapture = isCaptureM move
}
showPdn :: SomeRules -> GameRecord -> T.Text
showPdn (SomeRules rules) gr =
T.unlines (map showTag $ grTags gr) <> "\n" <>
T.unlines (zipWith showMove [1..] (head $ instructionsToMoves $ grMoves gr)) <> "\n" <>
showResult (grResult gr)
where
showResult Nothing = "*"
showResult (Just FirstWin) = "1-0"
showResult (Just SecondWin) = "0-1"
showResult (Just Draw) = "1/2-1/2"
showMove n (MoveRec (Just s1) Nothing) = T.pack (show n) <> ". " <> showSemiMove s1
showMove n (MoveRec (Just s1) (Just s2)) = T.pack (show n) <> ". " <> showSemiMove s1 <> " " <> showSemiMove s2
showSemiMove (ShortSemiMoveRec from to False) =
boardNotation rules from <> "-" <> boardNotation rules to
showSemiMove (ShortSemiMoveRec from to True) =
boardNotation rules from <> "x" <> boardNotation rules to
showTag (Event text) = T.pack (printf "[Event \"%s\"]" text)
showTag (Site text) = T.pack (printf "[Site \"%s\"]" text)
showTag (Date text) = T.pack (printf "[Date \"%s\"]" text)
showTag (Round text) = T.pack (printf "[Round \"%s\"]" text)
showTag (White text) = T.pack (printf "[White \"%s\"]" text)
showTag (Black text) = T.pack (printf "[Black \"%s\"]" text)
showTag (SetUp text) = T.pack (printf "[SetUp \"%s\"]" text)
showTag (Opening text) = T.pack (printf "[Opening \"%s\"]" text)
showTag (FEN fen) = T.pack (printf "[FEN \"%s\"]" (showFen (boardSize rules) fen))
showTag (GameType _) = T.pack (printf "[GameType \"%s\"]" (pdnId rules))
showTag (Unknown tag text) = T.pack (printf "[%s \"%s\"]" tag text)
pdnInfo :: GameRecord -> PdnInfo
pdnInfo gr = PdnInfo {
pdnTitle = titleFromTags (grTags gr)
, pdnRules = someRulesName (rulesFromTags $ grTags gr)
, pdnResult = grResult gr
, pdnNextMove = getNextMove (last $ head $ instructionsToMoves $ grMoves gr)
}
where
someRulesName Nothing = Nothing
someRulesName (Just (SomeRules rules)) = Just (rulesName rules)
getNextMove r =
case mrSecond r of
Nothing -> Second
_ -> First