Kedhareswer commited on
Commit
255fab2
·
verified ·
1 Parent(s): a309713

UCI engine: python -m chess_nnue.uci

Browse files
Files changed (4) hide show
  1. README.md +1 -0
  2. chess_nnue/uci.py +78 -0
  3. chess_nnue/ui.html +2 -0
  4. 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")):