Add heavy tests. Fix minor bugs. Initial work on UCI interface
This commit is contained in:
parent
52100835a9
commit
7cd16cea88
|
@ -6,3 +6,8 @@ For now, that's about it.
|
||||||
# Installation
|
# Installation
|
||||||
|
|
||||||
Just run `nimble install`
|
Just run `nimble install`
|
||||||
|
|
||||||
|
|
||||||
|
# Testing
|
||||||
|
|
||||||
|
Just run `nimble test`: sit back, relax, get yourself a cup of coffee and wait for it to finish :)
|
|
@ -18,5 +18,4 @@ requires "jsony >= 1.1.5"
|
||||||
|
|
||||||
task test, "Runs the test suite":
|
task test, "Runs the test suite":
|
||||||
exec "python tests/suite.py -d 6 -b -p -s"
|
exec "python tests/suite.py -d 6 -b -p -s"
|
||||||
# TODO: Also run fewer, more intense test
|
exec "python tests/suite.py -d 7 -b -p -s -f tests/heavy.txt"
|
||||||
# for more precise results
|
|
|
@ -46,7 +46,7 @@ proc toFEN*(self: Chessboard): string
|
||||||
proc updateChecksAndPins*(self: Chessboard)
|
proc updateChecksAndPins*(self: Chessboard)
|
||||||
|
|
||||||
|
|
||||||
proc newChessboard: Chessboard =
|
proc newChessboard*: Chessboard =
|
||||||
## Returns a new, empty chessboard
|
## Returns a new, empty chessboard
|
||||||
new(result)
|
new(result)
|
||||||
for i in 0..63:
|
for i in 0..63:
|
||||||
|
|
|
@ -16,6 +16,9 @@
|
||||||
import pieces
|
import pieces
|
||||||
|
|
||||||
|
|
||||||
|
import std/strformat
|
||||||
|
|
||||||
|
|
||||||
type
|
type
|
||||||
MoveFlag* = enum
|
MoveFlag* = enum
|
||||||
## An enumeration of move flags
|
## An enumeration of move flags
|
||||||
|
@ -159,3 +162,17 @@ func getFlags*(move: Move): seq[MoveFlag] =
|
||||||
result.add(flag)
|
result.add(flag)
|
||||||
if result.len() == 0:
|
if result.len() == 0:
|
||||||
result.add(Default)
|
result.add(Default)
|
||||||
|
|
||||||
|
|
||||||
|
func `$`*(self: Move): string =
|
||||||
|
## Returns a string representation
|
||||||
|
## for the move
|
||||||
|
result &= &"{self.startSquare}{self.targetSquare}"
|
||||||
|
let flags = self.getFlags()
|
||||||
|
if len(flags) > 0:
|
||||||
|
result &= " ("
|
||||||
|
for i, flag in flags:
|
||||||
|
result &= $flag
|
||||||
|
if i < flags.high():
|
||||||
|
result &= ", "
|
||||||
|
result &= ")"
|
|
@ -13,6 +13,7 @@
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
import movegen
|
import movegen
|
||||||
|
import uci
|
||||||
|
|
||||||
|
|
||||||
import std/strformat
|
import std/strformat
|
||||||
|
@ -317,12 +318,6 @@ proc handlePositionCommand(board: var Chessboard, command: seq[string]) =
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
||||||
proc handleUCICommand(board: var Chessboard, command: seq[string]) =
|
|
||||||
echo "id name Nimfish 0.1"
|
|
||||||
echo "id author Nocturn9x & Contributors (see LICENSE)"
|
|
||||||
# TODO
|
|
||||||
echo "uciok"
|
|
||||||
|
|
||||||
|
|
||||||
const HELP_TEXT = """Nimfish help menu:
|
const HELP_TEXT = """Nimfish help menu:
|
||||||
- go: Begin a search
|
- go: Begin a search
|
||||||
|
@ -374,24 +369,22 @@ proc commandLoop*: int =
|
||||||
echo "Nimfish by nocturn9x (see LICENSE)"
|
echo "Nimfish by nocturn9x (see LICENSE)"
|
||||||
var
|
var
|
||||||
board = newDefaultChessboard()
|
board = newDefaultChessboard()
|
||||||
uciMode = false
|
startUCI = false
|
||||||
while true:
|
while true:
|
||||||
var
|
var
|
||||||
cmd: seq[string]
|
cmd: seq[string]
|
||||||
cmdStr: string
|
cmdStr: string
|
||||||
try:
|
try:
|
||||||
if not uciMode:
|
|
||||||
stdout.write(">>> ")
|
stdout.write(">>> ")
|
||||||
stdout.flushFile()
|
stdout.flushFile()
|
||||||
cmdStr = readLine(stdin).strip(leading=true, trailing=true, chars={'\t', ' '})
|
cmdStr = readLine(stdin).strip(leading=true, trailing=true, chars={'\t', ' '})
|
||||||
if cmdStr.len() == 0:
|
if cmdStr.len() == 0:
|
||||||
continue
|
continue
|
||||||
cmd = cmdStr.splitWhitespace(maxsplit=2)
|
cmd = cmdStr.splitWhitespace(maxsplit=2)
|
||||||
|
|
||||||
case cmd[0]:
|
case cmd[0]:
|
||||||
of "uci":
|
of "uci":
|
||||||
handleUCICommand(board, cmd)
|
startUCI = true
|
||||||
uciMode = true
|
break
|
||||||
of "clear":
|
of "clear":
|
||||||
echo "\x1Bc"
|
echo "\x1Bc"
|
||||||
of "help":
|
of "help":
|
||||||
|
@ -460,3 +453,5 @@ proc commandLoop*: int =
|
||||||
except EOFError:
|
except EOFError:
|
||||||
echo ""
|
echo ""
|
||||||
return 0
|
return 0
|
||||||
|
if startUCI:
|
||||||
|
startUCISession()
|
|
@ -0,0 +1,240 @@
|
||||||
|
# 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)
|
|
@ -0,0 +1,7 @@
|
||||||
|
rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1
|
||||||
|
r3k2r/p1ppqpb1/bn2pnp1/3PN3/1p2P3/2N2Q1p/PPPBBPPP/R3K2R w KQkq -
|
||||||
|
8/2p5/3p4/KP5r/1R3p1k/8/4P1P1/8 w - -
|
||||||
|
r3k2r/Pppp1ppp/1b3nbN/nP6/BBP1P3/q4N2/Pp1P2PP/R2Q1RK1 w kq - 0 1
|
||||||
|
r2q1rk1/pP1p2pp/Q4n2/bbp1p3/Np6/1B3NBn/pPPP1PPP/R3K2R b KQ - 0 1
|
||||||
|
rnbq1k1r/pp1Pbppp/2p5/8/2B5/8/PPP1NnPP/RNBQK2R w KQ - 1 8
|
||||||
|
r4rk1/1pp1qppp/p1np1n2/2b1p1B1/2B1P1b1/P1NP1N2/1PP1QPPP/R4RK1 w - - 0 10
|
|
@ -10,14 +10,15 @@ from copy import deepcopy
|
||||||
|
|
||||||
def main(args: Namespace) -> int:
|
def main(args: Namespace) -> int:
|
||||||
# We try to be polite with resource usage
|
# We try to be polite with resource usage
|
||||||
|
successful = []
|
||||||
|
failed = []
|
||||||
|
positions = args.positions_file.read_text().splitlines()
|
||||||
|
print(f"[S] Loaded {len(positions)} position{'' if len(positions) == 1 else 's'}")
|
||||||
|
longest_fen = max(sorted([len(fen) for fen in positions]))
|
||||||
if not args.parallel:
|
if not args.parallel:
|
||||||
print("[S] Starting test suite")
|
print("[S] Starting test suite")
|
||||||
else:
|
else:
|
||||||
print(f"[S] Starting test suite with {args.workers} workers")
|
print(f"[S] Starting test suite with {args.workers} workers")
|
||||||
successful = []
|
|
||||||
failed = []
|
|
||||||
positions = args.positions.read_text().splitlines()
|
|
||||||
longest_fen = max(sorted([len(fen) for fen in positions]))
|
|
||||||
start = timeit.default_timer()
|
start = timeit.default_timer()
|
||||||
if not args.parallel:
|
if not args.parallel:
|
||||||
for i, fen in enumerate(positions):
|
for i, fen in enumerate(positions):
|
||||||
|
@ -72,8 +73,8 @@ if __name__ == "__main__":
|
||||||
parser.add_argument("-b", "--bulk", action="store_true", help="Enable bulk-counting for Nimfish (much faster)", default=False)
|
parser.add_argument("-b", "--bulk", action="store_true", help="Enable bulk-counting for Nimfish (much faster)", default=False)
|
||||||
parser.add_argument("--stockfish", type=Path, help="Path to the stockfish executable. Defaults to '' (detected automatically)", default=None)
|
parser.add_argument("--stockfish", type=Path, help="Path to the stockfish executable. Defaults to '' (detected automatically)", default=None)
|
||||||
parser.add_argument("--nimfish", type=Path, help="Path to the nimfish executable. Defaults to '' (detected automatically)", default=None)
|
parser.add_argument("--nimfish", type=Path, help="Path to the nimfish executable. Defaults to '' (detected automatically)", default=None)
|
||||||
parser.add_argument("--positions", type=Path, help="Location of the file containing FENs to test, one per line. Defaults to 'tests/positions.txt'",
|
parser.add_argument("--positions-file", "-f", type=Path, help="Location of the file containing FENs to test, one per line. Defaults to 'tests/all.txt'",
|
||||||
default=Path("tests/positions.txt"))
|
default=Path("tests/all.txt"))
|
||||||
parser.add_argument("--no-silent", action="store_true", help="Do not suppress output from compare_positions.py (defaults to False)", default=False)
|
parser.add_argument("--no-silent", action="store_true", help="Do not suppress output from compare_positions.py (defaults to False)", default=False)
|
||||||
parser.add_argument("-p", "--parallel", action="store_true", help="Run multiple tests in parallel", default=False)
|
parser.add_argument("-p", "--parallel", action="store_true", help="Run multiple tests in parallel", default=False)
|
||||||
parser.add_argument("--workers", "-w", type=int, required=False, help="How many workers to use in parallel mode (defaults to cpu_count() / 2)", default=cpu_count() // 2)
|
parser.add_argument("--workers", "-w", type=int, required=False, help="How many workers to use in parallel mode (defaults to cpu_count() / 2)", default=cpu_count() // 2)
|
||||||
|
|
Loading…
Reference in New Issue