hcheckers 0.1.0.1 → 0.1.0.2
raw patch · 38 files changed
+1114/−1335 lines, 38 filesdep +Globdep +list-tdep +modern-uri
Dependencies added: Glob, list-t, modern-uri, req
Files
- hcheckers.cabal +10/−4
- src/AI.hs +7/−5
- src/AI/AlphaBeta.hs +40/−42
- src/AI/AlphaBeta/Cache.hs +46/−146
- src/AI/AlphaBeta/Persistent.hs +64/−486
- src/AI/AlphaBeta/Types.hs +5/−10
- src/Battle.hs +202/−39
- src/Core/Board.hs +34/−2
- src/Core/BoardMap.hs +14/−25
- src/Core/CmdLine.hs +12/−1
- src/Core/Config.hs +4/−1
- src/Core/Evaluator.hs +10/−27
- src/Core/Game.hs +9/−1
- src/Core/Json.hs +14/−0
- src/Core/Parallel.hs +32/−8
- src/Core/Rest.hs +0/−338
- src/Core/Supervisor.hs +48/−9
- src/Core/Types.hs +54/−10
- src/Formats/Compact.hs +0/−1
- src/Formats/Fen.hs +0/−2
- src/Formats/Pdn.hs +17/−15
- src/Formats/Types.hs +0/−1
- src/Learn.hs +3/−13
- src/Main.hs +48/−113
- src/Rest/Battle.hs +51/−0
- src/Rest/Common.hs +139/−0
- src/Rest/Game.hs +235/−0
- src/Rules/Armenian.hs +4/−5
- src/Rules/Brazilian.hs +1/−3
- src/Rules/Canadian.hs +1/−3
- src/Rules/Czech.hs +1/−3
- src/Rules/Diagonal.hs +1/−3
- src/Rules/English.hs +1/−3
- src/Rules/International.hs +1/−3
- src/Rules/Russian.hs +1/−3
- src/Rules/Simple.hs +1/−3
- src/Rules/Spancirety.hs +1/−3
- src/Rules/Turkish.hs +3/−4
hcheckers.cabal view
@@ -4,10 +4,10 @@ -- -- see: https://github.com/sol/hpack ----- hash: 635560919d6a9de9e4c5b6a6b7f636cc443c21aa7be4d1d72d363288b4404426+-- hash: b7d691d6b82b6e15d986f5c2f31becaaeb09f9c1ea620d4a27ee842c5067f771 name: hcheckers-version: 0.1.0.1+version: 0.1.0.2 synopsis: Implementation of checkers ("draughts") board game - server application description: Please see the README on GitHub at <https://github.com/githubuser/hcheckers#readme> category: Games@@ -52,7 +52,6 @@ Core.Logging Core.Monitoring Core.Parallel- Core.Rest Core.Supervisor Core.Types Formats.Compact@@ -60,6 +59,9 @@ Formats.Pdn Formats.Types Learn+ Rest.Battle+ Rest.Common+ Rest.Game Rules.Armenian Rules.Brazilian Rules.Canadian@@ -77,7 +79,8 @@ src ghc-options: -threaded -rtsopts -with-rtsopts=-N -O2 -fwarn-unused-imports build-depends:- aeson+ Glob+ , aeson , array , base >=4.7 && <5 , binary@@ -99,8 +102,10 @@ , heavy-logger , hsyslog , http-types+ , list-t , megaparsec , microlens+ , modern-uri , monad-metrics , mtl , mwc-random@@ -110,6 +115,7 @@ , random , random-access-file , random-shuffle+ , req , scotty , stm , stm-containers
src/AI.hs view
@@ -8,15 +8,11 @@ module AI where -import Data.Maybe import Data.Default import Data.Aeson import Core.Types-import Core.Board-import Core.Logging-import Core.Supervisor-import Core.Evaluator+import Core.Supervisor () -- import instances only import AI.AlphaBeta.Types loadAi :: (GameRules rules) => String -> rules -> FilePath -> IO (AlphaBeta rules (EvaluatorForRules rules))@@ -28,4 +24,10 @@ let rules' = SomeRules rules ai = AlphaBeta def rules (dfltEvaluator rules) return $ updateAi ai value++parseAi :: (GameRules rules) => rules -> Value -> AlphaBeta rules (EvaluatorForRules rules)+parseAi rules json =+ let rules' = SomeRules rules+ ai = AlphaBeta def rules (dfltEvaluator rules)+ in updateAi ai json
src/AI/AlphaBeta.hs view
@@ -34,20 +34,14 @@ import Core.Types import Core.Board-import Core.BoardMap (labelSetMember)+import Core.BoardMap import Core.Game import Core.Parallel import Core.Logging import qualified Core.Monitoring as Monitoring import AI.AlphaBeta.Types import AI.AlphaBeta.Cache--chunksOf :: Int -> [a] -> [[a]]-chunksOf n list- | length list <= n = [list]- | otherwise =- let (first, other) = splitAt n list- in first : chunksOf n other+import AI.AlphaBeta.Persistent concatE :: [Int] -> [Either e [a]] -> [Either e a] concatE _ [] = []@@ -87,7 +81,7 @@ Object evalV = toJSON eval in Object $ H.union paramsV evalV -instance (GameRules rules, Evaluator eval) => GameAi (AlphaBeta rules eval) where+instance (GameRules rules, VectorEvaluator eval, ToJSON eval) => GameAi (AlphaBeta rules eval) where type AiStorage (AlphaBeta rules eval) = AICacheHandle rules eval @@ -95,8 +89,8 @@ cache <- loadAiCache scoreMoveGroup ai return cache - saveAiStorage (AlphaBeta params rules _) cache = do- -- saveAiCache rules params cache+ saveAiStorage (AlphaBeta params rules _) handle = do+ saveAiData rules (aichData handle) return () resetAiStorage ai cache = do@@ -114,8 +108,8 @@ aiName _ = "default" -instance (GameRules rules, VectorEvaluator eval) => VectorAi (AlphaBeta rules eval) where- type VectorAiSupport (AlphaBeta rules eval) r = (VectorEvaluatorSupport eval r, rules ~ r)+instance (GameRules rules, VectorEvaluator eval, ToJSON eval) => VectorAi (AlphaBeta rules eval) where+ type VectorAiSupport (AlphaBeta rules eval) r = (rules ~ r) aiToVector (AlphaBeta params rules eval) = aiVector V.++ evalVector where@@ -147,7 +141,7 @@ eval = evalFromVector rules v' -- | Calculate score of one possible move.-scoreMove :: (GameRules rules, Evaluator eval) => ScoreMoveInput rules eval -> Checkers MoveAndScore+scoreMove :: (GameRules rules, VectorEvaluator eval) => ScoreMoveInput rules eval -> Checkers MoveAndScore scoreMove (ScoreMoveInput {..}) = do let AlphaBeta params rules eval = smiAi score <- Monitoring.timed "ai.score.move" $ do@@ -162,7 +156,7 @@ return $ MoveAndScore smiMove score -scoreMoveGroup :: (GameRules rules, Evaluator eval) => [ScoreMoveInput rules eval] -> Checkers [MoveAndScore]+scoreMoveGroup :: (GameRules rules, VectorEvaluator eval) => [ScoreMoveInput rules eval] -> Checkers [MoveAndScore] scoreMoveGroup inputs = go worst [] inputs where input0 = head inputs@@ -229,24 +223,25 @@ -- return result class Monad m => EvalMoveMonad m where- checkPrimeVariation :: (GameRules rules, Evaluator eval) => AICacheHandle rules eval -> AlphaBetaParams -> Board -> DepthParams -> m (Maybe PerBoardData)+ checkPrimeVariation :: (GameRules rules, VectorEvaluator eval) => AICacheHandle rules eval -> eval -> AlphaBetaParams -> Board -> DepthParams -> m (Maybe PerBoardData) getKillerMove :: Int -> m (Maybe MoveAndScore) instance EvalMoveMonad Checkers where- checkPrimeVariation var params board dp = do- lookupAiCache params board dp var+ checkPrimeVariation var eval params board dp = do+ lookupAiCache eval params board dp var getKillerMove _ = return Nothing -- ScoreM instance instance EvalMoveMonad (StateT (ScoreState rules eval) Checkers) where- checkPrimeVariation var params board dp = do- lift $ lookupAiCache params board dp var+ checkPrimeVariation var eval params board dp = do+ lift $ lookupAiCache eval params board dp var getKillerMove = getGoodMove -evalMove :: (EvalMoveMonad m, GameRules rules, Evaluator eval)- => AlphaBetaParams+evalMove :: (EvalMoveMonad m, GameRules rules, VectorEvaluator eval)+ => eval+ -> AlphaBetaParams -> AICacheHandle rules eval -> Side -> DepthParams@@ -254,8 +249,8 @@ -> Maybe PossibleMove -> LabelSet -> PossibleMove -> m Int-evalMove params var side dp board mbPrevMove attacked move = do- prime <- checkPrimeVariation var params board dp+evalMove eval params var side dp board mbPrevMove attacked move = do+ prime <- checkPrimeVariation var eval params board dp let victimFields = pmVictims move -- nVictims = sum $ map victimWeight victimFields promotion = if isPromotion move then 1 else 0@@ -304,8 +299,9 @@ signedScore = if maximize then score else -score return $ fromIntegral signedScore -sortMoves :: (EvalMoveMonad m, GameRules rules, Evaluator eval)- => AlphaBetaParams+sortMoves :: (EvalMoveMonad m, GameRules rules, VectorEvaluator eval)+ => eval+ -> AlphaBetaParams -> AICacheHandle rules eval -> Side -> DepthParams@@ -313,12 +309,12 @@ -> Maybe PossibleMove -> [PossibleMove] -> m [PossibleMove]-sortMoves params var side dp board mbPrevMove moves = do+sortMoves eval params var side dp board mbPrevMove moves = do -- if length moves >= 4 -- then do let rules = aichRules var attacked = boardAttacked side board- interest <- mapM (evalMove params var side dp board mbPrevMove attacked) moves+ interest <- mapM (evalMove eval params var side dp board mbPrevMove attacked) moves if any (/= 0) interest then return $ map fst $ sortOn (negate . snd) $ zip moves interest else return moves@@ -779,7 +775,7 @@ return (goodMoves, maxScore) -- | Calculate score of the board-doScore :: (GameRules rules, Evaluator eval)+doScore :: (GameRules rules, VectorEvaluator eval) => rules -> eval -> AICacheHandle rules eval@@ -793,7 +789,7 @@ -> Checkers Score doScore rules eval var params gameId side dp board alpha beta = do initState <- mkInitState- out <- evalStateT (cachedScoreAB var params input) initState+ out <- evalStateT (cachedScoreAB var eval params input) initState return $ soScore out where input = ScoreInput side dp alpha beta board Nothing @@ -812,19 +808,20 @@ -- | Calculate score of the board. -- This uses the cache. It is called in the recursive call also.-cachedScoreAB :: forall rules eval. (GameRules rules, Evaluator eval)+cachedScoreAB :: forall rules eval. (GameRules rules, VectorEvaluator eval) => AICacheHandle rules eval+ -> eval -> AlphaBetaParams -> ScoreInput -> ScoreM rules eval ScoreOutput-cachedScoreAB var params input = do+cachedScoreAB var eval params input = do let depth = dpCurrent dp side = siSide input board = siBoard input dp = siDepth input alpha = siAlpha input beta = siBeta input- mbItem <- lift $ lookupAiCache params board dp var+ mbItem <- lift $ lookupAiCache eval params board dp var mbCached <- case mbItem of Just item -> do let cachedScore = itemScore item@@ -844,7 +841,7 @@ case mbCached of Just out -> return out Nothing -> do- out <- Monitoring.timed "ai.score.board" $ scoreAB var params input+ out <- Monitoring.timed "ai.score.board" $ scoreAB var eval params input let score = soScore out bound | score <= alpha = Alpha@@ -856,8 +853,8 @@ item = PerBoardData (dpLast dp) score bound item' = PerBoardData (dpLast dp) (negate score) bound when (bound == Exact && soQuiescene out && not (dpStaticMode dp)) $ do- lift $ putAiCache params board item var- lift $ putAiCache params (flipBoard board) item' var+ lift $ putAiCache eval params board item var+ lift $ putAiCache eval params (flipBoard board) item' var return out -- | Check if target depth is reached@@ -927,12 +924,13 @@ -- | Calculate score for the board. -- This implements the alpha-beta section algorithm itself.-scoreAB :: forall rules eval. (GameRules rules, Evaluator eval)+scoreAB :: forall rules eval. (GameRules rules, VectorEvaluator eval) => AICacheHandle rules eval+ -> eval -> AlphaBetaParams -> ScoreInput -> ScoreM rules eval ScoreOutput-scoreAB var params input+scoreAB var eval params input | alpha == beta = do $verbose "Alpha == Beta == {}, return it" (Single $ show alpha) quiescene <- checkQuiescene@@ -985,7 +983,7 @@ rules <- gets ssRules dp' <- updateDepth params moves dp let prevMove = siPrevMove input- moves' <- sortMoves params var side dp board prevMove moves+ moves' <- sortMoves eval params var side dp board prevMove moves -- let depths = correspondingDepths (length moves') score0 quiescene dp' let depths = repeat dp' out <- iterateMoves $ zip3 [1..] moves' depths@@ -1120,9 +1118,9 @@ let inputs = [input {siAlpha = alpha, siBeta = beta} | (alpha, beta) <- intervals] go inputs where- go [input] = cachedScoreAB var params input+ go [input] = cachedScoreAB var eval params input go (input : inputs) = do- out <- cachedScoreAB var params input+ out <- cachedScoreAB var eval params input let score = soScore out if maximize && score >= beta || minimize && score <= alpha then go inputs@@ -1156,7 +1154,7 @@ , siBoard = markAttacked rules $ applyMoveActions (pmResult move) board , siDepth = dp }- out <- cachedScoreAB var params input'+ out <- cachedScoreAB var eval params input' let score = soScore out $verbose "{}| score for side {}: {}" (indent, show side, show score)
src/AI/AlphaBeta/Cache.hs view
@@ -13,24 +13,13 @@ resetAiCache ) where -import Control.Monad+import Control.Concurrent import Control.Monad.State import Control.Monad.Reader-import Control.Monad.Catch-import Control.Concurrent import Control.Concurrent.STM-import Control.Exception (SomeException)-import qualified Control.Concurrent.ReadWriteLock as RWL+import qualified StmContainers.Map as SM import qualified Data.Map as M-import qualified Data.HashPSQ as PQ-import Data.Maybe import Data.Text.Format.Heavy (Single (..))-import System.FilePath-import System.Environment-import System.Directory-import System.Posix.IO-import qualified System.IO.RandomAccessFile as File-import System.Clock import System.Log.Heavy import System.Log.Heavy.TH @@ -42,6 +31,28 @@ import AI.AlphaBeta.Types import AI.AlphaBeta.Persistent +putAiData :: VectorEvaluator eval => AIData -> eval -> Board -> PerBoardData -> Checkers ()+putAiData aiData eval board item =+ liftIO $ atomically $ do+ perEval <- readTVar aiData+ let vec = evalToVector eval+ forEval <- case M.lookup vec perEval of+ Just map -> return map+ Nothing -> do+ map <- SM.new+ writeTVar aiData $ M.insert vec map perEval+ return map+ putBoardMapWith' forEval (<>) board item++lookupAiData :: VectorEvaluator eval => AIData -> eval -> Board -> Checkers (Maybe PerBoardData)+lookupAiData aiData eval board =+ liftIO $ atomically $ do+ perEval <- readTVar aiData+ let vec = evalToVector eval+ case M.lookup vec perEval of+ Nothing -> return Nothing+ Just forEval -> lookupBoardMap' forEval board+ -- | Prepare AI storage instance. -- This also contains Processor instance with several threads. loadAiCache :: (GameRules rules, Evaluator eval)@@ -52,58 +63,9 @@ let getKey inputs = map smiIndex inputs aiCfg <- asks (gcAiConfig . csConfig) processor <- runProcessor (aiThreads aiCfg) getKey scoreMove- cache <- liftIO newTBoardMap- cachePath <- do- home <- liftIO $ getEnv "HOME"- let directory = home </> ".cache" </> "hcheckers" </> rulesName rules </> "ai.cache"- liftIO $ createDirectoryIfMissing True directory- return directory- let indexPath = cachePath </> "index"- dataPath = cachePath </> "data"-- writeQueue <- liftIO $ atomically newTChan- cleanupQueue <- liftIO $ atomically $ newTVar PQ.empty- load <- asks (aiLoadCache . gcAiConfig . csConfig)- store <- asks (aiStoreCache . gcAiConfig . csConfig)- let mbMode- | store = Just ReadWrite- | load = Just ReadOnly- | otherwise = Nothing- (indexFile, dataFile, exist) <- case mbMode of- Nothing -> return (Nothing, Nothing, False)- Just mode -> do- indexExists <- liftIO $ doesFileExist indexPath- dataExists <- liftIO $ doesFileExist dataPath- let exist = dataExists && indexExists- if load && not store && not exist- then return (Nothing, Nothing, exist)- else liftIO $ do- let page = 1024*1024- params = File.MMapedParams page True- indexFd <- File.initFile params indexPath- dataFd <- File.initFile params dataPath- return (Just indexFd, Just dataFd, exist)- when (isJust mbMode) $- $info "Opened cache: {}" (Single cachePath)+ cache <- loadAiData rules st <- ask- indexLock <- liftIO RWL.new- dataLock <- liftIO RWL.new- indexBlockLocks <- liftIO $ atomically $ newTVar M.empty- dataBlockLocks <- liftIO $ atomically $ newTVar M.empty-- let indexHandle = case indexFile of- Nothing -> Nothing- Just fd -> Just $ FHandle {- fhOffset = 0,- fhHandle = fd- }- let dataHandle = case dataFile of- Nothing -> Nothing- Just fd -> Just $ FHandle {- fhOffset = 0,- fhHandle = fd- } counts <- liftIO $ atomically $ newTVar $ BoardCounts 50 50 50 50 moves <- liftIO newTBoardMap scoreShift <- liftIO $ atomically $ newTVar M.empty@@ -115,40 +77,20 @@ aichProcessor = processor, aichPossibleMoves = moves, aichLastMoveScoreShift = scoreShift,- aichWriteQueue = writeQueue,- aichCleanupQueue = cleanupQueue,- aichCurrentCounts = counts,- aichIndexFile = indexHandle,- aichDataFile = dataHandle+ aichCurrentCounts = counts }- when (store && isJust indexFile && not exist) $ do- runStorage handle initFile- when store $- forkCheckers $ cacheDumper rules params handle- -- forkCheckers $ cacheCleaner handle - return handle--cacheDumper :: (GameRules rules, Evaluator eval) => rules -> AlphaBetaParams -> AICacheHandle rules eval -> Checkers ()-cacheDumper rules params handle = do- store <- asks (aiStoreCache . gcAiConfig . csConfig)- when store $ forever $ do- repeatTimed "write" 30 $ do- -- threadDelay $ 100*1000- mbRecord <- liftIO $ atomically $ checkWriteQueue (aichWriteQueue handle)- case mbRecord of- Nothing -> return False- Just (board, value) -> do- Monitoring.increment "cache.records.writen"- runStorage handle $- putRecordFile board value- return True+ save <- asks (aiStoreCache . gcAiConfig . csConfig)+ when save $+ forkCheckers $ aiStorageSaver rules cache - liftIO $ threadDelay $ 30 * 1000 * 1000+ return handle -cacheCleaner :: AICacheHandle rules eval -> Checkers ()-cacheCleaner handle = forever $ do- liftIO $ threadDelay $ 30 * 1000 * 1000+aiStorageSaver :: GameRules rules => rules -> AIData -> Checkers ()+aiStorageSaver rules aiData = do+ saveAiData rules aiData+ liftIO $ threadDelay $ 10 * 1000 * 1000+ aiStorageSaver rules aiData normalize :: BoardSize -> (BoardCounts,BoardKey,Side) -> (BoardCounts,BoardKey,Side) normalize bsize (bc,bk,side) =@@ -160,41 +102,23 @@ -- | Look up for item in the cache. First lookup in the memory, -- then in the file (if it is open).-lookupAiCache :: (GameRules rules, Evaluator eval) => AlphaBetaParams -> Board -> DepthParams -> AICacheHandle rules eval -> Checkers (Maybe PerBoardData)-lookupAiCache params board depth handle = do+lookupAiCache :: (GameRules rules, VectorEvaluator eval) => eval -> AlphaBetaParams -> Board -> DepthParams -> AICacheHandle rules eval -> Checkers (Maybe PerBoardData)+lookupAiCache eval params board depth handle = do mbItem <- lookupMemory board case mbItem of Just item -> do Monitoring.increment "cache.hit.memory" return $ Just item Nothing -> do- mbFile <- lookupFile' board depth- case mbFile of- Nothing -> do- Monitoring.increment "cache.miss"- return Nothing- Just file -> do- putAiCache params board file handle- return $ Just file-+ Monitoring.increment "cache.miss"+ return Nothing where - avg :: Stats -> Score- avg s =- let Score n p = statsSumScore s- cnt = statsCount s- in Score (n `div` cnt) (p `div` cnt)-- checkStats :: Stats -> Maybe Stats- checkStats s- | statsCount s < 10 = Nothing- | otherwise = Just s- lookupMemory :: Board -> Checkers (Maybe PerBoardData) lookupMemory board = Monitoring.timed "cache.lookup.memory" $ do cfg <- asks (gcAiConfig . csConfig) let cache = aichData handle- mbItem <- liftIO $ lookupBoardMap cache board + mbItem <- lookupAiData cache eval board case mbItem of Nothing -> return Nothing Just item@(PerBoardData {..}) ->@@ -202,48 +126,24 @@ then return $ Just item else return Nothing - lookupFile' :: Board -> DepthParams -> Checkers (Maybe PerBoardData)- lookupFile' board depth = do- load <- asks (aiLoadCache . gcAiConfig . csConfig)- if load- then do- let bc = calcBoardCounts board- let total = bcFirstMen bc + bcSecondMen bc + bcFirstKings bc + bcSecondKings bc- cfg <- asks (gcAiConfig . csConfig)- if {-total <= aiUseCacheMaxPieces cfg &&-} dpTarget depth >= aiUseCacheMaxDepth cfg- then runStorage handle (lookupFile board depth)- `catch`- \(e :: SomeException) -> do- $reportError "Exception: lookupFile: {}" (Single $ show e)- return Nothing- else return Nothing- else return Nothing- -- | Put an item to the cache. -- It is always writen to the memory, -- and it is writen to the file if it is open.-putAiCache :: GameRules rules => AlphaBetaParams -> Board -> StorageValue -> AICacheHandle rules eval -> Checkers ()-putAiCache params board newItem handle = do+putAiCache :: (GameRules rules, VectorEvaluator eval) => eval -> AlphaBetaParams -> Board -> StorageValue -> AICacheHandle rules eval -> Checkers ()+putAiCache eval params board newItem handle = do let bc = calcBoardCounts board let bsize = boardSize (aichRules handle) let total = bcFirstMen bc + bcSecondMen bc + bcFirstKings bc + bcSecondKings bc let depth = itemDepth newItem cfg <- asks (gcAiConfig . csConfig)- let needWriteFile = {-total <= aiUpdateCacheMaxPieces cfg &&-} depth > aiUpdateCacheMaxDepth cfg Monitoring.timed "cache.put.memory" $ do- now <- liftIO $ getTime RealtimeCoarse Monitoring.increment "cache.records.put"- fileCacheEnabled <- asks (aiStoreCache . gcAiConfig . csConfig) let cache = aichData handle- liftIO $ putBoardMapWith cache (<>) board newItem-- liftIO $ atomically $ do- when (fileCacheEnabled && needWriteFile) $- putWriteQueue (aichWriteQueue handle) (board, newItem)- -- putCleanupQueue (aichCleanupQueue handle) (bc, bk) now+ putAiData cache eval board newItem resetAiCache :: AICacheHandle rules eval -> Checkers ()-resetAiCache handle= do+resetAiCache handle = do let cache = aichData handle- liftIO $ resetBoardMap cache+ liftIO $ atomically $+ writeTVar cache M.empty
src/AI/AlphaBeta/Persistent.hs view
@@ -9,24 +9,19 @@ {-# LANGUAGE TemplateHaskell #-} module AI.AlphaBeta.Persistent- (lookupFile,- putRecordFile,- putStatsFile,- putWriteQueue,- checkWriteQueue,- initFile,- -- checkDataFile',- loadIndexIO- ) where+ where import Control.Monad import Control.Monad.State+import Control.Monad.Reader+import qualified ListT import Control.Monad.Catch (catch, SomeException) import Control.Concurrent.STM-import qualified Data.HashPSQ as PQ+import qualified StmContainers.Map as SM import qualified Data.ByteString as B import qualified Data.ByteString.Lazy as BL import qualified Data.Map as M+import qualified Data.Vector as V import Data.Maybe import Data.Word import Data.Text.Format.Heavy@@ -38,12 +33,16 @@ import Text.Printf import GHC.Generics import System.Posix.Types-import qualified System.IO.RandomAccessFile as File import System.Log.Heavy import System.Log.Heavy.TH+import System.Environment+import System.FilePath+import System.FilePath.Glob+import System.Directory import Core.Types import Core.Board+import Core.BoardMap import qualified Core.Monitoring as Monitoring import AI.AlphaBeta.Types @@ -83,484 +82,63 @@ Data.Store.VarSize fn -> fromIntegral $ fn a Data.Store.ConstSize n -> fromIntegral n -unexistingBlock :: IndexBlockNumber-unexistingBlock = maxBound--putCleanupQueue :: CleanupQueue -> QueueKey -> TimeSpec -> STM ()-putCleanupQueue var key now = modifyTVar var $ \queue ->- let cleanupDelay = TimeSpec {sec=10, nsec=0}- in PQ.insert key (now + cleanupDelay) () queue--checkCleanupQueue :: CleanupQueue -> TimeSpec -> STM (Maybe QueueKey)-checkCleanupQueue var now = do- queue <- readTVar var- case PQ.minView queue of- Nothing -> return Nothing- Just (key, time, _, queue') ->- if time < now- then do- writeTVar var queue'- return $ Just key- else return Nothing--putWriteQueue :: WriteQueue -> (Board, StorageValue) -> STM ()-putWriteQueue = writeTChan--checkWriteQueue :: WriteQueue -> STM (Maybe (Board, StorageValue))-checkWriteQueue = tryReadTChan--getFd :: FileType -> Storage FileDescriptor-getFd file = do- fh <- getFh file- return $ fhHandle fh--getFh :: FileType -> Storage FHandle-getFh file = do- let selector = case file of- IndexFile -> ssIndex- DataFile -> ssData- st <- get- case selector st of- Nothing -> fail "getFh: file is not open"- Just fh -> return fh--updateFh :: FileType -> (FHandle -> FHandle) -> Storage ()-updateFh IndexFile fn = do- st <- get- case ssIndex st of- Nothing -> fail "updateFh: index file is not open"- Just fh -> put $ st {ssIndex = Just $ fn fh}-updateFh DataFile fn = do- st <- get- case ssData st of- Nothing -> fail "updateFh: data file is not open"- Just fh -> put $ st {ssData = Just $ fn fh}--tell :: FileType -> Storage FileOffset-tell file = do- fh <- getFh file- return $ fhOffset fh--seek :: FileType -> FileOffset -> Storage ()-seek file offset = do- updateFh file $ \fh -> fh {fhOffset = offset}--readBytes :: FileType -> ByteCount -> Storage B.ByteString-readBytes file size = do- fd <- getFd file- currentOffset <- tell file- result <- liftIO $ File.readBytes fd (fromIntegral currentOffset) (fromIntegral size)- seek file $ fromIntegral size + currentOffset- return result- -writeBytes :: FileType -> B.ByteString -> Storage ()-writeBytes file bstr = do- fd <- getFd file- currentOffset <- tell file- result <- liftIO $ File.writeBytes fd (fromIntegral currentOffset) bstr- let size = B.length bstr- seek file $ fromIntegral size + currentOffset--flush :: Storage ()-flush = return ()--- fd <- getFd--- handle <- liftIO $ fdToHandle fd--- liftIO $ hFlush handle--isEof :: Storage Bool-isEof = return False--- fd <- getFd--- handle <- liftIO $ fdToHandle fd--- liftIO $ hIsEOF handle---- | Read an item of appropriate type from file handle.--- Warning: this cannot work with data types which size is variable.-readData :: forall a. Data.Store.Store a => FileType -> Storage a-readData file = do- bstr <- readBytes file (fromIntegral $ sizeOf (error "unknown data size to read!" :: a))- when (B.null bstr) $ do- offset <- tell file- fail $ "readData: unexpected EOF, offset " ++ show offset- liftIO $ Data.Store.decodeIO bstr--writeData :: forall a. Data.Store.Store a => FileType -> a -> Storage ()-writeData file a = do- let bstr = Data.Store.encode a- writeBytes file bstr---- | Read an item of appropriate type from file handle.--- Assumes there is data size in Word16 format before data itself.-readDataSized :: forall a. Data.Store.Store a => FileType -> Storage a-readDataSized file = do- size <- readData file :: Storage Word16- bstr <- readBytes file (fromIntegral size)- when (B.null bstr) $ do- offset <- tell file- fail $ "readDataSized: zero data size, offset " ++ show offset- liftIO $ Data.Store.decodeIO bstr--writeDataSized :: forall a. Data.Store.Store a => FileType -> a -> Storage ()-writeDataSized file a = do- let bstr = Data.Store.encode a- let size = (fromIntegral $ B.length bstr) :: Word16- writeData file size- writeBytes file bstr--dataHeaderSize :: FileOffset-dataHeaderSize = fromIntegral $ sizeOf (0 :: DataBlockNumber)--indexHeaderSize :: FileOffset-indexHeaderSize = fromIntegral $ sizeOf (0 :: IndexBlockNumber)--indexRecordSize :: FileOffset-indexRecordSize = fromIntegral (sizeOf (0 :: DataBlockNumber) + sizeOf (0 :: IndexBlockNumber))--indexBlockSize :: BoardSize -> FileOffset-indexBlockSize (nrows, ncols) = 256 * indexRecordSize--dataBlockSize :: FileOffset-dataBlockSize = 128--calcIndexBlockOffset :: BoardSize -> IndexBlockNumber -> FileOffset-calcIndexBlockOffset bsize n = indexHeaderSize + indexBlockSize bsize * fromIntegral n--calcDataBlockOffset :: DataBlockNumber -> FileOffset-calcDataBlockOffset n = dataHeaderSize + dataBlockSize * fromIntegral n--calcIndexOffset :: BoardSize -> IndexBlockNumber -> Word8 -> FileOffset-calcIndexOffset bsize block char =- calcIndexBlockOffset bsize block + fromIntegral char * indexRecordSize--data IndexHeader = IndexHeader {- ihBlocksCount :: IndexBlockNumber- }- deriving (Show, Generic)--instance Data.Store.Store IndexHeader where- size = ConstSize $ fromIntegral $ sizeOf (0 :: IndexBlockNumber)-- poke h =- poke $ ihBlocksCount h-- peek = do- n <- peek- return $ IndexHeader n--data DataHeader = DataHeader {- dhBlocksCount :: DataBlockNumber- }- deriving (Show, Generic)--instance Data.Store.Store DataHeader where- size = ConstSize $ fromIntegral $ sizeOf (0 :: DataBlockNumber)-- poke h =- poke $ dhBlocksCount h-- peek = do- n <- peek- return $ DataHeader n--data IndexRecord = IndexRecord {- irIndexBlock :: IndexBlockNumber- , irDataBlock :: DataBlockNumber- }- deriving (Show)--instance Store IndexRecord where- size = ConstSize $ fromIntegral $ sizeOf (0 :: IndexBlockNumber) + sizeOf (0 :: DataBlockNumber)-- poke r = do- poke $ irIndexBlock r- poke $ irDataBlock r-- peek = do- idxBlock <- peek- dataBlock <- peek- return $ IndexRecord idxBlock dataBlock--lookupFileB :: B.ByteString -> Storage (Maybe PerBoardData)-lookupFileB bstr = do- st <- get- case ssData st of- Nothing -> return Nothing- Just _ -> loop 0 bstr- where- loop blockNumber bstr- | B.length bstr == 1 = do- bsize <- gets ssBoardSize- let idxOffset = calcIndexOffset bsize blockNumber (B.head bstr)- seek IndexFile idxOffset- record <- readData IndexFile- let dataBlockNumber = irDataBlock record- if dataBlockNumber == unexistingBlock- then return Nothing- else do- let dataOffset = calcDataBlockOffset dataBlockNumber- seek DataFile dataOffset- value <- readDataSized DataFile- return $ Just value- | otherwise = do- bsize <- gets ssBoardSize- let idxOffset = calcIndexOffset bsize blockNumber (B.head bstr)- seek IndexFile idxOffset- record <- readData IndexFile- let nextBlockNumber = irIndexBlock record- if nextBlockNumber == unexistingBlock- then return Nothing- else loop nextBlockNumber (B.tail bstr)- ---- | Returns: (cached result, stats-lookupFile :: Board -> DepthParams -> Storage (Maybe PerBoardData)-lookupFile board depth = Monitoring.timed "cache.lookup.file" $ do- mbRecord <- lookupFileB (encodeBoard board)- case mbRecord of- Nothing -> return Nothing- Just record -> do- return $- if itemDepth record >= dpLast depth- then Just record- else Nothing---- lookupStatsFile :: Board -> Storage (Maybe Stats)--- lookupStatsFile board = Monitoring.timed "stats.lookup.file" $ do--- mbItem <- lookupFileB (encodeBoard board)--- return $ join $ boardStats `fmap` mbItem--putRecordFileB :: B.ByteString -> PerBoardData -> Storage ()-putRecordFileB bstr newData = do- st <- get- case ssData st of- Nothing -> return ()- Just _ -> tryBlock 0 bstr- where- tryBlock blockNumber bstr- | B.length bstr == 1 = do- bsize <- gets ssBoardSize- let idxOffset = calcIndexOffset bsize blockNumber (B.head bstr)- seek IndexFile idxOffset- record <- readData IndexFile- let dataBlockNumber = irDataBlock record- if dataBlockNumber == unexistingBlock - then do- Monitoring.increment "storage.data.block.created"- newDataBlock <- createDataBlock- let record' = record {irDataBlock = newDataBlock}- seek IndexFile idxOffset- writeData IndexFile record'- seek DataFile $ calcDataBlockOffset newDataBlock- writeDataSized DataFile newData- return ()- else do- Monitoring.increment "storage.data.block.reused"- let dataOffset = calcDataBlockOffset dataBlockNumber- seek DataFile dataOffset- oldData <- readDataSized DataFile- `catch`- (\(e :: SomeException) -> do- $reportError "putRecordFileB: {}" (Single $ show e)- return mempty- )- let newData' = oldData <> newData- seek DataFile dataOffset- writeDataSized DataFile newData'- return ()- | otherwise = do- bsize <- gets ssBoardSize- let idxOffset = calcIndexOffset bsize blockNumber (B.head bstr)- seek IndexFile idxOffset- record <- readData IndexFile- let nextBlockNumber = irIndexBlock record- if nextBlockNumber == unexistingBlock- then do- Monitoring.increment "storage.index.block.created"- newIndexBlock <- createIndexBlock- let record' = record {irIndexBlock = newIndexBlock}- seek IndexFile idxOffset- writeData IndexFile record'- tryBlock newIndexBlock (B.tail bstr)- else do- Monitoring.increment "storage.index.block.reused"- tryBlock nextBlockNumber (B.tail bstr)- - createIndexBlock = do- seek IndexFile 0- header <- readData IndexFile- let newBlockNumber = ihBlocksCount header + 1- let header' = header {ihBlocksCount = newBlockNumber}- bsize <- gets ssBoardSize- seek IndexFile 0- writeData IndexFile header'- seek IndexFile $ calcIndexBlockOffset bsize newBlockNumber- let empty = B.replicate (fromIntegral $ indexBlockSize bsize) 0xff- writeBytes IndexFile empty- return newBlockNumber-- createDataBlock = do- seek DataFile 0- header <- readData DataFile- let newBlockNumber = dhBlocksCount header + 1- let header' = header {dhBlocksCount = newBlockNumber}- seek DataFile 0- writeData DataFile header'- seek DataFile $ calcDataBlockOffset newBlockNumber- let empty = B.replicate (fromIntegral dataBlockSize) 0- writeBytes DataFile empty- return newBlockNumber--putRecordFile :: Board -> StorageValue -> Storage ()-putRecordFile board value = Monitoring.timed "cache.put.file" $ do- let bstr = encodeBoard board- putRecordFileB bstr value--putStatsFile :: Board -> Stats -> Storage ()-putStatsFile board stats = do- return ()--- let newData = PerBoardData mempty (Just stats)--- bstr = encodeBoard board--- putRecordFileB bstr newData--initFile :: Storage ()-initFile = do- seek IndexFile 0- writeData IndexFile $ IndexHeader 0- bsize <- gets ssBoardSize- let empty = B.replicate (fromIntegral $ indexBlockSize bsize) 0xff- writeBytes IndexFile empty- seek DataFile 0- writeData DataFile $ DataHeader 0- return ()---- dumpFile :: FilePath -> IO ()--- dumpFile path = withFile path ReadMode $ \file -> do--- nBlocks <- readDataIO file :: IO Word16--- printf "Number of blocks: %d\n" nBlocks--- forM_ [0 .. hashIndiciesCount - 1] $ \hashIndexNr -> do--- printf "Hash index #%d:\n" hashIndexNr--- forM_ [0 .. hashesCount - 1] $ \hash -> do--- lastBlockNr <- readDataIO file :: IO BlockNumber--- when (lastBlockNr /= unexistingBlock) $ do--- printf " Hash %d, First:\tlast block #%d\n" hash lastBlockNr--- lastBlockNr <- readDataIO file :: IO BlockNumber--- when (lastBlockNr /= unexistingBlock) $ do--- printf " Hash %d, Second:\tlast block #%d\n" hash lastBlockNr--- forM_ [0 .. nBlocks - 1] $ \blockNr -> do--- let blockOffset = fromIntegral $ calcBlockOffset (fromIntegral blockNr)--- hSeek file AbsoluteSeek blockOffset--- printf "Block #%d, offset %d\n" blockNr blockOffset--- header <- readDataIO file :: IO BlockHeader--- let nRecords = bhRecordsCount header--- printf " Header: %s\n" (show header)--- when (nRecords > 0) $ do--- forM_ [0 .. nRecords - 1] $ \recordNr -> do--- printf " Record #%d:\n" recordNr--- ((bk, depth),item) <- readDataSizedIO file :: IO (StorageKey, StorageValue)--- printf " Board key: %s\n" (show bk)--- printf " Depth: %d\n" depth--- printf " Value: %s\n" (show item)--readDataIO :: forall a h. (Data.Store.Store a, File.FileAccess h) => h -> File.Offset -> IO a-readDataIO file offset = do- bstr <- File.readBytes file offset (fromIntegral $ sizeOf (error "unknown data size to read!" :: a))- when (B.null bstr) $ do- fail $ "readDataIO: unexpected EOF, offset " ++ show offset- Data.Store.decodeIO bstr--readDataSizedIO :: forall a h. (Data.Store.Store a, File.FileAccess h) => h -> File.Offset -> IO a-readDataSizedIO file offset = do- size <- readDataIO file offset :: IO Word16- bstr <- File.readBytes file (offset + 2) (fromIntegral size)- when (B.null bstr) $ do- fail $ printf "readDataSizedIO: zero data size, offset %s, size %s" (show offset) (show size)- Data.Store.decodeIO bstr--dumpIndexBlock :: File.FileAccess h => h -> BoardSize -> IndexBlockNumber -> IO ()-dumpIndexBlock h bsize n = do- forM_ [0 .. 255] $ \char -> do- let offset = fromIntegral $ calcIndexOffset bsize n char- record <- readDataIO h offset- when (irDataBlock record /= unexistingBlock || irIndexBlock record /= unexistingBlock) $- printf "Char #%d: next index #%d, data block #%d\n" char (irIndexBlock record) (irDataBlock record)--checkDataFile :: FilePath -> IO ()-checkDataFile path = do- let params = File.MMapedParams (1024*1024) False- file <- File.initFile params path- nBlocks <- readDataIO file 0 :: IO DataBlockNumber- forM_ [0 .. nBlocks - 1] $ \i -> do- let start = fromIntegral $ calcDataBlockOffset i- size <- readDataIO file start :: IO Word16- when (size > 0) $ do- bstr <- File.readBytes file (start + 2) (fromIntegral size)- record <- Data.Store.decodeIO bstr :: IO PerBoardData- printf "Block #%d: data: %s\n" i (show record)+getCachePath :: GameRules rules => rules -> Checkers FilePath+getCachePath rules = liftIO $ do+ home <- getEnv "HOME"+ let directory = home </> ".cache" </> "hcheckers" </> rulesName rules+ createDirectoryIfMissing True directory+ return directory --- checkDataFile' :: FilePath -> IO ()--- checkDataFile' path = do--- let params = File.MMapedParams (1024*1024) False--- file <- File.initFile params path--- nBlocks <- readDataIO file 0 :: IO DataBlockNumber--- forM_ [0 .. nBlocks - 1] $ \i -> do--- let start = fromIntegral $ calcDataBlockOffset i--- size <- readDataIO file start :: IO Word16--- when (size > 0) $ do--- bstr <- File.readBytes file (fromIntegral $ start + 2) (fromIntegral size)--- record <- Data.Store.decodeIO bstr :: IO PerBoardData--- case boardStats record of--- Nothing -> return ()--- Just stats -> --- when (statsCount stats > 10) $--- printf "Block #%d: data: %s\n" i (show record)+newAiData :: Checkers AIData+newAiData = liftIO $ atomically $ newTVar $ M.empty -data ParserState = ParserState {- psIndex :: File.MMaped- , psUnfinished :: M.Map IndexBlockNumber BL.ByteString- , psFinished :: M.Map BL.ByteString DataBlockNumber- }+loadAiData' :: FilePath -> Checkers (V.Vector Double, M.Map BoardHash PerBoardData)+loadAiData' path = do+ sHandle <- askSupervisor+ sState <- liftIO $ atomically $ readTVar sHandle+ bytes <- liftIO $ B.readFile path+ (vec, pairs) <- liftIO $ decodeIO bytes :: Checkers (V.Vector Double, [(BoardHash, PerBoardData)])+ $info "Load AI cache: {} - {} boards" (path, length pairs)+ return (vec, M.fromList pairs) -loadIndex :: StateT ParserState IO ()-loadIndex = do- index <- gets psIndex- header <- liftIO $ readDataIO index 0- let n = ihBlocksCount header- forM_ [0 .. n-1] loadBlock- where- bsize = (8,8)+loadAiData :: GameRules rules => rules -> Checkers AIData+loadAiData rules = do+ cachePath <- getCachePath rules+ aiData <- newAiData+ load <- asks (aiLoadCache . gcAiConfig . csConfig)+ if load+ then do+ paths <- liftIO $ glob (cachePath </> "*.data")+ sHandle <- askSupervisor+ sState <- liftIO $ atomically $ readTVar sHandle+ perEvalPairs <- forM paths $ \path -> do+ bytes <- liftIO $ B.readFile path+ (vec, pairs) <- liftIO $ decodeIO bytes :: Checkers (V.Vector Double, [(BoardHash, PerBoardData)])+ $info "Load AI cache: {} - {} boards" (path, length pairs)+ return (vec, pairs) - loadBlock :: IndexBlockNumber -> StateT ParserState IO ()- loadBlock i = do- index <- gets psIndex- finished <- gets psFinished- liftIO $ printf "Block #%d; finished: %d\n" i (M.size finished)- records <- forM [0 .. 255] $ \char -> do- let offset = calcIndexOffset bsize i char- liftIO $ readDataIO index (fromIntegral offset)- unfinished <- gets psUnfinished- modify $ \st -> st {psUnfinished = M.delete i (psUnfinished st)}- let prefix = fromMaybe "" $ M.lookup i unfinished- forM_ (zip [0..] records) $ \(j, record) -> do- let prefix' = prefix `BL.append` BL.singleton j- when (irDataBlock record /= unexistingBlock) $- modify $ \st -> st {psFinished = M.insert prefix' (irDataBlock record) (psFinished st)}- when (irIndexBlock record /= unexistingBlock) $- modify $ \st -> st {psUnfinished = M.insert (irIndexBlock record) prefix' (psUnfinished st)}- -loadIndexIO :: FilePath -> IO (M.Map BL.ByteString DataBlockNumber)-loadIndexIO indexPath = do - let params = File.MMapedParams (1024*1024) False- index <- File.initFile params indexPath- let st = ParserState index M.empty M.empty- st' <- execStateT loadIndex st- File.closeFile index- putStrLn "index loaded."- return $ psFinished st'+ let vecs = map fst perEvalPairs+ cachesForEval = map snd perEvalPairs+ maps <- forM cachesForEval $ \pairs -> liftIO $ atomically $ do+ bmap <- SM.new+ forM pairs $ \(bHash, item) -> do+ SM.insert item bHash bmap+ return bmap+ liftIO $ atomically $ do+ aiData <- newTVar $ M.fromList $ zip vecs maps+ return aiData+ else+ return aiData --- loadDataIO :: FilePath -> FilePath -> IO [(Board, PerBoardData)]--- loadDataIO indexPath dataPath = do--- index <- loadIndexIO indexPath--- let params = File.MMapedParams (1024*1024) False--- file <- File.initFile params--- forM (M.assocs index) $ \(bstr, block) -> do- +saveAiData :: GameRules rules => rules -> AIData -> Checkers ()+saveAiData rules var = do+ cachePath <- getCachePath rules+ perEvalMap <- liftIO $ atomically $ readTVar var+ forM_ (zip [1..] $ M.assocs perEvalMap) $ \(i, (vec, mapForEval)) -> do+ let path = cachePath </> show i ++ ".data"+ getPairs = ListT.toList $ SM.listT mapForEval+ boardsData <- liftIO $ atomically getPairs+ let fileData = (vec, boardsData) :: (V.Vector Double, [(BoardHash, PerBoardData)])+ liftIO $ B.writeFile path $ Data.Store.encode fileData+ $info "Save AI cache: {} - {} boards" (path, length boardsData)
src/AI/AlphaBeta/Types.hs view
@@ -39,6 +39,7 @@ import Control.Concurrent.STM import qualified Data.HashPSQ as PQ import qualified Data.Map as M+import qualified Data.Vector as V import Data.Word import Data.Binary import Data.Store@@ -59,6 +60,8 @@ data AlphaBeta rules eval = AlphaBeta AlphaBetaParams rules eval deriving (Eq, Ord, Show, Typeable) +type AlphaBetaR rules = AlphaBeta rules (EvaluatorForRules rules)+ data AlphaBetaParams = AlphaBetaParams { abDepth :: Depth , abStartDepth :: Maybe Depth@@ -151,7 +154,7 @@ instance Binary PerBoardData instance Store PerBoardData -type AIData = TBoardMap PerBoardData+type AIData = TVar (M.Map (V.Vector Double) (TBoardMap PerBoardData)) type StorageKey = (DepthParams, BoardKey) @@ -196,11 +199,7 @@ , aichProcessor :: Processor [Int] [ScoreMoveInput rules eval] [MoveAndScore] , aichPossibleMoves :: MovesMemo , aichLastMoveScoreShift :: TVar (M.Map GameId ScoreBase)- , aichWriteQueue :: WriteQueue- , aichCleanupQueue :: CleanupQueue , aichCurrentCounts :: TVar BoardCounts- , aichIndexFile :: Maybe FHandle- , aichDataFile :: Maybe FHandle } type WriteQueue = TChan (Board, StorageValue)@@ -221,8 +220,6 @@ , ssMetrics :: Metrics.Metrics , ssMetricsEnabled :: Bool , ssBoardSize :: BoardSize- , ssIndex :: Maybe FHandle- , ssData :: Maybe FHandle } -- | Storage monad.@@ -319,11 +316,9 @@ runStorage :: (GameRules rules, Evaluator eval) => AICacheHandle rules eval -> Storage a -> Checkers a runStorage handle actions = do lts <- asks csLogging- let indexHandle = aichIndexFile handle- let dataHandle = aichDataFile handle let bsize = boardSize (aichRules handle) metrics <- Metrics.getMetrics metricsEnabled <- isMetricsEnabled- let initState = StorageState lts metrics metricsEnabled bsize indexHandle dataHandle+ let initState = StorageState lts metrics metricsEnabled bsize liftIO $ evalStateT actions initState
src/Battle.hs view
@@ -7,14 +7,18 @@ {-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE DataKinds #-} module Battle where import Control.Monad import Control.Monad.IO.Class-import Data.List (intercalate, sortOn)-import Data.Aeson+import Control.Concurrent+import Data.List (sortOn)+import Data.Aeson hiding (json) import Data.Aeson.Types+import Data.Yaml+import Data.Maybe import qualified Data.Map as M import qualified Data.Vector as V import qualified Data.HashMap.Strict as H@@ -23,14 +27,48 @@ import Text.Printf import System.Random import System.Random.Shuffle+import Web.Scotty.Trans+import Network.HTTP.Req+import Text.URI (mkURI) -import Core.Types+import Core.Types hiding (timed) import Core.Board+import Core.Json () -- instances only import Core.Supervisor+import Core.Parallel+import Core.Monitoring+import Rest.Common import AI.AlphaBeta.Types+import AI type AB rules = AlphaBeta rules (EvaluatorForRules rules) +type BattleRunner = SomeRules -> (Int,SomeAi) -> (Int,SomeAi) -> FilePath -> Checkers GameResult++type MatchRunner = Int -> SomeRules -> [(Int,Int)] -> [SomeAi] -> Checkers [(Int, Int, Int)]++data GeneticSettings = GeneticSettings {+ gsRules :: String+ , gsUrls :: [T.Text]+ , gsGenerations :: Int+ , gsGames :: Int+ , gsGenerationSize :: Int+ , gsPrevGeneration :: Int+ , gsBestCount :: Int+ , gsInitFiles :: [FilePath]+ } deriving (Show)++instance FromJSON GeneticSettings where+ parseJSON = withObject "GeneticSettings" $ \v -> GeneticSettings+ <$> v .: "rules"+ <*> v .: "urls"+ <*> v .: "generations"+ <*> v .: "games_per_match"+ <*> v .: "generation_size"+ <*> v .: "keep_previous_generation"+ <*> v .: "select_best"+ <*> v .: "init_files"+ (<+>) :: Num a => V.Vector a -> V.Vector a -> V.Vector a v1 <+> v2 = V.zipWith (+) v1 v2 @@ -44,35 +82,89 @@ -- norm v = sqrt $ V.sum $ V.map (\x -> x*x) v norm v = (V.sum $ V.map abs v) / fromIntegral (V.length v) -cross :: (GameRules rules, VectorEvaluator (EvaluatorForRules rules), VectorEvaluatorSupport (EvaluatorForRules rules) rules) => rules -> (AB rules, AB rules) -> Checkers (AB rules)+mix :: V.Vector a -> V.Vector a -> IO (V.Vector a)+mix v1 v2 = do+ t <- randomRIO (0.0, 1.0) :: IO Double+ V.forM (V.zip v1 v2) $ \(x1, x2) -> do+ p <- randomRIO (0.0, 1.0)+ if p < t+ then return x1+ else return x2++cross :: (GameRules rules, VectorEvaluator (EvaluatorForRules rules)) => rules -> (AB rules, AB rules) -> Checkers (AB rules) cross rules (ai1, ai2) = do let v1 = aiToVector ai1 v2 = aiToVector ai2- t <- liftIO $ randomRIO (0.0, 1.0)- let mid = scale (1.0 - t) v1 <+> scale t v2+ mid <- liftIO $ mix v1 v2 p <- liftIO $ randomRIO (0.0, 1.0) :: Checkers Double- v3 <- if p < 0.9+ v3 <- if p < 0.7 then return mid else do- let delta = {-0.5 *-} norm (v1 <-> v2)- dv <- liftIO $ replicateM (V.length v1) $ randomRIO (-delta, delta)- -- liftIO $ print delta- return $ mid <+> V.fromList dv+ let delta = 0.05+ liftIO $ V.forM mid $ \v -> do+ let a = abs v+ randomRIO (v - delta*a, v + delta*a) let v3' = V.take 3 v1 V.++ V.drop 3 v3 return $ aiFromVector rules v3' -breed :: (GameRules rules, VectorEvaluator (EvaluatorForRules rules), VectorEvaluatorSupport (EvaluatorForRules rules) rules) => rules -> Int -> [AB rules] -> Checkers [AB rules]-breed rules nNew ais = do+breed :: (GameRules rules, VectorEvaluator (EvaluatorForRules rules)) => rules -> Int -> Int -> [AB rules] -> Checkers [AB rules]+breed rules nNew nOld ais = do let n = length ais idxPairs = [(i,j) | i <- [0..n-1], j <- [i+1 .. n-1]]+ nNew' = nNew - nOld idxPairs' <- liftIO $ shuffleM idxPairs let ais' = [(ais !! i, ais !! j) | (i,j) <- idxPairs']- mapM (cross rules) $ take nNew $ cycle ais'+ new <- mapM (cross rules) $ take nNew' $ cycle ais'+ let old = take nOld ais+ return $ old ++ new -runGenetics :: (GameRules rules, VectorEvaluator (EvaluatorForRules rules), VectorEvaluatorSupport (EvaluatorForRules rules) rules)- => rules -> Int -> Int -> Int -> [AB rules] -> Checkers [AB rules]-runGenetics rules nGenerations generationSize nBest ais = do- generation0 <- breed rules generationSize ais+runGeneticsJ :: FilePath -> Checkers [SomeAi]+runGeneticsJ cfgPath = do+ cfg <- liftIO $ decodeYamlFile cfgPath+ matchRunner <- if null (gsUrls cfg)+ then return $ dumbMatchRunner runBattleLocal+ else do+ let process url (gameNr, rules, (i,ai1), (j,ai2), path) = do+ liftIO $ printf "Battle AI#%d vs AI#%d on %s\n" i j (T.unpack url)+ timed ("battle.duration." <> url) $ do+ increment ("battle.count." <> url)+ runBattleRemote url rules (i,ai1) (j,ai2) path+ processor <- runProcessor' (gsUrls cfg) getJobKey process+ return $ mkRemoteRunner processor+ withRules (gsRules cfg) $ \rules -> do+ ais <- forM (gsInitFiles cfg) $ \path -> liftIO $ loadAi "default" rules path+ rs <- runGenetics matchRunner rules (gsGenerations cfg) (gsGenerationSize cfg) (gsPrevGeneration cfg) (gsBestCount cfg) (gsGames cfg) ais+ return $ map SomeAi rs++type BattleProcessor = Processor (Int,Int,Int) (Int, SomeRules, (Int,SomeAi), (Int,SomeAi), FilePath) GameResult++getJobKey (gameNr, rules, (i,ai1), (j,ai2), _) = (gameNr, i,j)++mkRemoteRunner :: BattleProcessor -> MatchRunner+mkRemoteRunner processor nGames rules idxPairs ais = do+ let inputs = [(gameNr, rules, (i, ais !! i), (j, ais !! j), "battle.pdn") | (i,j) <- idxPairs, gameNr <- [1..nGames]]+ keys = map getJobKey inputs+ gameResults <- process processor inputs+ let list = [((i,j), result) | (result, (gameNr, i,j)) <- zip gameResults keys]+ byPair = foldr (\(ij, result) m -> M.insertWith (++) ij [result] m) M.empty list+ groupedResults = [fromJust $ M.lookup ij byPair | ij <- idxPairs]+ totals = [calcMatchStats results | results <- groupedResults]+ forM_ (zip idxPairs totals) $ \((i,j), (first, second, draw)) -> do+ liftIO $ printf "Match: AI#%d: %d, AI#%d: %d, Draws(?): %d\n" i first j second draw+ return totals++runGenetics :: (GameRules rules, VectorEvaluator (EvaluatorForRules rules))+ => MatchRunner+ -> rules+ -> Int -- ^ Number of generations+ -> Int -- ^ Generation size+ -> Int -- ^ number of items to keep from previous generation+ -> Int -- ^ Number of best items to select for breeding+ -> Int -- ^ number of games in each match+ -> [AB rules]+ -> Checkers [AB rules]+runGenetics runMatches rules nGenerations generationSize nOld nBest nGames ais = do+ generation0 <- breed rules generationSize nOld ais run 1 generation0 where run n generation = do@@ -81,30 +173,49 @@ if n == nGenerations then return best else do- generation' <- breed rules generationSize best+ generation' <- breed rules generationSize nOld best run (n+1) generation' - nGames = 5 nMatches = generationSize selectBest generation = do- results <- runTournament rules generation nMatches nGames+ results <- runTournament runMatches rules generation nMatches nGames let best = take nBest $ sortOn (negate . snd) $ M.assocs results idxs = map fst best return [generation !! i | i <- idxs] -runTournament :: (GameRules rules, VectorEvaluator (EvaluatorForRules rules)) => rules -> [AlphaBeta rules (EvaluatorForRules rules)] -> Int -> Int -> Checkers (M.Map Int Int)-runTournament rules ais nMatches nGames = do+mapP :: Int -> (input -> Checkers output) -> [input] -> Checkers [output]+mapP nThreads fn inputs = do+ let groups = splitBy nThreads inputs+ vars <- replicateM nThreads $ liftIO $ newEmptyMVar+ forM_ (zip groups vars) $ \(group, var) -> do+ forkCheckers $ do+ rs <- forM group fn+ liftIO $ putMVar var rs+ rsGroups <- forM vars $ \var -> liftIO $ takeMVar var+ return $ concat rsGroups++forMP :: Int -> [input] -> (input -> Checkers output) -> Checkers [output]+forMP nThreads inputs fn = mapP nThreads fn inputs++dumbMatchRunner :: BattleRunner -> MatchRunner+dumbMatchRunner runBattle nGames rules idxPairs ais =+ forMP 4 idxPairs $ \(i,j) ->+ runMatch runBattle rules (i, ais !! i) (j, ais !! j) nGames++runTournament :: (GameRules rules, VectorEvaluator (EvaluatorForRules rules))+ => MatchRunner -> rules -> [AlphaBeta rules (EvaluatorForRules rules)] -> Int -> Int -> Checkers (M.Map Int Int)+runTournament runMatches rules ais nMatches nGames = do forM_ ais $ \ai -> liftIO $ print $ aiToVector ai let n = length ais idxPairs = [(i,j) | i <- [0..n-1], j <- [i+1 .. n-1]] ais' = map SomeAi ais idxPairs' <- liftIO $ shuffleM idxPairs- stats <- forM (take nMatches idxPairs') $ \(i,j) ->- runMatch (SomeRules rules) (ais' !! i) (ais' !! j) nGames- forM_ (zip idxPairs' stats) $ \((i,j),(first,second,draw)) -> do- liftIO $ printf "AI#%d vs AI#%d: First %d, Second %d, Draw %d\n" i j first second draw+ stats <- runMatches nGames (SomeRules rules) (take nMatches idxPairs') ais'+ liftIO $ putStrLn "Tournament results:"+ -- forM_ (zip idxPairs' stats) $ \((i,j),(first,second,draw)) -> do+ -- liftIO $ printf "AI#%d vs AI#%d: First %d, Second %d, Draw %d\n" i j first second draw let results1 = [(i, first - second) | ((i,j), (first,second,draw)) <- zip idxPairs' stats] results2 = [(j, second - first) | ((i,j), (first,second,draw)) <- zip idxPairs' stats]@@ -117,25 +228,36 @@ -- liftIO $ putStrLn str return results -runMatch :: SomeRules -> SomeAi -> SomeAi -> Int -> Checkers (Int, Int, Int)-runMatch rules ai1 ai2 nGames = do+runMatch :: BattleRunner -> SomeRules -> (Int, SomeAi) -> (Int, SomeAi) -> Int -> Checkers (Int, Int, Int)+runMatch runBattle rules (i,ai1) (j,ai2) nGames = do (nFirst, nSecond, nDraw) <- go 0 (0, 0, 0)- liftIO $ printf "First: %d, Second: %d, Draws(?): %d\n" nFirst nSecond nDraw+ liftIO $ printf "Match: AI#%d: %d, AI#%d: %d, Draws(?): %d\n" i nFirst j nSecond nDraw return (nFirst, nSecond, nDraw) where go :: Int -> (Int, Int, Int) -> Checkers (Int, Int, Int)- go i (first, second, draw)- | i >= nGames = return (first, second, draw)+ go k (first, second, draw)+ | k >= nGames = return (first, second, draw) | otherwise = do- result <- runBattle rules ai1 ai2 (printf "battle_%d.pdn" i)+ result <- runBattle rules (i,ai1) (j,ai2) (printf "battle_%d.pdn" k) let stats = case result of FirstWin -> (first+1, second, draw) SecondWin -> (first, second+1, draw) Draw -> (first, second, draw+1)- go (i+1) stats+ go (k+1) stats -runBattle :: SomeRules -> SomeAi -> SomeAi -> FilePath -> Checkers GameResult-runBattle rules ai1 ai2 path = do+calcMatchStats :: [GameResult] -> (Int, Int, Int)+calcMatchStats rs = go (0, 0, 0) rs+ where+ go (first, second, draw) [] = (first, second, draw)+ go (first, second, draw) (r : rs) =+ let stats = case r of+ FirstWin -> (first+1, second, draw)+ SecondWin -> (first, second+1, draw)+ Draw -> (first, second, draw+1)+ in go stats rs++runBattleLocal :: BattleRunner+runBattleLocal rules (i,ai1) (j,ai2) path = do initAiStorage rules ai1 let firstSide = First gameId <- newGame rules firstSide Nothing@@ -147,7 +269,7 @@ resetAiStorageG gameId Second runGame gameId result <- loopGame path gameId (opposite firstSide) 0- liftIO $ print result+ liftIO $ printf "Battle AI#%d vs AI#%d: %s\n" i j (show result) return result hasKing :: Side -> BoardRep -> Bool@@ -159,7 +281,7 @@ loopGame :: FilePath -> GameId -> Side -> Int -> Checkers GameResult loopGame path gameId side i = do StateRs board status side <- getState gameId- if (i > 100) || (i > 60 && boardRepLen board <= 8 && hasKing First board && hasKing Second board)+ if (i > 200) || (i > 120 && boardRepLen board <= 8 && hasKing First board && hasKing Second board) then do liftIO $ putStrLn "Too long a game, probably a draw" -- pdn <- getPdn gameId@@ -220,5 +342,46 @@ Nothing -> fail "Cannot load initial AI" Just initValue -> forM_ [1..n] $ \i -> do value <- generateVariation dv initValue- encodeFile (printf "ai_variation_%d.json" i) value+ Data.Aeson.encodeFile (printf "ai_variation_%d.json" i) value++decodeJsonFile :: FromJSON a => FilePath -> IO a+decodeJsonFile path = do+ r <- eitherDecodeFileStrict' path+ case r of+ Left err -> fail err+ Right x -> return x++decodeYamlFile :: FromJSON a => FilePath -> IO a+decodeYamlFile path = do+ r <- Data.Yaml.decodeFileEither path+ case r of+ Left err -> fail (show err)+ Right x -> return x++http_ :: T.Text -> (Url 'Http, Option s)+http_ text =+ case mkURI text of+ Nothing -> error $ "Can't parse URI: " ++ T.unpack text+ Just uri ->+ case useHttpURI uri of+ Nothing -> error $ "Unsupported URI: " ++ T.unpack text+ Just (url, opts) -> (url, opts)++runBattleRemote :: T.Text -> BattleRunner+runBattleRemote baseUrl (SomeRules rules) (i,SomeAi ai1) (j,SomeAi ai2) path = do+ let rq = BattleRq (rulesName rules) (toJSON ai1) (toJSON ai2)+ (url, opts) = http_ baseUrl+ rs <- liftIO $ runReq defaultHttpConfig $+ req POST (url /: "battle" /: "run") (ReqBodyJson rq) jsonResponse opts+ return (responseBody rs)++runBattleRemoteIO :: T.Text -> String -> FilePath -> FilePath -> IO GameResult+runBattleRemoteIO baseUrl rulesName aiPath1 aiPath2 = do+ ai1 <- decodeJsonFile aiPath1+ ai2 <- decodeJsonFile aiPath2+ let rq = BattleRq rulesName ai1 ai2+ (url, opts) = http_ baseUrl + rs <- runReq defaultHttpConfig $+ req POST (url /: "battle" /: "run") (ReqBodyJson rq) jsonResponse opts+ return (responseBody rs)
src/Core/Board.hs view
@@ -342,6 +342,9 @@ isCapture :: PossibleMove -> Bool isCapture pm = not $ null $ pmVictims pm +isPromotionM :: Move -> Bool+isPromotionM move = any sPromote (moveSteps move)+ isPromotion :: PossibleMove -> Bool isPromotion = pmPromote @@ -748,11 +751,40 @@ -- | Generic implementation of @getGameResult@, which suits most rules. -- This can not, however, recognize draws.-genericGameResult :: GameRules rules => rules -> Board -> Side -> Maybe GameResult-genericGameResult rules board side+genericGameResult :: GameRules rules => rules -> GameState -> Board -> Side -> Maybe GameResult+genericGameResult rules st board side | side == First && null (possibleMoves rules First board) = Just SecondWin | side == Second && null (possibleMoves rules Second board) = Just FirstWin+ | detectRepeatedPosition 3 (gsHistory st) board = Just Draw+ | detectStalemate (gsHistory st) board = Just Draw | otherwise = Nothing++detectRepeatedPosition :: Int -> [HistoryRecord] -> Board -> Bool+detectRepeatedPosition n history board =+ case history of+ [] -> False+ [_] -> False+ (record : prevRecord: history') ->+ if hrPrevBoard record == board+ then detectRepeatedPosition (n-1) history' (hrPrevBoard record)+ else False++detectStalemate :: [HistoryRecord] -> Board -> Bool+detectStalemate history board+ | totalCount board <= 3 = detectStalemate' 5 history board+ | totalCount board <= 5 = detectStalemate' 30 history board+ | totalCount board <= 7 = detectStalemate' 60 history board+ | otherwise = False+ +detectStalemate' :: Int -> [HistoryRecord] -> Board -> Bool+detectStalemate' nMoves history board =+ let counts = calcBoardCounts board+ change r = isCaptureM (hrMove r) || isPromotionM (hrMove r)+ nHalfMoves = 2*nMoves+ in if bcFirstKings counts > 0 && bcSecondKings counts > 0+ then length history > nHalfMoves &&+ (not $ any change $ take nHalfMoves history)+ else False instance IsString Label where fromString str =
src/Core/BoardMap.hs view
@@ -101,39 +101,28 @@ newTBoardMap :: IO (TBoardMap a) newTBoardMap = atomically SM.new -putBoardMap :: TBoardMap a -> Board -> a -> IO ()-putBoardMap bmap board value = atomically $ do+putBoardMap' :: TBoardMap a -> Board -> a -> STM ()+putBoardMap' bmap board value = SM.insert value (boardHash board) bmap--- mbByHash <- SM.lookup (boardCounts board) bmap--- case mbByHash of--- Just byHash -> SM.insert value (boardHash board) byHash--- Nothing -> do--- byHash <- SM.new--- SM.insert value (boardHash board) byHash--- SM.insert byHash (boardCounts board) bmap -putBoardMapWith :: TBoardMap a -> (a -> a -> a) -> Board -> a -> IO ()-putBoardMapWith bmap plus board value = atomically $ do+putBoardMap :: TBoardMap a -> Board -> a -> IO ()+putBoardMap bmap board value = atomically $ putBoardMap' bmap board value++putBoardMapWith' :: TBoardMap a -> (a -> a -> a) -> Board -> a -> STM ()+putBoardMapWith' bmap plus board value = do mbOld <- SM.lookup (boardHash board) bmap case mbOld of Nothing -> SM.insert value (boardHash board) bmap Just old -> SM.insert (plus old value) (boardHash board) bmap--- mbByHash <- SM.lookup (boardCounts board) bmap--- byHash <- case mbByHash of--- Nothing -> SM.new--- Just byHash -> return byHash--- mbOld <- SM.lookup (boardHash board) byHash--- case mbOld of--- Nothing -> SM.insert value (boardHash board) byHash--- Just old -> SM.insert (plus old value) (boardHash board) byHash +putBoardMapWith :: TBoardMap a -> (a -> a -> a) -> Board -> a -> IO ()+putBoardMapWith bmap plus board value = atomically $ putBoardMapWith' bmap plus board value++lookupBoardMap' :: TBoardMap a -> Board -> STM (Maybe a)+lookupBoardMap' bmap board = SM.lookup (boardHash board) bmap+ lookupBoardMap :: TBoardMap a -> Board -> IO (Maybe a)-lookupBoardMap bmap board = atomically $ do- SM.lookup (boardHash board) bmap--- mbByHash <- SM.lookup (boardCounts board) bmap--- case mbByHash of--- Nothing -> return Nothing--- Just byHash -> SM.lookup (boardHash board) byHash+lookupBoardMap bmap board = atomically $ lookupBoardMap' bmap board resetBoardMap :: TBoardMap a -> IO () resetBoardMap bmap = atomically $ SM.reset bmap
src/Core/CmdLine.hs view
@@ -2,12 +2,13 @@ module Core.CmdLine where import Options.Applicative-import Data.Semigroup ((<>)) import Data.Char (toLower) data CmdLine = CmdLine { cmdConfigPath :: Maybe FilePath , cmdLocal :: Maybe Bool+ , cmdBattleServer :: Maybe Bool+ , cmdMetrics :: Maybe Bool , cmdSpecial :: Maybe String } deriving (Eq, Show)@@ -33,6 +34,16 @@ <> short 'L' <> metavar "on|off" <> help "Run server in local mode" ) )+ <*> optional (option bool+ ( long "battle"+ <> short 'B'+ <> metavar "on|off"+ <> help "Run `battle server' instead of normal game server") )+ <*> optional (option bool+ ( long "metrics"+ <> metavar "on|off"+ <> help "Enable or disable metrics monitoring. This command-line option overrides one specified in the config file.")+ ) <*> optional (strOption ( long "special" <> metavar "COMMAND" ) )
src/Core/Config.hs view
@@ -45,5 +45,8 @@ let config' = case cmdLocal cmd of Nothing -> config Just local -> config {gcLocal = local}- return config'+ config'' = case cmdMetrics cmd of+ Nothing -> config'+ Just metrics -> config {gcEnableMetrics = metrics}+ return config''
src/Core/Evaluator.hs view
@@ -6,8 +6,6 @@ {-# LANGUAGE ConstraintKinds #-} module Core.Evaluator ( SimpleEvaluator (..),- SimpleEvaluatorInterface (..),- SimpleEvaluatorSupport (..), SimpleEvaluatorData (..), weightForSide, defaultEvaluator@@ -38,7 +36,7 @@ deriving (Show) data SimpleEvaluator = SimpleEvaluator {- seRules :: SimpleEvaluatorInterface,+ seRules :: SomeRules, seUsePositionalScore :: Bool, seMobilityWeight :: ScoreBase, seBackyardWeight :: ScoreBase,@@ -57,21 +55,7 @@ } deriving (Show) -class GameRules rules => SimpleEvaluatorSupport rules where- getBackDirections :: rules -> [PlayerDirection]- getBackDirections _ = [BackwardLeft, BackwardRight]-- getForwardDirections :: rules -> [PlayerDirection]- getForwardDirections _ = [ForwardLeft, ForwardRight]-- getAllAddresses :: rules -> [Address]--data SimpleEvaluatorInterface = forall g. SimpleEvaluatorSupport g => SimpleEvaluatorInterface g--instance Show SimpleEvaluatorInterface where- show (SimpleEvaluatorInterface rules) = rulesName rules--defaultEvaluator :: SimpleEvaluatorSupport rules => rules -> SimpleEvaluator+defaultEvaluator :: GameRules rules => rules -> SimpleEvaluator defaultEvaluator rules = SimpleEvaluator { seRules = iface , seUsePositionalScore = True@@ -91,7 +75,7 @@ , seCache = buildCache iface } where- iface = SimpleEvaluatorInterface rules+ iface = SomeRules rules parseEvaluator :: SimpleEvaluator -> Value -> AT.Parser SimpleEvaluator parseEvaluator def = withObject "Evaluator" $ \v -> SimpleEvaluator@@ -177,8 +161,8 @@ , psThreats = 0 } -waveRho :: SimpleEvaluatorInterface -> Side -> (Address -> Bool) -> Address -> ScoreBase -> ScoreBase-waveRho (SimpleEvaluatorInterface rules) side isGood addr best = go addr+waveRho :: SomeRules -> Side -> (Address -> Bool) -> Address -> ScoreBase -> ScoreBase+waveRho (SomeRules rules) side isGood addr best = go addr where go :: Address -> ScoreBase go addr@@ -191,8 +175,8 @@ Just dst -> max 0 $ go dst - 1 in maximum $ map check $ getForwardDirections rules -buildCache :: SimpleEvaluatorInterface -> M.Map Address SimpleEvaluatorData-buildCache iface@(SimpleEvaluatorInterface rules) = M.fromList [(addr, labelData addr) | addr <- getAllAddresses rules]+buildCache :: SomeRules -> M.Map Address SimpleEvaluatorData+buildCache iface@(SomeRules rules) = M.fromList [(addr, labelData addr) | addr <- getAllAddresses rules] where labelData addr = SimpleEvaluatorData $ SimpleEvaluatorWeights { sewFirst = waveRho iface First (isCenter . aLabel) addr best,@@ -214,7 +198,7 @@ -- && (row >= crow - halfRow && row < crow + halfRow) preEval :: SimpleEvaluator -> Side -> Board -> PreScore-preEval (SimpleEvaluator { seRules = iface@(SimpleEvaluatorInterface rules), ..}) side board =+preEval (SimpleEvaluator { seRules = iface@(SomeRules rules), ..}) side board = let kingCoef = seKingCoef -- King is much more useful when there are enough men to help it@@ -337,7 +321,7 @@ Nothing -> e Just e' -> e' - evalBoard eval@(SimpleEvaluator {seRules = SimpleEvaluatorInterface rules, ..}) whoAsks board =+ evalBoard eval@(SimpleEvaluator {seRules = SomeRules rules, ..}) whoAsks board = let ps1 = preEval eval whoAsks board ps2 = preEval eval (opposite whoAsks) board @@ -387,7 +371,6 @@ else (myScore - opponentScore) instance VectorEvaluator SimpleEvaluator where- type VectorEvaluatorSupport SimpleEvaluator rules = SimpleEvaluatorSupport rules evalToVector (SimpleEvaluator {..}) = V.fromList $ map fromIntegral $ [ seMobilityWeight, seBackyardWeight,@@ -417,7 +400,7 @@ , seCache = buildCache iface } where- iface = SimpleEvaluatorInterface rules+ iface = SomeRules rules -- data ComplexEvaluator rules = ComplexEvaluator {
src/Core/Game.hs view
@@ -95,6 +95,13 @@ let side = hrSide r in HistoryRecordRep side (moveRep rules side $ hrMove r) +setGameHistory :: [HistoryRecord] -> GameM ()+setGameHistory history = do+ checkStatus New+ modify $ \st -> st {+ gState = (gState st) {gsHistory = history}+ }+ -- | Number of half-moves done in this game gameMoveNumber :: Game -> Int gameMoveNumber g =@@ -114,9 +121,10 @@ if move `notElem` (map pmMove $ possibleMoves rules side board) then throwError NotAllowedMove else do+ st <- gets gState let (board', _, _) = applyMove rules side move board moveMsg = MoveNotify (opposite side) side (moveRep rules side move) (boardRep board')- mbResult = getGameResult rules board' (opposite side)+ mbResult = getGameResult rules st board' (opposite side) messages = case mbResult of Nothing -> [moveMsg] Just result ->
src/Core/Json.hs view
@@ -6,6 +6,7 @@ import Data.Aeson.Types import qualified Data.Text as T import Data.Default+import qualified Data.HashMap.Strict as HM import System.Log.Heavy import Core.Types@@ -25,6 +26,8 @@ instance ToJSON GameResult +instance FromJSON GameResult+ instance ToJSON BoardOrientation instance ToJSON BoardTopology@@ -183,6 +186,12 @@ <*> v .:? "update_cache_max_depth" .!= (aiUpdateCacheMaxDepth def) <*> v .:? "update_cache_max_pieces" .!= (aiUpdateCacheMaxPieces def) +instance FromJSON BattleServerConfig where+ parseJSON = withObject "BattleServerConfig" $ \v -> BattleServerConfig+ <$> v .:? "enable" .!= bsEnable def+ <*> v .:? "host" .!= bsHost def+ <*> v .:? "port" .!= bsPort def+ instance FromJSON GeneralConfig where parseJSON = withObject "GeneralConfig" $ \v -> GeneralConfig <$> v .:? "host" .!= (gcHost def)@@ -193,6 +202,7 @@ <*> v .:? "log_path" .!= (gcLogFile def) <*> v .:? "log_level" .!= (gcLogLevel def) <*> v .:? "ai" .!= (gcAiConfig def)+ <*> v .:? "battle_server" .!= (gcBattleServerConfig def) instance FromJSON Level where parseJSON (String "debug") = return debug_level@@ -204,4 +214,8 @@ parseJSON (String "fatal") = return fatal_level parseJSON (String "disable") = return disable_logging parseJSON invalid = typeMismatch "logging level" invalid++mergeObjects :: Value -> Value -> Value+mergeObjects (Object v1) (Object v2) = Object (HM.union v1 v2)+mergeObjects _ _ = error "not object value"
src/Core/Parallel.hs view
@@ -10,27 +10,41 @@ import Control.Concurrent import Data.Maybe import qualified Data.Map as M+import qualified Data.Text as T import System.Log.Heavy import Core.Types -data Processor key input output = Processor (input -> key) (Chan input) (Chan (key, Either Error output))+data Processor key input output = Processor (input -> key) (Chan (input, Chan (key, Either Error output))) runProcessor :: Int -> (input -> key) -> (input -> Checkers output) -> Checkers (Processor key input output) runProcessor nThreads getKey fn = do st <- ask inputChan <- liftIO newChan- outChan <- liftIO newChan forM_ [1 .. nThreads] $ \i -> do- liftIO $ forkIO $ worker st inputChan outChan i- return $ Processor getKey inputChan outChan+ liftIO $ forkIO $ worker st inputChan i+ return $ Processor getKey inputChan where- worker st inChan outChan i = forever $ do- input <- readChan inChan+ worker st inChan i = forever $ do+ (input, outChan) <- readChan inChan output <- runCheckersT (withLogVariable "thread" (i :: Int) $ fn input) st writeChan outChan (getKey input, output) +runProcessor' :: [T.Text] -> (input -> key) -> (T.Text -> input -> Checkers output) -> Checkers (Processor key input output)+runProcessor' labels getKey fn = do+ st <- ask+ inputChan <- liftIO newChan++ forM_ labels $ \label -> do+ liftIO $ forkIO $ worker st inputChan label+ return $ Processor getKey inputChan+ where+ worker st inChan label = forever $ do+ (input, outChan) <- readChan inChan+ output <- runCheckersT (withLogVariable "thread" label $ fn label input) st+ writeChan outChan (getKey input, output)+ process :: Ord key => Processor key input output -> [input] -> Checkers [output] process processor inputs = do results <- process' processor inputs@@ -39,12 +53,22 @@ Left err -> throwError err process' :: Ord key => Processor key input output -> [input] -> Checkers [Either Error output]-process' (Processor getKey inChan outChan) inputs = do+process' (Processor getKey inChan) inputs = do let n = length inputs+ outChan <- liftIO newChan forM_ inputs $ \input ->- liftIO $ writeChan inChan input+ liftIO $ writeChan inChan (input, outChan) results <- replicateM n $ liftIO $ readChan outChan let m = M.fromList results let results = [fromJust $ M.lookup (getKey input) m | input <- inputs] return results++processSingle :: Processor key input output -> input -> Checkers output+processSingle (Processor _ inChan) input = do+ outChan <- liftIO newChan+ liftIO $ writeChan inChan (input, outChan)+ result <- liftIO $ readChan outChan+ case snd result of+ Right output -> return output+ Left err -> throwError err
− src/Core/Rest.hs
@@ -1,338 +0,0 @@-{-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE TemplateHaskell #-}-{-# LANGUAGE ScopedTypeVariables #-}--module Core.Rest where--import Control.Monad.Reader-import Control.Concurrent-import Control.Concurrent.STM-import Control.Exception as E-import Control.Monad.Catch-import Data.String-import qualified Data.Text as T-import qualified Data.Text.Lazy as TL-import Data.Default-import Data.Maybe-import Data.Aeson hiding ( json )-import Web.Scotty.Trans-import qualified Network.HTTP.Types as H-import Network.HTTP.Types.Status-import Network.Wai.Handler.Warp-import qualified Network.Wai as Wai-import System.Log.Heavy-import System.Log.Heavy.TH-import Network.Socket-import System.Exit--import Core.Types-import Core.Board-import Core.Supervisor-import Core.Json ( ) -- import instances only-import Formats.Types-import Formats.Fen-import Formats.Pdn--type Rest a = ActionT Error Checkers a--error400 :: T.Text -> Rest ()-error400 message = do- json $ object ["error" .= message]- status status400--transformError :: Error -> Rest ()-transformError (Unhandled err) = do- error400 $ T.pack err-transformError (NoSuchMoveExt move side board possible) = do- json $ object [- "error" .= ("no such move" :: T.Text),- "move" .= move,- "side" .= side,- "board" .= board,- "possible" .= possible- ]- status status400-transformError err = do- error400 $ T.pack $ show err--raise500 :: Error -> Rest a-raise500 err = do- text $ TL.pack $ show err- raiseStatus status500 err--instance Parsable Side where- parseParam "1" = Right First- parseParam "2" = Right Second- parseParam text = Left $ "unknown side"--instance ScottyError Error where- stringError str = Unhandled str- showError err = TL.pack $ show err--withGameContext :: GameId -> Checkers a -> Checkers a-withGameContext gameId actions = withLogVariable "game" gameId actions--liftCheckers :: GameId -> Checkers a -> Rest a-liftCheckers gameId actions = liftCheckers' (Just gameId) actions--liftCheckers_ :: Checkers a -> Rest a-liftCheckers_ actions = liftCheckers' Nothing actions--liftCheckers' :: Maybe GameId -> Checkers a -> Rest a-liftCheckers' mbId actions = do- res <- lift $ wrap $ tryC actions- case res of- Right result -> return result- Left err -> raise500 err- where- wrap r = case mbId of- Nothing -> r- Just gameId -> withGameContext gameId r--boardRq :: SupervisorState -> SomeRules -> NewGameRq -> Rest (Maybe Side, Maybe BoardRep)-boardRq _ _ (NewGameRq { rqBoard = Just br, rqFen = Nothing, rqPdn = Nothing }) =- return $ (Nothing, Just br)-boardRq _ rules (NewGameRq { rqBoard = Nothing, rqFen = Just fen, rqPdn = Nothing })- = case parseFen rules fen of- Left err -> raise $ InvalidBoard err- Right (side, br) -> return (Just side, Just br)-boardRq rnd rules (NewGameRq { rqBoard = Nothing, rqFen = Nothing, rqPdn = Just pdn })- = case parsePdn (Just rules) pdn of- Left err -> raise $ InvalidBoard err- Right gr ->- case loadPdn rnd gr of- Left err -> raise err- Right board -> return (Nothing, Just $ boardRep board)-boardRq _ _ (NewGameRq { rqPrevBoard = Just gameId }) = do- board <- liftCheckers_ $ getInitialBoard gameId- return (Nothing, Just $ boardRep board)-boardRq _ _ (NewGameRq { rqBoard = Nothing, rqFen = Nothing, rqPdn = Nothing }) =- return (Nothing, Nothing)-boardRq _ _ _ =- raise $ InvalidBoard "only one of fields must be filled: board, fen, pdn"--parsePdnInfo :: PdnInfoRq -> Rest PdnInfo-parsePdnInfo (PdnInfoRq rname text) = do- case selectRules' rname of- Nothing -> raise UnknownRules- Just rules ->- case parsePdn (Just rules) text of- Left err -> raise $ InvalidBoard err- Right gr -> return $ pdnInfo gr--restServer :: MVar () -> ScottyT Error Checkers ()-restServer shutdownVar = do-- defaultHandler transformError-- post "/game/new" $ do- rq <- jsonData- case selectRules rq of- Nothing -> error400 "invalid game rules"- Just rules -> do- rnd <- liftCheckers_ $ do- sup <- askSupervisor- liftIO $ atomically $ readTVar sup- (mbFirstSide, board) <- boardRq rnd rules rq- let firstSide = fromMaybe First mbFirstSide- gameId <- liftCheckers_ $ newGame rules firstSide board- liftCheckers gameId $ $info- "Created new game #{}; First turn: {}; initial board: {}"- (gameId, show firstSide, show board)- json $ Response (NewGameRs gameId firstSide) []-- post "/game/:id/attach/ai/:side" $ do- gameId <- param "id"- side <- param "side"- rules <- liftCheckers gameId $ getRules gameId- rq <- jsonData- case selectAi rq rules of- Nothing -> error400 "invalid ai settings"- Just ai -> do- liftCheckers gameId $ do- $info "Attached AI: {} to game #{} as {}" (show ai, gameId, show side)- initAiStorage rules ai- attachAi gameId side ai- json $ Response AttachAiRs []-- post "/game/:id/attach/:name/:side" $ do- gameId <- param "id"- name <- param "name"- side <- param "side"- liftCheckers gameId $ do- registerUser gameId side name- $info "Attached player `{}' to game #{} as {}" (name, gameId, show side)- json $ Response RegisterUserRs []-- post "/game/:id/run" $ do- gameId <- param "id"- liftCheckers gameId $ runGame gameId- json $ Response RunGameRs []-- get "/game/:id/state" $ do- gameId <- param "id"- rs <- liftCheckers gameId $ getState gameId- json $ Response rs []-- get "/game/:id/fen" $ do- gameId <- param "id"- rs <- liftCheckers gameId $ getFen gameId- Web.Scotty.Trans.text $ TL.fromStrict rs-- get "/game/:id/pdn" $ do- gameId <- param "id"- rs <- liftCheckers gameId $ getPdn gameId- Web.Scotty.Trans.text $ TL.fromStrict rs-- get "/game/:id/history" $ do- gameId <- param "id"- rs <- liftCheckers gameId $ getHistory gameId- json $ Response (HistoryRs rs) []-- post "/game/:id/move/:name" $ do- gameId <- param "id"- name <- param "name"- moveRq <- jsonData- board <- liftCheckers gameId $ doMove gameId name moveRq- messages <- liftCheckers gameId $ getMessages name- json $ Response (MoveRs board) messages-- get "/game/:id/moves/:name" $ do- gameId <- param "id"- name <- param "name"- side <- liftCheckers gameId $ getSideByUser gameId name- moves <- liftCheckers gameId $ getPossibleMoves gameId side- messages <- liftCheckers gameId $ getMessages name- json $ Response (PossibleMovesRs moves) messages-- post "/game/:id/undo/:name" $ do- gameId <- param "id"- name <- param "name"- board <- liftCheckers gameId $ doUndo gameId name- messages <- liftCheckers gameId $ getMessages name- json $ Response (UndoRs board) messages-- post "/game/:id/capitulate/:name" $ do- gameId <- param "id"- name <- param "name"- liftCheckers gameId $ doCapitulate gameId name- messages <- liftCheckers gameId $ getMessages name- json $ Response CapitulateRs messages-- post "/game/:id/draw/request/:name" $ do- gameId <- param "id"- name <- param "name"- liftCheckers gameId $ doDrawRequest gameId name- messages <- liftCheckers gameId $ getMessages name- json $ Response DrawRqRs messages-- post "/game/:id/draw/accept/:name" $ do- gameId <- param "id"- name <- param "name"- liftCheckers gameId $ doDrawAccept gameId name True- messages <- liftCheckers gameId $ getMessages name- json $ Response (DrawAcceptRs True) messages-- post "/game/:id/draw/decline/:name" $ do- gameId <- param "id"- name <- param "name"- liftCheckers gameId $ doDrawAccept gameId name False- messages <- liftCheckers gameId $ getMessages name- json $ Response (DrawAcceptRs False) messages-- get "/poll/:name" $ do- name <- param "name"- messages <- liftCheckers_ $ getMessages name- json $ Response (PollRs messages) []-- get "/lobby/:rules" $ do- rules <- param "rules"- games <- liftCheckers_ $ getGames (Just rules)- json $ Response (LobbyRs games) []-- get "/lobby" $ do- games <- liftCheckers_ $ getGames Nothing- json $ Response (LobbyRs games) []-- get "/notation/:rules" $ do- rules <- param "rules"- (size, orientation, notation) <- liftCheckers_ $ getNotation rules- json $ Response (NotationRs size orientation notation) []-- get "/topology/:rules" $ do- rules <- param "rules"- topology <- liftCheckers_ $ getTopology rules- json $ Response (TopologyRs topology) []-- post "/file/info/pdn" $ do- rq <- jsonData- info <- parsePdnInfo rq- json $ Response (PdnInfoRs info) []-- post "/server/shutdown" $ do- isLocal <- lift $ asks (gcLocal . csConfig)- if isLocal- then do- json $ Response ShutdownRs []- liftIO $ putMVar shutdownVar ()- else error400 "Server is not running in local mode"-- get "/status" $ do- json $ object [- "status" .= ("ready" :: T.Text)- ]--restOptions :: String -> Port -> Web.Scotty.Trans.Options-restOptions host port =- Options 0 $ setOnExceptionResponse errorHandler $- setHost (fromString host) $- setPort port (settings def)--errorHandler :: SomeException -> Wai.Response-errorHandler e- | Just (err :: Error) <- fromException e =- Wai.responseLBS H.internalServerError500- [(H.hContentType, "text/plain; charset=utf-8")]- (fromString $ show err)- | otherwise =- Wai.responseLBS H.internalServerError500- [(H.hContentType, "text/plain; charset=utf-8")]- (fromString $ show e)---- openSocket :: AddrInfo -> IO Socket--- openSocket addr = socket (addrFamily addr) (addrSocketType addr) (addrProtocol addr)--- --- checkPort :: HostName -> Port -> IO Bool--- checkPort host port = do--- addr <- head <$> getAddrInfo (Just defaultHints) (Just host) (Just $ show port)--- E.bracketOnError (openSocket addr) close $ \sock -> do--- withFdSocket sock setCloseOnExecIfNeeded--- r <- E.try $ bind sock $ addrAddress addr--- case r of--- Right _ -> return True--- Left (e :: SomeException) -> do--- print e--- return False--ioErrorHandler :: IOError -> Checkers ()-ioErrorHandler err = liftIO $ do- putStrLn $ "IO error: " ++ show err- exitWith (ExitFailure 2)--runRestServer :: Checkers ()-runRestServer = do- cs <- ask- let getResponse m = do- res <- runCheckersT m cs- case res of- Right response -> return response- Left err -> fail $ show err- host <- asks (T.unpack . gcHost . csConfig)- port <- asks (gcPort . csConfig)- -- portOpen <- liftIO $ checkPort host port- shutdownVar <- liftIO newEmptyMVar- forkCheckers $ handleIOError ioErrorHandler $ scottyOptsT (restOptions host (fromIntegral port)) getResponse (restServer shutdownVar)- liftIO $ takeMVar shutdownVar- -- REST thread should be able to write the response to Shutdown request.- liftIO $ threadDelay (1000 * 1000)-
src/Core/Supervisor.hs view
@@ -21,10 +21,12 @@ import qualified Data.Map as M import qualified Data.Text as T import Data.Array.IArray as A+import qualified Data.ByteString as B import Data.Text.Format.Heavy import Data.Default import Data.Aeson hiding (Error) import Data.Dynamic+import Data.Store import GHC.Generics import System.Random import System.Log.Heavy@@ -32,6 +34,9 @@ import System.Log.Heavy.Format import System.Log.FastLogger.Date import System.Random.MWC+import System.Environment+import System.FilePath+import System.Directory import Core.Types import Core.Board@@ -102,14 +107,33 @@ | ShutdownRs deriving (Eq, Show, Generic) --- | Create supervisor handle-mkSupervisor :: IO SupervisorHandle-mkSupervisor = do+newRandomTable :: IO RandomTable+newRandomTable = do random <- withSystemRandom . asGenIO $ \gen -> forM [1 .. 4] $ \unboxedPiece -> forM [1 .. 16*16] $ \index -> uniform gen let randomArray = A.listArray ((1,0), (4, 16*16-1)) $ concat random+ return randomArray++loadRandomTable :: IO RandomTable+loadRandomTable = do+ home <- getEnv "HOME"+ let path = home </> ".cache" </> "hcheckers" </> "random.table"+ ex <- doesFileExist path+ if ex+ then do+ bytes <- B.readFile path+ Data.Store.decodeIO bytes+ else do+ table <- newRandomTable+ B.writeFile path $ Data.Store.encode table+ return table++-- | Create supervisor handle+mkSupervisor :: IO SupervisorHandle+mkSupervisor = do+ randomArray <- loadRandomTable var <- atomically $ newTVar $ SupervisorState M.empty 0 M.empty $! randomArray return var @@ -185,15 +209,26 @@ initAiStorage :: SomeRules -> SomeAi -> Checkers () initAiStorage (SomeRules rules) (SomeAi ai) = do var <- askSupervisor- st <- liftIO $ atomically $ readTVar var let key = (rulesName rules, aiName ai)- case M.lookup key (ssAiStorages st) of- Nothing -> do- storage <- createAiStorage ai- liftIO $ atomically $ modifyTVar var $ \st ->+ storage <- createAiStorage ai++ liftIO $ atomically $ do+ st <- readTVar var+ case M.lookup key (ssAiStorages st) of+ Nothing -> do+ modifyTVar var $ \st -> st {ssAiStorages = M.insert key (toDyn storage) (ssAiStorages st)}- Just _ -> return ()+ Just _ -> return () +-- initAiStorage_ :: SomeRules -> SomeAi -> Checkers ()+-- initAiStorage_ (SomeRules rules) (SomeAi ai) = do+-- var <- askSupervisor+-- let key = (rulesName rules, aiName ai)+-- storage <- createAiStorage ai+-- liftIO $ atomically $ modifyTVar var $ \st ->+-- st {ssAiStorages = M.insert key (toDyn storage) (ssAiStorages st)}+-- return ()+ -- | Create a game in the New state newGame :: SomeRules -> Side -> Maybe BoardRep -> Checkers GameId newGame r@(SomeRules rules) firstSide mbBoardRep = do@@ -206,6 +241,10 @@ game <- mkGame st' rules gameId firstSide mbBoardRep modifyTVar var $ \st -> st {ssGames = M.insert (show gameId) game (ssGames st)} return $ show gameId++setHistory :: GameId -> [HistoryRecord] -> Checkers ()+setHistory gameId history = + withGame gameId $ \_ -> setGameHistory history -- | Register a user in the game registerUser :: GameId -> Side -> String -> Checkers ()
src/Core/Types.hs view
@@ -17,9 +17,9 @@ import Control.Monad.Metrics as Metrics import Control.Concurrent import Control.Concurrent.STM-import Control.Exception as E (try) import Data.List import Data.Array.Unboxed+import Data.Aeson.Types import qualified Data.Vector as V import qualified Data.Map as M import qualified Data.IntMap.Strict as IM@@ -31,7 +31,6 @@ import qualified StmContainers.Map as SM import Data.Text.Format.Heavy import Data.Dynamic-import Data.Aeson (Value) import Data.Int import Data.Word import Data.Binary@@ -43,6 +42,7 @@ import System.Log.Heavy import System.Log.Heavy.TH import System.Clock+import Web.Scotty.Trans import GHC.Exts (Constraint) import Debug.Trace (traceEventIO)@@ -86,6 +86,8 @@ data PieceKind = Man | King deriving (Eq, Ord, Generic, Typeable) +instance Store PieceKind+ instance Show PieceKind where show Man = "M" show King = "K"@@ -128,8 +130,10 @@ pieceKind :: PieceKind , pieceSide :: Side }- deriving (Eq, Ord, Typeable)+ deriving (Eq, Ord, Typeable, Generic) +instance Store Piece+ instance Show Piece where show (Piece k s) = show k ++ show s @@ -379,8 +383,10 @@ -- | Representation of Board for JSON data BoardRep = BoardRep [(Label, Piece)]- deriving (Eq, Ord, Show, Typeable)+ deriving (Eq, Ord, Show, Typeable, Generic) +instance Store BoardRep+ boardRepLen :: BoardRep -> Int boardRepLen (BoardRep lst) = length lst @@ -435,7 +441,7 @@ boardTopology :: a -> BoardTopology -- | Interface of game rules-class (Typeable g, Show g, HasBoardOrientation g, HasTopology g, VectorEvaluator (EvaluatorForRules g)) => GameRules g where+class (Typeable g, Show g, HasBoardOrientation g, HasTopology g, VectorEvaluator (EvaluatorForRules g), ToJSON (EvaluatorForRules g)) => GameRules g where type EvaluatorForRules g -- | Initial board with initial pieces position initBoard :: SupervisorState -> g -> Board@@ -455,10 +461,18 @@ mobilityScore g side board = length $ possibleMoves g side board updateRules :: g -> Value -> g- getGameResult :: g -> Board -> Side -> Maybe GameResult+ getGameResult :: g -> GameState -> Board -> Side -> Maybe GameResult rulesName :: g -> String pdnId :: g -> String + getBackDirections :: g -> [PlayerDirection]+ getBackDirections _ = [BackwardLeft, BackwardRight]++ getForwardDirections :: g -> [PlayerDirection]+ getForwardDirections _ = [ForwardLeft, ForwardRight]++ getAllAddresses :: g -> [Address]+ fieldsCount :: GameRules rules => rules -> Line fieldsCount rules = let (nrows, ncols) = boardSize rules@@ -565,9 +579,8 @@ updateEval e _ = e class Evaluator e => VectorEvaluator e where- type VectorEvaluatorSupport e rules :: Constraint evalToVector :: e -> V.Vector Double- evalFromVector :: VectorEvaluatorSupport e rules => rules -> V.Vector Double -> e+ evalFromVector :: GameRules rules => rules -> V.Vector Double -> e data SomeEval = forall e. VectorEvaluator e => SomeEval e deriving (Typeable)@@ -580,7 +593,7 @@ evaluatorName (SomeEval e) = evaluatorName e updateEval (SomeEval e) v = SomeEval (updateEval e v) -class (Show ai, Typeable (AiStorage ai)) => GameAi ai where+class (Show ai, Typeable (AiStorage ai), ToJSON ai) => GameAi ai where type AiStorage ai createAiStorage :: ai -> Checkers (AiStorage ai)@@ -747,6 +760,20 @@ , aiUpdateCacheMaxPieces = 8 } +data BattleServerConfig = BattleServerConfig {+ bsEnable :: Bool+ , bsHost :: T.Text+ , bsPort :: Int+ }+ deriving (Show, Typeable, Generic)++instance Default BattleServerConfig where+ def = BattleServerConfig {+ bsEnable = False+ , bsHost = "localhost"+ , bsPort = 8865+ }+ data GeneralConfig = GeneralConfig { gcHost :: T.Text , gcPort :: Int@@ -756,6 +783,7 @@ , gcLogFile :: FilePath , gcLogLevel :: Level , gcAiConfig :: AiConfig+ , gcBattleServerConfig :: BattleServerConfig } deriving (Show, Typeable, Generic) @@ -768,7 +796,8 @@ gcMetricsPort = 8000, gcLogFile = "hcheckers.log", gcLogLevel = info_level,- gcAiConfig = def+ gcAiConfig = def,+ gcBattleServerConfig = def } -- | Commonly used data@@ -810,6 +839,8 @@ instance MonadFail Checkers where fail msg = throwError (Unhandled msg) +type Rest a = ActionT Error Checkers a+ runCheckersT :: Checkers a -> CheckersState -> IO (Either Error a) runCheckersT actions st = runReaderT (runExceptT $ runCheckers actions) st @@ -917,4 +948,17 @@ Nothing -> do $info "{}: work done, in {} iterations" (label, i) return result++chunksOf :: Int -> [a] -> [[a]]+chunksOf n list+ | length list <= n = [list]+ | otherwise =+ let (first, other) = splitAt n list+ in first : chunksOf n other++splitBy :: Int -> [a] -> [[a]]+splitBy n xs =+ let nxs = length xs+ m = (nxs `div` n) + 1+ in chunksOf m xs
src/Formats/Compact.hs view
@@ -12,7 +12,6 @@ import qualified Data.Text as T import Text.Megaparsec hiding (Label, State) import Text.Megaparsec.Char-import Text.Megaparsec.Error (errorBundlePretty) import qualified Data.Text.IO as TIO import Text.Printf
src/Formats/Fen.hs view
@@ -6,10 +6,8 @@ import Control.Monad.State import qualified Data.Text as T-import Data.Monoid ((<>)) import Text.Megaparsec hiding (Label) import Text.Megaparsec.Char-import Text.Megaparsec.Error (errorBundlePretty) import Core.Types import Core.Board
src/Formats/Pdn.hs view
@@ -14,7 +14,6 @@ import qualified Data.Text as T import Text.Megaparsec hiding (Label, State) import Text.Megaparsec.Char-import Text.Megaparsec.Error (errorBundlePretty) import qualified Data.Text.IO as TIO import Text.Printf @@ -301,34 +300,37 @@ state = execState (forM_ instructions interpret) initState in map M.elems $ M.elems $ isVariants state -loadPdn :: SupervisorState -> GameRecord -> Either Error Board+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 Board+ withRules :: SomeRules -> Either Error ([HistoryRecord], Board) withRules some@(SomeRules rules) = do let board0 = initBoardFromTags rnd some (grTags r) - go :: Board -> [MoveRec] -> Either Error Board- go board [] = return board- go board0 (moveRec : rest) = do- board1 <- case mrFirst moveRec of- Just rec -> do- move1 <- parseMoveRec rules First board0 rec- let (board1,_,_) = applyMove rules First move1 board0- return board1- Nothing -> return board0+ 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 board1+ Nothing -> return (records1 ++ history, board1) Just rec -> do move2 <- parseMoveRec rules Second board1 rec let (board2,_,_) = applyMove rules Second move2 board1- go board2 rest+ record = HistoryRecord Second move2 board1+ go ([record] ++ records1 ++ history, board2) rest case instructionsToMoves (grMoves r) of- [moves] -> go board0 moves+ [moves] -> go ([], board0) moves vars -> Left $ AmbigousPdnInstruction $ show vars case findRules (grTags r) of
src/Formats/Types.hs view
@@ -11,7 +11,6 @@ import Data.Typeable import Text.Megaparsec hiding (Label, State) import Data.Void-import Data.Aeson import GHC.Generics import Core.Types
src/Learn.hs view
@@ -8,7 +8,6 @@ import Control.Monad.State import Control.Concurrent.STM import qualified Control.Monad.Metrics as Metrics-import Control.Monad.Catch import Data.Text.Format.Heavy import System.Log.Heavy import System.Log.Heavy.TH@@ -18,7 +17,7 @@ import AI.AlphaBeta import AI.AlphaBeta.Types import AI.AlphaBeta.Cache-import AI.AlphaBeta.Persistent+-- import AI.AlphaBeta.Persistent import Formats.Types import Formats.Pdn @@ -29,15 +28,9 @@ let startBoard = initBoardFromTags supervisor (SomeRules rules) (grTags gameRec) let result = resultFromTags $ grTags gameRec $info "Initial board: {}; result: {}" (show startBoard, show result)- forM_ (instructionsToMoves $ grMoves gameRec) $ \moves -> (do+ forM_ (instructionsToMoves $ grMoves gameRec) $ \moves -> do let (endScore, allBoards) = go [] startBoard result moves $info "End score: {}" (Single endScore)- runStorage var $ forM_ allBoards $ \board -> do- let stats = Stats 1 endScore endScore endScore- putStatsFile board stats- )- `catch`- (\(e :: SomeException) -> $reportError "Exception: {}" (Single $ show e)) where go boards lastBoard (Just result) [] = (resultToScore result, lastBoard : boards) go boards lastBoard Nothing [] =@@ -78,9 +71,6 @@ forM_ (instructionsToMoves $ grMoves gameRec) $ \moves -> do (endScore, allBoards) <- go (0, []) startBoard [] moves $info "End score: {}" (Single endScore)- runStorage var $ forM_ allBoards $ \board -> do- let stats = Stats 1 endScore endScore endScore- putStatsFile board stats where go (score, boards) lastBoard _ [] = return (score, lastBoard : boards)@@ -123,7 +113,7 @@ $info "Processed: side {}, move: {}, depth: {} => score {}; we think next best moves are: {}" (show side, show move, abDepth params, show score, show moves) return (moves, score) -learnPdn :: (GameRules rules, Evaluator eval) => AlphaBeta rules eval -> FilePath -> Checkers ()+learnPdn :: (GameRules rules, VectorEvaluator eval) => AlphaBeta rules eval -> FilePath -> Checkers () learnPdn ai@(AlphaBeta params rules eval) path = do cache <- loadAiCache scoreMoveGroup ai pdn <- liftIO $ parsePdnFile (Just $ SomeRules rules) path
src/Main.hs view
@@ -6,28 +6,35 @@ import Control.Monad.Reader import Control.Concurrent.STM import Data.Default+import Data.Maybe+import Data.List import qualified Data.Map as M+import qualified Data.Text as T+import qualified Data.Text.IO as TIO import qualified Data.ByteString.Lazy.Char8 as C8 import qualified Data.Aeson as Aeson import System.Log.Heavy import Options.Applicative import Text.Printf+import System.Random -import Core.Types-import Core.Board+import Core.Types hiding (timed) import AI import AI.AlphaBeta.Types-import AI.AlphaBeta.Persistent-import Core.Rest+import AI.AlphaBeta.Persistent (loadAiData')+import Rest.Common (runRestServer)+import Rest.Game (restServer)+import Rest.Battle (restServer) import Core.Checkers import Core.CmdLine import Core.Supervisor (withRules)-import Core.Evaluator+import Core.Parallel+import Core.Monitoring import Learn import Battle import Rules.Russian-import Rules.Spancirety+-- import Rules.Spancirety -- let stdout = LoggingSettings $ filtering defaultLogFilter defStdoutSettings -- debug = LoggingSettings $ Filtering (\m -> lmLevel m == trace_level) ((defFileSettings "trace.log") {lsFormat = "{time} {source} [{thread}]: {message}\n"})@@ -39,10 +46,18 @@ case cmdSpecial cmd of Nothing -> withCheckers cmd $ do- cfg <- asks csConfig- let fltr = [([], gcLogLevel cfg)]- withLogContext (LogContextFrame [] (include fltr)) $- runRestServer+ logLevel <- asks (gcLogLevel . csConfig)+ host <- asks (T.unpack . gcHost . csConfig)+ port <- asks (gcPort . csConfig)+ bsConfig <- asks (gcBattleServerConfig . csConfig)+ let fltr = [([], logLevel)]+ withLogContext (LogContextFrame [] (include fltr)) $ do+ if fromMaybe False (cmdBattleServer cmd)+ then if bsEnable bsConfig+ then runRestServer (T.unpack $ bsHost bsConfig) (bsPort bsConfig) Rest.Battle.restServer+ else fail "Battle server is not enabled in config"+ else runRestServer host port Rest.Game.restServer+ Just str -> special cmd (words str) special :: CmdLine -> [String] -> IO ()@@ -66,9 +81,19 @@ ai2 <- loadAi "default" rules path2 withCheckers cmd $ withLogContext (LogContextFrame [] (include defaultLogFilter)) $ do- runBattle (SomeRules rules) (SomeAi ai1) (SomeAi ai2) "battle.pdn"+ runBattleLocal (SomeRules rules) (1,SomeAi ai1) (2,SomeAi ai2) "battle.pdn" return () + ["battle-remote", host, rulesName, path1, path2] -> do+ withRules rulesName $ \rules -> do+ ai1 <- loadAi "default" rules path1+ ai2 <- loadAi "default" rules path2+ withCheckers cmd $+ withLogContext (LogContextFrame [] (include defaultLogFilter)) $ do+ rs <- runBattleRemote (T.pack host) (SomeRules rules) (1,SomeAi ai1) (2,SomeAi ai2) "battle.pdn"+ liftIO $ putStrLn $ "Remote: " ++ show rs+ return ()+ ["match", rulesName, ns, path1, path2] -> do withRules rulesName $ \rules -> do let n = read ns@@ -78,7 +103,7 @@ putStrLn $ "AI2: " ++ show ai2 withCheckers cmd $ withLogContext (LogContextFrame [] (include defaultLogFilter)) $ do- runMatch (SomeRules rules) (SomeAi ai1) (SomeAi ai2) n+ runMatch runBattleLocal (SomeRules rules) (1, SomeAi ai1) (2, SomeAi ai2) n return () ("tournament": matches : games : paths) -> do @@ -88,19 +113,14 @@ ais <- forM paths $ \path -> loadAi "default" rules path withCheckers cmd $ withLogContext (LogContextFrame [] (include defaultLogFilter)) $ do- runTournament rules ais nMatches nGames+ runTournament (dumbMatchRunner runBattleLocal) rules ais nMatches nGames return () - ("genetics": generations : size : best : paths) -> do- let rules = spancirety- nGenerations = read generations- generationSize = read size- nBest = read best- ais <- forM paths $ \path -> loadAi "default" rules path- withCheckers cmd $+ ["genetics", yamlPath] -> do+ withCheckers cmd $ do withLogContext (LogContextFrame [] (include defaultLogFilter)) $ do- result <- runGenetics rules nGenerations generationSize nBest ais- forM_ result $ \ai -> liftIO $ C8.putStrLn $ Aeson.encode ai+ result <- runGeneticsJ yamlPath+ forM_ result $ \(SomeAi ai) -> liftIO $ C8.putStrLn $ Aeson.encode ai return () ["generate", ns, deltas, path] -> do@@ -108,96 +128,11 @@ delta = read deltas generateAiVariations n delta path - -- ["dump", path] -> checkDataFile' path- ["load", path] -> do- idx <- loadIndexIO path- print path-- ["test"] -> do+ ["dump", path] -> do withCheckers cmd $ do let rules = russian- cache = seCache $ defaultEvaluator rules- forM_ (M.assocs cache) $ \(addr, sed) -> do- liftIO $ printf "%s => %s, %s\n"- (show $ aLabel addr)- (show $ weightForSide First $ sedCenter sed)- (show $ weightForSide Second $ sedCenter sed)- --- main :: IO ()--- main = do--- let a3 = resolve "a3" board8--- let b4 = resolve "b4" board8--- let c5 = resolve "c5" board8--- let b6 = resolve "b6" board8--- let board8' = movePiece' "h8" "b4" $ removePiece' "a7" board8--- let capture = simpleCapture First a3 ForwardRight--- putStrLn "1."--- print capture--- --- putStrLn "2."--- let (board', addr', p) = applyMove First capture board8'--- print $ getPiece a3 board'--- print $ getPiece b4 board'--- print $ getPiece c5 board'--- print $ getPiece b6 board'--- --- let capture' = simpleCapture First c5--- --- putStrLn "3."--- print $--- let addr = resolve "c5" board'--- piece = fromJust $ getPiece addr board'--- in captures1 piece board' addr--- --- putStrLn "4."--- print $--- let addr = resolve "a3" board8'--- piece = fromJust $ getPiece addr board8'--- in manCaptures piece board8' addr--- --- putStrLn "5."--- print $ possibleMoves Russian First board8'--- --- putStrLn "6."--- let board8'' = setPiece' "b2" (Piece King First) $--- setManyPieces' ["d4", "d6", "g7"] (Piece Man Second) $ buildBoard 8--- print $ kingSimpleMoves First board8'' (resolve "b2" board8'')--- --- putStrLn "7."--- print $ possibleMoves Russian First board8''--- --- putStrLn "8."--- let board = board8''--- let moves = possibleMoves Russian Second board--- forM_ moves $ \move -> do--- print move--- let (board', addr', _) = applyMove Second move board--- moves' = possibleMoves Russian Second board'--- let score1 = evalBoard Russian First board'--- score2 = evalBoard Russian Second board'--- putStrLn $ show score1 ++ " vs " ++ show score2--- --- putStrLn "9."--- let board = movePiece' "e3" "f4" board8--- let moves = possibleMoves Russian Second board--- forM_ moves $ \move -> do--- let (board', addr', _) = applyMove Second move board--- moves' = possibleMoves Russian Second board'--- let score1 = evalBoard Russian First board'--- score2 = evalBoard Russian Second board'--- putStrLn $ show move ++ " => " ++ show score1 ++ " vs " ++ show score2--- --- putStrLn "10."--- let board = setManyPieces' ["c3", "e3"] (Piece Man First) $ --- setManyPieces' ["e5", "f6"] (Piece Man Second) $ buildBoard 8--- let ai = AlphaBeta 2 Russian Russian--- print =<< chooseMove ai Second board--- --- --- putStrLn "11."--- let board = setManyPieces' ["c3", "e3"] (Piece Man First) $ --- setManyPieces' ["e5", "f6"] (Piece Man Second) $ buildBoard 8--- let ai = AlphaBeta 2 Russian Russian--- print =<< chooseMove ai First board--- --- + (vec, bmap) <- loadAiData' path+ liftIO $ printf "Evaluator: %s\n" (show vec)+ forM_ (M.assocs bmap) $ \(bHash, item) -> do+ liftIO $ printf "Hash: %d => %s\n" bHash (show item)+
+ src/Rest/Battle.hs view
@@ -0,0 +1,51 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE RankNTypes #-}+{-# LANGUAGE DataKinds #-}++module Rest.Battle where++import Control.Monad.Reader+import Control.Concurrent+import qualified Data.Text as T+import Data.Aeson hiding ( json )+import Web.Scotty.Trans+import Network.HTTP.Req+import Text.URI (mkURI)++import Core.Types+import Core.Json ( ) -- import instances only+import Core.Supervisor (selectRules')++import Battle (BattleRunner, runBattleLocal)+import AI (parseAi)+import Rest.Common++withRulesR :: String -> (forall rules. GameRules rules => rules -> Rest a) -> Rest a+withRulesR rname fn =+ case selectRules' rname of+ Just (SomeRules rules) -> fn rules+ Nothing -> raise UnknownRules++restServer :: MVar () -> ScottyT Error Checkers ()+restServer shutdownVar = do+ + post "/battle/run" $ do+ rq <- jsonData+ withRulesR (brqRules rq) $ \rules -> do+ let ab1 = parseAi rules (brqAi1 rq)+ ab2 = parseAi rules (brqAi2 rq)+ ai1 = SomeAi ab1+ ai2 = SomeAi ab2+ result <- liftCheckers_ $ runBattleLocal (SomeRules rules) (1,ai1) (2,ai2) "battle.pdn"+ json result++ post "/server/shutdown" $ do+ isLocal <- lift $ asks (gcLocal . csConfig)+ if isLocal+ then do+ json $ object ["shutdown" .= ("ok" :: T.Text)]+ liftIO $ putMVar shutdownVar ()+ else error400 "Server is not running in local mode"+
+ src/Rest/Common.hs view
@@ -0,0 +1,139 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE ScopedTypeVariables #-}++module Rest.Common where++import Control.Monad.Reader+import Control.Concurrent+import Control.Exception as E+import Control.Monad.Catch+import qualified Data.Text as T+import qualified Data.Text.Lazy as TL+import Data.Default+import Data.Aeson hiding (json)+import Data.String+import System.Log.Heavy+import Web.Scotty.Trans+import Network.HTTP.Types.Status+import qualified Network.HTTP.Types as H+import qualified Network.Wai as Wai+import Network.Wai.Handler.Warp+import System.Exit++import Core.Types+import Core.Json () -- import instances only++withGameContext :: GameId -> Checkers a -> Checkers a+withGameContext gameId actions = withLogVariable "game" gameId actions++liftCheckers :: GameId -> Checkers a -> Rest a+liftCheckers gameId actions = liftCheckers' (Just gameId) actions++liftCheckers_ :: Checkers a -> Rest a+liftCheckers_ actions = liftCheckers' Nothing actions++liftCheckers' :: Maybe GameId -> Checkers a -> Rest a+liftCheckers' mbId actions = do+ res <- lift $ wrap $ tryC actions+ case res of+ Right result -> return result+ Left err -> raise500 err+ where+ wrap r = case mbId of+ Nothing -> r+ Just gameId -> withGameContext gameId r++raise500 :: Error -> Rest a+raise500 err = do+ text $ TL.pack $ show err+ raiseStatus status500 err++error400 :: T.Text -> Rest ()+error400 message = do+ json $ object ["error" .= message]+ status status400++transformError :: Error -> Rest ()+transformError (Unhandled err) = do+ error400 $ T.pack err+transformError (NoSuchMoveExt move side board possible) = do+ json $ object [+ "error" .= ("no such move" :: T.Text),+ "move" .= move,+ "side" .= side,+ "board" .= board,+ "possible" .= possible+ ]+ status status400+transformError (InvalidGameStatus expected actual) = do+ json $ object [+ "error" .= ("invalid game status" :: T.Text),+ "expected" .= expected,+ "actual" .= actual+ ]+transformError err = do+ error400 $ T.pack $ show err++instance Parsable Side where+ parseParam "1" = Right First+ parseParam "2" = Right Second+ parseParam text = Left $ "unknown side"++instance ScottyError Error where+ stringError str = Unhandled str+ showError err = TL.pack $ show err++restOptions :: String -> Port -> Web.Scotty.Trans.Options+restOptions host port =+ Options 0 $ setOnExceptionResponse errorHandler $+ setHost (fromString host) $+ setPort port (settings def)++errorHandler :: SomeException -> Wai.Response+errorHandler e+ | Just (err :: Error) <- fromException e =+ Wai.responseLBS H.internalServerError500+ [(H.hContentType, "text/plain; charset=utf-8")]+ (fromString $ show err)+ | otherwise =+ Wai.responseLBS H.internalServerError500+ [(H.hContentType, "text/plain; charset=utf-8")]+ (fromString $ show e)++ioErrorHandler :: IOError -> Checkers ()+ioErrorHandler err = liftIO $ do+ putStrLn $ "IO error: " ++ show err+ exitWith (ExitFailure 2)++runRestServer :: String -> Int -> (MVar () -> ScottyT Error Checkers ()) -> Checkers ()+runRestServer host port restServer = do+ cs <- ask+ let getResponse m = do+ res <- runCheckersT m cs+ case res of+ Right response -> return response+ Left err -> fail $ show err+ -- portOpen <- liftIO $ checkPort host port+ shutdownVar <- liftIO newEmptyMVar+ forkCheckers $ handleIOError ioErrorHandler $ scottyOptsT (restOptions host (fromIntegral port)) getResponse (restServer shutdownVar)+ liftIO $ takeMVar shutdownVar+ -- REST thread should be able to write the response to Shutdown request.+ liftIO $ threadDelay (1000 * 1000)++data BattleRq = BattleRq {+ brqRules :: String+ , brqAi1 :: Value+ , brqAi2 :: Value+ }++instance ToJSON BattleRq where+ toJSON rq =+ object ["rules" .= brqRules rq, "ai1" .= brqAi1 rq, "ai2" .= brqAi2 rq]++instance FromJSON BattleRq where+ parseJSON (Object v) = BattleRq+ <$> v .: "rules"+ <*> v .: "ai1"+ <*> v .: "ai2"+
+ src/Rest/Game.hs view
@@ -0,0 +1,235 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE ScopedTypeVariables #-}++module Rest.Game where++import Control.Monad.Reader+import Control.Concurrent+import Control.Concurrent.STM+import qualified Data.Text as T+import qualified Data.Text.Lazy as TL+import Data.Maybe+import Data.Aeson hiding ( json )+import Web.Scotty.Trans+import System.Log.Heavy+import System.Log.Heavy.TH++import Core.Types+import Core.Board+import Core.Supervisor+import Core.Json ( ) -- import instances only+import Formats.Types+import Formats.Fen+import Formats.Pdn+import Rest.Common++boardRq :: SupervisorState -> SomeRules -> NewGameRq -> Rest (Maybe Side, [HistoryRecord], Maybe BoardRep)+boardRq _ _ (NewGameRq { rqBoard = Just br, rqFen = Nothing, rqPdn = Nothing }) =+ return $ (Nothing, [], Just br)+boardRq _ rules (NewGameRq { rqBoard = Nothing, rqFen = Just fen, rqPdn = Nothing })+ = case parseFen rules fen of+ Left err -> raise $ InvalidBoard err+ Right (side, br) -> return (Just side, [], Just br)+boardRq rnd rules (NewGameRq { rqBoard = Nothing, rqFen = Nothing, rqPdn = Just pdn })+ = case parsePdn (Just rules) pdn of+ Left err -> raise $ InvalidBoard err+ Right gr ->+ case loadPdn rnd gr of+ Left err -> raise err+ Right (history, board) -> return (Nothing, history, Just $ boardRep board)+boardRq _ _ (NewGameRq { rqPrevBoard = Just gameId }) = do+ board <- liftCheckers_ $ getInitialBoard gameId+ return (Nothing, [], Just $ boardRep board)+boardRq _ _ (NewGameRq { rqBoard = Nothing, rqFen = Nothing, rqPdn = Nothing }) =+ return (Nothing, [], Nothing)+boardRq _ _ _ =+ raise $ InvalidBoard "only one of fields must be filled: board, fen, pdn"++parsePdnInfo :: PdnInfoRq -> Rest PdnInfo+parsePdnInfo (PdnInfoRq rname text) = do+ case selectRules' rname of+ Nothing -> raise UnknownRules+ Just rules ->+ case parsePdn (Just rules) text of+ Left err -> raise $ InvalidBoard err+ Right gr -> return $ pdnInfo gr++restServer :: MVar () -> ScottyT Error Checkers ()+restServer shutdownVar = do++ defaultHandler transformError++ post "/game/new" $ do+ rq <- jsonData+ case selectRules rq of+ Nothing -> error400 "invalid game rules"+ Just rules -> do+ rnd <- liftCheckers_ $ do+ sup <- askSupervisor+ liftIO $ atomically $ readTVar sup+ (mbFirstSide, history, board) <- boardRq rnd rules rq+ let firstSide = fromMaybe First mbFirstSide+ gameId <- liftCheckers_ $ newGame rules firstSide board+ liftCheckers_ $ setHistory gameId history+ liftCheckers gameId $ $info+ "Created new game #{}; First turn: {}; initial board: {}"+ (gameId, show firstSide, show board)+ json $ Response (NewGameRs gameId firstSide) []++ post "/game/:id/attach/ai/:side" $ do+ gameId <- param "id"+ side <- param "side"+ rules <- liftCheckers gameId $ getRules gameId+ rq <- jsonData+ case selectAi rq rules of+ Nothing -> error400 "invalid ai settings"+ Just ai -> do+ liftCheckers gameId $ do+ $info "Attached AI: {} to game #{} as {}" (show ai, gameId, show side)+ initAiStorage rules ai+ attachAi gameId side ai+ json $ Response AttachAiRs []++ post "/game/:id/attach/:name/:side" $ do+ gameId <- param "id"+ name <- param "name"+ side <- param "side"+ liftCheckers gameId $ do+ registerUser gameId side name+ $info "Attached player `{}' to game #{} as {}" (name, gameId, show side)+ json $ Response RegisterUserRs []++ post "/game/:id/run" $ do+ gameId <- param "id"+ liftCheckers gameId $ runGame gameId+ json $ Response RunGameRs []++ get "/game/:id/state" $ do+ gameId <- param "id"+ rs <- liftCheckers gameId $ getState gameId+ json $ Response rs []++ get "/game/:id/fen" $ do+ gameId <- param "id"+ rs <- liftCheckers gameId $ getFen gameId+ Web.Scotty.Trans.text $ TL.fromStrict rs++ get "/game/:id/pdn" $ do+ gameId <- param "id"+ rs <- liftCheckers gameId $ getPdn gameId+ Web.Scotty.Trans.text $ TL.fromStrict rs++ get "/game/:id/history" $ do+ gameId <- param "id"+ rs <- liftCheckers gameId $ getHistory gameId+ json $ Response (HistoryRs rs) []++ post "/game/:id/move/:name" $ do+ gameId <- param "id"+ name <- param "name"+ moveRq <- jsonData+ board <- liftCheckers gameId $ doMove gameId name moveRq+ messages <- liftCheckers gameId $ getMessages name+ json $ Response (MoveRs board) messages++ get "/game/:id/moves/:name" $ do+ gameId <- param "id"+ name <- param "name"+ side <- liftCheckers gameId $ getSideByUser gameId name+ moves <- liftCheckers gameId $ getPossibleMoves gameId side+ messages <- liftCheckers gameId $ getMessages name+ json $ Response (PossibleMovesRs moves) messages++ post "/game/:id/undo/:name" $ do+ gameId <- param "id"+ name <- param "name"+ board <- liftCheckers gameId $ doUndo gameId name+ messages <- liftCheckers gameId $ getMessages name+ json $ Response (UndoRs board) messages++ post "/game/:id/capitulate/:name" $ do+ gameId <- param "id"+ name <- param "name"+ liftCheckers gameId $ doCapitulate gameId name+ messages <- liftCheckers gameId $ getMessages name+ json $ Response CapitulateRs messages++ post "/game/:id/draw/request/:name" $ do+ gameId <- param "id"+ name <- param "name"+ liftCheckers gameId $ doDrawRequest gameId name+ messages <- liftCheckers gameId $ getMessages name+ json $ Response DrawRqRs messages++ post "/game/:id/draw/accept/:name" $ do+ gameId <- param "id"+ name <- param "name"+ liftCheckers gameId $ doDrawAccept gameId name True+ messages <- liftCheckers gameId $ getMessages name+ json $ Response (DrawAcceptRs True) messages++ post "/game/:id/draw/decline/:name" $ do+ gameId <- param "id"+ name <- param "name"+ liftCheckers gameId $ doDrawAccept gameId name False+ messages <- liftCheckers gameId $ getMessages name+ json $ Response (DrawAcceptRs False) messages++ get "/poll/:name" $ do+ name <- param "name"+ messages <- liftCheckers_ $ getMessages name+ json $ Response (PollRs messages) []++ get "/lobby/:rules" $ do+ rules <- param "rules"+ games <- liftCheckers_ $ getGames (Just rules)+ json $ Response (LobbyRs games) []++ get "/lobby" $ do+ games <- liftCheckers_ $ getGames Nothing+ json $ Response (LobbyRs games) []++ get "/notation/:rules" $ do+ rules <- param "rules"+ (size, orientation, notation) <- liftCheckers_ $ getNotation rules+ json $ Response (NotationRs size orientation notation) []++ get "/topology/:rules" $ do+ rules <- param "rules"+ topology <- liftCheckers_ $ getTopology rules+ json $ Response (TopologyRs topology) []++ post "/file/info/pdn" $ do+ rq <- jsonData+ info <- parsePdnInfo rq+ json $ Response (PdnInfoRs info) []++ post "/server/shutdown" $ do+ isLocal <- lift $ asks (gcLocal . csConfig)+ if isLocal+ then do+ json $ Response ShutdownRs []+ liftIO $ putMVar shutdownVar ()+ else error400 "Server is not running in local mode"++ get "/status" $ do+ json $ object [+ "status" .= ("ready" :: T.Text)+ ]++-- openSocket :: AddrInfo -> IO Socket+-- openSocket addr = socket (addrFamily addr) (addrSocketType addr) (addrProtocol addr)+-- +-- checkPort :: HostName -> Port -> IO Bool+-- checkPort host port = do+-- addr <- head <$> getAddrInfo (Just defaultHints) (Just host) (Just $ show port)+-- E.bracketOnError (openSocket addr) close $ \sock -> do+-- withFdSocket sock setCloseOnExecIfNeeded+-- r <- E.try $ bind sock $ addrAddress addr+-- case r of+-- Right _ -> return True+-- Left (e :: SomeException) -> do+-- print e+-- return False+
src/Rules/Armenian.hs view
@@ -23,11 +23,6 @@ instance HasTopology Armenian where boardTopology _ = DiagonalAndOrthogonal -instance SimpleEvaluatorSupport Armenian where- getBackDirections _ = [Backward]- getForwardDirections _ = [ForwardLeft, Forward, ForwardRight]- getAllAddresses r = addresses8 r- instance GameRules Armenian where type EvaluatorForRules Armenian = SimpleEvaluator @@ -65,6 +60,10 @@ getGameResult = genericGameResult pdnId _ = "44"++ getBackDirections _ = [Backward]+ getForwardDirections _ = [ForwardLeft, Forward, ForwardRight]+ getAllAddresses r = addresses8 r armenianBase :: GenericRules -> GenericRules armenianBase =
src/Rules/Brazilian.hs view
@@ -19,9 +19,6 @@ instance HasTopology Brazilian where boardTopology _ = Diagonal -instance SimpleEvaluatorSupport Brazilian where- getAllAddresses r = addresses8 r- instance Show Brazilian where show = rulesName @@ -49,6 +46,7 @@ mobilityScore (Brazilian rules) side board = gMobilityScore rules side board getGameResult = genericGameResult+ getAllAddresses r = addresses8 r brazilian :: Brazilian brazilian = Brazilian $
src/Rules/Canadian.hs view
@@ -21,9 +21,6 @@ instance HasTopology Canadian where boardTopology _ = Diagonal -instance SimpleEvaluatorSupport Canadian where- getAllAddresses r = addresses12 r- instance GameRules Canadian where type EvaluatorForRules Canadian = SimpleEvaluator @@ -63,6 +60,7 @@ possibleMoves (Canadian rules) side board = gPossibleMoves rules side board mobilityScore (Canadian rules) side board = gMobilityScore rules side board+ getAllAddresses r = addresses12 r canadian :: Canadian canadian = Canadian $
src/Rules/Czech.hs view
@@ -21,9 +21,6 @@ instance HasTopology Czech where boardTopology _ = Core.Types.Diagonal -instance SimpleEvaluatorSupport Czech where- getAllAddresses r = addresses8 r- instance GameRules Czech where type EvaluatorForRules Czech = SimpleEvaluator pdnId _ = "29"@@ -47,6 +44,7 @@ updateRules r _ = r getGameResult = genericGameResult+ getAllAddresses r = addresses8 r manCaptures :: GenericRules -> CaptureState -> [PossibleMove] manCaptures rules ct@(CaptureState {..}) =
src/Rules/Diagonal.hs view
@@ -21,9 +21,6 @@ instance HasTopology DiagonalRussian where boardTopology _ = Core.Types.Diagonal -instance SimpleEvaluatorSupport DiagonalRussian where- getAllAddresses r = addresses8 r- instance GameRules DiagonalRussian where type EvaluatorForRules DiagonalRussian = SimpleEvaluator initBoard rnd r =@@ -62,6 +59,7 @@ getGameResult = genericGameResult pdnId _ = "42"+ getAllAddresses r = addresses8 r diagonal :: DiagonalRussian diagonal = DiagonalRussian $
src/Rules/English.hs view
@@ -23,9 +23,6 @@ instance HasTopology English where boardTopology _ = Diagonal -instance SimpleEvaluatorSupport English where- getAllAddresses r = addresses8' r- instance GameRules English where type EvaluatorForRules English = SimpleEvaluator boardSize _ = boardSize Russian.russian@@ -51,6 +48,7 @@ mobilityScore (English rules) side board = gMobilityScore rules side board pdnId _ = "21"+ getAllAddresses r = addresses8' r english :: English english = English $
src/Rules/International.hs view
@@ -24,9 +24,6 @@ instance HasTopology International where boardTopology _ = Diagonal -instance SimpleEvaluatorSupport International where- getAllAddresses r = addresses10 r- instance GameRules International where type EvaluatorForRules International = SimpleEvaluator boardSize _ = (10, 10)@@ -63,6 +60,7 @@ mobilityScore (International rules) side board = gMobilityScore rules side board pdnId _ = "20"+ getAllAddresses r = addresses10 r internationalBase :: GenericRules -> GenericRules internationalBase =
src/Rules/Russian.hs view
@@ -21,9 +21,6 @@ instance Show Russian where show = rulesName -instance SimpleEvaluatorSupport Russian where- getAllAddresses r = addresses8 r- instance HasTopology Russian where boardTopology _ = Diagonal @@ -51,6 +48,7 @@ getGameResult = genericGameResult pdnId _ = "25"+ getAllAddresses r = addresses8 r russianBase :: GenericRules -> GenericRules russianBase =
src/Rules/Simple.hs view
@@ -19,9 +19,6 @@ instance Show Simple where show = rulesName -instance SimpleEvaluatorSupport Simple where- getAllAddresses r = addresses8 r- instance HasTopology Simple where boardTopology _ = Diagonal @@ -47,6 +44,7 @@ mobilityScore (Simple rules) side board = gMobilityScore rules side board pdnId _ = "43"+ getAllAddresses r = addresses8 r simple :: Simple simple = Simple $
src/Rules/Spancirety.hs view
@@ -22,9 +22,6 @@ instance HasTopology Spancirety where boardTopology _ = Diagonal -instance SimpleEvaluatorSupport Spancirety where- getAllAddresses r = IM.elems $ bAddresses $ buildBoard DummyRandomTableProvider r FirstAtBottom (8, 10)- instance GameRules Spancirety where type EvaluatorForRules Spancirety = SimpleEvaluator initBoard rnd r =@@ -57,6 +54,7 @@ getGameResult = genericGameResult pdnId _ = "41"+ getAllAddresses r = IM.elems $ bAddresses $ buildBoard DummyRandomTableProvider r FirstAtBottom (8, 10) spancirety :: Spancirety spancirety = Spancirety $
src/Rules/Turkish.hs view
@@ -24,10 +24,6 @@ instance HasTopology Turkish where boardTopology _ = Orthogonal -instance SimpleEvaluatorSupport Turkish where- getBackDirections _ = [Backward]- getAllAddresses r = addresses8 r- instance GameRules Turkish where type EvaluatorForRules Turkish = SimpleEvaluator initBoard rnd r =@@ -57,6 +53,9 @@ getGameResult = genericGameResult pdnId _ = "43"++ getBackDirections _ = [Backward]+ getAllAddresses r = addresses8 r turkishBase :: GenericRules -> GenericRules turkishBase =