Skip to content

Windows build & fixes #1

@jimablett

Description

@jimablett

Hi,

I've managed to build a working Windows executable >

Engine & fixed src

Proton link:
https://drive.proton.me/urls/VDDRMXZRXC#3lZeyMtEn1So

Smash link:
https://fromsmash.com/Trout10JA

I had uci parsing errors and divide by zero errors which needed fixing to produce a working executable.

Here are the fixed files.

Uci.hs

module Trout.Uci (doUci, UciState (..), newUciState) where

import Control.Concurrent
  ( MVar,
    ThreadId,
    forkIO,
    killThread,
    newEmptyMVar,
    newMVar,
    putMVar,
    readMVar,
    swapMVar,
    tryReadMVar,
    tryTakeMVar,
  )
import Control.Exception (evaluate)
import Control.Monad.ST (RealWorld, stToIO)
import Control.Monad.Trans.Reader (ReaderT (runReaderT))
import Data.Bifunctor (first, second)
import Data.Foldable (foldl')
import Data.Function ((&))
import Data.Int (Int16)
import Data.Maybe (fromMaybe)
import System.IO (hFlush, hPutStrLn, stderr, stdout)
import System.Timeout (timeout)
import Text.Printf (printf)
import Text.Read (readEither)
import Trout.Fen.Parse (fenToGame)
import Trout.Game
  ( Game (..),
    allMoves,
    gameBoard,
    makeMove,
    startingGame,
  )
import Trout.Game.Board (boardTurn)
import Trout.Game.Move
  ( Move (..),
    SpecialMove (Promotion),
    uciShowMove,
  )
import Trout.Piece (Color (..))
import Trout.Search (SearchEnv, bestMove, clearEnv, newEnv, pvWalk)
import Trout.Search.TranspositionTable (sizeOfEntry)
import Trout.Uci.Parse
  ( CommGoArg (..),
    CommPositionInit (..),
    UciCommand (..),
    UciMove (..),
    readUciLine,
  )

data UciState = UciState
  { uciGame :: Game,
    uciIsDebug :: Bool,
    uciSearch :: Maybe (ThreadId, MVar Move),
    uciSearchEnv :: MVar (SearchEnv RealWorld)
  }

-- Safe division function that handles divide-by-zero and overflow
safeQuot :: Int -> Int -> Int
safeQuot _ 0 = 0
safeQuot n d 
  | d == -1 && n == minBound = maxBound  -- Handle overflow case
  | otherwise = n `quot` d

newUciState :: IO UciState
newUciState = do
  let entrySize = sizeOfEntry
      hashSize = if entrySize <= 0 
                 then 1000  -- Fallback if sizeOfEntry is 0 or negative
                 else 16000000 `safeQuot` entrySize
  searchEnv <- stToIO (newEnv (max 1 hashSize)) >>= newMVar
  pure $ UciState startingGame False Nothing searchEnv

modUciStateHash :: Int -> UciState -> IO UciState
modUciStateHash hashMB state = do
  let entrySize = sizeOfEntry
  if entrySize <= 0
    then do
      hPutStrLn stderr "Error: Transposition table entry size is zero or negative, cannot set hash."
      pure state
    else do
      let newHashSize = max 1 (hashMB * 1000000 `safeQuot` entrySize)
      newSearchEnv <- stToIO (newEnv newHashSize)
      var <- newMVar newSearchEnv
      pure $ state {uciSearchEnv = var}

data PlayerTime = PlayerTime
  { playerTime :: Int,
    playerInc :: Int
  }
  deriving (Eq, Show)

data GoSettings = GoSettings
  { goMovetime :: Maybe Int,
    goTimes :: (Int, Int),
    goIncs :: (Int, Int),
    goMaxDepth :: Int
  }
  deriving (Show)

defaultSettings :: GoSettings
defaultSettings =
  GoSettings
    { goMovetime = Nothing,
      goTimes = (maxBound, maxBound),
      goIncs = (0, 0),
      goMaxDepth = maxBound
    }

reportMove :: MVar Move -> IO ()
reportMove moveVar = do
  moveMaybe <- tryTakeMVar moveVar
  let move = uciShowMove (fromMaybe NullMove moveMaybe)
  putStrLn ("bestmove " ++ move)
  hFlush stdout

-- Extract time calculation to a separate function for better debugging
calculateTimeMs :: Maybe Int -> (Int, Int) -> (Int, Int) -> Game -> Int
calculateTimeMs movetime times incs game = 
  max 1000 $ fromMaybe defaultTime movetime
  where
    defaultTime = 
      let currentTime = getter times
          currentInc = getter incs
          baseTime = if currentTime == maxBound || currentTime <= 0
                    then 30000  -- Default 30 seconds if maxBound or invalid
                    else max 1000 (currentTime `safeQuot` 20)
          incTime = if currentInc < 0 then 0 else currentInc `safeQuot` 2
          totalTime = baseTime + incTime
      in max 1000 totalTime  -- Ensure positive result, minimum 1 second
    
    getter = case boardTurn (gameBoard game) of
      White -> fst
      Black -> snd

launchGo :: MVar Move -> MVar (SearchEnv RealWorld) -> Game -> GoSettings -> IO ()
launchGo moveVar ssVar game (GoSettings movetime times incs maxDepth) = do
  let timeMs = calculateTimeMs movetime times incs game
      -- Convert to microseconds safely, avoiding overflow
      timeoutMicros = if timeMs > (maxBound `safeQuot` 1000)
                      then maxBound `safeQuot` 2  -- Large but safe value
                      else timeMs * 1000
  _ <- timeout timeoutMicros (searches (1 :: Int))
  reportMove moveVar
  where
    searches depth
      | depth <= fromIntegral maxDepth && depth > 0 = do
          stateVec <- readMVar ssVar
          result <- stToIO (runReaderT (bestMove (fromIntegral depth) game) stateVec)
          case result of
            (score, move) -> do
              _ <- evaluate score
              _ <- tryTakeMVar moveVar
              putMVar moveVar move
              _ <- swapMVar ssVar stateVec
              pv <- stToIO (runReaderT (pvWalk game) stateVec)
              let pvMoves = foldr (\a str -> ' ' : (uciShowMove a ++ str)) "" pv
              let pvStr = if null pvMoves then "" else " pv" ++ pvMoves
              -- Use putStrLn instead of printf to avoid potential format issues
              putStrLn $ "info depth " ++ show depth ++ " score cp " ++ show score ++ pvStr
              hFlush stdout
              searches (depth + 1)
      | otherwise = pure ()

doUci :: UciState -> IO ()
doUci uciState = do
  line <- getLine
  let command = readUciLine line
  case command of
    Right CommUci -> do
      putStrLn "id name Trout"
      putStrLn "id author Osrepnay"
      putStrLn $ "option name Hash type spin default 16 min 1 max " ++ show (min 1000000 (maxBound `safeQuot` 1000000) :: Int)
      putStrLn "uciok"
      hFlush stdout
      doUci uciState
    Right (CommDebug debug) -> doUci (uciState {uciIsDebug = debug})
    Right CommDont -> do
      putStrLn "miss the annual ShredderChess Annual Barbeque"
      hFlush stdout
      doUci uciState
    Right CommIsready -> do
      putStrLn "readyok"
      hFlush stdout
      doUci uciState
    Right (CommSetoption name value) -> do
      uciState' <- case name of
        "Hash" -> case readEither value of
          Left err -> do
            hPutStrLn stderr err
            pure uciState
          Right hashMB -> if hashMB > 0 
                         then modUciStateHash hashMB uciState
                         else do
                           hPutStrLn stderr "Hash size must be positive"
                           pure uciState
        _ -> do
          hPutStrLn stderr $ "option not supported: " ++ name
          hFlush stderr
          pure uciState
      doUci uciState'
    Right (CommRegister _) -> doUci uciState
    Right CommUcinewgame -> do
      let envMVar = uciSearchEnv uciState
      maybeEnv <- tryReadMVar envMVar
      case maybeEnv of
        Just env -> stToIO (clearEnv env)
        Nothing -> pure ()
      doUci $
        uciState {uciGame = startingGame}
    Right (CommPosition posInit moves) ->
      let ng = case posInit of
            PositionStartpos -> startingGame
            PositionFen fen -> fenToGame fen
       in case playMoves ng moves of
            Left err -> do
              hPutStrLn stderr err
              hFlush stderr
              doUci uciState
            Right game -> do
              doUci (uciState {uciGame = game})
    Right (CommGo args) -> do
      goVar <- newEmptyMVar
      let ssVar = uciSearchEnv uciState
      thread <-
        forkIO $
          launchGo
            goVar
            ssVar
            (uciGame uciState)
            (foldl' (&) defaultSettings (doGoArg <$> args))
      doUci
        ( uciState
            { uciSearch = Just (thread, goVar),
              uciSearchEnv = ssVar
            }
        )
    Right CommStop -> case uciSearch uciState of
      Just (searchId, moveVar) -> do
        killThread searchId
        reportMove moveVar
        doUci uciState
      Nothing -> doUci uciState
    Right CommQuit -> pure ()
    Right CommInfo -> doUci uciState
    Left err -> do
      hPutStrLn stderr err
      doUci uciState
    _ -> doUci uciState
  where
    playMoves g [] = Right g
    playMoves g ((UciMove from to promote) : ms) = case gMoves of
      (move : _) -> case makeMove g move of
        Just ng -> playMoves ng ms
        Nothing -> Left "illegal move"
      [] -> Left "ILLEGAL move"
      where
        moveMatches (Move _ (Promotion p) f t) =
          Just p == promote
            && f == from
            && t == to
        moveMatches (Move _ _ f t) = f == from && t == to
        gMoves = filter moveMatches (allMoves (gameBoard g))
    doGoArg arg gs@(GoSettings mt ts is depth) = case arg of
      GoSearchMoves _ -> gs
      GoPonder -> gs
      GoWtime t -> GoSettings mt (first (const (max 0 t)) ts) is depth
      GoWinc i -> GoSettings mt ts (first (const (max 0 i)) is) depth
      GoBtime t -> GoSettings mt (second (const (max 0 t)) ts) is depth
      GoBinc i -> GoSettings mt ts (second (const (max 0 i)) is) depth
      GoMovestogo _ -> gs
      GoDepth d -> GoSettings mt ts is (max 1 (fromIntegral d))
      GoNodes _ -> gs
      GoMate _ -> gs
      GoMovetime m -> GoSettings (Just (max 1000 m)) ts is depth
      GoInfinite -> GoSettings (Just maxBound) ts is depth

Parse.hs


{-# OPTIONS_GHC -Wno-unrecognised-pragmas #-}

{-# HLINT ignore "Use lambda-case" #-}
module Trout.Uci.Parse
  ( CommPositionInit (..),
    CommGoArg (..),
    UciCommand (..),
    UciMove (..),
    parseUciCommand,
    readUciLine,
  )
where

import Data.Bifunctor (Bifunctor (first))
import Data.Char (ord)
import Data.Functor (($>), (<&>))
import Data.Int (Int16)
import Text.Parsec
  ( alphaNum,
    anyChar,
    char,
    digit,
    eof,
    many,
    many1,
    manyTill,
    oneOf,
    optionMaybe,
    parse,
    skipMany1,
    space,
    spaces,
    string,
    string',
    try,
    (<|>),
  )
import Text.Parsec.String (Parser)
import Trout.Fen.Parse (Fen, parseFen)
import Trout.Piece (PieceType (..))

data CommPositionInit
  = PositionStartpos
  | PositionFen Fen
  deriving (Eq, Show)

-- has less info than trout move
data UciMove = UciMove
  { uciMoveFrom :: Int,
    uciMoveTo :: Int,
    uciMovePromote :: Maybe PieceType
  }
  deriving (Eq, Show)

data CommGoArg
  = GoSearchMoves [String]
  | GoPonder
  | GoWtime Int
  | GoWinc Int
  | GoBtime Int
  | GoBinc Int
  | GoMovestogo Int16
  | GoDepth Int16
  | GoNodes Int
  | GoMate Int16
  | GoMovetime Int
  | GoInfinite
  deriving (Eq, Show)

data UciCommand
  = CommUci
  | CommDebug Bool
  | CommDont
  | CommIsready
  | CommSetoption String String
  | CommRegister String -- not actual type, but we dont need register
  | CommUcinewgame
  | CommPosition CommPositionInit [UciMove]
  | CommGo [CommGoArg]
  | CommStop
  | CommPonderhit
  | CommQuit
  | CommInfo
  deriving (Eq, Show)

-- TODO a lot of this is kinda questionably correct, run through it fully sometime

-- make sure there aren't any trailing bits
commBreak :: Parser ()
commBreak = (many1 space $> ()) <|> eof

spaces1 :: Parser ()
spaces1 = skipMany1 space

-- simple commands without arguments
parseArgless :: Parser UciCommand
parseArgless =
  try (string' "uci" *> commBreak) $> CommUci
    <|> try (string' "Dont" *> commBreak) $> CommDont
    <|> try (string' "isready" *> commBreak) $> CommIsready
    <|> try (string' "ucinewgame" *> commBreak) $> CommUcinewgame
    <|> try (string' "stop" *> commBreak) $> CommStop
    <|> try (string' "ponderhit" *> commBreak) $> CommPonderhit
    <|> try (string' "quit" *> commBreak) $> CommQuit

parseDebug :: Parser UciCommand
parseDebug =
  string' "debug"
    *> spaces1
    *> (string' "on" $> True <|> string' "off" $> False)
    <* commBreak
    <&> CommDebug

parseSetoption :: Parser UciCommand
parseSetoption =
  CommSetoption
    <$> ( string' "setoption"
            *> spaces1
            *> string "name"
            *> spaces1
            *> manyTill anyChar (try (spaces1 *> string' "value"))
        )
    <*> (spaces1 *> many anyChar)
    <* commBreak

-- garbage command for normie engines!
parseRegister :: Parser UciCommand
parseRegister = CommRegister <$> (string' "register" *> many anyChar)

parsePosition :: Parser UciCommand
parsePosition =
  string' "position"
    *> spaces1
    $> CommPosition
    <*> ( string' "startpos" $> PositionStartpos
            <|> PositionFen <$> (string' "fen" *> spaces1 *> parseFen)
        )
    <*> ( spaces1
            *> string "moves"
            *> many parseUciMoves
            <|> pure [] -- maybe should be more strict; reject if mangled here
        )
    <* commBreak
  where
    makeUciMove fromCol fromRow toCol toRow maybePromote =
      UciMove
        fromSq
        toSq
        maybePromotePiece
      where
        fromSq = (ord fromRow - ord '1') * 8 + ord fromCol - ord 'a'
        toSq = (ord toRow - ord '1') * 8 + ord toCol - ord 'a'
        maybePromotePiece =
          maybePromote
            >>= \p -> case p of
              'n' -> Just Knight
              'b' -> Just Bishop
              'r' -> Just Rook
              'q' -> Just Queen
              _ -> Nothing
    parseUciMove :: Parser UciMove
    parseUciMove =
      makeUciMove
        <$> oneOf "abcdefgh"
        <*> oneOf "12345678"
        <*> oneOf "abcdefgh"
        <*> oneOf "12345678"
        <*> optionMaybe (oneOf "nbrq")
    parseUciMoves = spaces1 *> parseUciMove

parseGo :: Parser UciCommand
parseGo = string' "go" *> many (spaces1 *> parseArg) <&> CommGo
  where
    parseIntArg :: (Read a) => String -> (a -> CommGoArg) -> Parser CommGoArg
    parseIntArg n c =
      string' n
        *> spaces1
        *> (c . read <$> ((((:) <$> char '-') <*> many1 digit) <|> many1 digit))
    parseArg =
      ( string' "searchmoves"
          *> many (try (spaces *> many alphaNum))
          <&> GoSearchMoves
      )
        <|> string' "ponder" $> GoPonder
        <|> parseIntArg "wtime" GoWtime
        <|> parseIntArg "winc" GoWinc
        <|> parseIntArg "btime" GoBtime
        <|> parseIntArg "binc" GoBinc
        <|> parseIntArg "movestogo" GoMovestogo
        <|> parseIntArg "depth" GoDepth
        <|> parseIntArg "nodes" GoNodes
        <|> parseIntArg "mate" GoMate
        <|> parseIntArg "movetime" GoMovetime
        <|> string' "infinite" $> GoInfinite

-- NEW: Parser for info lines
parseInfo :: Parser UciCommand
parseInfo = try (string' "info") *> manyTill anyChar eof $> CommInfo

parseUciCommand :: Parser UciCommand
parseUciCommand =
  parseArgless
    <|> parseDebug
    <|> parseSetoption
    <|> parseRegister
    <|> parsePosition
    <|> parseGo
    <|> parseInfo

-- dont require parsec import
readUciLine :: String -> Either String UciCommand
readUciLine = first show . parse parseUciCommand ""

Best regards,
Jim Ablett.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions