From a6bf7c3d74ba911214900103c69c44f83888f92d Mon Sep 17 00:00:00 2001 From: DualFroz Date: Wed, 26 Aug 2026 19:15:00 +0200 Subject: [PATCH] feat(protocol): add uci protocol handler --- Uci.scala | 110 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 110 insertions(+) create mode 100644 Uci.scala diff --git a/Uci.scala b/Uci.scala new file mode 100644 index 0000000..c5aed9c --- /dev/null +++ b/Uci.scala @@ -0,0 +1,110 @@ +package gambit + +final class Uci: + private val searcher = new Searcher + private var pos = Fen.parse(Fen.startPosition) + private val history = scala.collection.mutable.ArrayBuffer[Long](pos.hash) + private var worker: Thread = null + + def handle(line: String): List[String] = + val tokens = line.trim.split("\\s+").filter(_.nonEmpty).toList + tokens match + case Nil => Nil + case cmd :: rest => + cmd match + case "uci" => idLines + case "isready" => List("readyok") + case "ucinewgame" => resetGame(); Nil + case "position" => setPosition(rest); Nil + case "go" => + val collected = scala.collection.mutable.ListBuffer[String]() + runGo(rest, collected += _) + collected.toList + case "stop" => searcher.stop(); Nil + case "quit" => Nil + case _ => Nil + + def run(input: Iterator[String], out: String => Unit): Unit = + var quit = false + while !quit && input.hasNext do + val line = input.next() + val tokens = line.trim.split("\\s+").filter(_.nonEmpty).toList + tokens match + case "quit" :: _ => searcher.stop(); joinWorker(); quit = true + case "stop" :: _ => searcher.stop(); joinWorker() + case "go" :: rest => + joinWorker() + worker = new Thread(() => runGo(rest, out)) + worker.setDaemon(true) + worker.start() + case ("position" | "ucinewgame") :: _ => + joinWorker() + handle(line).foreach(out) + case _ => handle(line).foreach(out) + + private def idLines: List[String] = + List("id name gambit", "id author gambit", "uciok") + + private def joinWorker(): Unit = + if worker != null then + worker.join() + worker = null + + private def resetGame(): Unit = + searcher.tt.clear() + pos = Fen.parse(Fen.startPosition) + history.clear() + history += pos.hash + + private def setPosition(args: List[String]): Unit = + val rest = + args match + case "startpos" :: tail => + pos = Fen.parse(Fen.startPosition) + tail + case "fen" :: tail => + val movesIdx = tail.indexOf("moves") + val fenTokens = if movesIdx < 0 then tail else tail.take(movesIdx) + pos = Fen.parse(fenTokens.mkString(" ")) + if movesIdx < 0 then Nil else tail.drop(movesIdx) + case other => other + history.clear() + history += pos.hash + rest match + case "moves" :: moveTokens => applyMoves(moveTokens) + case _ => () + + private def applyMoves(moveTokens: List[String]): Unit = + for token <- moveTokens do + val legal = new MoveList() + MoveGen.generateLegal(pos, legal) + var i = 0 + var found = -1 + while i < legal.size && found < 0 do + if Move.uci(legal(i)) == token then found = legal(i) + i += 1 + if found >= 0 then + pos.make(found) + history += pos.hash + + private def runGo(args: List[String], emit: String => Unit): Unit = + val limits = TimeControl.parse(args) + val whiteToMove = pos.sideToMove == Color.White + val plan = TimeControl.plan(limits, whiteToMove) + searcher.onIteration = (d, score, nodes, ms, pv) => emit(infoLine(d, score, nodes, ms, pv)) + searcher.setGameHistory(history.toArray, history.size - 1) + val result = searcher.search(pos, plan.depth, plan.timeMs) + searcher.onIteration = (_, _, _, _, _) => () + val bestMove = if result.bestMove != 0 then Move.uci(result.bestMove) else "0000" + emit(s"bestmove $bestMove") + + private def infoLine(depth: Int, score: Int, nodes: Long, ms: Long, pv: List[Int]): String = + val scoreStr = + if math.abs(score) >= TT.MateBound then + val plies = Searcher.MateValue - math.abs(score) + val movesToMate = (plies + 1) / 2 + s"mate ${if score > 0 then movesToMate else -movesToMate}" + else s"cp $score" + val nps = if ms > 0 then nodes * 1000 / ms else nodes + val pvStr = pv.map(Move.uci).mkString(" ") + s"info depth $depth score $scoreStr nodes $nodes nps $nps time $ms pv $pvStr"