CPG/Chess/nimfish/nimfishpkg/uci.nim

240 lines
8.8 KiB
Nim

# Copyright 2024 Mattia Giambirtone & All Contributors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
## Implementation of a UCI compatible server
import std/strutils
import std/strformat
import board
import movegen
type
UCISession = ref object
debug: bool
board: Chessboard
UCICommandType = enum
Unknown
IsReady
NewGame
Quit,
Debug,
Position
UCICommand = object
case kind: UCICommandType
of Debug:
value: bool
of Position:
fen: string
moves: seq[string]
of Unknown:
reason: string
else:
discard
proc handleUCIMove(session: UCISession, move: string): tuple[move: Move, cmd: UCICommand] {.discardable.} =
var
startSquare: Square
targetSquare: Square
flags: seq[MoveFlag]
if len(move) notin 4..5:
return (nullMove(), UCICommand(kind: Unknown, reason: &"invalid move syntax"))
try:
startSquare = move[0..1].toSquare()
except ValueError:
return (nullMove(), UCICommand(kind: Unknown, reason: &"invalid start square {move[0..1]}"))
try:
targetSquare = move[2..3].toSquare()
except ValueError:
return (nullMove(), UCICommand(kind: Unknown, reason: &"invalid target square {move[2..3]}"))
# Since the client tells us just the source and target square of the move,
# we have to figure out all the flags by ourselves (whether it's a double
# push, a capture, a promotion, etc.)
if session.board.getPiece(targetSquare).kind != Empty:
flags.add(Capture)
if session.board.getPiece(startSquare).kind == Pawn and abs(rankFromSquare(startSquare) - rankFromSquare(targetSquare)) == 2:
flags.add(DoublePush)
if len(move) == 5:
# Promotion
case move[4]:
of 'b':
flags.add(PromoteToBishop)
of 'n':
flags.add(PromoteToKnight)
of 'q':
flags.add(PromoteToQueen)
of 'r':
flags.add(PromoteToRook)
else:
return
let piece = session.board.getPiece(startSquare)
if piece.kind == King and startSquare == session.board.position.sideToMove.getKingStartingSquare():
if targetSquare in [piece.kingSideCastling(), piece.queenSideCastling()]:
flags.add(Castle)
elif targetSquare == session.board.position.enPassantSquare:
flags.add(EnPassant)
let move = createMove(startSquare, targetSquare, flags)
if session.debug:
echo &"info string making move {move}"
result.move = session.board.makeMove(move)
proc handleUCIPositionCommand(session: UCISession, command: seq[string]): UCICommand =
# Makes sure we don't leave the board in an invalid state if
# some error occurs
result = UCICommand(kind: Position)
case command[1]:
of "startpos":
result.fen = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1"
session.board = newDefaultChessboard()
if command.len() > 2:
let args = command[2..^1]
if args.len() > 0:
var i = 0
while i < args.len():
case args[i]:
of "moves":
var j = i + 1
while j < args.len():
let r = handleUCIMove(session, args[j])
if r.move == nullMove():
if r.cmd.reason.len() > 0:
return UCICommand(kind: Unknown, reason: &"move {args[j]} is illegal or invalid ({r.cmd.reason})")
else:
return UCICommand(kind: Unknown, reason: &"move {args[j]} is illegal or invalid")
result.moves.add(args[j])
inc(j)
inc(i)
of "fen":
var
args = command[2..^1]
fenString = ""
stop = 0
for i, arg in args:
if arg in ["moves", ]:
break
if i > 0:
fenString &= " "
fenString &= arg
inc(stop)
result.fen = fenString
args = args[stop..^1]
session.board = newChessboardFromFEN(fenString)
if args.len() > 0:
var i = 0
while i < args.len():
case args[i]:
of "moves":
var j = i + 1
while j < args.len():
while j < args.len():
let r = handleUCIMove(session, args[j])
if r.move == nullMove():
if r.cmd.reason.len() > 0:
return UCICommand(kind: Unknown, reason: &"move {args[j]} is illegal or invalid ({r.cmd.reason})")
else:
return UCICommand(kind: Unknown, reason: &"move {args[j]} is illegal or invalid")
result.moves.add(args[j])
inc(j)
inc(i)
else:
return UCICommand(kind: Unknown, reason: &"unknown subcomponent '{command[1]}'")
proc parseUCICommand(session: UCISession, command: string): UCICommand =
## Attempts to parse the given UCI command
var cmd = command.replace("\t", "").splitWhitespace()
result = UCICommand(kind: Unknown)
var current = 0
while current < cmd.len():
case cmd[current]:
of "isready":
return UCICommand(kind: IsReady)
of "ucinewgame":
return UCICommand(kind: NewGame)
of "quit":
return UCICommand(kind: Quit)
of "debug":
if current == cmd.high():
return
case cmd[current + 1]:
of "on":
return UCICommand(kind: Debug, value: true)
of "off":
return UCICommand(kind: Debug, value: false)
else:
return
of "position":
return session.handleUCIPositionCommand(cmd)
else:
# Unknown UCI commands should be ignored. Attempt
# to make sense of the input regardless
inc(current)
proc startUCISession* =
## Begins listening for UCI commands
echo "id name Nimfish 0.1"
echo "id author Nocturn9x & Contributors (see LICENSE)"
echo "uciok"
var
cmd: UCICommand
cmdStr: string
session = UCISession()
while true:
try:
cmdStr = readLine(stdin).strip(leading=true, trailing=true, chars={'\t', ' '})
if cmdStr.len() == 0:
if session.debug:
echo "info string received empty input, ignoring it"
continue
cmd = session.parseUCICommand(cmdStr)
if cmd.kind == Unknown:
if session.debug:
echo &"info string received unknown or invalid command '{cmdStr}' -> {cmd.reason}"
continue
if session.debug:
echo &"info string received command '{cmdStr}' -> {cmd}"
case cmd.kind:
of Quit:
quit(0)
of IsReady:
echo "readyok"
of Debug:
session.debug = cmd.value
of NewGame:
session.board = newDefaultChessboard()
of Position:
discard
else:
discard
except IOError:
if session.debug:
echo "info string I/O error while reading from stdin, exiting"
echo ""
quit(0)
except EOFError:
if session.debug:
echo "info string EOF received while reading from stdin, exiting"
echo ""
quit(0)