UCI engine: python -m chess_nnue.uci
Browse files- README.md +1 -0
- chess_nnue/uci.py +78 -0
- chess_nnue/ui.html +2 -0
- tests/test_chess_nnue.py +17 -0
README.md
CHANGED
|
@@ -197,6 +197,7 @@ print(move, info) # e.g. d2d4 {'depth': 6, 'nodes': ..., 'score': ..., 'ms': .
|
|
| 197 |
```
|
| 198 |
|
| 199 |
- Browser board: `python -m chess_nnue.ui` (http://127.0.0.1:7863): drag or click to move, promotion picker, eval bar, take back, flip.
|
|
|
|
| 200 |
- Lichess bot: `chess_nnue/lichess_bot.py` is a [lichess-bot](https://github.com/lichess-bot-devs/lichess-bot) "homemade" engine.
|
| 201 |
|
| 202 |
## Run the benchmarks
|
|
|
|
| 197 |
```
|
| 198 |
|
| 199 |
- Browser board: `python -m chess_nnue.ui` (http://127.0.0.1:7863): drag or click to move, promotion picker, eval bar, take back, flip.
|
| 200 |
+
- UCI engine (any chess GUI or harness, e.g. Cute Chess, Arena, python-chess): `python -m chess_nnue.uci` from `reasoning_harness/`.
|
| 201 |
- Lichess bot: `chess_nnue/lichess_bot.py` is a [lichess-bot](https://github.com/lichess-bot-devs/lichess-bot) "homemade" engine.
|
| 202 |
|
| 203 |
## Run the benchmarks
|
chess_nnue/uci.py
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""depth 5 as a UCI engine, so any chess GUI or harness can run it.
|
| 2 |
+
|
| 3 |
+
python -m chess_nnue.uci [--weights PATH] (run from reasoning_harness/)
|
| 4 |
+
|
| 5 |
+
Supports: uci, isready, ucinewgame, position (startpos/fen + moves), go
|
| 6 |
+
(movetime / wtime btime winc binc / depth), quit.
|
| 7 |
+
shortcut: search runs synchronously, so "stop" is ignored until the move is found
|
| 8 |
+
(the search always ends at its deadline); add a search thread if a GUI needs ponder/infinite.
|
| 9 |
+
"""
|
| 10 |
+
import argparse
|
| 11 |
+
import sys
|
| 12 |
+
from pathlib import Path
|
| 13 |
+
|
| 14 |
+
import chess
|
| 15 |
+
|
| 16 |
+
from .engine import Engine, MATE
|
| 17 |
+
from .net import NNUE
|
| 18 |
+
|
| 19 |
+
DEFAULT = Path(__file__).parent.parent / "artifacts" / "chess_nnue" / "chess.nnue"
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
def budget_ms(board, args):
|
| 23 |
+
"""Same clock rule as the benchmarks: a 20th of the clock plus most of the increment."""
|
| 24 |
+
opt = dict(zip(args[::2], args[1::2]))
|
| 25 |
+
if "movetime" in opt:
|
| 26 |
+
return max(10, int(opt["movetime"]) - 20)
|
| 27 |
+
side = "w" if board.turn == chess.WHITE else "b"
|
| 28 |
+
if side + "time" in opt:
|
| 29 |
+
clock, inc = int(opt[side + "time"]), int(opt.get(side + "inc", 0))
|
| 30 |
+
return max(10, min(clock / 20 + inc * 0.75, clock / 2 - 50))
|
| 31 |
+
return 1000
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
def main(argv=None, inp=sys.stdin, out=sys.stdout):
|
| 35 |
+
ap = argparse.ArgumentParser()
|
| 36 |
+
ap.add_argument("--weights", default=str(DEFAULT))
|
| 37 |
+
a = ap.parse_args(argv)
|
| 38 |
+
nnue = NNUE(a.weights)
|
| 39 |
+
engine, board = Engine(nnue), chess.Board()
|
| 40 |
+
|
| 41 |
+
def say(line):
|
| 42 |
+
out.write(line + "\n")
|
| 43 |
+
out.flush()
|
| 44 |
+
|
| 45 |
+
for line in inp:
|
| 46 |
+
parts = line.split()
|
| 47 |
+
if not parts:
|
| 48 |
+
continue
|
| 49 |
+
cmd, args = parts[0], parts[1:]
|
| 50 |
+
if cmd == "uci":
|
| 51 |
+
say("id name depth 5")
|
| 52 |
+
say("id author Kedhareswer")
|
| 53 |
+
say("uciok")
|
| 54 |
+
elif cmd == "isready":
|
| 55 |
+
say("readyok")
|
| 56 |
+
elif cmd == "ucinewgame":
|
| 57 |
+
engine = Engine(nnue)
|
| 58 |
+
elif cmd == "position":
|
| 59 |
+
if args[0] == "startpos":
|
| 60 |
+
board, rest = chess.Board(), args[1:]
|
| 61 |
+
else:
|
| 62 |
+
end = args.index("moves") if "moves" in args else len(args)
|
| 63 |
+
board, rest = chess.Board(" ".join(args[1:end])), args[end:]
|
| 64 |
+
for u in rest[1:]:
|
| 65 |
+
board.push_uci(u)
|
| 66 |
+
elif cmd == "go":
|
| 67 |
+
opt = dict(zip(args[::2], args[1::2]))
|
| 68 |
+
move, info = engine.search(board, budget_ms(board, args), int(opt.get("depth", 64)))
|
| 69 |
+
s = info["score"]
|
| 70 |
+
score = f"mate {(MATE - abs(s) + 1) // 2 * (1 if s > 0 else -1)}" if abs(s) >= MATE - 100 else f"cp {s}"
|
| 71 |
+
say(f"info depth {info['depth']} nodes {info['nodes']} time {int(info['ms'])} score {score}")
|
| 72 |
+
say(f"bestmove {move.uci()}")
|
| 73 |
+
elif cmd == "quit":
|
| 74 |
+
break
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
if __name__ == "__main__":
|
| 78 |
+
main()
|
chess_nnue/ui.html
CHANGED
|
@@ -62,6 +62,7 @@
|
|
| 62 |
.card { background: var(--panel); border: 1px solid var(--line); border-radius: 10px; padding: 14px 16px; }
|
| 63 |
h1 { font-size: 18px; margin: 0; letter-spacing: -.01em; }
|
| 64 |
.sub { color: var(--muted); font-size: 13px; margin: 2px 0 0; }
|
|
|
|
| 65 |
.player { display: flex; align-items: center; gap: 10px; padding: 6px 0; }
|
| 66 |
.player + .player { border-top: 1px solid var(--line); }
|
| 67 |
.swatch { width: 14px; height: 14px; border-radius: 3px; border: 1px solid var(--line); flex: none; }
|
|
@@ -106,6 +107,7 @@
|
|
| 106 |
<div class="card c-about">
|
| 107 |
<h1>depth 5</h1>
|
| 108 |
<p class="sub">A 4.8 MB neural network plus alpha-beta search, running on CPU. Drag a piece or click it, then click a square.</p>
|
|
|
|
| 109 |
</div>
|
| 110 |
<div class="card c-players" aria-live="polite">
|
| 111 |
<div class="player" id="p-engine"><span class="swatch"></span><span class="name">depth 5</span><span class="meta" id="eng-meta"></span><span class="dots" aria-label="thinking"><i></i><i></i><i></i></span></div>
|
|
|
|
| 62 |
.card { background: var(--panel); border: 1px solid var(--line); border-radius: 10px; padding: 14px 16px; }
|
| 63 |
h1 { font-size: 18px; margin: 0; letter-spacing: -.01em; }
|
| 64 |
.sub { color: var(--muted); font-size: 13px; margin: 2px 0 0; }
|
| 65 |
+
.sub a { color: var(--accent); }
|
| 66 |
.player { display: flex; align-items: center; gap: 10px; padding: 6px 0; }
|
| 67 |
.player + .player { border-top: 1px solid var(--line); }
|
| 68 |
.swatch { width: 14px; height: 14px; border-radius: 3px; border: 1px solid var(--line); flex: none; }
|
|
|
|
| 107 |
<div class="card c-about">
|
| 108 |
<h1>depth 5</h1>
|
| 109 |
<p class="sub">A 4.8 MB neural network plus alpha-beta search, running on CPU. Drag a piece or click it, then click a square.</p>
|
| 110 |
+
<p class="sub"><a href="https://llm-chess-agentic.vercel.app" target="_blank" rel="noopener">Play it against LLMs in LLM Chess Arena</a> · <a href="https://huggingface.co/Kedhareswer/depth-5" target="_blank" rel="noopener">model and benchmarks</a></p>
|
| 111 |
</div>
|
| 112 |
<div class="card c-players" aria-live="polite">
|
| 113 |
<div class="player" id="p-engine"><span class="swatch"></span><span class="name">depth 5</span><span class="meta" id="eng-meta"></span><span class="dots" aria-label="thinking"><i></i><i></i><i></i></span></div>
|
tests/test_chess_nnue.py
CHANGED
|
@@ -115,6 +115,23 @@ class NNUETests(unittest.TestCase):
|
|
| 115 |
b = chess.Board("r1bqkb1r/ppp2p2/2n2n1p/4p1p1/P5P1/5P1N/1PPBP2P/RN1QKB1R w KQkq - 2 8")
|
| 116 |
self.assertNotEqual(Engine(PST_Eval(), "plain").search(b, 1500)[0], chess.Move.from_uci("h3g5"))
|
| 117 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 118 |
def test_finds_mate_in_one(self):
|
| 119 |
b = chess.Board("6k1/5ppp/8/8/8/8/8/R5K1 w - - 0 1")
|
| 120 |
for eng in (Engine(self.nn), Engine(PST_Eval(), "plain")):
|
|
|
|
| 115 |
b = chess.Board("r1bqkb1r/ppp2p2/2n2n1p/4p1p1/P5P1/5P1N/1PPBP2P/RN1QKB1R w KQkq - 2 8")
|
| 116 |
self.assertNotEqual(Engine(PST_Eval(), "plain").search(b, 1500)[0], chess.Move.from_uci("h3g5"))
|
| 117 |
|
| 118 |
+
def test_uci_engine_plays_legal_moves(self):
|
| 119 |
+
import sys
|
| 120 |
+
import chess.engine
|
| 121 |
+
root = Path(__file__).parent.parent
|
| 122 |
+
uci = chess.engine.SimpleEngine.popen_uci([sys.executable, "-m", "chess_nnue.uci", "--weights", str(self.path)], cwd=root)
|
| 123 |
+
try:
|
| 124 |
+
self.assertEqual(uci.id["name"], "depth 5")
|
| 125 |
+
board = chess.Board()
|
| 126 |
+
for _ in range(6):
|
| 127 |
+
move = uci.play(board, chess.engine.Limit(time=0.2)).move
|
| 128 |
+
self.assertIn(move, board.legal_moves)
|
| 129 |
+
board.push(move)
|
| 130 |
+
mate = chess.Board("6k1/5ppp/8/8/8/8/8/R5K1 w - - 0 1")
|
| 131 |
+
self.assertEqual(uci.play(mate, chess.engine.Limit(white_clock=5, black_clock=5)).move, chess.Move.from_uci("a1a8"))
|
| 132 |
+
finally:
|
| 133 |
+
uci.quit()
|
| 134 |
+
|
| 135 |
def test_finds_mate_in_one(self):
|
| 136 |
b = chess.Board("6k1/5ppp/8/8/8/8/8/R5K1 w - - 0 1")
|
| 137 |
for eng in (Engine(self.nn), Engine(PST_Eval(), "plain")):
|