chessathon-engine 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,2 @@
1
+ /target
2
+ /.venv-dev
@@ -0,0 +1,187 @@
1
+ # This file is automatically @generated by Cargo.
2
+ # It is not intended for manual editing.
3
+ version = 3
4
+
5
+ [[package]]
6
+ name = "arrayvec"
7
+ version = "0.7.8"
8
+ source = "registry+https://github.com/rust-lang/crates.io-index"
9
+ checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56"
10
+
11
+ [[package]]
12
+ name = "autocfg"
13
+ version = "1.5.1"
14
+ source = "registry+https://github.com/rust-lang/crates.io-index"
15
+ checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
16
+
17
+ [[package]]
18
+ name = "cfg-if"
19
+ version = "1.0.4"
20
+ source = "registry+https://github.com/rust-lang/crates.io-index"
21
+ checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
22
+
23
+ [[package]]
24
+ name = "chessathon-engine"
25
+ version = "0.1.0"
26
+ dependencies = [
27
+ "arrayvec",
28
+ "pyo3",
29
+ ]
30
+
31
+ [[package]]
32
+ name = "heck"
33
+ version = "0.5.0"
34
+ source = "registry+https://github.com/rust-lang/crates.io-index"
35
+ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
36
+
37
+ [[package]]
38
+ name = "indoc"
39
+ version = "2.0.7"
40
+ source = "registry+https://github.com/rust-lang/crates.io-index"
41
+ checksum = "79cf5c93f93228cf8efb3ba362535fb11199ac548a09ce117c9b1adc3030d706"
42
+ dependencies = [
43
+ "rustversion",
44
+ ]
45
+
46
+ [[package]]
47
+ name = "libc"
48
+ version = "0.2.189"
49
+ source = "registry+https://github.com/rust-lang/crates.io-index"
50
+ checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2"
51
+
52
+ [[package]]
53
+ name = "memoffset"
54
+ version = "0.9.1"
55
+ source = "registry+https://github.com/rust-lang/crates.io-index"
56
+ checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a"
57
+ dependencies = [
58
+ "autocfg",
59
+ ]
60
+
61
+ [[package]]
62
+ name = "once_cell"
63
+ version = "1.21.4"
64
+ source = "registry+https://github.com/rust-lang/crates.io-index"
65
+ checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
66
+
67
+ [[package]]
68
+ name = "portable-atomic"
69
+ version = "1.15.0"
70
+ source = "registry+https://github.com/rust-lang/crates.io-index"
71
+ checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85"
72
+
73
+ [[package]]
74
+ name = "proc-macro2"
75
+ version = "1.0.107"
76
+ source = "registry+https://github.com/rust-lang/crates.io-index"
77
+ checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9"
78
+ dependencies = [
79
+ "unicode-ident",
80
+ ]
81
+
82
+ [[package]]
83
+ name = "pyo3"
84
+ version = "0.22.6"
85
+ source = "registry+https://github.com/rust-lang/crates.io-index"
86
+ checksum = "f402062616ab18202ae8319da13fa4279883a2b8a9d9f83f20dbade813ce1884"
87
+ dependencies = [
88
+ "cfg-if",
89
+ "indoc",
90
+ "libc",
91
+ "memoffset",
92
+ "once_cell",
93
+ "portable-atomic",
94
+ "pyo3-build-config",
95
+ "pyo3-ffi",
96
+ "pyo3-macros",
97
+ "unindent",
98
+ ]
99
+
100
+ [[package]]
101
+ name = "pyo3-build-config"
102
+ version = "0.22.6"
103
+ source = "registry+https://github.com/rust-lang/crates.io-index"
104
+ checksum = "b14b5775b5ff446dd1056212d778012cbe8a0fbffd368029fd9e25b514479c38"
105
+ dependencies = [
106
+ "once_cell",
107
+ "target-lexicon",
108
+ ]
109
+
110
+ [[package]]
111
+ name = "pyo3-ffi"
112
+ version = "0.22.6"
113
+ source = "registry+https://github.com/rust-lang/crates.io-index"
114
+ checksum = "9ab5bcf04a2cdcbb50c7d6105de943f543f9ed92af55818fd17b660390fc8636"
115
+ dependencies = [
116
+ "libc",
117
+ "pyo3-build-config",
118
+ ]
119
+
120
+ [[package]]
121
+ name = "pyo3-macros"
122
+ version = "0.22.6"
123
+ source = "registry+https://github.com/rust-lang/crates.io-index"
124
+ checksum = "0fd24d897903a9e6d80b968368a34e1525aeb719d568dba8b3d4bfa5dc67d453"
125
+ dependencies = [
126
+ "proc-macro2",
127
+ "pyo3-macros-backend",
128
+ "quote",
129
+ "syn",
130
+ ]
131
+
132
+ [[package]]
133
+ name = "pyo3-macros-backend"
134
+ version = "0.22.6"
135
+ source = "registry+https://github.com/rust-lang/crates.io-index"
136
+ checksum = "36c011a03ba1e50152b4b394b479826cad97e7a21eb52df179cd91ac411cbfbe"
137
+ dependencies = [
138
+ "heck",
139
+ "proc-macro2",
140
+ "pyo3-build-config",
141
+ "quote",
142
+ "syn",
143
+ ]
144
+
145
+ [[package]]
146
+ name = "quote"
147
+ version = "1.0.47"
148
+ source = "registry+https://github.com/rust-lang/crates.io-index"
149
+ checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001"
150
+ dependencies = [
151
+ "proc-macro2",
152
+ ]
153
+
154
+ [[package]]
155
+ name = "rustversion"
156
+ version = "1.0.23"
157
+ source = "registry+https://github.com/rust-lang/crates.io-index"
158
+ checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f"
159
+
160
+ [[package]]
161
+ name = "syn"
162
+ version = "2.0.119"
163
+ source = "registry+https://github.com/rust-lang/crates.io-index"
164
+ checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297"
165
+ dependencies = [
166
+ "proc-macro2",
167
+ "quote",
168
+ "unicode-ident",
169
+ ]
170
+
171
+ [[package]]
172
+ name = "target-lexicon"
173
+ version = "0.12.16"
174
+ source = "registry+https://github.com/rust-lang/crates.io-index"
175
+ checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1"
176
+
177
+ [[package]]
178
+ name = "unicode-ident"
179
+ version = "1.0.24"
180
+ source = "registry+https://github.com/rust-lang/crates.io-index"
181
+ checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
182
+
183
+ [[package]]
184
+ name = "unindent"
185
+ version = "0.2.4"
186
+ source = "registry+https://github.com/rust-lang/crates.io-index"
187
+ checksum = "7264e107f553ccae879d21fbea1d6724ac785e8c3bfc762137959b5802826ef3"
@@ -0,0 +1,33 @@
1
+ [package]
2
+ name = "chessathon-engine"
3
+ version = "0.1.0"
4
+ edition = "2021"
5
+ rust-version = "1.74"
6
+ description = "Bitboard chess move generation, search and evaluation for the AI Chessathon agent"
7
+ license = "MIT"
8
+ repository = "https://github.com/prog-dj/aichessathon-dev"
9
+ readme = "README.md"
10
+
11
+ [lib]
12
+ name = "chessathon_engine"
13
+ crate-type = ["cdylib", "rlib"]
14
+
15
+ [dependencies]
16
+ pyo3 = { version = "0.22", default-features = false, features = ["abi3-py312", "macros"] }
17
+ arrayvec = "0.7"
18
+
19
+ [features]
20
+ default = []
21
+ # maturin turns this on; it stops pyo3 linking libpython so the wheel loads
22
+ # against the host interpreter. `cargo test` runs without it.
23
+ extension-module = ["pyo3/extension-module"]
24
+
25
+ [profile.release]
26
+ opt-level = 3
27
+ lto = "fat"
28
+ codegen-units = 1
29
+ # keep unwind (not abort): a panic then surfaces as a Python exception and
30
+ # agent.py falls back to its own engine instead of the process dying.
31
+
32
+ [profile.test]
33
+ opt-level = 3
@@ -0,0 +1,32 @@
1
+ Metadata-Version: 2.4
2
+ Name: chessathon-engine
3
+ Version: 0.1.0
4
+ Classifier: Programming Language :: Rust
5
+ Classifier: Programming Language :: Python :: Implementation :: CPython
6
+ Classifier: Topic :: Games/Entertainment :: Board Games
7
+ Summary: Bitboard chess move generation, search and evaluation for the AI Chessathon agent
8
+ License: MIT
9
+ Requires-Python: >=3.12
10
+ Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
11
+
12
+ # chessathon-engine
13
+
14
+ Bitboard chess move generation, evaluation and search for the AI Chessathon
15
+ agent. Built as a Python extension with [maturin](https://www.maturin.rs/) /
16
+ [pyo3](https://pyo3.rs/); `agent.py` imports it and falls back to a pure-Python
17
+ engine if the wheel is unavailable.
18
+
19
+ ```python
20
+ import chessathon_engine as ce
21
+ ce.init()
22
+ ce.best_move("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1", 2000, 2500)
23
+ ```
24
+
25
+ Local dev:
26
+
27
+ ```
28
+ cd engine
29
+ maturin develop --release # build + install into the active venv
30
+ cargo test --release # perft suite
31
+ ```
32
+
@@ -0,0 +1,20 @@
1
+ # chessathon-engine
2
+
3
+ Bitboard chess move generation, evaluation and search for the AI Chessathon
4
+ agent. Built as a Python extension with [maturin](https://www.maturin.rs/) /
5
+ [pyo3](https://pyo3.rs/); `agent.py` imports it and falls back to a pure-Python
6
+ engine if the wheel is unavailable.
7
+
8
+ ```python
9
+ import chessathon_engine as ce
10
+ ce.init()
11
+ ce.best_move("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1", 2000, 2500)
12
+ ```
13
+
14
+ Local dev:
15
+
16
+ ```
17
+ cd engine
18
+ maturin develop --release # build + install into the active venv
19
+ cargo test --release # perft suite
20
+ ```
@@ -0,0 +1,21 @@
1
+ [build-system]
2
+ requires = ["maturin>=1.7,<2.0"]
3
+ build-backend = "maturin"
4
+
5
+ [project]
6
+ name = "chessathon-engine"
7
+ version = "0.1.0"
8
+ description = "Bitboard chess move generation, search and evaluation for the AI Chessathon agent"
9
+ readme = "README.md"
10
+ requires-python = ">=3.12"
11
+ license = { text = "MIT" }
12
+ classifiers = [
13
+ "Programming Language :: Rust",
14
+ "Programming Language :: Python :: Implementation :: CPython",
15
+ "Topic :: Games/Entertainment :: Board Games",
16
+ ]
17
+
18
+ [tool.maturin]
19
+ # abi3 wheel: one wheel works on 3.12, 3.13, ...
20
+ features = ["extension-module"]
21
+ strip = true
@@ -0,0 +1,229 @@
1
+ //! Hand-crafted evaluation. Tapered material + piece-square tables plus a few
2
+ //! structural terms. Centipawns, from the side to move's point of view.
3
+
4
+ use crate::position::Position;
5
+ use crate::tables::tables;
6
+ use crate::types::*;
7
+
8
+ pub const MATE: i32 = 30_000;
9
+ pub const MATE_IN_MAX: i32 = MATE - 512;
10
+
11
+ // midgame / endgame piece values
12
+ const MG_VAL: [i32; 6] = [82, 337, 365, 477, 1025, 0];
13
+ const EG_VAL: [i32; 6] = [94, 281, 297, 512, 936, 0];
14
+ const PHASE_W: [i32; 6] = [0, 1, 1, 2, 4, 0];
15
+ const PHASE_MAX: i32 = 24;
16
+
17
+ // piece-square tables, White's view, a1..h8 (index 0 = a1). From PeSTO.
18
+ #[rustfmt::skip]
19
+ const MG_PST: [[i32; 64]; 6] = [
20
+ // pawn
21
+ [ 0,0,0,0,0,0,0,0, -35,-1,-20,-23,-15,24,38,-22, -26,-4,-4,-10,3,3,33,-12,
22
+ -27,-2,-5,12,17,6,10,-25, -14,13,6,21,23,12,17,-23, -6,7,26,31,65,56,25,-20,
23
+ 98,134,61,95,68,126,34,-11, 0,0,0,0,0,0,0,0 ],
24
+ // knight
25
+ [ -105,-21,-58,-33,-17,-28,-19,-23, -29,-53,-12,-3,-1,18,-14,-19, -23,-9,12,10,19,17,25,-16,
26
+ -13,4,16,13,28,19,21,-8, -9,17,19,53,37,69,18,22, -47,60,37,65,84,129,73,44,
27
+ -73,-41,72,36,23,62,7,-17, -167,-89,-34,-49,61,-97,-15,-107 ],
28
+ // bishop
29
+ [ -33,-3,-14,-21,-13,-12,-39,-21, 4,15,16,0,7,21,33,1, 0,15,15,15,14,27,18,10,
30
+ -6,13,13,26,34,12,10,4, -4,5,19,50,37,37,7,-2, -16,37,43,40,35,50,37,-2,
31
+ -26,16,-18,-13,30,59,18,-47, -29,4,-82,-37,-25,-42,7,-8 ],
32
+ // rook
33
+ [ -19,-13,1,17,16,7,-37,-26, -44,-16,-20,-9,-1,11,-6,-71, -45,-25,-16,-17,3,0,-5,-33,
34
+ -36,-26,-12,-1,9,-7,6,-23, -24,-11,7,26,24,35,-8,-20, -5,19,26,36,17,45,61,16,
35
+ 27,32,58,62,80,67,26,44, 32,42,32,51,63,9,31,43 ],
36
+ // queen
37
+ [ -1,-18,-9,10,-15,-25,-31,-50, -35,-8,11,2,8,15,-3,1, -14,2,-11,-2,-5,2,14,5,
38
+ -9,-26,-9,-10,-2,-4,3,-3, -27,-27,-16,-16,-1,17,-2,1, -13,-17,7,8,29,56,47,57,
39
+ -24,-39,-5,1,-16,57,28,54, -28,0,29,12,59,44,43,45 ],
40
+ // king
41
+ [ -15,36,12,-54,8,-28,24,14, 1,7,-8,-64,-43,-16,9,8, -14,-14,-22,-46,-44,-30,-15,-27,
42
+ -49,-1,-27,-39,-46,-44,-33,-51, -17,-20,-12,-27,-30,-25,-14,-36, -9,24,2,-16,-20,6,22,-22,
43
+ 29,-1,-20,-7,-8,-4,-38,-29, -65,23,16,-15,-56,-34,2,13 ],
44
+ ];
45
+
46
+ #[rustfmt::skip]
47
+ const EG_PST: [[i32; 64]; 6] = [
48
+ [ 0,0,0,0,0,0,0,0, 13,8,8,10,13,0,2,-7, 4,7,-6,1,0,-5,-1,-8, 13,9,-3,-7,-7,-8,3,-1,
49
+ 32,24,13,5,-2,4,17,17, 94,100,85,67,56,53,82,84, 178,173,158,134,147,132,165,187, 0,0,0,0,0,0,0,0 ],
50
+ [ -29,-51,-23,-15,-22,-18,-50,-64, -42,-20,-10,-5,-2,-20,-23,-44, -23,-3,-1,15,10,-3,-20,-22,
51
+ -18,-6,16,25,16,17,4,-18, -17,3,22,22,22,11,8,-18, -24,-20,10,9,-1,-9,-19,-41,
52
+ -25,-8,-25,-2,-9,-25,-24,-52, -58,-38,-13,-28,-31,-27,-63,-99 ],
53
+ [ -23,-9,-23,-5,-9,-16,-5,-17, -14,-18,-7,-1,4,-9,-15,-27, -12,-3,8,10,13,3,-7,-15,
54
+ -6,3,13,19,7,10,-3,-9, -3,9,12,9,14,10,3,2, 2,-8,0,-1,-2,6,0,4,
55
+ -8,-4,7,-12,-3,-13,-4,-14, -14,-21,-11,-8,-7,-9,-17,-24 ],
56
+ [ -9,2,3,-1,-5,-13,4,-20, -6,-6,0,2,-9,-9,-11,-3, -4,0,-5,-1,-7,-12,-8,-16,
57
+ 3,5,8,4,-5,-6,-8,-11, 4,3,13,1,2,1,-1,2, 7,7,7,5,4,-3,-5,-3,
58
+ 11,13,13,11,-3,3,8,3, 13,10,18,15,12,12,8,5 ],
59
+ [ -33,-28,-22,-43,-5,-32,-20,-41, -22,-23,-30,-16,-16,-23,-36,-32, -16,-27,15,6,9,17,10,5,
60
+ -18,28,19,47,31,34,39,23, 3,22,24,45,57,40,57,36, -20,6,9,49,47,35,19,9,
61
+ -17,20,32,41,58,25,30,0, -9,22,22,27,27,19,10,20 ],
62
+ [ -53,-34,-21,-11,-28,-14,-24,-43, -27,-11,4,13,14,4,-5,-17, -19,-3,11,21,23,16,7,-9,
63
+ -18,-4,21,24,27,23,9,-11, -8,22,24,27,26,33,26,3, 10,17,23,15,20,45,44,13,
64
+ -12,17,14,17,17,38,23,11, -74,-35,-18,-18,-11,15,4,-17 ],
65
+ ];
66
+
67
+ fn phase(pos: &Position) -> i32 {
68
+ let mut p = 0;
69
+ for pt in [
70
+ PieceType::Knight,
71
+ PieceType::Bishop,
72
+ PieceType::Rook,
73
+ PieceType::Queen,
74
+ ] {
75
+ p += PHASE_W[pt.index()] * pos.pieces[pt.index()].count_ones() as i32;
76
+ }
77
+ p.min(PHASE_MAX)
78
+ }
79
+
80
+ #[inline]
81
+ fn pst_sq(sq: Square, c: Color) -> usize {
82
+ (if c == Color::White { sq } else { flip_square(sq) }) as usize
83
+ }
84
+
85
+ const PASSED_MG: [i32; 8] = [0, 5, 10, 15, 30, 55, 90, 0];
86
+ const PASSED_EG: [i32; 8] = [0, 10, 18, 30, 55, 95, 160, 0];
87
+ const ISOLATED: i32 = 12;
88
+ const DOUBLED: i32 = 10;
89
+ const BISHOP_PAIR: i32 = 25;
90
+ const ROOK_OPEN: i32 = 22;
91
+ const ROOK_HALF: i32 = 10;
92
+ const TEMPO: i32 = 14;
93
+
94
+ pub fn evaluate(pos: &Position) -> i32 {
95
+ let t = tables();
96
+ let ph = phase(pos);
97
+ let occ = pos.occupied();
98
+ let mut mg = 0i32;
99
+ let mut eg = 0i32;
100
+
101
+ let mut safety = [0i32; 2]; // enemy pressure on [white king, black king]
102
+ let mut mobility_mg = [0i32; 2];
103
+
104
+ for c in [Color::White, Color::Black] {
105
+ let sign = if c == Color::White { 1 } else { -1 };
106
+ let ci = c.index();
107
+ let them = c.flip();
108
+ let king_ring = t.king[pos.king_sq(them) as usize] | bb(pos.king_sq(them));
109
+
110
+ for pt in PieceType::ALL {
111
+ let mut b = pos.piece_bb(c, pt);
112
+ while b != 0 {
113
+ let sq = b.trailing_zeros() as Square;
114
+ b &= b - 1;
115
+ let idx = pst_sq(sq, c);
116
+ mg += sign * (MG_VAL[pt.index()] + MG_PST[pt.index()][idx]);
117
+ eg += sign * (EG_VAL[pt.index()] + EG_PST[pt.index()][idx]);
118
+
119
+ match pt {
120
+ PieceType::Knight | PieceType::Bishop | PieceType::Rook | PieceType::Queen => {
121
+ let att = t.attacks(pt, sq, occ) & !pos.color[ci];
122
+ mobility_mg[ci] += (att.count_ones() as i32) * 2;
123
+ let ring_hits = (att & king_ring).count_ones() as i32;
124
+ if ring_hits > 0 {
125
+ safety[them.index()] += ring_hits
126
+ * match pt {
127
+ PieceType::Knight | PieceType::Bishop => 2,
128
+ PieceType::Rook => 3,
129
+ _ => 5,
130
+ };
131
+ }
132
+ }
133
+ _ => {}
134
+ }
135
+ }
136
+ }
137
+
138
+ // bishop pair
139
+ if pos.piece_bb(c, PieceType::Bishop).count_ones() >= 2 {
140
+ mg += sign * BISHOP_PAIR;
141
+ eg += sign * BISHOP_PAIR;
142
+ }
143
+
144
+ // pawns: passed / isolated / doubled
145
+ let own_pawns = pos.piece_bb(c, PieceType::Pawn);
146
+ let enemy_pawns = pos.piece_bb(them, PieceType::Pawn);
147
+ let mut p = own_pawns;
148
+ while p != 0 {
149
+ let sq = p.trailing_zeros() as Square;
150
+ p &= p - 1;
151
+ let f = file_of(sq);
152
+ let file_mask = file_bb(f);
153
+ let adj = (if f > 0 { file_bb(f - 1) } else { 0 })
154
+ | (if f < 7 { file_bb(f + 1) } else { 0 });
155
+ // isolated
156
+ if own_pawns & adj == 0 {
157
+ mg -= sign * ISOLATED;
158
+ eg -= sign * ISOLATED;
159
+ }
160
+ // doubled
161
+ if (own_pawns & file_mask).count_ones() > 1 {
162
+ mg -= sign * DOUBLED / 2;
163
+ eg -= sign * DOUBLED;
164
+ }
165
+ // passed
166
+ let front = if c == Color::White {
167
+ pawn_front_span_white(sq)
168
+ } else {
169
+ pawn_front_span_black(sq)
170
+ };
171
+ if (file_mask | adj) & front & enemy_pawns == 0 {
172
+ let rr = if c == Color::White {
173
+ rank_of(sq)
174
+ } else {
175
+ 7 - rank_of(sq)
176
+ } as usize;
177
+ mg += sign * PASSED_MG[rr];
178
+ eg += sign * PASSED_EG[rr];
179
+ }
180
+ }
181
+
182
+ // rooks on open / half-open files
183
+ let mut r = pos.piece_bb(c, PieceType::Rook);
184
+ while r != 0 {
185
+ let sq = r.trailing_zeros() as Square;
186
+ r &= r - 1;
187
+ let fm = file_bb(file_of(sq));
188
+ if fm & pos.pieces[PieceType::Pawn.index()] == 0 {
189
+ mg += sign * ROOK_OPEN;
190
+ } else if fm & own_pawns == 0 {
191
+ mg += sign * ROOK_HALF;
192
+ }
193
+ }
194
+ }
195
+
196
+ // king danger -> superlinear penalty
197
+ let wd = king_danger(safety[Color::White.index()]);
198
+ let bd = king_danger(safety[Color::Black.index()]);
199
+ mg += bd - wd;
200
+
201
+ mg += mobility_mg[Color::White.index()] - mobility_mg[Color::Black.index()];
202
+
203
+ let score = (mg * ph + eg * (PHASE_MAX - ph)) / PHASE_MAX;
204
+ let stm = if pos.stm == Color::White { score } else { -score };
205
+ stm + TEMPO
206
+ }
207
+
208
+ #[inline]
209
+ fn king_danger(units: i32) -> i32 {
210
+ let u = units.min(40);
211
+ (u * u * 3) / 8
212
+ }
213
+
214
+ fn pawn_front_span_white(sq: Square) -> Bitboard {
215
+ let r = rank_of(sq);
216
+ let mut m = 0u64;
217
+ for rr in (r + 1)..8 {
218
+ m |= rank_bb(rr);
219
+ }
220
+ m
221
+ }
222
+ fn pawn_front_span_black(sq: Square) -> Bitboard {
223
+ let r = rank_of(sq);
224
+ let mut m = 0u64;
225
+ for rr in 0..r {
226
+ m |= rank_bb(rr);
227
+ }
228
+ m
229
+ }
@@ -0,0 +1,154 @@
1
+ //! Python bindings for the Chessathon engine.
2
+ //!
3
+ //! `best_move(fen, time_left_ms, ...)` runs an iterative-deepening search and
4
+ //! returns the chosen move in UCI. Everything heavy (movegen, eval, search)
5
+ //! lives in Rust; `agent.py` is a thin wrapper with a pure-Python fallback.
6
+
7
+ mod eval;
8
+ mod movegen;
9
+ mod perft;
10
+ mod position;
11
+ mod search;
12
+ mod see;
13
+ mod tables;
14
+ mod tt;
15
+ mod types;
16
+
17
+ use position::Position;
18
+ use pyo3::exceptions::PyValueError;
19
+ use pyo3::prelude::*;
20
+ use search::{Limits, Searcher};
21
+ use std::sync::Mutex;
22
+ use types::Move;
23
+
24
+ const VERSION: &str = env!("CARGO_PKG_VERSION");
25
+
26
+ // one searcher (and its TT) kept alive across calls within a process
27
+ struct Engine {
28
+ searcher: Searcher,
29
+ }
30
+
31
+ fn engine() -> &'static Mutex<Engine> {
32
+ use std::sync::OnceLock;
33
+ static E: OnceLock<Mutex<Engine>> = OnceLock::new();
34
+ E.get_or_init(|| {
35
+ // touch the tables so the ~ms of magic generation happens at import
36
+ tables::tables();
37
+ Mutex::new(Engine {
38
+ searcher: Searcher::new(128),
39
+ })
40
+ })
41
+ }
42
+
43
+ #[pyfunction]
44
+ fn version() -> String {
45
+ format!("chessathon-engine {VERSION}")
46
+ }
47
+
48
+ /// Warm the attack tables and TT. Call once at import.
49
+ #[pyfunction]
50
+ fn init() -> bool {
51
+ let guard = engine().lock().unwrap();
52
+ drop(guard);
53
+ true
54
+ }
55
+
56
+ fn parse(fen: &str) -> PyResult<Position> {
57
+ Position::from_fen(fen).map_err(|e| PyValueError::new_err(format!("bad fen: {e}")))
58
+ }
59
+
60
+ /// Apply a list of UCI moves to a position, so the search sees the real game
61
+ /// history (for repetition detection).
62
+ fn apply_moves(pos: &mut Position, moves: &[String]) -> PyResult<()> {
63
+ for uci in moves {
64
+ let legal = pos.legal_moves();
65
+ let mv = legal.iter().find(|m| m.to_uci() == *uci).copied();
66
+ match mv {
67
+ Some(m) => pos.make_move(m),
68
+ None => return Err(PyValueError::new_err(format!("illegal history move {uci}"))),
69
+ }
70
+ }
71
+ Ok(())
72
+ }
73
+
74
+ /// Pick a move. `budget_ms` is the soft target, `hard_ms` the absolute cap.
75
+ /// `moves` are UCI moves already played from `fen` to reach the position to search.
76
+ #[pyfunction]
77
+ #[pyo3(signature = (fen, budget_ms, hard_ms, moves=Vec::new(), max_depth=64))]
78
+ fn best_move(
79
+ fen: &str,
80
+ budget_ms: u64,
81
+ hard_ms: u64,
82
+ moves: Vec<String>,
83
+ max_depth: i32,
84
+ ) -> PyResult<String> {
85
+ let mut pos = parse(fen)?;
86
+ apply_moves(&mut pos, &moves)?;
87
+ let mut guard = engine().lock().unwrap();
88
+ let limits = Limits {
89
+ soft_ms: budget_ms,
90
+ hard_ms: hard_ms.max(budget_ms),
91
+ max_depth: max_depth.clamp(1, 120),
92
+ max_nodes: u64::MAX,
93
+ };
94
+ let r = guard.searcher.search(&mut pos, &limits);
95
+ if r.best.is_none() {
96
+ return Err(PyValueError::new_err("no legal move"));
97
+ }
98
+ Ok(r.best.to_uci())
99
+ }
100
+
101
+ /// Search to a fixed depth (for tests / analysis). Returns (uci, score_cp, depth, nodes).
102
+ #[pyfunction]
103
+ fn search_depth(fen: &str, depth: i32) -> PyResult<(String, i32, i32, u64)> {
104
+ let mut pos = parse(fen)?;
105
+ let mut guard = engine().lock().unwrap();
106
+ let limits = Limits {
107
+ soft_ms: u64::MAX,
108
+ hard_ms: u64::MAX,
109
+ max_depth: depth.clamp(1, 120),
110
+ max_nodes: u64::MAX,
111
+ };
112
+ let r = guard.searcher.search(&mut pos, &limits);
113
+ Ok((r.best.to_uci(), r.score, r.depth, r.nodes))
114
+ }
115
+
116
+ #[pyfunction]
117
+ fn evaluate_fen(fen: &str) -> PyResult<i32> {
118
+ let pos = parse(fen)?;
119
+ Ok(eval::evaluate(&pos))
120
+ }
121
+
122
+ #[pyfunction]
123
+ #[pyo3(name = "perft")]
124
+ fn perft_fen(fen: &str, depth: u32) -> PyResult<u64> {
125
+ let mut pos = parse(fen)?;
126
+ Ok(perft::perft(&mut pos, depth))
127
+ }
128
+
129
+ #[pyfunction]
130
+ fn legal_moves(fen: &str) -> PyResult<Vec<String>> {
131
+ let pos = parse(fen)?;
132
+ Ok(pos.legal_moves().iter().map(|m| m.to_uci()).collect())
133
+ }
134
+
135
+ /// Clear the transposition table (call between games).
136
+ #[pyfunction]
137
+ fn reset() {
138
+ engine().lock().unwrap().searcher.tt.clear();
139
+ }
140
+
141
+ #[pymodule]
142
+ fn chessathon_engine(m: &Bound<'_, PyModule>) -> PyResult<()> {
143
+ m.add("__version__", VERSION)?;
144
+ m.add_function(wrap_pyfunction!(version, m)?)?;
145
+ m.add_function(wrap_pyfunction!(init, m)?)?;
146
+ m.add_function(wrap_pyfunction!(best_move, m)?)?;
147
+ m.add_function(wrap_pyfunction!(search_depth, m)?)?;
148
+ m.add_function(wrap_pyfunction!(evaluate_fen, m)?)?;
149
+ m.add_function(wrap_pyfunction!(perft_fen, m)?)?;
150
+ m.add_function(wrap_pyfunction!(legal_moves, m)?)?;
151
+ m.add_function(wrap_pyfunction!(reset, m)?)?;
152
+ let _ = Move::none();
153
+ Ok(())
154
+ }