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.
- chessathon_engine-0.1.0/.gitignore +2 -0
- chessathon_engine-0.1.0/Cargo.lock +187 -0
- chessathon_engine-0.1.0/Cargo.toml +33 -0
- chessathon_engine-0.1.0/PKG-INFO +32 -0
- chessathon_engine-0.1.0/README.md +20 -0
- chessathon_engine-0.1.0/pyproject.toml +21 -0
- chessathon_engine-0.1.0/src/eval.rs +229 -0
- chessathon_engine-0.1.0/src/lib.rs +154 -0
- chessathon_engine-0.1.0/src/movegen.rs +353 -0
- chessathon_engine-0.1.0/src/perft.rs +118 -0
- chessathon_engine-0.1.0/src/position.rs +546 -0
- chessathon_engine-0.1.0/src/search.rs +533 -0
- chessathon_engine-0.1.0/src/see.rs +110 -0
- chessathon_engine-0.1.0/src/tables.rs +261 -0
- chessathon_engine-0.1.0/src/tt.rs +119 -0
- chessathon_engine-0.1.0/src/types.rs +198 -0
|
@@ -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
|
+
}
|