├── .cargo
└── config.toml
├── .github
└── workflows
│ ├── release.yml
│ ├── release_pgo.yml
│ └── test.yml
├── .gitignore
├── Cargo.lock
├── Cargo.toml
├── LICENSE
├── Makefile
├── README.md
├── artifacts
└── release_notes.md
├── engine
├── Cargo.toml
├── nets
│ ├── velvet_layer_in_bias.qnn
│ ├── velvet_layer_in_weights.qnn
│ ├── velvet_layer_out_bias.qnn
│ └── velvet_layer_out_weights.qnn
├── src
│ ├── align.rs
│ ├── bitboard.rs
│ ├── board.rs
│ ├── board
│ │ ├── castling.rs
│ │ └── cycledetection.rs
│ ├── colors.rs
│ ├── engine.rs
│ ├── engine
│ │ └── bench.rs
│ ├── fen.rs
│ ├── history_heuristics.rs
│ ├── init.rs
│ ├── lib.rs
│ ├── magics.rs
│ ├── main.rs
│ ├── move_gen.rs
│ ├── moves.rs
│ ├── nn.rs
│ ├── nn
│ │ ├── eval.rs
│ │ └── io.rs
│ ├── params.rs
│ ├── params
│ │ └── macros.rs
│ ├── perft.rs
│ ├── pieces.rs
│ ├── pos_history.rs
│ ├── random.rs
│ ├── scores.rs
│ ├── search.rs
│ ├── search_context.rs
│ ├── slices.rs
│ ├── syzygy.rs
│ ├── time_management.rs
│ ├── transposition_table.rs
│ ├── uci.rs
│ ├── uci_move.rs
│ └── zobrist.rs
└── tests
│ └── perft_tests.rs
├── fathomrs
├── Cargo.toml
├── build.rs
├── fathom
│ ├── LICENSE
│ ├── README.md
│ └── src
│ │ ├── stdendian.h
│ │ ├── tbchess.c
│ │ ├── tbconfig.h
│ │ ├── tbprobe.c
│ │ └── tbprobe.h
└── src
│ ├── bindings.rs
│ ├── lib.rs
│ └── tb.rs
├── genmagics
├── Cargo.toml
└── src
│ └── main.rs
├── gensets
├── Cargo.toml
└── src
│ ├── main.rs
│ └── writer.rs
├── gputrainer
├── Cargo.toml
└── src
│ ├── layer.rs
│ ├── main.rs
│ └── sets.rs
├── logo
└── velvet_logo.png
├── pgo
└── merged_avx512_linux.profdata
├── pytools
└── patch-verifier
│ ├── Pipfile
│ ├── Pipfile.lock
│ ├── client.py
│ ├── common.py
│ └── server.py
├── rustfmt.toml
├── selfplay
├── Cargo.toml
└── src
│ ├── lib.rs
│ ├── openings.rs
│ ├── pentanomial.rs
│ └── selfplay.rs
├── sprt
├── Cargo.toml
└── src
│ ├── main.rs
│ └── sprt.rs
├── tournament
├── Cargo.toml
└── src
│ ├── affinity.rs
│ ├── config.rs
│ ├── main.rs
│ ├── pgn.rs
│ ├── san.rs
│ ├── tournament.rs
│ └── uci_engine.rs
├── traincommon
├── Cargo.toml
└── src
│ ├── idsource.rs
│ ├── lib.rs
│ └── sets.rs
├── tuner
├── Cargo.toml
└── src
│ └── main.rs
└── verify-patch
/.cargo/config.toml:
--------------------------------------------------------------------------------
1 | [target.aarch64-unknown-linux-gnu]
2 | linker = "aarch64-linux-gnu-gcc"
3 |
4 | [build]
5 | rustflags = ["-Ctarget-cpu=native"]
6 |
7 | [env]
8 | CFLAGS = "-march=native"
9 |
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: Release
2 |
3 | on:
4 | push:
5 | tags: "v*"
6 |
7 | jobs:
8 | build-linux:
9 | runs-on: ubuntu-latest
10 | strategy:
11 | matrix:
12 | include:
13 | - exec_postfix: "x86_64-avx512"
14 | add_rustflags: "-Ctarget-feature=+crt-static -Ctarget-cpu=x86-64-v4"
15 | toolchain: "x86_64-unknown-linux-musl"
16 | cflags: "-march=x86-64-v4"
17 | - exec_postfix: "x86_64-avx2"
18 | add_rustflags: "-Ctarget-feature=+crt-static,-bmi2 -Ctarget-cpu=x86-64-v3"
19 | toolchain: "x86_64-unknown-linux-musl"
20 | cflags: "-march=x86-64-v3"
21 | - exec_postfix: "x86_64-sse4-popcnt"
22 | add_rustflags: "-Ctarget-feature=+crt-static -Ctarget-cpu=x86-64-v2"
23 | toolchain: "x86_64-unknown-linux-musl"
24 | cflags: "-march=x86-64-v2"
25 | - exec_postfix: "x86_64-nopopcnt"
26 | add_rustflags: "-Ctarget-feature=+crt-static"
27 | toolchain: "x86_64-unknown-linux-musl"
28 | cflags: "-march=x86-64 -DTB_CUSTOM_POP_COUNT"
29 |
30 | steps:
31 | - name: Checkout code
32 | uses: actions/checkout@v4
33 |
34 | - name: Build
35 | env:
36 | RUSTFLAGS: '${{ matrix.add_rustflags }}'
37 | CFLAGS: '${{ matrix.cflags }}'
38 | run: |
39 | sudo apt-get install -y musl-tools
40 | if [ "${{ matrix.exec_postfix }}" == "x86_64-avx512" ]; then
41 | rustup toolchain install nightly-2025-04-05
42 | rustup override set nightly-2025-04-05
43 | rustup target add ${{ matrix.toolchain }}
44 | cargo build --release --target ${{ matrix.toolchain }} --bin velvet --features=fathomrs,avx512
45 | else
46 | rustup override set 1.86.0
47 | rustup target add ${{ matrix.toolchain }}
48 | cargo build --release --target ${{ matrix.toolchain }} --bin velvet
49 | fi
50 | mv target/${{ matrix.toolchain }}/release/velvet velvet-linux-${{ matrix.exec_postfix }}
51 |
52 | - name: Upload artifacts
53 | uses: actions/upload-artifact@v4
54 | with:
55 | name: velvet-linux-${{ matrix.exec_postfix }}
56 | path: velvet-linux-${{ matrix.exec_postfix }}
57 |
58 | build-windows:
59 | runs-on: windows-latest
60 | strategy:
61 | matrix:
62 | include:
63 | - exec_postfix: "avx2"
64 | add_rustflags: "-Ctarget-feature=+crt-static,-bmi2 -Ctarget-cpu=x86-64-v3"
65 | cflags: "-march=x86-64-v3"
66 | - exec_postfix: "sse4-popcnt"
67 | add_rustflags: "-Ctarget-feature=+crt-static -Ctarget-cpu=x86-64-v2"
68 | cflags: "-march=x86-64-v2"
69 | - exec_postfix: "nopopcnt"
70 | add_rustflags: "-Ctarget-feature=+crt-static"
71 | cflags: "-march=x86-64 -DTB_CUSTOM_POP_COUNT"
72 |
73 | steps:
74 | - name: Checkout code
75 | uses: actions/checkout@v4
76 |
77 | - name: Build
78 | env:
79 | RUSTFLAGS: '${{ matrix.add_rustflags }}'
80 | CFLAGS: '${{ matrix.cflags }}'
81 | run: |
82 | if ("${{ matrix.exec_postfix }}" -match "avx512") {
83 | rustup toolchain install nightly-2025-04-05
84 | rustup override set nightly-2025-04-05
85 | cargo build --release --bin velvet --features=fathomrs,avx512
86 | } else {
87 | rustup override set 1.86.0
88 | cargo build --release --bin velvet
89 | }
90 | mv .\target\release\velvet.exe velvet-windows-x86_64-${{ matrix.exec_postfix }}.exe
91 |
92 | - name: Upload artifacts
93 | uses: actions/upload-artifact@v4
94 | with:
95 | name: velvet-windows-${{ matrix.exec_postfix }}
96 | path: velvet-windows-x86_64-${{ matrix.exec_postfix }}.exe
97 |
98 | build-macos:
99 | runs-on: macos-latest
100 | strategy:
101 | matrix:
102 | include:
103 | - exec_postfix: "avx2"
104 | toolchain: x86_64-apple-darwin
105 | add_rustflags: "-Ctarget-feature=+crt-static,-bmi2 -Ctarget-cpu=x86-64-v3"
106 | cflags: "-march=x86-64-v3"
107 | - exec_postfix: "sse4-popcnt"
108 | toolchain: x86_64-apple-darwin
109 | add_rustflags: "-Ctarget-feature=+crt-static -Ctarget-cpu=x86-64-v2"
110 | cflags: "-march=x86-64-v2"
111 | - exec_postfix: "nopopcnt"
112 | toolchain: x86_64-apple-darwin
113 | add_rustflags: "-Ctarget-feature=+crt-static"
114 | cflags: "-march=x86-64 -DTB_CUSTOM_POP_COUNT"
115 | - exec_postfix: "apple-silicon"
116 | toolchain: aarch64-apple-darwin
117 | add_rustflags: "-Ctarget-feature=+crt-static"
118 | cflags: ""
119 |
120 | steps:
121 | - name: Checkout code
122 | uses: actions/checkout@v4
123 |
124 | - name: Build
125 | env:
126 | RUSTFLAGS: '${{ matrix.add_rustflags }}'
127 | CFLAGS: '${{ matrix.cflags }}'
128 | run: |
129 | rustup override set 1.86.0
130 | rustup target add ${{ matrix.toolchain }}
131 | cargo build --release --target ${{ matrix.toolchain }} --bin velvet
132 | mv target/${{ matrix.toolchain }}/release/velvet velvet-macOS-${{ matrix.exec_postfix }}
133 |
134 | - name: Upload artifacts
135 | uses: actions/upload-artifact@v4
136 | with:
137 | name: velvet-macOS-${{ matrix.exec_postfix }}
138 | path: velvet-macOS-${{ matrix.exec_postfix }}
139 |
140 | release:
141 | if: github.repository == 'mhonert/velvet-chess'
142 | needs: [build-linux, build-windows]
143 | name: Publish release
144 | runs-on: ubuntu-latest
145 | steps:
146 | - name: Checkout code
147 | uses: actions/checkout@v4
148 |
149 | - uses: actions/download-artifact@v4
150 | with:
151 | pattern: velvet-linux-*
152 | merge-multiple: true
153 |
154 | - uses: actions/download-artifact@v4
155 | with:
156 | pattern: velvet-windows-*
157 | merge-multiple: true
158 |
159 | - uses: actions/download-artifact@v4
160 | with:
161 | pattern: velvet-macOS-*
162 | merge-multiple: true
163 |
164 | - name: Create Release
165 | env:
166 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
167 | run: |
168 | tag_name="${GITHUB_REF##*/}"
169 | ls -l
170 | chmod +x velvet-linux-x86_64*
171 | chmod +x velvet-macOS*
172 | mv velvet-windows-x86_64-avx2.exe velvet-${tag_name}-x86_64-avx2.exe
173 | mv velvet-windows-x86_64-sse4-popcnt.exe velvet-${tag_name}-x86_64-sse4-popcnt.exe
174 | mv velvet-windows-x86_64-nopopcnt.exe velvet-${tag_name}-x86_64-nopopcnt.exe
175 | mv velvet-linux-x86_64-avx512 velvet-${tag_name}-x86_64-avx512
176 | mv velvet-linux-x86_64-avx2 velvet-${tag_name}-x86_64-avx2
177 | mv velvet-linux-x86_64-sse4-popcnt velvet-${tag_name}-x86_64-sse4-popcnt
178 | mv velvet-linux-x86_64-nopopcnt velvet-${tag_name}-x86_64-nopopcnt
179 | mv velvet-macOS-avx2 velvet-${tag_name}-macOS-x86_64-avx2
180 | mv velvet-macOS-sse4-popcnt velvet-${tag_name}-macOS-x86_64-sse4-popcnt
181 | mv velvet-macOS-nopopcnt velvet-${tag_name}-macOS-x86_64-nopopcnt
182 | mv velvet-macOS-apple-silicon velvet-${tag_name}-macOS-apple-silicon
183 | sha256sum velvet-* > checksums.txt
184 | gh release create "$tag_name" --draft \
185 | --title "$tag_name" \
186 | --notes-file artifacts/release_notes.md \
187 | "checksums.txt#Checksums" \
188 | "velvet-${tag_name}-x86_64-avx2.exe#Velvet - Windows (x86_64 - AVX2)" \
189 | "velvet-${tag_name}-x86_64-sse4-popcnt.exe#Velvet - Windows (x86_64 - SSE4.2+POPCNT)" \
190 | "velvet-${tag_name}-x86_64-nopopcnt.exe#Velvet - Windows (x86_64 - No POPCNT)" \
191 | "velvet-${tag_name}-x86_64-avx512#Velvet - Linux (x86_64 - AVX512)" \
192 | "velvet-${tag_name}-x86_64-avx2#Velvet - Linux (x86_64 - AVX2)" \
193 | "velvet-${tag_name}-x86_64-sse4-popcnt#Velvet - Linux (x86_64 - SSE4.2+POPCNT)" \
194 | "velvet-${tag_name}-x86_64-nopopcnt#Velvet - Linux (x86_64 - No POPCNT)" \
195 | "velvet-${tag_name}-macOS-apple-silicon#Velvet - macOS (Apple Silicon)" \
196 | "velvet-${tag_name}-macOS-x86_64-avx2#Velvet - macOS (x86_64 - AVX2)" \
197 | "velvet-${tag_name}-macOS-x86_64-sse4-popcnt#Velvet - macOS (x86_64 - SSE4.2+POPCNT)" \
198 | "velvet-${tag_name}-macOS-x86_64-nopopcnt#Velvet - macOS (x86_64 - No POPCNT)"
199 |
--------------------------------------------------------------------------------
/.github/workflows/release_pgo.yml:
--------------------------------------------------------------------------------
1 | name: Release
2 |
3 | on:
4 | push:
5 | branches: ["dev9"]
6 |
7 | jobs:
8 | # build-linux:
9 | # runs-on: ubuntu-latest
10 | # strategy:
11 | # matrix:
12 | # include:
13 | # - exec_postfix: "x86_64-avx512"
14 | # add_rustflags: "-Ctarget-feature=+crt-static -Ctarget-cpu=x86-64-v4 -Cprofile-use=./pgo/merged_avx512_linux.profdata"
15 | # toolchain: "x86_64-unknown-linux-musl"
16 | # cflags: "-march=x86-64-v4"
17 | # - exec_postfix: "x86_64-avx2"
18 | # add_rustflags: "-Ctarget-feature=+crt-static,-bmi2 -Ctarget-cpu=x86-64-v3"
19 | # toolchain: "x86_64-unknown-linux-musl"
20 | # cflags: "-march=x86-64-v3"
21 | #
22 | # steps:
23 | # - name: Checkout code
24 | # uses: actions/checkout@v4
25 | #
26 | # - name: Build
27 | # env:
28 | # RUSTFLAGS: '${{ matrix.add_rustflags }}'
29 | # CFLAGS: '${{ matrix.cflags }}'
30 | # run: |
31 | # sudo apt-get install -y musl-tools
32 | # if [ "${{ matrix.exec_postfix }}" == "x86_64-avx512" ]; then
33 | # rustup override set nightly-2025-04-27
34 | # rustup target add ${{ matrix.toolchain }}
35 | # cargo build --release --target ${{ matrix.toolchain }} --bin velvet --features=fathomrs,avx512
36 | # else
37 | # rustup override set 1.86.0
38 | # rustup target add ${{ matrix.toolchain }}
39 | # export CARGO_OPTS="--target ${{ matrix.toolchain }} --bin velvet --features=fathomrs"
40 | # RUSTFLAGS= make pgo-init
41 | # make pgo-build
42 | # fi
43 | # mv target/${{ matrix.toolchain }}/release/velvet velvet-linux-${{ matrix.exec_postfix }}
44 | #
45 | # - name: Upload artifacts
46 | # uses: actions/upload-artifact@v4
47 | # with:
48 | # name: velvet-linux-${{ matrix.exec_postfix }}
49 | # path: velvet-linux-${{ matrix.exec_postfix }}
50 |
51 | # build-windows:
52 | # runs-on: windows-latest
53 | # strategy:
54 | # matrix:
55 | # include:
56 | # - exec_postfix: "avx2"
57 | # add_rustflags: "-Ctarget-feature=+crt-static,-bmi2 -Ctarget-cpu=x86-64-v3"
58 | # cflags: "-march=x86-64-v3"
59 | # - exec_postfix: "sse4-popcnt"
60 | # add_rustflags: "-Ctarget-feature=+crt-static -Ctarget-cpu=x86-64-v2"
61 | # cflags: "-march=x86-64-v2"
62 | # - exec_postfix: "nopopcnt"
63 | # add_rustflags: "-Ctarget-feature=+crt-static"
64 | # cflags: "-march=x86-64 -DTB_CUSTOM_POP_COUNT"
65 | # steps:
66 | # - name: Checkout code
67 | # uses: actions/checkout@v4
68 | #
69 | # - name: Prepare
70 | # run: |
71 | # rustup override set 1.86.0
72 | # cargo install cargo-pgo
73 | # rustup component add llvm-tools-preview
74 | #
75 | # - name: Build
76 | # env:
77 | # RUSTFLAGS: '${{ matrix.add_rustflags }}'
78 | # CFLAGS: '${{ matrix.cflags }}'
79 | # run: |
80 | # cargo pgo run -- --bin velvet --features=fathomrs -- multibench
81 | # cargo pgo optimize build -- --bin velvet --features=fathomrs
82 | # mv ./target/release/velvet.exe velvet-windows-x86_64-${{ matrix.exec_postfix }}.exe
83 | #
84 | # - name: Upload artifacts
85 | # uses: actions/upload-artifact@v4
86 | # with:
87 | # name: velvet-windows-${{ matrix.exec_postfix }}
88 | # path: velvet-windows-x86_64-${{ matrix.exec_postfix }}.exe
89 |
90 | build-macos:
91 | runs-on: macos-latest
92 | strategy:
93 | matrix:
94 | include:
95 | # - exec_postfix: "avx2"
96 | # toolchain: x86_64-apple-darwin
97 | # add_rustflags: "-Ctarget-feature=+crt-static,-bmi2 -Ctarget-cpu=x86-64-v3"
98 | # cflags: "-march=x86-64-v3"
99 | # - exec_postfix: "sse4-popcnt"
100 | # toolchain: x86_64-apple-darwin
101 | # add_rustflags: "-Ctarget-feature=+crt-static -Ctarget-cpu=x86-64-v2"
102 | # cflags: "-march=x86-64-v2"
103 | # - exec_postfix: "nopopcnt"
104 | # toolchain: x86_64-apple-darwin
105 | # add_rustflags: "-Ctarget-feature=+crt-static"
106 | # cflags: "-march=x86-64 -DTB_CUSTOM_POP_COUNT"
107 | - exec_postfix: "apple-silicon"
108 | toolchain: aarch64-apple-darwin
109 | add_rustflags: "-Ctarget-feature=+crt-static"
110 | cflags: ""
111 |
112 | steps:
113 | - name: Checkout code
114 | uses: actions/checkout@v4
115 |
116 | - name: Prepare
117 | run: |
118 | rustup override set 1.86.0
119 | rustup target add ${{ matrix.toolchain }}
120 | cargo install cargo-pgo
121 | rustup component add llvm-tools-preview
122 |
123 | - name: Build
124 | env:
125 | RUSTFLAGS: '${{ matrix.add_rustflags }}'
126 | CFLAGS: '${{ matrix.cflags }}'
127 | run: |
128 | cargo pgo run -- --target ${{ matrix.toolchain }} --bin velvet --features=fathomrs -- multibench
129 | cargo pgo optimize build -- --target ${{ matrix.toolchain }} --bin velvet --features=fathomrs
130 | mv target/${{ matrix.toolchain }}/release/velvet velvet-macOS-${{ matrix.exec_postfix }}
131 |
132 | - name: Upload artifacts
133 | uses: actions/upload-artifact@v4
134 | with:
135 | name: velvet-macOS-${{ matrix.exec_postfix }}
136 | path: velvet-macOS-${{ matrix.exec_postfix }}
137 |
--------------------------------------------------------------------------------
/.github/workflows/test.yml:
--------------------------------------------------------------------------------
1 | name: Test
2 |
3 | on: [push]
4 |
5 | jobs:
6 | build:
7 |
8 | runs-on: ubuntu-latest
9 |
10 | steps:
11 | - name: Checkout code
12 | uses: actions/checkout@v4
13 |
14 | - name: Run tests
15 | run: |
16 | rustup override set 1.86.0
17 | cargo test --no-default-features --release -p velvet --lib
18 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /target
2 | /data
3 | .idea
4 | fen
5 | epd
6 | __pycache__
7 | build*.sh
8 | pytools/patch-verifier/tmp
9 | *.log
10 |
--------------------------------------------------------------------------------
/Cargo.toml:
--------------------------------------------------------------------------------
1 | [workspace]
2 | resolver = "2"
3 |
4 | members = [
5 | "engine",
6 | "fathomrs",
7 | "gensets",
8 | "genmagics",
9 | "gputrainer",
10 | "selfplay",
11 | "sprt",
12 | "traincommon",
13 | "tournament",
14 | "tuner",
15 | ]
16 |
17 | [profile.release]
18 | codegen-units = 1
19 | lto = "fat"
20 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | RUSTFLAGS ?= -Ctarget-cpu=native
2 | CFLAGS ?= -march=native
3 | CARGO_OPTS ?= --bin velvet --features=fathomrs
4 |
5 | export RUSTFLAGS
6 | export CFLAGS
7 | export CARGO_OPTS
8 |
9 | default: build
10 |
11 | # Regular build
12 | build:
13 | cargo build --release $(CARGO_OPTS)
14 |
15 | # Profile Guided Optimization build
16 | pgo-build: pgo-profile pgo-optimize
17 |
18 | # Profile Guided Optimization: create profile
19 | pgo-profile:
20 | cargo pgo run -- $(CARGO_OPTS) -- multibench
21 |
22 | # Profile Guided Optimization: create optimized build
23 | pgo-optimize:
24 | cargo pgo optimize build -- $(CARGO_OPTS)
25 |
26 | # Install cargo sub command for Profile Guided Optimization
27 | pgo-init:
28 | cargo install cargo-pgo
29 | rustup component add llvm-tools-preview
30 | cargo pgo info || echo "... but BOLT is not used here, so it is OK, if [llvm-bolt] and [merge-fdata] are not available."
31 |
32 | # Run tests
33 | test:
34 | cargo test --no-default-features --release -p velvet --lib
35 |
36 | # Show current build configuration
37 | show-config:
38 | @echo "RUSTFLAGS: $(RUSTFLAGS)"
39 | @echo "CARGO_OPTS: $(CARGO_OPTS)"
40 | @echo "CFLAGS: $(CFLAGS)"
41 |
42 | .PHONY: show-config pgo-init test pgo-build build default pgo-optimize pgo-profile
43 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ## :chess_pawn: Velvet Chess Engine
2 |
3 | [
](logo/velvet_logo.png?raw=true)
4 |
5 | 
6 | 
7 | [](https://www.gnu.org/licenses/gpl-3.0)
8 |
9 | **Velvet Chess Engine** is a UCI chess engine written in [Rust](https://www.rust-lang.org).
10 |
11 | Current (as of April 2025) Elo ratings from different rating lists (v8.1.1 if not noted otherwise):
12 |
13 | | Rating List | Time Control | Variant | Threads | Elo | Note |
14 | |-------------|---------------|----------|---------------|------|--------|
15 | | CCRL | 2'+1" (Blitz) | Standard | 8 | 3736 | |
16 | | CCRL | 2'+1" (Blitz) | Standard | 1 | 3660 | |
17 | | CCRL | 40/4 | Standard | 4 | 3578 | |
18 | | CCRL | 40/4 | Standard | 1 | 3532 | |
19 | | CCRL | 40/2 | FRC | 1 | 3884 | |
20 | | CCRL | 1'+1" | Chess324 | 1 | 3655 | v8.1.0 |
21 | | CEGT | 40/4 | Standard | 1 | 3518 | v8.0.0 |
22 | | CEGT | 40/20 | Standard | 1 | 3500 | |
23 | | CEGT | 5' + 3" | Standard | 1 (Ponder on) | 3532 | v8.0.0 |
24 |
25 | - [CCRL Blitz](https://www.computerchess.org.uk/ccrl/404/cgi/compare_engines.cgi?family=Velvet&print=Rating+list)
26 | - [CCRL 40/15](https://www.computerchess.org.uk/ccrl/4040/cgi/compare_engines.cgi?family=Velvet&print=Rating+list)
27 | - [CCRL 40/2 FRC](https://www.computerchess.org.uk/ccrl/404FRC)
28 |
29 | In order to play against Velvet, you need a Chess GUI with support for the UCI protocol.
30 |
31 | ### Features
32 |
33 | #### Configurable strength
34 |
35 | In order to make Velvet a viable sparring partner for human players of different skill levels, its strength can be limited.
36 | To enable this feature, set `UCI_LimitStrength` to true and adjust `UCI_Elo` to your desired Elo rating (between 1225 and 3000).
37 |
38 | Please note that these Elo ratings might not yet correspond well to Elo ratings of human players.
39 | A better calibration would require a lot of games against human players of different skill levels.
40 | I recommend to experiment with different settings to find the optimal match for your current skill.
41 |
42 | In addition, the new `SimulateThinkingTime` option allows Velvet to mimic human-like thinking times by utilizing a portion of its remaining time budget.
43 | You can disable this feature by setting `SimulateThinkingTime` to false.
44 |
45 | These options can be combined with the "style" settings.
46 |
47 | #### Support for different playing styles
48 |
49 | Velvet offers two distinct embedded neural networks, each designed to reflect a different playing style.
50 | You can easily toggle between these styles using the `Style` UCI option to match your strategic preferences.
51 |
52 | * **Normal Style**: The default setting, offering a balanced approach to gameplay. While Velvet is still capable of sacrifices and aggressive attacks, it places slightly more emphasis on avoiding unfavorable positions if an attack doesn’t succeed, compared to the Risky style.
53 | * **Risky Style**: This setting pushes Velvet to adopt a bolder, more aggressive approach, taking greater risks to create dynamic and challenging positions on the board.
54 |
55 | Note: the riskier playing style comes with the downside, that the strength is reduced by around 25 Elo depending upon the opponent.
56 |
57 | #### Multi PV Support for Analysis
58 | Velvet can analyze multiple lines of play simultaneously to provide a deeper understanding of positions by showing the top N moves.
59 | The number of variations can be configured using the UCI `MultiPV` option.
60 |
61 | #### Endgame Tablebase Support
62 | The engine supports to probe up to 7-men Syzygy tablebases to improve evaluations of endgame positions.
63 |
64 | #### Ponder Support
65 | Pondering allows the engine to think during the opponent's turn.
66 | This setting is disabled by default, but can be enabled using the `Ponder` UCI option.
67 |
68 | #### FRC and DFRC Support
69 | Besides standard chess, Velvet also supports the variants Fischer Random Chess (FRC or Chess960) and Double Fischer Random Chess (DFRC).
70 |
71 | #### Cross-Platform Support
72 | Velvet binaries are available for Windows, macOS and Linux in the download section of the [releases page](https://github.com/mhonert/velvet-chess/releases) page.
73 |
74 | #### Parallel Search
75 | Velvet uses Lazy SMP to make use of multi-core processors.
76 |
77 | ### :inbox_tray: Download
78 |
79 | Executables for Windows, macOS and Linux can be downloaded from the [releases page](https://github.com/mhonert/velvet-chess/releases).
80 |
81 | ### :computer: Manual compilation
82 |
83 | Since Velvet is written in Rust, a manual compilation requires the installation of the Rust tool chain (e.g. using [rustup](https://rustup.rs/)).
84 | The installed Rust version must support the Rust 2021 Edition (i.e. v1.56 and upwards).
85 |
86 | Then you can compile the engine using **cargo**:
87 |
88 | ```shell
89 | cargo build --release --bin velvet
90 | ```
91 |
92 | To compile the engine without Syzygy tablebase support (e.g. when the target architecture is not supported by the Fathom library),
93 | you can pass the `no-default-features` flag:
94 |
95 | ```shell
96 | cargo build --no-default-features --release --bin velvet
97 | ```
98 |
99 | Note: the AVX-512 compiles require a Rust nightly version and the feature 'avx512'.
100 |
101 | ### :scroll: License
102 | This project is licensed under the GNU General Public License - see the [LICENSE](LICENSE) for details.
103 |
104 | ### :tada: Acknowledgements
105 | - The [Chess Programming Wiki (CPW)](https://www.chessprogramming.org/Main_Page) has excellent articles and descriptions
106 | - A big thanks to all the chess engine testers out there
107 |
--------------------------------------------------------------------------------
/artifacts/release_notes.md:
--------------------------------------------------------------------------------
1 | Preview release for TCEC - not intended for rating lists
2 |
3 | ## Changes
4 |
5 | - Bugfix for movelists out of bound access, which caused segfault in TCEC test game
6 |
7 | ## Notes
8 |
9 | Due to the lack of an ARM-based (Apple Silicon) computer, the "apple-silicon" builds are untested.
10 |
11 | ## Installation
12 | The chess engine is available for Windows and Linux and requires a 64 Bit CPU.
13 | There are optimized executables available for different CPU micro-architecture generations.
14 |
15 | Starting with Velvet v4.1.0 there are also builds for macOS provided.
16 | Currently there are no specific optimizations for the ARM-based/Apple Silicon builds implemented, so
17 | the macOS builds for x86_64 might be faster.
18 |
19 | If you have a relatively modern CPU (2013+) with AVX2 support, then the *...-x86_64-avx2* executable is highly recommended for best performance.
20 |
21 | | Executable | Description | Min. CPU Generation | Required Instruction Sets |
22 | |--------------------|------------------------------------------------------------------------------|-------------------------------|---------------------------|
23 | | x86_64-avx512 | Higher performance for CPUs supporting AVX-512 | x86-64-v4 | AVX-512 |
24 | | x86_64-avx2 | Recommended for best performance on most modern CPUs without AVX-512 support | Intel Haswell / Zen1 | AVX2, BMI1 |
25 | | x86_64-sse4-popcnt | Lower performance, recommended for CPUs without AVX2 support | Intel Nehalem / AMD Bulldozer | SSE4.2, SSE3, POPCNT |
26 | | x86_64-nopopcnt | Lowest performance, but compatible with most x86_64 CPUs | --- | SSE2, CMOV |
27 | | apple-silicon | Native builds for Apple Silicon processors (ARM aarch64) | Apple M1 | |
28 |
--------------------------------------------------------------------------------
/engine/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "velvet"
3 | license = "GPL-3.0-or-later"
4 | version = "9.0.0-dev6"
5 | authors = ["mhonert"]
6 | description = "Velvet is a UCI chess engine"
7 | readme = "../README.md"
8 | publish = false
9 | repository = "https://github.com/mhonert/velvet-chess"
10 | edition = "2021"
11 | default-run = "velvet"
12 |
13 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
14 |
15 | [[bin]]
16 | name = "velvet"
17 | path = "src/main.rs"
18 |
19 | [dependencies]
20 | fathomrs = { path = "../fathomrs", optional = true }
21 |
22 | [features]
23 | default = ["fathomrs"]
24 | fathomrs = ["dep:fathomrs"]
25 | tune = []
26 | avx512 = []
27 | checked_slice_access = []
28 |
--------------------------------------------------------------------------------
/engine/nets/velvet_layer_in_bias.qnn:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mhonert/velvet-chess/b0e6c8f87527dbf258d4401bd886ca07ee4bc5fc/engine/nets/velvet_layer_in_bias.qnn
--------------------------------------------------------------------------------
/engine/nets/velvet_layer_in_weights.qnn:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mhonert/velvet-chess/b0e6c8f87527dbf258d4401bd886ca07ee4bc5fc/engine/nets/velvet_layer_in_weights.qnn
--------------------------------------------------------------------------------
/engine/nets/velvet_layer_out_bias.qnn:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/engine/nets/velvet_layer_out_weights.qnn:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mhonert/velvet-chess/b0e6c8f87527dbf258d4401bd886ca07ee4bc5fc/engine/nets/velvet_layer_out_weights.qnn
--------------------------------------------------------------------------------
/engine/src/align.rs:
--------------------------------------------------------------------------------
1 | /*
2 | * Velvet Chess Engine
3 | * Copyright (C) 2025 mhonert (https://github.com/mhonert)
4 | *
5 | * This program is free software: you can redistribute it and/or modify
6 | * it under the terms of the GNU General Public License as published by
7 | * the Free Software Foundation, either version 3 of the License, or
8 | * (at your option) any later version.
9 | *
10 | * This program is distributed in the hope that it will be useful,
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | * GNU General Public License for more details.
14 | *
15 | * You should have received a copy of the GNU General Public License
16 | * along with this program. If not, see .
17 | */
18 |
19 | #[derive(Clone)]
20 | #[repr(align(64))]
21 | pub struct A64(pub T); // Wrapper to ensure 64 Byte alignment (e.g. for cache line alignment)
22 |
--------------------------------------------------------------------------------
/engine/src/board/castling.rs:
--------------------------------------------------------------------------------
1 | /*
2 | * Velvet Chess Engine
3 | * Copyright (C) 2025 mhonert (https://github.com/mhonert)
4 | *
5 | * This program is free software: you can redistribute it and/or modify
6 | * it under the terms of the GNU General Public License as published by
7 | * the Free Software Foundation, either version 3 of the License, or
8 | * (at your option) any later version.
9 | *
10 | * This program is distributed in the hope that it will be useful,
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | * GNU General Public License for more details.
14 | *
15 | * You should have received a copy of the GNU General Public License
16 | * along with this program. If not, see .
17 | */
18 |
19 | use crate::bitboard::{get_from_to_mask, BitBoard};
20 | use crate::board::castling::Castling::{BlackKingSide, BlackQueenSide, WhiteKingSide, WhiteQueenSide};
21 | use crate::board::Board;
22 | use crate::colors::Color;
23 | use crate::slices::SliceElementAccess;
24 | use crate::zobrist::castling_zobrist_key;
25 |
26 | #[repr(u8)]
27 | #[derive(Clone, Copy)]
28 | pub enum Castling {
29 | WhiteKingSide = 1 << 0,
30 | BlackKingSide = 1 << 1,
31 | WhiteQueenSide = 1 << 2,
32 | BlackQueenSide = 1 << 3,
33 | }
34 |
35 | #[derive(Default, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Debug)]
36 | pub struct CastlingState(u8);
37 |
38 | static CLEAR_BY_COLOR: [u8; 2] =
39 | [!(WhiteQueenSide as u8 | WhiteKingSide as u8), !(BlackQueenSide as u8 | BlackKingSide as u8)];
40 |
41 | pub static KING_SIDE_CASTLING: [Castling; 2] = [Castling::WhiteKingSide, Castling::BlackKingSide];
42 |
43 | pub static QUEEN_SIDE_CASTLING: [Castling; 2] = [Castling::WhiteQueenSide, Castling::BlackQueenSide];
44 |
45 | impl CastlingState {
46 | const ALL_CASTLING: u8 = Castling::WhiteKingSide as u8
47 | | Castling::WhiteQueenSide as u8
48 | | Castling::BlackKingSide as u8
49 | | Castling::BlackQueenSide as u8;
50 |
51 | pub const ALL: CastlingState = CastlingState(Self::ALL_CASTLING);
52 |
53 | pub fn can_castle(&self, side: Castling) -> bool {
54 | (self.0 & side as u8) != 0
55 | }
56 |
57 | pub fn any_castling(&self) -> bool {
58 | self.0 != 0
59 | }
60 |
61 | pub fn can_castle_king_side(&self, color: Color) -> bool {
62 | self.can_castle(Self::king_side(color))
63 | }
64 |
65 | pub fn can_castle_queen_side(&self, color: Color) -> bool {
66 | self.can_castle(Self::queen_side(color))
67 | }
68 |
69 | pub fn set_has_castled(&mut self, color: Color) {
70 | let idx = color.idx();
71 | self.0 &= CLEAR_BY_COLOR[idx];
72 | }
73 |
74 | pub fn set_can_castle(&mut self, castling: Castling) {
75 | self.0 |= castling as u8;
76 | }
77 |
78 | pub fn clear(&mut self, color: Color) {
79 | self.0 &= CLEAR_BY_COLOR[color.idx()];
80 | }
81 |
82 | pub fn clear_side(&mut self, side: Castling) {
83 | self.0 ^= side as u8;
84 | }
85 |
86 | pub fn zobrist_key(&self) -> u64 {
87 | castling_zobrist_key(self.0)
88 | }
89 |
90 | fn king_side(color: Color) -> Castling {
91 | KING_SIDE_CASTLING[color.idx()]
92 | }
93 |
94 | fn queen_side(color: Color) -> Castling {
95 | QUEEN_SIDE_CASTLING[color.idx()]
96 | }
97 | }
98 |
99 | #[derive(Clone, Copy)]
100 | pub struct CastlingRules {
101 | chess960: bool,
102 | king_start: [i8; 2],
103 | king_side_rook: [i8; 2],
104 | queen_side_rook: [i8; 2],
105 | }
106 |
107 | impl Default for CastlingRules {
108 | /// Returns the Castling Rules for standard chess
109 | fn default() -> Self {
110 | CastlingRules::new(false, 4, 7, 0, 4, 7, 0)
111 | }
112 | }
113 |
114 | static KS_KING_END: [u8; 2] = [63 - 1, 7 - 1];
115 | static KS_ROOK_END: [u8; 2] = [63 - 2, 7 - 2];
116 |
117 | static QS_KING_END: [u8; 2] = [56 + 2, 2];
118 | static QS_ROOK_END: [u8; 2] = [56 + 3, 3];
119 |
120 | impl CastlingRules {
121 |
122 | pub fn new(
123 | chess960: bool, w_king_start_col: i8, w_king_side_rook_col: i8, w_queen_side_rook_col: i8,
124 | b_king_start_col: i8, b_king_side_rook_col: i8, b_queen_side_rook_col: i8,
125 | ) -> Self {
126 | let w_king_start = 56 + w_king_start_col;
127 | let w_king_side_rook = 56 + w_king_side_rook_col;
128 | let w_queen_side_rook = 56 + w_queen_side_rook_col;
129 |
130 | let b_king_start = b_king_start_col;
131 | let b_king_side_rook = b_king_side_rook_col;
132 | let b_queen_side_rook = b_queen_side_rook_col;
133 |
134 | CastlingRules {
135 | chess960,
136 | king_start: [w_king_start, b_king_start],
137 | king_side_rook: [w_king_side_rook, b_king_side_rook],
138 | queen_side_rook: [w_queen_side_rook, b_queen_side_rook],
139 | }
140 | }
141 |
142 | pub fn is_chess960(&self) -> bool {
143 | self.chess960
144 | }
145 |
146 | pub fn is_ks_castling(&self, color: Color, move_to: i8) -> bool {
147 | self.ks_rook_start(color) == move_to
148 | }
149 |
150 | pub fn is_qs_castling(&self, color: Color, move_to: i8) -> bool {
151 | self.qs_rook_start(color) == move_to
152 | }
153 |
154 | pub fn is_king_start(&self, color: Color, pos: i8) -> bool {
155 | self.king_start(color) == pos
156 | }
157 |
158 | pub fn king_start(&self, color: Color) -> i8 {
159 | *self.king_start.el(color.idx())
160 | }
161 |
162 | pub fn ks_rook_start(&self, color: Color) -> i8 {
163 | *self.king_side_rook.el(color.idx())
164 | }
165 |
166 | pub fn qs_rook_start(&self, color: Color) -> i8 {
167 | *self.queen_side_rook.el(color.idx())
168 | }
169 |
170 | pub fn ks_king_end(color: Color) -> i8 {
171 | *KS_KING_END.el(color.idx()) as i8
172 | }
173 |
174 | pub fn ks_rook_end(color: Color) -> i8 {
175 | *KS_ROOK_END.el(color.idx()) as i8
176 | }
177 |
178 | pub fn qs_king_end(color: Color) -> i8 {
179 | *QS_KING_END.el(color.idx()) as i8
180 | }
181 |
182 | pub fn qs_rook_end(color: Color) -> i8 {
183 | *QS_ROOK_END.el(color.idx()) as i8
184 | }
185 |
186 | pub fn is_ks_castling_valid(&self, color: Color, board: &Board, empty_bb: BitBoard) -> bool {
187 | let king_start = self.king_start(color);
188 | let king_end = Self::ks_king_end(color);
189 | let rook_start = self.ks_rook_start(color);
190 | let rook_end = Self::ks_rook_end(color);
191 | Self::is_castling_valid(board, color.flip(), empty_bb, king_start, king_end, rook_start, rook_end)
192 | }
193 |
194 | pub fn is_qs_castling_valid(&self, color: Color, board: &Board, empty_bb: BitBoard) -> bool {
195 | let king_start = self.king_start(color);
196 | let king_end = Self::qs_king_end(color);
197 | let rook_start = self.qs_rook_start(color);
198 | let rook_end = Self::qs_rook_end(color);
199 | Self::is_castling_valid(board, color.flip(), empty_bb, king_start, king_end, rook_start, rook_end)
200 | }
201 |
202 | fn is_castling_valid(
203 | board: &Board, opp_color: Color, mut empty_bb: BitBoard, king_start: i8, king_end: i8, rook_start: i8,
204 | rook_end: i8,
205 | ) -> bool {
206 | empty_bb |= BitBoard((1u64 << king_start) | (1u64 << rook_start));
207 |
208 | let king_route_bb = BitBoard(get_from_to_mask(king_start, king_end));
209 | if !empty_bb.contains(king_route_bb) {
210 | return false;
211 | }
212 |
213 | let rook_route_bb = BitBoard(get_from_to_mask(rook_start, rook_end));
214 | if !empty_bb.contains(rook_route_bb) {
215 | return false;
216 | }
217 |
218 | for pos in king_route_bb {
219 | if board.is_attacked(opp_color, pos as usize) {
220 | return false;
221 | }
222 | }
223 |
224 | true
225 | }
226 | }
227 |
--------------------------------------------------------------------------------
/engine/src/board/cycledetection.rs:
--------------------------------------------------------------------------------
1 | /*
2 | * Velvet Chess Engine
3 | * Copyright (C) 2025 mhonert (https://github.com/mhonert)
4 | *
5 | * This program is free software: you can redistribute it and/or modify
6 | * it under the terms of the GNU General Public License as published by
7 | * the Free Software Foundation, either version 3 of the License, or
8 | * (at your option) any later version.
9 | *
10 | * This program is distributed in the hope that it will be useful,
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | * GNU General Public License for more details.
14 | *
15 | * You should have received a copy of the GNU General Public License
16 | * along with this program. If not, see .
17 | */
18 | use std::mem::swap;
19 | use crate::bitboard::{get_king_attacks, get_knight_attacks, BitBoard};
20 | use crate::colors::{BLACK, WHITE};
21 | use crate::magics::{get_bishop_attacks, get_queen_attacks, get_ray, get_rook_attacks};
22 | use crate::zobrist::{piece_zobrist_key, player_zobrist_key};
23 |
24 | static mut CUCKOO_KEYS: [u64; 8192] = [0; 8192];
25 | static mut CUCKOO_MOVES: [u16; 8192] = [0; 8192];
26 |
27 | pub fn has_cycle_move(key: u64, occupancy: BitBoard) -> bool {
28 | let mut i = cuckoo_hash1(key);
29 | if key != get_cuckoo_key(i) {
30 | i = cuckoo_hash2(key);
31 | if key != get_cuckoo_key(i) {
32 | return false;
33 | }
34 | }
35 |
36 | let m = get_cuckoo_move(i);
37 | if (occupancy & get_ray(m)).is_occupied() {
38 | return false
39 | }
40 |
41 | true
42 | }
43 |
44 | fn get_cuckoo_key(i: usize) -> u64 {
45 | unsafe {
46 | let ptr = &raw const CUCKOO_KEYS;
47 | *(*ptr).get_unchecked(i)
48 | }
49 | }
50 |
51 | fn get_cuckoo_move(i: usize) -> u16 {
52 | unsafe {
53 | let ptr = &raw const CUCKOO_MOVES;
54 | *(*ptr).get_unchecked(i)
55 | }
56 | }
57 |
58 | fn cuckoo_hash1(hash: u64) -> usize {
59 | (hash & 0x1FFF) as usize
60 | }
61 |
62 | fn cuckoo_hash2(hash: u64) -> usize {
63 | ((hash >> 16) & 0x1FFF) as usize
64 | }
65 |
66 | pub fn init() {
67 | let mut count = 0;
68 | for player in [WHITE, BLACK] {
69 | for piece in 2..=6 {
70 | for start in 0..64 {
71 | for end in (start + 1)..64 {
72 | let targets = attacks(piece, start);
73 | if !targets.is_set(end) {
74 | continue;
75 | }
76 | let mut m = (start << 6 | end) as u16;
77 | let mut hash = piece_zobrist_key(player.piece(piece), start) ^ piece_zobrist_key(player.piece(piece), end) ^ player_zobrist_key();
78 |
79 | let mut i = cuckoo_hash1(hash);
80 | while m != 0 {
81 | swap(unsafe { &mut CUCKOO_KEYS[i] }, &mut hash);
82 | swap(unsafe { &mut CUCKOO_MOVES[i] }, &mut m);
83 | i = if i == cuckoo_hash1(hash) { cuckoo_hash2(hash) } else { cuckoo_hash1(hash) };
84 | }
85 | count += 1;
86 | }
87 | }
88 | }
89 | }
90 | assert_eq!(count, 3668);
91 | }
92 |
93 | fn attacks(piece: i8, pos: usize) -> BitBoard {
94 | match piece {
95 | 2 => get_knight_attacks(pos),
96 | 3 => get_bishop_attacks(!0, pos),
97 | 4 => get_rook_attacks(!0, pos),
98 | 5 => get_queen_attacks(!0, pos),
99 | 6 => get_king_attacks(pos),
100 | _ => unreachable!()
101 | }
102 | }
103 |
--------------------------------------------------------------------------------
/engine/src/colors.rs:
--------------------------------------------------------------------------------
1 | /*
2 | * Velvet Chess Engine
3 | * Copyright (C) 2023 mhonert (https://github.com/mhonert)
4 | *
5 | * This program is free software: you can redistribute it and/or modify
6 | * it under the terms of the GNU General Public License as published by
7 | * the Free Software Foundation, either version 3 of the License, or
8 | * (at your option) any later version.
9 | *
10 | * This program is distributed in the hope that it will be useful,
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | * GNU General Public License for more details.
14 | *
15 | * You should have received a copy of the GNU General Public License
16 | * along with this program. If not, see .
17 | */
18 |
19 | #[derive(Copy, Clone, Debug)]
20 | pub struct Color(pub u8);
21 |
22 | pub const WHITE: Color = Color(0);
23 | pub const BLACK: Color = Color(1);
24 |
25 | impl Color {
26 | /// Returns the color of the given piece
27 | pub fn from_piece(piece: i8) -> Self {
28 | if piece > 0 {
29 | WHITE
30 | } else {
31 | BLACK
32 | }
33 | }
34 |
35 | /// Determines the active color from the halfmove count
36 | pub fn from_halfmove_count(halfmove_count: u16) -> Self {
37 | Color((halfmove_count & 1) as u8)
38 | }
39 |
40 | /// Returns 0 for white and 1 for black
41 | pub fn idx(self) -> usize {
42 | self.0 as usize
43 | }
44 |
45 | /// Flips the current color (WHITE -> BLACK and vice versa)
46 | pub fn flip(self) -> Self {
47 | Color(self.0 ^ 1)
48 | }
49 |
50 | pub fn is_white(self) -> bool {
51 | self.0 == 0
52 | }
53 |
54 | pub fn is_black(self) -> bool {
55 | self.0 != 0
56 | }
57 |
58 | /// Returns a piece for the current color using the given piece ID (1-6)
59 | pub fn piece(self, piece_id: i8) -> i8 {
60 | debug_assert!((1..=6).contains(&piece_id));
61 | if self.0 == 0 {
62 | piece_id
63 | } else {
64 | -piece_id
65 | }
66 | }
67 |
68 | pub fn is_own_piece(self, piece: i8) -> bool {
69 | debug_assert_ne!(piece, 0);
70 | piece.is_positive() == (self.0 == 0)
71 | }
72 |
73 | pub fn is_opp_piece(self, piece: i8) -> bool {
74 | debug_assert_ne!(piece, 0);
75 | piece.is_negative() == (self.0 == 0)
76 | }
77 |
78 | /// Converts the given score (white perspective) to a score from the own perspective
79 | pub fn score(self, score_white_pov: i16) -> i16 {
80 | if self.0 == 0 {
81 | score_white_pov
82 | } else {
83 | -score_white_pov
84 | }
85 | }
86 | }
87 |
88 | #[cfg(test)]
89 | mod tests {
90 | use crate::colors::{Color, BLACK, WHITE};
91 |
92 | #[test]
93 | fn test_from_piece() {
94 | for i in 1..=6 {
95 | assert!(Color::from_piece(i).is_white());
96 | assert!(Color::from_piece(-i).is_black());
97 | }
98 | }
99 |
100 | #[test]
101 | fn test_index() {
102 | assert_eq!(0, WHITE.idx());
103 | assert_eq!(1, BLACK.idx());
104 | }
105 |
106 | #[test]
107 | fn test_flip() {
108 | assert_eq!(WHITE.idx(), BLACK.flip().idx());
109 | assert_eq!(BLACK.idx(), WHITE.flip().idx());
110 | }
111 |
112 | #[test]
113 | fn test_is_white() {
114 | assert!(WHITE.is_white());
115 | assert!(!BLACK.is_white());
116 | }
117 |
118 | #[test]
119 | fn test_is_black() {
120 | assert!(BLACK.is_black());
121 | assert!(!WHITE.is_black());
122 | }
123 |
124 | #[test]
125 | fn test_piece() {
126 | assert_eq!(2, WHITE.piece(2));
127 | assert_eq!(-2, BLACK.piece(2));
128 | }
129 |
130 | #[test]
131 | fn test_is_own_piece() {
132 | assert!(WHITE.is_own_piece(2));
133 | assert!(BLACK.is_own_piece(-2));
134 |
135 | assert!(!WHITE.is_own_piece(-2));
136 | assert!(!BLACK.is_own_piece(2));
137 | }
138 |
139 | #[test]
140 | fn test_is_opp_piece() {
141 | assert!(WHITE.is_opp_piece(-4));
142 | assert!(BLACK.is_opp_piece(4));
143 |
144 | assert!(!WHITE.is_opp_piece(4));
145 | assert!(!BLACK.is_opp_piece(-4));
146 | }
147 |
148 | #[test]
149 | fn test_score() {
150 | assert_eq!(123, WHITE.score(123));
151 | assert_eq!(-123, WHITE.score(-123));
152 |
153 | assert_eq!(-123, BLACK.score(123));
154 | assert_eq!(123, BLACK.score(-123));
155 | }
156 | }
157 |
--------------------------------------------------------------------------------
/engine/src/engine/bench.rs:
--------------------------------------------------------------------------------
1 | /*
2 | * Velvet Chess Engine
3 | * Copyright (C) 2024 mhonert (https://github.com/mhonert)
4 | *
5 | * This program is free software: you can redistribute it and/or modify
6 | * it under the terms of the GNU General Public License as published by
7 | * the Free Software Foundation, either version 3 of the License, or
8 | * (at your option) any later version.
9 | *
10 | * This program is distributed in the hope that it will be useful,
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | * GNU General Public License for more details.
14 | *
15 | * You should have received a copy of the GNU General Public License
16 | * along with this program. If not, see .
17 | */
18 |
19 | use std::time::Instant;
20 | use crate::engine::Engine;
21 | use crate::fen::START_POS;
22 | use crate::time_management::SearchLimits;
23 |
24 | static BENCH_FENS: [(&str, i32); 34] = [
25 | ("8/4R3/8/p7/8/6Rk/8/6K1 b - - 0 59", 14),
26 | ("8/8/8/1k3Q1K/1b1q1P2/8/7P/8 b - - 0 62", 10),
27 | ("8/8/1P6/6R1/4k2K/1r5P/8/8 b - - 0 67", 15),
28 | ("6kb/8/4K1N1/6P1/5P2/4B3/8/8 b - - 0 65", 17),
29 | ("8/3nN3/3Pk3/6K1/8/8/8/8 b - - 0 69", 16),
30 | ("8/8/4n3/7k/3BB2p/3P1KpP/8/8 b - - 0 45", 14),
31 | ("2r4k/4q3/7Q/P7/4B1P1/3R3K/8/8 b - - 0 61", 18),
32 | ("8/p5k1/5p2/6p1/2bp4/q2n2Q1/2B1R3/1K6 w - - 0 57", 17),
33 | ("8/2R5/7p/5p1k/BK4b1/2p1r3/8/8 w - - 0 48", 13),
34 | ("3Q4/1B2Rpk1/6n1/8/4N2p/8/2K5/6q1 w - - 0 50", 10),
35 | ("8/8/2k5/R5K1/5N2/8/P4n1r/8 b - - 0 60", 11),
36 | ("8/4nk2/b1n5/1B2P3/1P6/2N4P/6P1/6K1 b - - 0 39", 13),
37 | ("k3Q3/pp6/2b4P/6p1/5n2/5PP1/1P1qP3/R4K1R b - - 0 24", 13),
38 | ("r4rk1/pp2p2p/2n1pnp1/8/8/2P5/P1P1BPPP/2BR1RK1 b - - 1 18", 11),
39 | ("4r2k/2rb1pb1/3p1pp1/5P1p/3BP2P/n5P1/Q5BK/8 w - - 0 44", 12),
40 | ("8/1N3k2/P1r3pp/1n2K3/7P/6Pb/8/8 w - - 0 56", 13),
41 | ("3k4/2q5/1bB2r2/7Q/3P4/P6P/1P6/1KR5 b - - 0 58", 10),
42 | ("8/5p2/3p1q1k/pQ6/5r2/2N3R1/2K3B1/5n2 w - - 0 53", 11),
43 | ("3b3k/n4q2/P4N2/1r1P4/4BP2/7Q/4PP2/4BK2 b - - 0 44", 15),
44 | ("6k1/1p1n4/8/1p2N1p1/1B1K3p/8/r5b1/1R6 w - - 0 40", 12),
45 | ("4r1k1/p2q1ppp/2p2n2/4p3/2Pr4/1P2Q1P1/P3RPBP/R5K1 b - - 0 19", 12),
46 | ("r7/1p3kp1/2p1b2p/p2p1q2/3P3P/2NQ2P1/PPB2PK1/8 w - - 0 29", 13),
47 | ("4r1k1/5p1p/3p2p1/p3p3/Q1rPP3/P1P4P/3b1PP1/BR2R1K1 b - - 0 30", 13),
48 | ("8/1R4pk/q3p1np/3pp2n/4P1PP/3P1N2/2QN1PK1/8 b - - 0 38", 11),
49 | ("3rr1k1/p5p1/2p1p2p/N1P1qp2/1Pp1nP2/8/P5PP/R3R1K1 w - - 0 23", 12),
50 | ("5k2/3pRb2/2pN1Np1/p5n1/P5B1/1P6/2PP2Pb/2K5 b - - 0 26", 15),
51 | ("2k3r1/4bR2/N2q4/2B5/4P3/3P1Qn1/PKP5/8 b - - 0 48", 12),
52 | ("3r1rk1/p1p1q1pp/Q4p2/3P4/1b3P2/3BB3/PP4PP/R2K2NR b - - 0 17", 11),
53 | ("1k2r3/8/1p3p2/2R2qp1/p2p1Pn1/P2P1B1p/1P1QN2P/6K1 b - - 0 29", 11),
54 | ("5k2/5p2/1p1n4/3P4/3P3b/3Q3N/4PB1K/rBq3r1 w - - 0 39", 11),
55 | ("r1b2rk1/ppp1pp1p/1n4p1/4P3/1nP5/2N2N1P/PP1qBPP1/R3K2R w KQ - 0 13", 11),
56 | ("rnbqkbnr/pppp2pp/4pp2/8/6P1/8/PPPPPP1P/RNBQKBNR w KQkq - 0 3", 12),
57 | ("r3k2r/p1ppqpb1/bn2pnp1/3PN3/1p2P3/2N2Q1p/PPPBBPPP/R3K2R w KQkq -", 10),
58 | (START_POS, 12),
59 | ];
60 |
61 | impl Engine {
62 | pub fn bench(&mut self) {
63 | self.check_readiness();
64 | self.search.clear_tt();
65 | self.search.hh.clear();
66 |
67 | let mut limit = SearchLimits::default();
68 |
69 | let mut nodes = 0;
70 | let start = Instant::now();
71 | for &(fen, depth) in BENCH_FENS.iter() {
72 | limit.set_depth_limit(depth);
73 | self.set_position(String::from(fen), Vec::with_capacity(0));
74 | self.go(limit, false, None);
75 | nodes += self.search.node_count();
76 | }
77 |
78 | let duration_ms = start.elapsed().as_millis() as u64;
79 | let nps = nodes * 1000 / duration_ms;
80 |
81 | println!("\ninfo string bench total time : {}ms", duration_ms);
82 | println!("info string bench nodes : {}", nodes);
83 | println!("info string bench NPS : {}", nps);
84 |
85 | self.tt_clean = false;
86 | }
87 | }
--------------------------------------------------------------------------------
/engine/src/init.rs:
--------------------------------------------------------------------------------
1 | /*
2 | * Velvet Chess Engine
3 | * Copyright (C) 2024 mhonert (https://github.com/mhonert)
4 | *
5 | * This program is free software: you can redistribute it and/or modify
6 | * it under the terms of the GNU General Public License as published by
7 | * the Free Software Foundation, either version 3 of the License, or
8 | * (at your option) any later version.
9 | *
10 | * This program is distributed in the hope that it will be useful,
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | * GNU General Public License for more details.
14 | *
15 | * You should have received a copy of the GNU General Public License
16 | * along with this program. If not, see .
17 | */
18 | use std::sync::Once;
19 | use crate::board::cycledetection;
20 | use crate::magics;
21 |
22 | static INIT: Once = Once::new();
23 |
24 | pub fn init() {
25 | INIT.call_once(|| {
26 | magics::init();
27 | cycledetection::init();
28 | });
29 | }
30 |
--------------------------------------------------------------------------------
/engine/src/lib.rs:
--------------------------------------------------------------------------------
1 | /*
2 | * Velvet Chess Engine
3 | * Copyright (C) 2025 mhonert (https://github.com/mhonert)
4 | *
5 | * This program is free software: you can redistribute it and/or modify
6 | * it under the terms of the GNU General Public License as published by
7 | * the Free Software Foundation, either version 3 of the License, or
8 | * (at your option) any later version.
9 | *
10 | * This program is distributed in the hope that it will be useful,
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | * GNU General Public License for more details.
14 | *
15 | * You should have received a copy of the GNU General Public License
16 | * along with this program. If not, see .
17 | */
18 |
19 | #![cfg_attr(feature = "avx512", feature(stdarch_x86_avx512))]
20 |
21 | pub mod align;
22 | pub mod bitboard;
23 | pub mod board;
24 | pub mod colors;
25 | pub mod engine;
26 | pub mod fen;
27 | pub mod history_heuristics;
28 | pub mod init;
29 | pub mod magics;
30 | pub mod move_gen;
31 | pub mod moves;
32 | pub mod nn;
33 | pub mod perft;
34 | pub mod pieces;
35 | pub mod random;
36 | pub mod scores;
37 | pub mod search;
38 | pub mod search_context;
39 | pub mod syzygy;
40 | pub mod time_management;
41 | pub mod transposition_table;
42 | pub mod uci;
43 | pub mod uci_move;
44 |
45 | pub mod params;
46 | mod pos_history;
47 | mod zobrist;
48 | mod slices;
49 |
--------------------------------------------------------------------------------
/engine/src/main.rs:
--------------------------------------------------------------------------------
1 | /*
2 | * Velvet Chess Engine
3 | * Copyright (C) 2025 mhonert (https://github.com/mhonert)
4 | *
5 | * This program is free software: you can redistribute it and/or modify
6 | * it under the terms of the GNU General Public License as published by
7 | * the Free Software Foundation, either version 3 of the License, or
8 | * (at your option) any later version.
9 | *
10 | * This program is distributed in the hope that it will be useful,
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | * GNU General Public License for more details.
14 | *
15 | * You should have received a copy of the GNU General Public License
16 | * along with this program. If not, see .
17 | */
18 | extern crate velvet;
19 |
20 | use velvet::engine;
21 | use velvet::engine::Message;
22 | use velvet::init::init;
23 | use velvet::uci;
24 |
25 | fn main() {
26 | init();
27 | let tx = engine::spawn_engine_thread();
28 |
29 | if let Some(arg) = std::env::args().nth(1) {
30 | match arg.as_str() {
31 | "bench" | "profile" => {
32 | tx.send(Message::Profile).expect("Failed to send message");
33 | }
34 | "multibench" => {
35 | tx.send(Message::SetThreadCount(2)).expect("Failed to send message");
36 | tx.send(Message::SetTranspositionTableSize(64)).expect("Failed to send message");
37 | tx.send(Message::IsReady).expect("Failed to send message");
38 | tx.send(Message::Profile).expect("Failed to send message");
39 | }
40 | _ => {}
41 | }
42 | }
43 |
44 | uci::start_uci_loop(&tx);
45 | }
46 |
--------------------------------------------------------------------------------
/engine/src/nn.rs:
--------------------------------------------------------------------------------
1 | /*
2 | * Velvet Chess Engine
3 | * Copyright (C) 2025 mhonert (https://github.com/mhonert)
4 | *
5 | * This program is free software: you can redistribute it and/or modify
6 | * it under the terms of the GNU General Public License as published by
7 | * the Free Software Foundation, either version 3 of the License, or
8 | * (at your option) any later version.
9 | *
10 | * This program is distributed in the hope that it will be useful,
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | * GNU General Public License for more details.
14 | *
15 | * You should have received a copy of the GNU General Public License
16 | * along with this program. If not, see .
17 | */
18 | use crate::align::A64;
19 |
20 | pub mod eval;
21 | pub mod io;
22 |
23 | // NN layer size
24 | pub const BUCKETS: usize = 32;
25 | pub const BUCKET_SIZE: usize = 6 * 64 * 2;
26 | pub const INPUTS: usize = BUCKET_SIZE * BUCKETS;
27 |
28 | pub const HL1_NODES: usize = 2 * HL1_HALF_NODES;
29 | pub const HL1_HALF_NODES: usize = 1536;
30 |
31 | pub const MAX_RELU: f32 = 2.499;
32 | pub const FP_MAX_RELU: i16 = (MAX_RELU * FP_IN_MULTIPLIER as f32) as i16;
33 |
34 | pub const INPUT_WEIGHT_COUNT: usize = INPUTS * HL1_HALF_NODES;
35 |
36 | // Fixed point number precision
37 | pub const FP_IN_PRECISION_BITS: u8 = 6;
38 | pub const FP_IN_MULTIPLIER: i64 = 1 << FP_IN_PRECISION_BITS;
39 |
40 | pub const FP_OUT_PRECISION_BITS: u8 = 10; // must be an even number
41 | pub const FP_OUT_MULTIPLIER: i64 = 1 << FP_OUT_PRECISION_BITS;
42 |
43 | pub const SCORE_SCALE: i16 = 1024;
44 |
45 | macro_rules! include_layer {
46 | ($file:expr, $T:ty, $S:expr) => {{
47 | let layer_bytes = include_bytes!($file);
48 | let layer: A64<[$T; $S]> = A64(unsafe { std::mem::transmute_copy(layer_bytes) });
49 | layer
50 | }};
51 | }
52 |
53 | pub static IN_TO_H1_WEIGHTS: A64<[i8; INPUT_WEIGHT_COUNT]> = include_layer!("../nets/velvet_layer_in_weights.qnn", i8, INPUT_WEIGHT_COUNT);
54 |
55 | pub static H1_BIASES: A64<[i8; HL1_NODES]> = include_layer!("../nets/velvet_layer_in_bias.qnn", i8, HL1_NODES);
56 |
57 | pub static H1_TO_OUT_WEIGHTS: A64<[i16; HL1_NODES]> = include_layer!("../nets/velvet_layer_out_weights.qnn", i16, HL1_NODES);
58 |
59 | pub static OUT_BIASES: A64<[i16; 1]> = include_layer!("../nets/velvet_layer_out_bias.qnn", i16, 1);
60 |
61 | pub const fn piece_idx(piece_id: i8) -> u16 {
62 | (piece_id - 1) as u16
63 | }
64 |
65 | pub fn king_bucket(pos: u16) -> u16 {
66 | let row = pos / 8;
67 | let col = pos & 3;
68 |
69 | row * 4 + col
70 | }
71 |
--------------------------------------------------------------------------------
/engine/src/nn/io.rs:
--------------------------------------------------------------------------------
1 | /*
2 | * Velvet Chess Engine
3 | * Copyright (C) 2025 mhonert (https://github.com/mhonert)
4 | *
5 | * This program is free software: you can redistribute it and/or modify
6 | * it under the terms of the GNU General Public License as published by
7 | * the Free Software Foundation, either version 3 of the License, or
8 | * (at your option) any later version.
9 | *
10 | * This program is distributed in the hope that it will be useful,
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | * GNU General Public License for more details.
14 | *
15 | * You should have received a copy of the GNU General Public License
16 | * along with this program. If not, see .
17 | */
18 |
19 | use std::hash::{Hasher};
20 | use std::io::{Error, Read, Write};
21 |
22 | #[derive(Default)]
23 | pub struct FastHasher(u64);
24 |
25 | const GOLDEN_RATIO: u64 = 0x9E3779B97F4A7C15;
26 |
27 | impl Hasher for FastHasher {
28 | fn finish(&self) -> u64 {
29 | self.0
30 | }
31 |
32 | fn write(&mut self, _: &[u8]) {
33 | panic!("write for &[u8] not implemented")
34 | }
35 |
36 | fn write_u8(&mut self, value: u8) {
37 | self.0 = (self.0 ^ value as u64).wrapping_mul(GOLDEN_RATIO);
38 | }
39 |
40 | fn write_u32(&mut self, value: u32) {
41 | self.0 = (self.0 ^ value as u64).wrapping_mul(GOLDEN_RATIO);
42 | }
43 |
44 | fn write_u64(&mut self, value: u64) {
45 | self.0 = (self.0 ^ value).wrapping_mul(GOLDEN_RATIO);
46 | }
47 |
48 | fn write_i16(&mut self, value: i16) {
49 | self.0 = (self.0 ^ value as u64).wrapping_mul(GOLDEN_RATIO);
50 | }
51 | }
52 |
53 | pub fn read_u8(reader: &mut dyn Read) -> Result {
54 | let mut buf = [0; 1];
55 | reader.read_exact(&mut buf)?;
56 | Ok(buf[0])
57 | }
58 |
59 | pub fn read_i8(reader: &mut dyn Read) -> Result {
60 | let mut buf = [0; 1];
61 | reader.read_exact(&mut buf)?;
62 | Ok(buf[0] as i8)
63 | }
64 |
65 | pub fn read_u16(reader: &mut dyn Read) -> Result {
66 | let mut buf = [0; 2];
67 | reader.read_exact(&mut buf)?;
68 | Ok(u16::from_le_bytes(buf))
69 | }
70 |
71 | pub fn read_u32(reader: &mut dyn Read) -> Result {
72 | let mut buf = [0; 4];
73 | reader.read_exact(&mut buf)?;
74 | Ok(u32::from_le_bytes(buf))
75 | }
76 |
77 | pub fn read_u64(reader: &mut dyn Read) -> Result {
78 | let mut buf = [0; 8];
79 | reader.read_exact(&mut buf)?;
80 | Ok(u64::from_le_bytes(buf))
81 | }
82 |
83 | pub fn read_f32(reader: &mut dyn Read) -> Result {
84 | let mut buf = [0; 4];
85 | reader.read_exact(&mut buf)?;
86 | Ok(f32::from_le_bytes(buf))
87 | }
88 |
89 | pub fn read_i16(reader: &mut dyn Read) -> Result {
90 | let mut buf = [0; 2];
91 | reader.read_exact(&mut buf)?;
92 | Ok(i16::from_le_bytes(buf))
93 | }
94 |
95 | pub fn write_u8(writer: &mut dyn Write, v: u8) -> Result<(), Error> {
96 | writer.write_all(&[v])
97 | }
98 |
99 | pub fn write_i8(writer: &mut dyn Write, v: i8) -> Result<(), Error> {
100 | writer.write_all(&v.to_le_bytes())
101 | }
102 |
103 | pub fn write_u16(writer: &mut dyn Write, v: u16) -> Result<(), Error> {
104 | writer.write_all(&v.to_le_bytes())
105 | }
106 |
107 | pub fn write_u64(writer: &mut dyn Write, v: u64) -> Result<(), Error> {
108 | writer.write_all(&v.to_le_bytes())
109 | }
110 |
111 | pub fn write_u32(writer: &mut dyn Write, v: u32) -> Result<(), Error> {
112 | writer.write_all(&v.to_le_bytes())
113 | }
114 |
115 | pub fn write_f32(writer: &mut dyn Write, v: f32) -> Result<(), Error> {
116 | writer.write_all(&v.to_le_bytes())
117 | }
118 |
119 | pub fn write_i16(writer: &mut dyn Write, v: i16) -> Result<(), Error> {
120 | writer.write_all(&v.to_le_bytes())
121 | }
122 |
--------------------------------------------------------------------------------
/engine/src/params.rs:
--------------------------------------------------------------------------------
1 | /*
2 | * Velvet Chess Engine
3 | * Copyright (C) 2025 mhonert (https://github.com/mhonert)
4 | *
5 | * This program is free software: you can redistribute it and/or modify
6 | * it under the terms of the GNU General Public License as published by
7 | * the Free Software Foundation, either version 3 of the License, or
8 | * (at your option) any later version.
9 | *
10 | * This program is distributed in the hope that it will be useful,
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | * GNU General Public License for more details.
14 | *
15 | * You should have received a copy of the GNU General Public License
16 | * along with this program. If not, see .
17 | */
18 |
19 |
20 | #[macro_use]
21 | mod macros;
22 |
23 | #[allow(unused)]
24 | use std::str::FromStr;
25 |
26 | tunable_params!(
27 | fp_base_margin = 17
28 | fp_margin_multiplier = 22
29 |
30 | razor_margin_multiplier = 200
31 | rfp_margin_multiplier = 64
32 |
33 | nmp_base = 768
34 | nmp_divider = 668
35 |
36 | se_double_ext_margin = 4
37 | se_double_ext_limit = 12
38 |
39 | prob_cut_margin = 152
40 | prob_cut_depth = 4
41 |
42 | lmr_base = 253
43 | lmr_divider = 1024
44 |
45 | lmp_max_depth = 4
46 | lmp_improving_base = 3
47 | lmp_improving_multiplier = 65
48 | lmp_not_improving_base = 2
49 | lmp_not_improving_multiplier = 35
50 |
51 | nmp_enabled = 1
52 | razoring_enabled = 1
53 | rfp_enabled = 1
54 | prob_cut_enabled = 1
55 | fp_enabled = 1
56 | se_enabled = 1
57 | );
58 |
59 | derived_array_params!(
60 | lmr: [MAX_LMR_MOVES] = calc_late_move_reductions
61 | );
62 |
63 | static STRENGTH_NODE_LIMITS: [u16; 72] = [
64 | 2, 3, 4, 5, 6, 7, 8, 9, 11, 13, 15, 18, 21, 25, 31, 40, 59, 102, 149, 196, 239, 276, 318, 354, 394,
65 | 438, 486, 539, 597, 661, 731, 826, 933, 1053, 1188, 1340, 1492, 1661, 1825, 2005, 2202, 2418,
66 | 2678, 2965, 3282, 3632, 4019, 4447, 4920, 5443, 6021, 6660, 7290, 7979, 8733, 9558, 10460, 11447,
67 | 12527, 13708, 15000, 16413, 17959, 19435, 21032, 22760, 24629, 26651, 28839, 31527, 34465, 37676,
68 | ];
69 |
70 |
71 | #[cfg(not(feature = "tune"))]
72 | pub fn print_options() {}
73 |
74 | #[cfg(feature = "tune")]
75 | pub fn print_options() {
76 | print_single_options();
77 | }
78 |
79 | pub fn calc_node_limit_from_elo(elo: i32) -> u64 {
80 | let elo_normalized = (elo.clamp(1225, 3000) - 1225) / 25;
81 | STRENGTH_NODE_LIMITS[elo_normalized as usize] as u64
82 | }
83 |
84 | const MAX_LMR_MOVES: usize = 64;
85 |
86 | pub fn lmr_idx(moves: i16) -> usize {
87 | (moves as usize).min(MAX_LMR_MOVES - 1)
88 | }
89 |
90 | fn calc_late_move_reductions(params: &SingleParams) -> [i16; MAX_LMR_MOVES] {
91 | let mut lmr = [0i16; MAX_LMR_MOVES];
92 | for moves in 1..MAX_LMR_MOVES {
93 | lmr[lmr_idx(moves as i16)] = (from_fp(params.lmr_base()) + (moves as f64) / from_fp(params.lmr_divider())).log2() as i16;
94 | }
95 |
96 | lmr
97 | }
98 |
99 | impl SingleParams {
100 | pub fn lmp(&self, improving: bool, depth: i32) -> i32 {
101 | if improving {
102 | (depth * depth + self.lmp_improving_base() as i32) * self.lmp_improving_multiplier() as i32 / 64
103 | } else {
104 | (depth * depth + self.lmp_not_improving_base() as i32) * self.lmp_not_improving_multiplier() as i32 / 64
105 | }
106 | }
107 | }
108 |
109 |
110 | // Convert a fixed point value to a floating point value.
111 | fn from_fp(fp: i16) -> f64 {
112 | (fp as f64) / 256.0
113 | }
114 |
--------------------------------------------------------------------------------
/engine/src/params/macros.rs:
--------------------------------------------------------------------------------
1 | /*
2 | * Velvet Chess Engine
3 | * Copyright (C) 2025 mhonert (https://github.com/mhonert)
4 | *
5 | * This program is free software: you can redistribute it and/or modify
6 | * it under the terms of the GNU General Public License as published by
7 | * the Free Software Foundation, either version 3 of the License, or
8 | * (at your option) any later version.
9 | *
10 | * This program is distributed in the hope that it will be useful,
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | * GNU General Public License for more details.
14 | *
15 | * You should have received a copy of the GNU General Public License
16 | * along with this program. If not, see .
17 | */
18 |
19 | #[cfg(feature = "tune")]
20 | macro_rules! tunable_params {
21 | ($($name:ident = $value:literal)+) => {
22 | #[derive(Copy, Clone)]
23 | pub struct SingleParams {
24 | $(
25 | $name: i16,
26 | )+
27 | }
28 |
29 | impl Default for SingleParams {
30 | fn default() -> Self {
31 | SingleParams {
32 | $(
33 | $name: $value,
34 | )+
35 | }
36 | }
37 | }
38 |
39 | impl SingleParams {
40 | $(
41 | pub fn $name(&self) -> i16 { self.$name }
42 | )+
43 |
44 | pub fn set_param(&mut self, name: &str, value: i16) -> Option {
45 | match name {
46 | $(
47 | stringify!($name) => {
48 | let prev = self.$name;
49 | self.$name = value;
50 | Some(prev != value)
51 | },
52 | )+
53 | _ => None
54 | }
55 | }
56 | }
57 |
58 | fn print_single_options() {
59 | $(
60 | println!("option name {} type spin default {} min {} max {}", stringify!($name), $value, i16::MIN, i16::MAX);
61 | )+
62 | }
63 | }
64 | }
65 |
66 | #[cfg(not(feature = "tune"))]
67 | macro_rules! tunable_params {
68 | ($($name:ident = $value:literal)+) => {
69 | #[derive(Copy, Clone, Default)]
70 | pub struct SingleParams;
71 |
72 | impl SingleParams {
73 | $(
74 | pub fn $name(&self) -> i16 { $value }
75 | )+
76 |
77 | pub fn set_param(&self, _name: &str, _value: i16) -> Option {
78 | None
79 | }
80 | }
81 | }
82 | }
83 |
84 | // Creates a struct with array params that are derived from SingleParams using a function.
85 | // e.g. lmr[MAX_LMR_MOVES] = calc_late_move_reductions
86 | macro_rules! derived_array_params {
87 | ($($name:ident: [$size:ident] = $func:ident)*) => {
88 | #[derive(Copy, Clone)]
89 | pub struct DerivedArrayParams {
90 | $(
91 | $name: [i16; $size],
92 | )+
93 | }
94 |
95 | impl DerivedArrayParams {
96 | pub fn new(sp: &SingleParams) -> Self {
97 | Self {
98 | $(
99 | $name: $func(sp),
100 | )+
101 | }
102 | }
103 |
104 | pub fn update(&mut self, sp: &SingleParams) {
105 | $(
106 | self.$name = $func(sp);
107 | )+
108 | }
109 |
110 | $(
111 | pub fn $name(&self, i: usize) -> i16 { self.$name[i] }
112 | )+
113 | }
114 | }
115 | }
--------------------------------------------------------------------------------
/engine/src/perft.rs:
--------------------------------------------------------------------------------
1 | /*
2 | * Velvet Chess Engine
3 | * Copyright (C) 2025 mhonert (https://github.com/mhonert)
4 | *
5 | * This program is free software: you can redistribute it and/or modify
6 | * it under the terms of the GNU General Public License as published by
7 | * the Free Software Foundation, either version 3 of the License, or
8 | * (at your option) any later version.
9 | *
10 | * This program is distributed in the hope that it will be useful,
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | * GNU General Public License for more details.
14 | *
15 | * You should have received a copy of the GNU General Public License
16 | * along with this program. If not, see .
17 | */
18 | use crate::board::Board;
19 | use crate::history_heuristics::{EMPTY_HISTORY, HistoryHeuristics};
20 | use crate::moves::NO_MOVE;
21 | use crate::next_ply;
22 | use crate::search_context::SearchContext;
23 |
24 | /* Perft (performance test, move path enumeration) test helper function to verify the move generator.
25 | It generates all possible moves up to the specified depth and counts the number of leaf nodes.
26 | This number can then be compared to precalculated numbers that are known to be correct
27 |
28 | Another use case for this function is to test the performance of the move generator.
29 | */
30 | pub fn perft(ctx: &mut SearchContext, hh: &HistoryHeuristics, board: &mut Board, depth: i32) -> u64 {
31 | if depth == 0 {
32 | return 1;
33 | }
34 |
35 | let mut nodes: u64 = 0;
36 |
37 | let active_player = board.active_player();
38 | ctx.prepare_moves(active_player, NO_MOVE, EMPTY_HISTORY);
39 |
40 | let in_check = board.is_in_check(active_player);
41 |
42 | while let Some(m) = ctx.next_move(hh, board) {
43 | let (previous_piece, removed_piece_id) = board.perform_move(m);
44 |
45 | if !board.is_left_in_check(active_player, in_check, m) {
46 | nodes += next_ply!(ctx, perft(ctx, hh, board, depth - 1));
47 | }
48 |
49 | board.undo_move(m, previous_piece, removed_piece_id);
50 | }
51 |
52 | nodes
53 | }
54 |
--------------------------------------------------------------------------------
/engine/src/pieces.rs:
--------------------------------------------------------------------------------
1 | /*
2 | * Velvet Chess Engine
3 | * Copyright (C) 2022 mhonert (https://github.com/mhonert)
4 | *
5 | * This program is free software: you can redistribute it and/or modify
6 | * it under the terms of the GNU General Public License as published by
7 | * the Free Software Foundation, either version 3 of the License, or
8 | * (at your option) any later version.
9 | *
10 | * This program is distributed in the hope that it will be useful,
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | * GNU General Public License for more details.
14 | *
15 | * You should have received a copy of the GNU General Public License
16 | * along with this program. If not, see .
17 | */
18 |
19 | pub const EMPTY: i8 = 0;
20 | pub const P: i8 = 1;
21 | pub const N: i8 = 2;
22 | pub const B: i8 = 3;
23 | pub const R: i8 = 4;
24 | pub const Q: i8 = 5;
25 | pub const K: i8 = 6;
26 |
--------------------------------------------------------------------------------
/engine/src/pos_history.rs:
--------------------------------------------------------------------------------
1 | /*
2 | * Velvet Chess Engine
3 | * Copyright (C) 2024 mhonert (https://github.com/mhonert)
4 | *
5 | * This program is free software: you can redistribute it and/or modify
6 | * it under the terms of the GNU General Public License as published by
7 | * the Free Software Foundation, either version 3 of the License, or
8 | * (at your option) any later version.
9 | *
10 | * This program is distributed in the hope that it will be useful,
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | * GNU General Public License for more details.
14 | *
15 | * You should have received a copy of the GNU General Public License
16 | * along with this program. If not, see .
17 | */
18 | use crate::bitboard::BitBoard;
19 | use crate::board::cycledetection;
20 | use crate::zobrist::player_zobrist_key;
21 |
22 | #[derive(Clone)]
23 | pub struct PositionHistory {
24 | positions: Vec,
25 | root: usize,
26 | }
27 |
28 | #[derive(Clone)]
29 | struct Entry {
30 | hash: u64,
31 | is_after_root: bool,
32 | is_repetition: bool,
33 | }
34 |
35 | impl Default for PositionHistory {
36 | fn default() -> Self {
37 | PositionHistory { positions: Vec::with_capacity(16), root: 0 }
38 | }
39 | }
40 |
41 | impl PositionHistory {
42 | pub fn push(&mut self, hash: u64) {
43 | self.positions.push(Entry{hash, is_after_root: true, is_repetition: false});
44 | }
45 |
46 | pub fn pop(&mut self) {
47 | self.positions.pop();
48 | }
49 |
50 | pub fn is_repetition_draw(&self, hash: u64, halfmove_clock: u8) -> bool {
51 | for entry in self.positions.iter().rev().skip(1).step_by(2).take(halfmove_clock as usize / 2) {
52 | if entry.hash == hash && (entry.is_after_root || entry.is_repetition) {
53 | return true;
54 | }
55 | }
56 |
57 | false
58 | }
59 |
60 | pub fn has_upcoming_repetition(&self, occupancy: BitBoard, hash: u64, halfmove_clock: u8) -> bool {
61 | if halfmove_clock < 3 {
62 | return false;
63 | }
64 |
65 | let last_opp = self.positions.last().unwrap();
66 | let mut other = hash ^ last_opp.hash ^ player_zobrist_key();
67 |
68 | for (own, opp) in self.positions.iter().rev().skip(1).step_by(2)
69 | .zip(self.positions.iter().rev().skip(2).step_by(2)).take(halfmove_clock as usize / 2) {
70 |
71 | other ^= own.hash ^ opp.hash ^ player_zobrist_key();
72 | if other != 0 {
73 | continue;
74 | }
75 |
76 | if (opp.is_after_root || opp.is_repetition) && cycledetection::has_cycle_move(hash ^ opp.hash, occupancy) {
77 | return true;
78 | }
79 | }
80 |
81 | false
82 | }
83 |
84 | pub fn clear(&mut self) {
85 | self.positions.clear();
86 | self.root = 0;
87 | }
88 |
89 | pub fn mark_root(&mut self, halfmove_clock: u8) {
90 | self.root = self.positions.len();
91 | let mut existing = StackSet::new();
92 | for entry in self.positions.iter_mut().rev().take(halfmove_clock as usize) {
93 | entry.is_after_root = false;
94 | if !existing.insert(entry.hash) {
95 | entry.is_repetition = true;
96 | }
97 | }
98 | }
99 | }
100 |
101 | struct StackSet([u64; 256]);
102 |
103 | impl StackSet {
104 | pub fn new() -> Self {
105 | StackSet([0; 256])
106 | }
107 |
108 | fn insert(&mut self, hash: u64) -> bool {
109 | let mut idx = (hash as usize) & 255;
110 | loop {
111 | let existing_value = self.0[idx];
112 | if existing_value == 0 {
113 | self.0[idx] = hash;
114 | return true;
115 | } else if existing_value == hash {
116 | return false;
117 | }
118 | idx = (idx + 1) & 255;
119 | }
120 | }
121 | }
122 |
123 | #[cfg(test)]
124 | mod tests {
125 | use super::*;
126 |
127 | #[test]
128 | fn detects_single_repetition() {
129 | let mut history = PositionHistory::default();
130 | history.push(1000);
131 | history.push(1001);
132 | history.push(1002);
133 | history.push(1003);
134 | history.push(1004);
135 |
136 | assert!(!history.is_repetition_draw(1, 1));
137 |
138 | history.push(1);
139 | assert!(!history.is_repetition_draw(2, 2));
140 |
141 | history.push(2);
142 | assert!(history.is_repetition_draw(1, 3));
143 | }
144 | }
145 |
--------------------------------------------------------------------------------
/engine/src/random.rs:
--------------------------------------------------------------------------------
1 | /*
2 | * Velvet Chess Engine
3 | * Copyright (C) 2025 mhonert (https://github.com/mhonert)
4 | *
5 | * This program is free software: you can redistribute it and/or modify
6 | * it under the terms of the GNU General Public License as published by
7 | * the Free Software Foundation, either version 3 of the License, or
8 | * (at your option) any later version.
9 | *
10 | * This program is distributed in the hope that it will be useful,
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | * GNU General Public License for more details.
14 | *
15 | * You should have received a copy of the GNU General Public License
16 | * along with this program. If not, see .
17 | */
18 |
19 | const MULTIPLIER: u64 = 6364136223846793005;
20 | const INCREMENT: u64 = 1442695040888963407;
21 |
22 | #[derive(Clone)]
23 | pub struct Random {
24 | state: u64,
25 | }
26 |
27 | impl Random {
28 | pub fn new() -> Self {
29 | Random { state: 0x4d595df4d0f33173 }
30 | }
31 |
32 | pub fn new_with_seed(seed: u64) -> Self {
33 | Random { state: seed.wrapping_mul(0x4d595df4d0f33173) }
34 | }
35 |
36 | pub fn rand32(&mut self) -> u32 {
37 | let (new_state, random_value) = rand(self.state);
38 | self.state = new_state;
39 | random_value
40 | }
41 |
42 | pub fn rand64(&mut self) -> u64 {
43 | ((self.rand32() as u64) << 32) | (self.rand32() as u64)
44 | }
45 |
46 | pub fn rand128(&mut self) -> u128 {
47 | ((self.rand64() as u128) << 64) | (self.rand64() as u128)
48 | }
49 | }
50 |
51 | pub const fn rand(mut state: u64) -> (u64, u32) {
52 | let mut x = state;
53 | let count = (x >> 59) as u32;
54 | state = x.wrapping_mul(MULTIPLIER).wrapping_add(INCREMENT);
55 | x ^= x >> 18;
56 |
57 | (state, ((x >> 27) as u32).rotate_right(count))
58 | }
59 |
60 | pub const fn rand64(state: u64) -> (u64, u64) {
61 | let (state, low) = rand(state);
62 | let (state, high) = rand(state);
63 | (state, low as u64 | ((high as u64) << 32))
64 | }
65 |
66 | #[cfg(test)]
67 | mod tests {
68 | use super::*;
69 |
70 | #[test]
71 | fn generate_evenly_distributed_random_numbers() {
72 | let mut rnd = Random::new();
73 | let mut number_counts: [i32; 6] = [0, 0, 0, 0, 0, 0];
74 | let iterations = 1_000_000;
75 |
76 | for _ in 0..iterations {
77 | let number = (rnd.rand64() % 6) as i32;
78 | number_counts[number as usize] += 1;
79 | }
80 |
81 | let deviation_tolerance = (iterations as f64 * 0.001) as i32; // accept a low deviation from the "ideal" random distribution
82 |
83 | let ideal_distribution = iterations / 6;
84 | for i in 0..6 {
85 | let number_count = number_counts[i];
86 | let deviation_from_ideal = (ideal_distribution - number_count).abs();
87 | assert!(deviation_from_ideal < deviation_tolerance);
88 | }
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/engine/src/scores.rs:
--------------------------------------------------------------------------------
1 | /*
2 | * Velvet Chess Engine
3 | * Copyright (C) 2024 mhonert (https://github.com/mhonert)
4 | *
5 | * This program is free software: you can redistribute it and/or modify
6 | * it under the terms of the GNU General Public License as published by
7 | * the Free Software Foundation, either version 3 of the License, or
8 | * (at your option) any later version.
9 | *
10 | * This program is distributed in the hope that it will be useful,
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | * GNU General Public License for more details.
14 | *
15 | * You should have received a copy of the GNU General Public License
16 | * along with this program. If not, see .
17 | */
18 |
19 | pub const MAX_SCORE: i16 = 16383;
20 | pub const MIN_SCORE: i16 = -MAX_SCORE;
21 |
22 | pub const MATE_SCORE: i16 = 13000;
23 | pub const MATED_SCORE: i16 = -MATE_SCORE;
24 | const MATE_SCORE_RANGE: i16 = 499;
25 |
26 | pub const MAX_EVAL: i16 = 9999;
27 | pub const MIN_EVAL: i16 = -MAX_EVAL;
28 |
29 | pub fn is_mate_or_mated_score(score: i16) -> bool {
30 | score.abs() >= (MATE_SCORE - MATE_SCORE_RANGE)
31 | }
32 |
33 | pub fn is_mate_score(score: i16) -> bool {
34 | score >= (MATE_SCORE - MATE_SCORE_RANGE)
35 | }
36 |
37 | pub fn is_mated_score(score: i16) -> bool {
38 | score <= (MATED_SCORE + MATE_SCORE_RANGE)
39 | }
40 |
41 | pub fn is_eval_score(score: i16) -> bool {
42 | score.abs() <= MAX_EVAL
43 | }
44 |
45 | pub fn mate_in(score: i16) -> Option {
46 | let mate_ply_distance = MATE_SCORE - score;
47 | if (0..=MATE_SCORE_RANGE).contains(&mate_ply_distance) {
48 | Some((mate_ply_distance + 1) / 2)
49 | } else {
50 | None
51 | }
52 | }
53 |
54 | pub fn sanitize_score(score: i16) -> i16 {
55 | let tmp = score.clamp(MATED_SCORE, MATE_SCORE);
56 | if is_mate_or_mated_score(tmp) {
57 | tmp
58 | } else {
59 | tmp.clamp(MIN_EVAL, MAX_EVAL)
60 | }
61 | }
62 |
63 | pub fn sanitize_eval_score(score: i32) -> i32 {
64 | score.clamp(MIN_EVAL as i32, MAX_EVAL as i32)
65 | }
66 |
67 | pub fn sanitize_mate_score(score: i16) -> i16 {
68 | score.clamp(MATE_SCORE - MATE_SCORE_RANGE, MATE_SCORE)
69 | }
70 |
71 | pub fn sanitize_mated_score(score: i16) -> i16 {
72 | score.clamp(MATED_SCORE, MATED_SCORE + MATE_SCORE_RANGE)
73 | }
74 |
75 | // clock_scaled_eval scales the evaluation by the remaining halfmove clock (50-move counter).
76 | pub fn clock_scaled_eval(halfmove_clock: u8, eval: i16) -> i16 {
77 | if is_mate_or_mated_score(eval) {
78 | return eval;
79 | }
80 | ((eval as i32) * (128 - halfmove_clock as i32) / 128) as i16
81 | }
82 |
83 |
--------------------------------------------------------------------------------
/engine/src/search_context.rs:
--------------------------------------------------------------------------------
1 | /*
2 | * Velvet Chess Engine
3 | * Copyright (C) 2025 mhonert (https://github.com/mhonert)
4 | *
5 | * This program is free software: you can redistribute it and/or modify
6 | * it under the terms of the GNU General Public License as published by
7 | * the Free Software Foundation, either version 3 of the License, or
8 | * (at your option) any later version.
9 | *
10 | * This program is distributed in the hope that it will be useful,
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | * GNU General Public License for more details.
14 | *
15 | * You should have received a copy of the GNU General Public License
16 | * along with this program. If not, see .
17 | */
18 |
19 | use std::array;
20 | use crate::board::Board;
21 | use crate::colors::Color;
22 | use crate::history_heuristics::{EMPTY_HISTORY, HistoryHeuristics, MoveHistory};
23 | use crate::move_gen::{MoveList};
24 | use crate::moves::{Move, NO_MOVE};
25 | use crate::slices::SliceElementAccess;
26 | use crate::transposition_table::MAX_DEPTH;
27 | use crate::zobrist::piece_zobrist_key;
28 |
29 | pub struct SearchContext {
30 | ply: usize,
31 |
32 | movelists: [MoveList; MAX_DEPTH + 1],
33 | ply_entries: [PlyEntry; MAX_DEPTH + 4 + 2 + 1],
34 |
35 | root_move_randomization: bool,
36 | }
37 |
38 | impl Default for SearchContext {
39 | fn default() -> Self {
40 | SearchContext{
41 | ply: 0,
42 | movelists: array::from_fn(|_| MoveList::default()),
43 | ply_entries: [PlyEntry::default(); MAX_DEPTH + 4 + 2 + 1],
44 | root_move_randomization: false,
45 | }
46 | }
47 | }
48 |
49 | impl SearchContext {
50 | pub fn enter_ply(&mut self) {
51 | self.ply += 1;
52 | self.ply_entry_mut(self.pe_idx()).double_extensions = self.ply_entry(self.pe_idx() - 1).double_extensions;
53 | }
54 |
55 | pub fn leave_ply(&mut self) {
56 | self.ply -= 1;
57 | }
58 |
59 | pub fn max_qs_depth_reached(&self) -> bool {
60 | self.ply >= MAX_DEPTH
61 | }
62 |
63 | pub fn max_search_depth_reached(&self) -> bool {
64 | self.ply >= MAX_DEPTH - 16
65 | }
66 |
67 | fn movelist_mut(&mut self) -> &mut MoveList {
68 | self.movelists.el_mut(self.ply)
69 | }
70 |
71 | fn movelist(&self) -> &MoveList {
72 | self.movelists.el(self.ply)
73 | }
74 |
75 | pub fn set_root_move_randomization(&mut self, state: bool) {
76 | self.root_move_randomization = state;
77 | }
78 |
79 | pub fn next_move(&mut self, hh: &HistoryHeuristics, board: &Board) -> Option {
80 | let ply = self.ply;
81 | self.movelist_mut().next_move(ply, hh, board)
82 | }
83 |
84 | pub fn generate_qs_captures(&mut self, board: &Board) {
85 | self.movelist_mut().generate_qs_captures(board);
86 | }
87 |
88 | pub fn next_good_capture_move(&mut self, board: &Board) -> Option {
89 | self.movelist_mut().next_good_capture_move(board)
90 | }
91 |
92 | pub fn is_bad_capture_move(&self) -> bool {
93 | self.movelist().is_bad_capture_move()
94 | }
95 |
96 | pub fn reset_root_moves(&mut self) {
97 | self.movelist_mut().reset_root_moves();
98 | }
99 |
100 | pub fn next_root_move(&mut self, hh: &HistoryHeuristics, board: &mut Board) -> Option {
101 | let randomize = self.root_move_randomization;
102 | self.movelist_mut().next_root_move(hh, board, randomize)
103 | }
104 |
105 | pub fn update_root_move(&mut self, m: Move) {
106 | self.movelist_mut().update_root_move(m);
107 | }
108 |
109 | pub fn reorder_root_moves(&mut self, best_move: Move, sort_other_moves: bool) {
110 | self.movelist_mut().reorder_root_moves(best_move, sort_other_moves);
111 | }
112 |
113 | pub fn prepare_moves(&mut self, active_player: Color, hash_move: Move, move_history: MoveHistory) {
114 | self.movelist_mut().init(active_player, hash_move, move_history);
115 | }
116 |
117 | pub fn move_history(&self) -> MoveHistory {
118 | let curr = self.ply_entry(self.pe_idx());
119 | let prev_opp = self.ply_entry(self.pe_idx() - 1);
120 |
121 | MoveHistory {
122 | last_opp: curr.opp_move,
123 | prev_own: prev_opp.opp_move,
124 | }
125 | }
126 |
127 | pub fn move_history_hash(&self) -> u16 {
128 | let curr = self.ply_entry(self.pe_idx());
129 | let prev_opp = self.ply_entry(self.pe_idx() - 1);
130 |
131 | ((opp_move_hash(curr.opp_move) ^ own_move_hash(prev_opp.opp_move)) & 0xFFFF) as u16
132 | }
133 |
134 | pub fn root_move_count(&self) -> usize {
135 | self.movelist().root_move_count()
136 | }
137 |
138 | fn pe_idx(&self) -> usize {
139 | self.ply + 4
140 | }
141 |
142 | fn ply_entry(&self, idx: usize) -> &PlyEntry {
143 | self.ply_entries.el(idx)
144 | }
145 |
146 | fn ply_entry_mut(&mut self, idx: usize) -> &mut PlyEntry {
147 | self.ply_entries.el_mut(idx)
148 | }
149 |
150 | pub fn is_improving(&self) -> bool {
151 | let curr_ply = self.ply_entry(self.pe_idx());
152 |
153 | if curr_ply.in_check {
154 | return false;
155 | }
156 | let prev_own_ply = self.ply_entry(self.pe_idx() - 2);
157 | if self.ply >= 2 && !prev_own_ply.in_check {
158 | return prev_own_ply.eval < curr_ply.eval;
159 | }
160 |
161 | let prev_prev_own_ply = self.ply_entry(self.pe_idx() - 4);
162 | if self.ply >= 4 && !prev_prev_own_ply.in_check {
163 | return prev_prev_own_ply.eval < curr_ply.eval;
164 | }
165 |
166 | true
167 | }
168 |
169 | pub fn eval(&self) -> i16 {
170 | self.ply_entry(self.pe_idx()).eval
171 | }
172 |
173 | pub fn in_check(&self) -> bool {
174 | self.ply_entry(self.pe_idx()).in_check
175 | }
176 |
177 | pub fn update_next_ply_entry(&mut self, opp_m: Move, gives_check: bool) {
178 | let entry = self.ply_entry_mut(self.pe_idx() + 1);
179 | entry.opp_move = opp_m;
180 | entry.in_check = gives_check;
181 | }
182 |
183 | pub fn set_eval(&mut self, score: i16) {
184 | self.ply_entry_mut(self.pe_idx()).eval = score;
185 | }
186 |
187 | pub fn inc_double_extensions(&mut self) {
188 | self.ply_entry_mut(self.pe_idx()).double_extensions += 1;
189 | }
190 |
191 | pub fn double_extensions(&self) -> i16 {
192 | self.ply_entry(self.pe_idx()).double_extensions
193 | }
194 |
195 | pub fn has_any_legal_move(&mut self, active_player: Color, hh: &HistoryHeuristics, board: &mut Board) -> bool {
196 | self.prepare_moves(active_player, NO_MOVE, EMPTY_HISTORY);
197 |
198 | while let Some(m) = self.next_move(hh, board) {
199 | let (previous_piece, removed_piece_id) = board.perform_move(m);
200 | let is_legal = !board.is_in_check(active_player);
201 | board.undo_move(m, previous_piece, removed_piece_id);
202 | if is_legal {
203 | return true;
204 | }
205 | }
206 |
207 | false
208 | }
209 |
210 | pub fn clear_cutoff_count(&mut self) {
211 | self.ply_entry_mut(self.pe_idx() + 2).cutoff_count = 0;
212 | }
213 |
214 | pub fn inc_cutoff_count(&mut self) {
215 | self.ply_entry_mut(self.pe_idx()).cutoff_count += 1;
216 | }
217 |
218 | pub fn next_ply_cutoff_count(&self) -> u32 {
219 | self.ply_entry(self.pe_idx() + 1).cutoff_count
220 | }
221 |
222 | pub fn ply(&self) -> usize {
223 | self.ply
224 | }
225 | }
226 |
227 | fn own_move_hash(m: Move) -> u64 {
228 | piece_zobrist_key(m.move_type().piece_id(), m.end() as usize)
229 | }
230 |
231 | fn opp_move_hash(m: Move) -> u64 {
232 | piece_zobrist_key(-m.move_type().piece_id(), m.end() as usize)
233 | }
234 |
235 | #[derive(Copy, Clone, Default)]
236 | pub struct PlyEntry {
237 | eval: i16,
238 | in_check: bool,
239 | opp_move: Move,
240 | double_extensions: i16,
241 | cutoff_count: u32,
242 | }
243 |
244 | #[macro_export]
245 | macro_rules! next_ply {
246 | ($ctx:expr, $func_call:expr) => {
247 | {
248 | $ctx.enter_ply();
249 | let result = $func_call;
250 | $ctx.leave_ply();
251 | result
252 | }
253 | };
254 | }
255 |
--------------------------------------------------------------------------------
/engine/src/slices.rs:
--------------------------------------------------------------------------------
1 | /*
2 | * Velvet Chess Engine
3 | * Copyright (C) 2025 mhonert (https://github.com/mhonert)
4 | *
5 | * This program is free software: you can redistribute it and/or modify
6 | * it under the terms of the GNU General Public License as published by
7 | * the Free Software Foundation, either version 3 of the License, or
8 | * (at your option) any later version.
9 | *
10 | * This program is distributed in the hope that it will be useful,
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | * GNU General Public License for more details.
14 | *
15 | * You should have received a copy of the GNU General Public License
16 | * along with this program. If not, see .
17 | */
18 |
19 | /// Trait for accessing elements of slices.
20 | /// Bounds checking can be enabled via the `checked_slice_access` feature.
21 | pub trait SliceElementAccess {
22 | fn el(&self, idx: usize) -> &T;
23 | fn el_mut(&mut self, idx: usize) -> &mut T;
24 | }
25 |
26 | impl SliceElementAccess for [T] {
27 | fn el(&self, idx: usize) -> &T {
28 | #[cfg(not(feature = "checked_slice_access"))]
29 | {
30 | unsafe { self.get_unchecked(idx) }
31 | }
32 | #[cfg(feature = "checked_slice_access")]
33 | {
34 | &self[idx]
35 | }
36 | }
37 |
38 | fn el_mut(&mut self, idx: usize) -> &mut T {
39 | #[cfg(not(feature = "checked_slice_access"))]
40 | {
41 | unsafe { self.get_unchecked_mut(idx) }
42 | }
43 | #[cfg(feature = "checked_slice_access")]
44 | {
45 | &mut self[idx]
46 | }
47 | }
48 | }
--------------------------------------------------------------------------------
/engine/src/syzygy.rs:
--------------------------------------------------------------------------------
1 | /*
2 | * Velvet Chess Engine
3 | * Copyright (C) 2024 mhonert (https://github.com/mhonert)
4 | *
5 | * This program is free software: you can redistribute it and/or modify
6 | * it under the terms of the GNU General Public License as published by
7 | * the Free Software Foundation, either version 3 of the License, or
8 | * (at your option) any later version.
9 | *
10 | * This program is distributed in the hope that it will be useful,
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | * GNU General Public License for more details.
14 | *
15 | * You should have received a copy of the GNU General Public License
16 | * along with this program. If not, see .
17 | */
18 | use crate::moves::Move;
19 |
20 | #[cfg(not(feature = "fathomrs"))]
21 | pub const HAS_TB_SUPPORT: bool = false;
22 |
23 | #[cfg(feature = "fathomrs")]
24 | pub const HAS_TB_SUPPORT: bool = true;
25 |
26 | pub const DEFAULT_TB_PROBE_DEPTH: i32 = 0;
27 |
28 | pub trait ProbeTB {
29 | fn probe_wdl(&self) -> Option;
30 | fn probe_root(&self) -> Option<(tb::TBResult, Vec)>;
31 | }
32 |
33 | #[cfg(not(feature = "fathomrs"))]
34 | pub mod tb {
35 | use crate::board::Board;
36 | use crate::moves::Move;
37 | use crate::syzygy::ProbeTB;
38 |
39 | #[derive(Eq, PartialEq, Copy, Clone)]
40 | #[allow(dead_code)]
41 | pub enum TBResult { Loss, BlessedLoss, Draw, CursedWin, Win }
42 |
43 | pub fn init(_path: String) -> bool {
44 | false
45 | }
46 |
47 | pub fn max_piece_count() -> u32 {
48 | 0
49 | }
50 |
51 | impl ProbeTB for Board {
52 | fn probe_wdl(&self) -> Option {
53 | None
54 | }
55 |
56 | fn probe_root(&self) -> Option<(TBResult, Vec)> {
57 | None
58 | }
59 | }
60 | }
61 |
62 | #[cfg(feature = "fathomrs")]
63 | pub mod tb {
64 | use fathomrs::tb::{extract_move, Promotion};
65 | use crate::bitboard::{v_mirror_i8};
66 | use crate::board::Board;
67 | use crate::colors::{BLACK, WHITE};
68 | use crate::moves::Move;
69 | use crate::pieces::{B, EMPTY, K, N, P, Q, R};
70 | use crate::syzygy::ProbeTB;
71 | use crate::uci_move::UCIMove;
72 |
73 | pub type TBResult = fathomrs::tb::TBResult;
74 |
75 | pub fn init(path: String) -> bool {
76 | fathomrs::tb::init(path)
77 | }
78 |
79 | pub fn max_piece_count() -> u32 {
80 | fathomrs::tb::max_piece_count()
81 | }
82 |
83 | impl ProbeTB for Board {
84 | fn probe_wdl(&self) -> Option {
85 | if self.halfmove_clock() != 0 || self.any_castling() || self.piece_count() > fathomrs::tb::max_piece_count() {
86 | return None;
87 | }
88 |
89 | let ep_target = self.enpassant_target();
90 | let ep = if ep_target != 0 {
91 | v_mirror_i8(ep_target as i8) as u16
92 | } else {
93 | 0
94 | };
95 |
96 | fathomrs::tb::probe_wdl(
97 | self.get_all_piece_bitboard(WHITE).0.swap_bytes(),
98 | self.get_all_piece_bitboard(BLACK).0.swap_bytes(),
99 | (self.get_bitboard(K) | self.get_bitboard(-K)).0.swap_bytes(),
100 | (self.get_bitboard(Q) | self.get_bitboard(-Q)).0.swap_bytes(),
101 | (self.get_bitboard(R) | self.get_bitboard(-R)).0.swap_bytes(),
102 | (self.get_bitboard(B) | self.get_bitboard(-B)).0.swap_bytes(),
103 | (self.get_bitboard(N) | self.get_bitboard(-N)).0.swap_bytes(),
104 | (self.get_bitboard(P) | self.get_bitboard(-P)).0.swap_bytes(),
105 | ep,
106 | self.active_player().is_white()
107 | )
108 | }
109 |
110 | fn probe_root(&self) -> Option<(TBResult, Vec)> {
111 | if self.any_castling() || self.piece_count() > fathomrs::tb::max_piece_count() {
112 | return None;
113 | }
114 |
115 |
116 | let ep_target = self.enpassant_target();
117 | let ep = if ep_target != 0 {
118 | v_mirror_i8(ep_target as i8) as u16
119 | } else {
120 | 0
121 | };
122 |
123 | let (result, moves) = fathomrs::tb::probe_root(
124 | self.get_all_piece_bitboard(WHITE).0.swap_bytes(),
125 | self.get_all_piece_bitboard(BLACK).0.swap_bytes(),
126 | (self.get_bitboard(K) | self.get_bitboard(-K)).0.swap_bytes(),
127 | (self.get_bitboard(Q) | self.get_bitboard(-Q)).0.swap_bytes(),
128 | (self.get_bitboard(R) | self.get_bitboard(-R)).0.swap_bytes(),
129 | (self.get_bitboard(B) | self.get_bitboard(-B)).0.swap_bytes(),
130 | (self.get_bitboard(N) | self.get_bitboard(-N)).0.swap_bytes(),
131 | (self.get_bitboard(P) | self.get_bitboard(-P)).0.swap_bytes(),
132 | self.halfmove_clock(),
133 | ep,
134 | self.active_player().is_white()
135 | );
136 |
137 | if fathomrs::tb::is_failed_result(result) {
138 | return None;
139 | }
140 |
141 | let best_tb_result = TBResult::from_result(result);
142 |
143 | Some((best_tb_result, moves.iter().filter(|&m| *m != 0).map(|&m| {
144 | let tb_result = TBResult::from_result(m);
145 | let (from, to, promotion) = extract_move(m);
146 | (tb_result, from, to, promotion)
147 | }).filter(|(tb_result, _, _, _)| tb_result < &best_tb_result)
148 | .map(|(_, from, to, promotion)| {
149 | let promotion_piece = match promotion {
150 | Promotion::Queen => Q,
151 | Promotion::Rook => R,
152 | Promotion::Bishop => B,
153 | Promotion::Knight => N,
154 | _ => EMPTY
155 | };
156 |
157 | UCIMove::new(v_mirror_i8(from), v_mirror_i8(to), promotion_piece).to_move(self)
158 | }).collect()))
159 | }
160 | }
161 | }
--------------------------------------------------------------------------------
/engine/src/time_management.rs:
--------------------------------------------------------------------------------
1 | /*
2 | * Velvet Chess Engine
3 | * Copyright (C) 2024 mhonert (https://github.com/mhonert)
4 | *
5 | * This program is free software: you can redistribute it and/or modify
6 | * it under the terms of the GNU General Public License as published by
7 | * the Free Software Foundation, either version 3 of the License, or
8 | * (at your option) any later version.
9 | *
10 | * This program is distributed in the hope that it will be useful,
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | * GNU General Public License for more details.
14 | *
15 | * You should have received a copy of the GNU General Public License
16 | * along with this program. If not, see .
17 | */
18 |
19 | use crate::colors::Color;
20 | use crate::moves::{Move, NO_MOVE};
21 | use crate::transposition_table::MAX_DEPTH;
22 | use std::time::{Duration, Instant};
23 |
24 | pub const TIMEEXT_MULTIPLIER: i32 = 3;
25 | pub const MAX_TIMELIMIT_MS: i32 = i32::MAX;
26 |
27 | pub const DEFAULT_MOVE_OVERHEAD_MS: i32 = 20;
28 | pub const MAX_MOVE_OVERHEAD_MS: i32 = 1000;
29 | pub const MIN_MOVE_OVERHEAD_MS: i32 = 0;
30 |
31 | #[derive(Clone)]
32 | pub struct TimeManager {
33 | starttime: Instant,
34 | timelimit_ms: i32,
35 |
36 | allow_time_extension: bool,
37 | time_extended: bool,
38 |
39 | next_index: usize,
40 | current_depth: i32,
41 | history: Vec,
42 |
43 | expected_best_move: Move,
44 | }
45 |
46 | impl TimeManager {
47 | pub fn new() -> Self {
48 | TimeManager {
49 | starttime: Instant::now(),
50 | timelimit_ms: 0,
51 | allow_time_extension: true,
52 | time_extended: false,
53 | next_index: 0,
54 | current_depth: 0,
55 | history: vec![NO_MOVE; MAX_DEPTH],
56 | expected_best_move: NO_MOVE,
57 | }
58 | }
59 |
60 | pub fn reset(&mut self, limit: SearchLimits) {
61 | self.starttime = Instant::now();
62 | self.timelimit_ms = limit.time_limit_ms;
63 |
64 | self.allow_time_extension = !limit.strict_time_limit;
65 | self.history.fill(NO_MOVE);
66 | self.next_index = 0;
67 | self.current_depth = 0;
68 | self.time_extended = false;
69 | }
70 |
71 | pub fn update_best_move(&mut self, new_best_move: Move, depth: i32) {
72 | if depth > self.current_depth {
73 | self.current_depth = depth;
74 | self.next_index += 1;
75 | }
76 | self.history[self.next_index - 1] = new_best_move;
77 | }
78 |
79 | pub fn is_time_for_another_iteration(&self, now: Instant, previous_iteration_time: Duration) -> bool {
80 | if self.time_extended && !(self.score_dropped() || self.best_move_changed()) {
81 | return false;
82 | }
83 |
84 | let duration_ms = previous_iteration_time.as_millis() as i32;
85 | let estimated_iteration_duration = duration_ms * 7 / 4;
86 | let remaining_time = self.remaining_time_ms(now);
87 | if remaining_time <= estimated_iteration_duration * 7 / 4 && self.best_move_consistent() {
88 | return false;
89 | }
90 |
91 | remaining_time >= estimated_iteration_duration
92 | }
93 |
94 | fn best_move_consistent(&self) -> bool {
95 | self.expected_best_move != NO_MOVE && !self.history.is_empty() && self.history.iter().take(self.next_index - 1).all(|&m| m == self.expected_best_move)
96 | }
97 |
98 | pub fn search_duration_ms(&self, now: Instant) -> i32 {
99 | self.search_duration(now).as_millis() as i32
100 | }
101 |
102 | pub fn search_duration(&self, now: Instant) -> Duration {
103 | now.duration_since(self.starttime)
104 | }
105 |
106 | pub fn remaining_time_ms(&self, now: Instant) -> i32 {
107 | self.timelimit_ms - self.search_duration_ms(now)
108 | }
109 |
110 | pub fn is_timelimit_exceeded(&self, now: Instant) -> bool {
111 | self.remaining_time_ms(now) <= 0
112 | }
113 |
114 | pub fn try_extend_timelimit(&mut self) -> bool {
115 | if !self.allow_time_extension {
116 | return false;
117 | }
118 |
119 | if self.best_move_changed() || self.score_dropped() {
120 | self.allow_time_extension = false;
121 | self.timelimit_ms *= TIMEEXT_MULTIPLIER;
122 | self.time_extended = true;
123 | return true;
124 | }
125 |
126 | false
127 | }
128 |
129 | fn score_dropped(&self) -> bool {
130 | if self.next_index < 2 {
131 | false
132 | } else {
133 | (self.history[self.next_index - 2].score() - self.history[self.next_index - 1].score()) > 0
134 | }
135 | }
136 |
137 | fn best_move_changed(&self) -> bool {
138 | if self.next_index < 2 {
139 | false
140 | } else {
141 | self.history[self.next_index - 2] != self.history[self.next_index - 1]
142 | }
143 | }
144 |
145 | pub fn reduce_timelimit(&mut self) {
146 | self.allow_time_extension = false;
147 | self.timelimit_ms /= 32;
148 | }
149 |
150 | pub fn set_expected_best_move(&mut self, m: Move) {
151 | self.expected_best_move = m;
152 | }
153 | }
154 |
155 | #[derive(Copy, Clone, Debug)]
156 | pub struct SearchLimits {
157 | node_limit: u64,
158 | depth_limit: i32,
159 | mate_limit: i16,
160 | time_limit_ms: i32,
161 | strict_time_limit: bool,
162 |
163 | wtime: i32,
164 | btime: i32,
165 | winc: i32,
166 | binc: i32,
167 | move_time: i32,
168 | moves_to_go: i32,
169 | }
170 |
171 | impl SearchLimits {
172 | pub fn default() -> Self {
173 | SearchLimits {
174 | node_limit: u64::MAX,
175 | depth_limit: MAX_DEPTH as i32,
176 | mate_limit: 0,
177 | time_limit_ms: i32::MAX,
178 | strict_time_limit: true,
179 |
180 | wtime: -1,
181 | btime: -1,
182 | winc: 0,
183 | binc: 0,
184 | move_time: i32::MAX,
185 | moves_to_go: 1,
186 | }
187 | }
188 |
189 | pub fn infinite() -> SearchLimits {
190 | let mut limits = SearchLimits::default();
191 | limits.time_limit_ms = MAX_TIMELIMIT_MS;
192 | limits
193 | }
194 |
195 | pub fn nodes(node_limit: u64) -> SearchLimits {
196 | let mut limits = SearchLimits::default();
197 | limits.node_limit = node_limit;
198 |
199 | limits
200 | }
201 |
202 | pub fn new(
203 | depth_limit: Option, node_limit: Option, wtime: Option, btime: Option, winc: Option,
204 | binc: Option, move_time: Option, moves_to_go: Option, mate_limit: Option,
205 | ) -> Result {
206 | let depth_limit = depth_limit.unwrap_or(MAX_DEPTH as i32);
207 | if depth_limit <= 0 {
208 | return Err("depth limit must be > 0");
209 | }
210 |
211 | let node_limit = node_limit.unwrap_or(u64::MAX);
212 | let mate_limit = mate_limit.unwrap_or(0);
213 |
214 | Ok(SearchLimits {
215 | depth_limit,
216 | node_limit,
217 | mate_limit,
218 | time_limit_ms: i32::MAX,
219 | strict_time_limit: true,
220 |
221 | wtime: wtime.unwrap_or(-1),
222 | btime: btime.unwrap_or(-1),
223 | winc: winc.unwrap_or(0),
224 | binc: binc.unwrap_or(0),
225 | move_time: move_time.unwrap_or(-1),
226 | moves_to_go: moves_to_go.unwrap_or(25),
227 | })
228 | }
229 |
230 | pub fn update(&mut self, active_player: Color, move_overhead_ms: i32) {
231 | let (time_left, inc) = if active_player.is_white() { (self.wtime, self.winc) } else { (self.btime, self.binc) };
232 |
233 | self.time_limit_ms = calc_time_limit(self.move_time, time_left, inc, self.moves_to_go, move_overhead_ms);
234 |
235 | self.strict_time_limit = self.move_time > 0
236 | || self.time_limit_ms == MAX_TIMELIMIT_MS
237 | || self.moves_to_go == 1
238 | || (time_left - (TIMEEXT_MULTIPLIER * self.time_limit_ms) <= move_overhead_ms);
239 | }
240 |
241 | pub fn node_limit(&self) -> u64 {
242 | self.node_limit
243 | }
244 |
245 | pub fn set_node_limit(&mut self, limit: u64) {
246 | self.node_limit = limit;
247 | }
248 |
249 | pub fn set_depth_limit(&mut self, limit: i32) {
250 | self.depth_limit = limit;
251 | }
252 |
253 | pub fn set_strict_time_limit(&mut self, strict: bool) {
254 | self.strict_time_limit = strict;
255 | }
256 |
257 | pub fn depth_limit(&self) -> i32 {
258 | self.depth_limit
259 | }
260 |
261 | pub fn mate_limit(&self) -> i16 {
262 | self.mate_limit
263 | }
264 |
265 | pub fn is_infinite(&self) -> bool {
266 | self.time_limit_ms == MAX_TIMELIMIT_MS && self.node_limit == u64::MAX && self.mate_limit == 0 && self.depth_limit == MAX_DEPTH as i32
267 | }
268 |
269 | pub fn has_time_limit(&self) -> bool {
270 | self.time_limit_ms != MAX_TIMELIMIT_MS
271 | }
272 | }
273 |
274 | fn calc_time_limit(movetime: i32, mut time_left: i32, time_increment: i32, moves_to_go: i32, move_overhead_ms: i32) -> i32 {
275 | if movetime == -1 && time_left == -1 {
276 | return MAX_TIMELIMIT_MS;
277 | }
278 |
279 | if movetime > 0 {
280 | return (movetime - move_overhead_ms).max(0);
281 | }
282 |
283 | time_left -= move_overhead_ms;
284 | if time_left <= 0 {
285 | return 0;
286 | }
287 |
288 | let time_for_move = time_left / moves_to_go.max(1);
289 |
290 | if time_for_move > time_left {
291 | return time_left;
292 | }
293 |
294 | if time_for_move + time_increment <= time_left {
295 | time_for_move + time_increment
296 | } else {
297 | time_for_move
298 | }
299 | }
300 |
--------------------------------------------------------------------------------
/engine/src/zobrist.rs:
--------------------------------------------------------------------------------
1 | /*
2 | * Velvet Chess Engine
3 | * Copyright (C) 2025 mhonert (https://github.com/mhonert)
4 | *
5 | * This program is free software: you can redistribute it and/or modify
6 | * it under the terms of the GNU General Public License as published by
7 | * the Free Software Foundation, either version 3 of the License, or
8 | * (at your option) any later version.
9 | *
10 | * This program is distributed in the hope that it will be useful,
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | * GNU General Public License for more details.
14 | *
15 | * You should have received a copy of the GNU General Public License
16 | * along with this program. If not, see .
17 | */
18 | use crate::slices::SliceElementAccess;
19 |
20 | const PLAYER: u64 = 0x8000000000000001;
21 | const EP: u64 = 0x42a6344d1227098d;
22 | const CASTLING: u64 = 0xab28bc31b46cbb3c;
23 | static PIECE: [u64; 13] = [ 0x7eb5140a57a894c8, 0x467813d5c298de63, 0xc5c1f1e2594b941c, 0xf319da8df6cf96b4, 0xdc8b55eebfca3a40, 0x5418f15d4c08f4e2, 0x1d3350493f26ec1e, 0xd0c4b14bdb230807, 0x73ef23b69de88e14, 0xb9219d4683de93d9, 0xe8c0a3740dbb1c7a, 0x59fd9c7dc2c9298a, 0x1ffc53c9670efd27 ];
24 |
25 | pub fn player_zobrist_key() -> u64 {
26 | PLAYER
27 | }
28 |
29 | pub fn enpassant_zobrist_key(en_passant_state: u8) -> u64 {
30 | EP.rotate_left(en_passant_state as u32)
31 | }
32 |
33 | pub fn castling_zobrist_key(castling_state: u8) -> u64 {
34 | CASTLING.rotate_left(castling_state as u32)
35 | }
36 |
37 | pub fn piece_zobrist_key(piece: i8, pos: usize) -> u64 {
38 | let piece_key = *PIECE.el((piece + 6) as usize);
39 | piece_key.rotate_left(pos as u32)
40 | }
41 |
42 | #[cfg(test)]
43 | mod tests {
44 | use crate::board::castling::{Castling, CastlingState};
45 | use crate::board::{BlackBoardPos, WhiteBoardPos};
46 | use crate::zobrist::{enpassant_zobrist_key, player_zobrist_key};
47 |
48 | #[test]
49 | fn check_key_quality() {
50 | let mut all_keys = Vec::new();
51 | all_keys.push(player_zobrist_key());
52 |
53 | let mut castlings = Vec::new();
54 | for side1 in [Some(Castling::WhiteKingSide), Some(Castling::WhiteQueenSide), Some(Castling::BlackKingSide), Some(Castling::BlackQueenSide), None].iter() {
55 | for side2 in [Some(Castling::WhiteKingSide), Some(Castling::WhiteQueenSide), Some(Castling::BlackKingSide), Some(Castling::BlackQueenSide), None].iter() {
56 | for side3 in [Some(Castling::WhiteKingSide), Some(Castling::WhiteQueenSide), Some(Castling::BlackKingSide), Some(Castling::BlackQueenSide), None].iter() {
57 | for side4 in [Some(Castling::WhiteKingSide), Some(Castling::WhiteQueenSide), Some(Castling::BlackKingSide), Some(Castling::BlackQueenSide), None].iter() {
58 | let mut cs = CastlingState::ALL;
59 | side1.map(|side| cs.clear_side(side));
60 | side2.map(|side| cs.clear_side(side));
61 | side3.map(|side| cs.clear_side(side));
62 | side4.map(|side| cs.clear_side(side));
63 | castlings.push(cs);
64 | }
65 | }
66 | }
67 | }
68 | castlings.sort();
69 | castlings.dedup();
70 | castlings.iter().for_each(|c| all_keys.push(c.zobrist_key()));
71 |
72 | for ep in (WhiteBoardPos::EnPassantLineStart as u8)..=(WhiteBoardPos::EnPassantLineEnd as u8) {
73 | all_keys.push(enpassant_zobrist_key(ep));
74 | }
75 | for ep in (BlackBoardPos::EnPassantLineStart as u8)..=(BlackBoardPos::EnPassantLineEnd as u8) {
76 | all_keys.push(enpassant_zobrist_key(ep));
77 | }
78 |
79 | for piece in -6..=6 {
80 | if piece == 0 {
81 | continue;
82 | }
83 | for pos in 0..64 {
84 | all_keys.push(super::piece_zobrist_key(piece, pos));
85 | }
86 | }
87 |
88 | let mut duplicates = all_keys.len();
89 | all_keys.sort_unstable();
90 | all_keys.dedup();
91 | duplicates -= all_keys.len();
92 |
93 | assert_eq!(0, duplicates);
94 | assert_eq!(0, all_keys.iter().filter(|&k| *k == 0 || *k == u64::MAX).count());
95 | }
96 | }
--------------------------------------------------------------------------------
/engine/tests/perft_tests.rs:
--------------------------------------------------------------------------------
1 | /*
2 | * Velvet Chess Engine
3 | * Copyright (C) 2023 mhonert (https://github.com/mhonert)
4 | *
5 | * This program is free software: you can redistribute it and/or modify
6 | * it under the terms of the GNU General Public License as published by
7 | * the Free Software Foundation, either version 3 of the License, or
8 | * (at your option) any later version.
9 | *
10 | * This program is distributed in the hope that it will be useful,
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | * GNU General Public License for more details.
14 | *
15 | * You should have received a copy of the GNU General Public License
16 | * along with this program. If not, see .
17 | */
18 |
19 | extern crate velvet;
20 |
21 | use velvet::fen::create_from_fen;
22 | use velvet::init::init;
23 | use velvet::perft::perft;
24 | use velvet::search_context::SearchContext;
25 |
26 | #[test]
27 | fn test_perft_startpos() {
28 | let fen = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1";
29 |
30 | assert_eq!(1, perft_for_fen(fen, 0));
31 | assert_eq!(20, perft_for_fen(fen, 1));
32 | assert_eq!(400, perft_for_fen(fen, 2));
33 | assert_eq!(8902, perft_for_fen(fen, 3));
34 | assert_eq!(197281, perft_for_fen(fen, 4));
35 | }
36 |
37 | #[test]
38 | fn test_perft_testpos2() {
39 | let fen = "r3k2r/p1ppqpb1/bn2pnp1/3PN3/1p2P3/2N2Q1p/PPPBBPPP/R3K2R w KQkq - 0 1";
40 |
41 | assert_eq!(48, perft_for_fen(fen, 1));
42 | assert_eq!(2039, perft_for_fen(fen, 2));
43 | assert_eq!(97862, perft_for_fen(fen, 3));
44 | }
45 |
46 | #[test]
47 | fn test_perft_testpos3() {
48 | let fen = "8/2p5/3p4/KP5r/1R3p1k/8/4P1P1/8 w - - 0 1";
49 |
50 | assert_eq!(14, perft_for_fen(fen, 1));
51 | assert_eq!(191, perft_for_fen(fen, 2));
52 | assert_eq!(2812, perft_for_fen(fen, 3));
53 | assert_eq!(43238, perft_for_fen(fen, 4));
54 | assert_eq!(674624, perft_for_fen(fen, 5));
55 | }
56 |
57 | #[test]
58 | fn test_perft_testpos4() {
59 | let fen = "r2q1rk1/pP1p2pp/Q4n2/bbp1p3/Np6/1B3NBn/pPPP1PPP/R3K2R b KQ - 0 1";
60 |
61 | assert_eq!(6, perft_for_fen(fen, 1));
62 | assert_eq!(264, perft_for_fen(fen, 2));
63 | assert_eq!(9467, perft_for_fen(fen, 3));
64 | assert_eq!(422333, perft_for_fen(fen, 4));
65 | }
66 |
67 | #[test]
68 | fn test_perft_testpos5() {
69 | let fen = "rnbq1k1r/pp1Pbppp/2p5/8/2B5/8/PPP1NnPP/RNBQK2R w KQ - 1 8";
70 |
71 | assert_eq!(44, perft_for_fen(fen, 1));
72 | assert_eq!(1486, perft_for_fen(fen, 2));
73 | assert_eq!(62379, perft_for_fen(fen, 3));
74 | assert_eq!(2103487, perft_for_fen(fen, 4));
75 | }
76 |
77 | #[test]
78 | fn test_perft_testpos6() {
79 | let fen = "r4rk1/1pp1qppp/p1np1n2/2b1p1B1/2B1P1b1/P1NP1N2/1PP1QPPP/R4RK1 w - - 0 10";
80 |
81 | assert_eq!(46, perft_for_fen(fen, 1));
82 | assert_eq!(2079, perft_for_fen(fen, 2));
83 | assert_eq!(89890, perft_for_fen(fen, 3));
84 | }
85 |
86 | #[test]
87 | fn test_perft_chess960() {
88 | assert_eq!(6417013, perft_for_fen("b1q1rrkb/pppppppp/3nn3/8/P7/1PPP4/4PPPP/BQNNRKRB w GE - 1 9", 5));
89 | assert_eq!(9183776, perft_for_fen("qbbnnrkr/2pp2pp/p7/1p2pp2/8/P3PP2/1PPP1KPP/QBBNNR1R w hf - 0 9", 5));
90 | assert_eq!(6718715, perft_for_fen("b1qnrrkb/ppp1pp1p/n2p1Pp1/8/8/P7/1PPPP1PP/BNQNRKRB w GE - 0 9", 5));
91 | assert_eq!(7697880, perft_for_fen("nq1bbrkr/pp2nppp/2pp4/4p3/1PP1P3/1B6/P2P1PPP/NQN1BRKR w HFhf - 2 9", 5));
92 | assert_eq!(22760, perft_for_fen("rk2r1bn/pqbppppp/1pp2n2/8/5P2/3P1N2/PPPQPRPP/RK1B2BN b a - 8 11", 3));
93 | }
94 |
95 | fn perft_for_fen(fen: &str, depth: i32) -> u64 {
96 | init();
97 | let mut ctx = SearchContext::default();
98 | let mut board = create_from_fen(fen);
99 | perft(&mut ctx, &mut board, depth)
100 | }
101 |
--------------------------------------------------------------------------------
/fathomrs/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "fathomrs"
3 | version = "1.0.0"
4 | edition = "2021"
5 |
6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
7 |
8 | [build-dependencies]
9 | bindgen = "0.69.1"
10 | cc = "1.0.83"
11 |
--------------------------------------------------------------------------------
/fathomrs/build.rs:
--------------------------------------------------------------------------------
1 | /*
2 | * Velvet Chess Engine
3 | * Copyright (C) 2023 mhonert (https://github.com/mhonert)
4 | *
5 | * This program is free software: you can redistribute it and/or modify
6 | * it under the terms of the GNU General Public License as published by
7 | * the Free Software Foundation, either version 3 of the License, or
8 | * (at your option) any later version.
9 | *
10 | * This program is distributed in the hope that it will be useful,
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | * GNU General Public License for more details.
14 | *
15 | * You should have received a copy of the GNU General Public License
16 | * along with this program. If not, see .
17 | */
18 |
19 | extern crate bindgen;
20 | extern crate cc;
21 |
22 | use std::env;
23 | use std::path::PathBuf;
24 |
25 | fn main() {
26 | let mut build = cc::Build::default();
27 |
28 | build.include("fathom/src")
29 | .file("fathom/src/tbprobe.c")
30 | .static_flag(true)
31 | .static_crt(true);
32 |
33 | build.compiler("clang");
34 | if env::consts::OS == "windows" {
35 | build.define("_CRT_SECURE_NO_WARNINGS", None);
36 | }
37 |
38 | build.compile("fathom");
39 |
40 | // Tell cargo to invalidate the built crate whenever the wrapper changes
41 | println!("cargo:rerun-if-changed=fathom/src/tbprobe.h");
42 |
43 | let bindings = bindgen::Builder::default()
44 | .header("fathom/src/tbprobe.h")
45 | .parse_callbacks(Box::new(bindgen::CargoCallbacks::new()))
46 | .generate()
47 | .expect("Unable to generate bindings");
48 |
49 | let out_path = PathBuf::from(env::var("OUT_DIR").unwrap());
50 | bindings
51 | .write_to_file(out_path.join("bindings.rs"))
52 | .expect("Couldn't write bindings!");
53 | }
54 |
--------------------------------------------------------------------------------
/fathomrs/fathom/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2013-2018 Ronald de Man
4 | Copyright (c) 2015 basil00
5 | Copyright (c) 2016-2020 by Jon Dart
6 |
7 | Permission is hereby granted, free of charge, to any person obtaining a copy
8 | of this software and associated documentation files (the "Software"), to deal
9 | in the Software without restriction, including without limitation the rights
10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11 | copies of the Software, and to permit persons to whom the Software is
12 | furnished to do so, subject to the following conditions:
13 |
14 | The above copyright notice and this permission notice shall be included in all
15 | copies or substantial portions of the Software.
16 |
17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23 | SOFTWARE.
24 |
25 |
--------------------------------------------------------------------------------
/fathomrs/fathom/README.md:
--------------------------------------------------------------------------------
1 | Fathom
2 | ======
3 |
4 | Fathom is a stand-alone Syzygy tablebase probing tool. The aims of Fathom
5 | are:
6 |
7 | * To make it easy to integrate the Syzygy tablebases into existing chess
8 | engines;
9 | * To make it easy to create stand-alone applications that use the Syzygy
10 | tablebases;
11 |
12 | Fathom is compilable under either C99 or C++ and supports a variety of
13 | platforms, including at least Windows, Linux, and MacOS.
14 |
15 | Tool
16 | ----
17 |
18 | Fathom includes a stand-alone command-line syzygy probing tool `fathom`. To
19 | probe a position, simply run the command:
20 |
21 | fathom --path= "FEN-string"
22 |
23 | The tool will print out a PGN representation of the probe result, including:
24 |
25 | * Result: "1-0" (white wins), "1/2-1/2" (draw), or "0-1" (black wins)
26 | * The Win-Draw-Loss (WDL) value for the next move: "Win", "Draw", "Loss",
27 | "CursedWin" (win but 50-move draw) or "BlessedLoss" (loss but 50-move draw)
28 | * The Distance-To-Zero (DTZ) value (in plys) for the next move
29 | * WinningMoves: The list of all winning moves
30 | * DrawingMoves: The list of all drawing moves
31 | * LosingMoves: The list of all losing moves
32 | * A pseudo "principal variation" of Syzygy vs. Syzygy for the input position.
33 |
34 | For more information, run the following command:
35 |
36 | fathom --help
37 |
38 | Programming API
39 | ---------------
40 |
41 | Fathom provides a simple API. Following are the main function calls:
42 |
43 | * `tb_init` initializes the tablebases.
44 | * `tb_free` releases any resources allocated by Fathom.
45 | * `tb_probe_wdl` probes the Win-Draw-Loss (WDL) table for a given position.
46 | * `tb_probe_root` probes the Distance-To-Zero (DTZ) table for the given
47 | position. It returns a recommended move, and also a list of unsigned
48 | integers, each one encoding a possible move and its DTZ and WDL values.
49 | * `tb_probe_root_dtz` probes the Distance-To-Zero (DTZ) at the root position.
50 | It returns a score and a rank for each possible move.
51 | * `tb_probe_root_wdl` probes the Win-Draw-Loss (WDL) at the root position.
52 | it returns a score and a rank for each possible move.
53 |
54 | Fathom does not require the callee to provide any additional functionality
55 | (e.g. move generation). A simple set of chess-related functions including move
56 | generation is provided in file `tbchess.c`. However, chess engines can opt to
57 | replace some of this functionality for better performance (see below).
58 |
59 | Chess engines
60 | -------------
61 |
62 | Chess engines can use `tb_probe_wdl` to get the WDL value during
63 | search. This function is thread safe (unless TB_NO_THREADS is
64 | set). The various "probe_root" functions are intended for probing only
65 | at the root node and are not thread-safe.
66 |
67 | Chess engines and other clients can modify some features of Fathom and
68 | override some of its internal functions by configuring
69 | `tbconfig.h`. `tbconfig.h` is included in Fathom's code with angle
70 | brackets. This allows a client of Fathom to override tbconfig.h by
71 | placing its own modified copy in its include path before the Fathom
72 | source directory.
73 |
74 | One option provided by `tbconfig.h` is to define macros that replace
75 | some aspects of Fathom's functionality, such as calculating piece
76 | attacks, avoiding duplication of functionality. If doing this,
77 | however, be careful with including typedefs or defines from your own
78 | code into `tbconfig.h`, since these may clash with internal definitions
79 | used by Fathom. I recommend instead interfacing to external
80 | functions via a small module, with an interface something like this:
81 |
82 | ```
83 | #ifndef _TB_ATTACK_INTERFACE
84 | #define _TB_ATTACK_INTERFACE
85 |
86 | #ifdef __cplusplus
87 | #include
88 | #else
89 | #include
90 | #endif
91 |
92 | extern tb_knight_attacks(unsigned square);
93 | extern tb_king_attacks(unsigned square);
94 | extern tb_root_attacks(unsigned square, uint64_t occ);
95 | extern tb_bishop_attacks(unsigned square, uint64_t occ);
96 | extern tb_queen_attacks(unsigned square, uint64_t occ);
97 | extern tb_pawn_attacks(unsigned square, uint64_t occ);
98 |
99 | #endif
100 | ```
101 |
102 | You can add if wanted other function definitions such as a popcnt
103 | function based on the chess engine's native popcnt support.
104 |
105 | `tbconfig.h` can then reference these functions safety because the
106 | interface depends only on types defined in standard headers. The
107 | implementation, however, can use any types from the chess engine or
108 | other client that are necessary. (A good optimizer with link-time
109 | optimization will inline the implementation code even though it is not
110 | visible in the interface).
111 |
112 | History and Credits
113 | -------------------
114 |
115 | The Syzygy tablebases were created by Ronald de Man. The original version of Fathom
116 | (https://github.com/basil00/Fathom) combined probing code from Ronald de Man, originally written for
117 | Stockfish, with chess-related functions and other support code from Basil Falcinelli.
118 | That codebase is no longer being maintained. This repository was originaly a fork of
119 | that codebase, with additional modifications by Jon Dart.
120 |
121 | However, the current Fathom code in this repository is no longer
122 | derived directly from the probing code written for Stockfish, but
123 | instead derives from tbprobe.c, which is a component of the Cfish
124 | chess engine (https://github.com/syzygy1/Cfish), a Stockfish
125 | derivative. tbprobe.c includes 7-man tablebase support. It was written
126 | by Ronald de Man and released for unrestricted distribution and use.
127 |
128 | This fork of Fathom replaces the Cfish board representation and move
129 | generation code used in tbprobe.c with simpler, MIT-licensed code from the original
130 | Fathom source by Basil. The code has been reorganized so that
131 | `tbchess.c` contains all move generation and most chess-related typedefs
132 | and functions, while `tbprobe.c` contains all the tablebase probing
133 | code. The code replacement and reorganization was done by Jon Dart.
134 |
135 | License
136 | -------
137 |
138 | (C) 2013-2015 Ronald de Man (original code)
139 | (C) 2015 basil (new modifications)
140 | (C) 2016-2020 Jon Dart (additional modifications)
141 |
142 | This version of Fathom is released under the MIT License:
143 |
144 | Permission is hereby granted, free of charge, to any person obtaining a copy of
145 | this software and associated documentation files (the "Software"), to deal in
146 | the Software without restriction, including without limitation the rights to
147 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
148 | of the Software, and to permit persons to whom the Software is furnished to do
149 | so, subject to the following conditions:
150 |
151 | The above copyright notice and this permission notice shall be included in all
152 | copies or substantial portions of the Software.
153 |
154 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
155 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
156 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
157 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
158 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
159 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
160 | SOFTWARE.
161 |
162 |
--------------------------------------------------------------------------------
/fathomrs/fathom/src/tbconfig.h:
--------------------------------------------------------------------------------
1 | /*
2 | * tbconfig.h
3 | * (C) 2015 basil, all rights reserved,
4 | * Modifications Copyright 2016-2017 Jon Dart
5 | *
6 | * Permission is hereby granted, free of charge, to any person obtaining a
7 | * copy of this software and associated documentation files (the "Software"),
8 | * to deal in the Software without restriction, including without limitation
9 | * the rights to use, copy, modify, merge, publish, distribute, sublicense,
10 | * and/or sell copies of the Software, and to permit persons to whom the
11 | * Software is furnished to do so, subject to the following conditions:
12 | *
13 | * The above copyright notice and this permission notice shall be included in
14 | * all copies or substantial portions of the Software.
15 | *
16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
21 | * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
22 | * DEALINGS IN THE SOFTWARE.
23 | */
24 |
25 | #ifndef TBCONFIG_H
26 | #define TBCONFIG_H
27 |
28 | /****************************************************************************/
29 | /* BUILD CONFIG: */
30 | /****************************************************************************/
31 |
32 | /*
33 | * Define TB_CUSTOM_POP_COUNT to override the internal popcount
34 | * implementation. To do this supply a macro or function definition
35 | * here:
36 | */
37 | /* #define TB_CUSTOM_POP_COUNT(x) */
38 |
39 | /*
40 | * Define TB_CUSTOM_LSB to override the internal lsb
41 | * implementation. To do this supply a macro or function definition
42 | * here:
43 | */
44 | /* #define TB_CUSTOM_LSB(x) */
45 |
46 | /*
47 | * Define TB_NO_STDINT if you do not want to use or it is not
48 | * available.
49 | */
50 | /* #define TB_NO_STDINT */
51 |
52 | /*
53 | * Define TB_NO_STDBOOL if you do not want to use or it is not
54 | * available or unnecessary (e.g. C++).
55 | */
56 | /* #define TB_NO_STDBOOL */
57 |
58 | /*
59 | * Define TB_NO_THREADS if your program is not multi-threaded.
60 | */
61 | /* #define TB_NO_THREADS */
62 |
63 | /*
64 | * Define TB_NO_HELPER_API if you do not need the helper API.
65 | */
66 | #define TB_NO_HELPER_API
67 |
68 | /*
69 | * Define TB_NO_HW_POP_COUNT if there is no hardware popcount instruction.
70 | *
71 | * Note: if defined, TB_CUSTOM_POP_COUNT is always used in preference
72 | * to any built-in popcount functions.
73 | *
74 | * If no custom popcount function is defined, and if the following
75 | * define is not set, the code will attempt to use an available hardware
76 | * popcnt (currently supported on x86_64 architecture only) and otherwise
77 | * will fall back to a software implementation.
78 | */
79 | /* #define TB_NO_HW_POP_COUNT */
80 |
81 | /***************************************************************************/
82 | /* SCORING CONSTANTS */
83 | /***************************************************************************/
84 | /*
85 | * Fathom can produce scores for tablebase moves. These depend on the
86 | * value of a pawn, and the magnitude of mate scores. The following
87 | * constants are representative values but will likely need
88 | * modification to adapt to an engine's own internal score values.
89 | */
90 | #define TB_VALUE_PAWN 100 /* value of pawn in endgame */
91 | #define TB_VALUE_MATE 8000
92 | #define TB_VALUE_INFINITE 8191 /* value above all normal score values */
93 | #define TB_VALUE_DRAW 0
94 | #define TB_MAX_MATE_PLY 255
95 |
96 | /***************************************************************************/
97 | /* ENGINE INTEGRATION CONFIG */
98 | /***************************************************************************/
99 |
100 | /*
101 | * If you are integrating tbprobe into an engine, you can replace some of
102 | * tbprobe's built-in functionality with that already provided by the engine.
103 | * This is OPTIONAL. If no definition are provided then tbprobe will use its
104 | * own internal defaults. That said, for engines it is generally a good idea
105 | * to avoid redundancy.
106 | */
107 |
108 | /*
109 | * Define TB_KING_ATTACKS(square) to return the king attacks bitboard for a
110 | * king at `square'.
111 | */
112 | /* #define TB_KING_ATTACKS(square) */
113 |
114 | /*
115 | * Define TB_KNIGHT_ATTACKS(square) to return the knight attacks bitboard for
116 | * a knight at `square'.
117 | */
118 | /* #define TB_KNIGHT_ATTACKS(square) */
119 |
120 | /*
121 | * Define TB_ROOK_ATTACKS(square, occ) to return the rook attacks bitboard
122 | * for a rook at `square' assuming the given `occ' occupancy bitboard.
123 | */
124 | /* #define TB_ROOK_ATTACKS(square, occ) */
125 |
126 | /*
127 | * Define TB_BISHOP_ATTACKS(square, occ) to return the bishop attacks bitboard
128 | * for a bishop at `square' assuming the given `occ' occupancy bitboard.
129 | */
130 | /* #define TB_BISHOP_ATTACKS(square, occ) */
131 |
132 | /*
133 | * Define TB_QUEEN_ATTACKS(square, occ) to return the queen attacks bitboard
134 | * for a queen at `square' assuming the given `occ' occupancy bitboard.
135 | * NOTE: If no definition is provided then tbprobe will use:
136 | * TB_ROOK_ATTACKS(square, occ) | TB_BISHOP_ATTACKS(square, occ)
137 | */
138 | /* #define TB_QUEEN_ATTACKS(square, occ) */
139 |
140 | /*
141 | * Define TB_PAWN_ATTACKS(square, color) to return the pawn attacks bitboard
142 | * for a `color' pawn at `square'.
143 | * NOTE: This definition must work for pawns on ranks 1 and 8. For example,
144 | * a white pawn on e1 attacks d2 and f2. A black pawn on e1 attacks
145 | * nothing. Etc.
146 | * NOTE: This definition must not include en passant captures.
147 | */
148 | /* #define TB_PAWN_ATTACKS(square, color) */
149 |
150 | #endif
151 |
--------------------------------------------------------------------------------
/fathomrs/src/bindings.rs:
--------------------------------------------------------------------------------
1 | /*
2 | * Velvet Chess Engine
3 | * Copyright (C) 2023 mhonert (https://github.com/mhonert)
4 | *
5 | * This program is free software: you can redistribute it and/or modify
6 | * it under the terms of the GNU General Public License as published by
7 | * the Free Software Foundation, either version 3 of the License, or
8 | * (at your option) any later version.
9 | *
10 | * This program is distributed in the hope that it will be useful,
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | * GNU General Public License for more details.
14 | *
15 | * You should have received a copy of the GNU General Public License
16 | * along with this program. If not, see .
17 | */
18 |
19 | #![allow(non_upper_case_globals)]
20 | #![allow(non_camel_case_types)]
21 | #![allow(non_snake_case)]
22 | #![allow(unused)]
23 | include!(concat!(env!("OUT_DIR"), "/bindings.rs"));
24 |
25 | impl Default for TbRootMoves {
26 | fn default() -> Self {
27 | TbRootMoves{
28 | size: 0,
29 | moves: [TbRootMove::default(); TB_MAX_MOVES as usize],
30 | }
31 | }
32 | }
33 |
34 | impl Default for TbRootMove {
35 | fn default() -> Self {
36 | TbRootMove{
37 | move_: 0,
38 | pv: [0; TB_MAX_PLY as usize],
39 | pvSize: 0,
40 | tbScore: 0,
41 | tbRank: 0,
42 | }
43 | }
44 | }
45 |
46 |
--------------------------------------------------------------------------------
/fathomrs/src/lib.rs:
--------------------------------------------------------------------------------
1 | /*
2 | * Velvet Chess Engine
3 | * Copyright (C) 2023 mhonert (https://github.com/mhonert)
4 | *
5 | * This program is free software: you can redistribute it and/or modify
6 | * it under the terms of the GNU General Public License as published by
7 | * the Free Software Foundation, either version 3 of the License, or
8 | * (at your option) any later version.
9 | *
10 | * This program is distributed in the hope that it will be useful,
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | * GNU General Public License for more details.
14 | *
15 | * You should have received a copy of the GNU General Public License
16 | * along with this program. If not, see .
17 | */
18 |
19 | pub mod tb;
20 | mod bindings;
--------------------------------------------------------------------------------
/fathomrs/src/tb.rs:
--------------------------------------------------------------------------------
1 | /*
2 | * Velvet Chess Engine
3 | * Copyright (C) 2023 mhonert (https://github.com/mhonert)
4 | *
5 | * This program is free software: you can redistribute it and/or modify
6 | * it under the terms of the GNU General Public License as published by
7 | * the Free Software Foundation, either version 3 of the License, or
8 | * (at your option) any later version.
9 | *
10 | * This program is distributed in the hope that it will be useful,
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | * GNU General Public License for more details.
14 | *
15 | * You should have received a copy of the GNU General Public License
16 | * along with this program. If not, see .
17 | */
18 |
19 | use std::cmp::Ordering;
20 | use std::ffi::{c_uint, CString};
21 | use std::mem::transmute;
22 | use crate::bindings::{TB_BLESSED_LOSS, TB_CURSED_WIN, TB_DRAW, tb_free, tb_init, TB_LARGEST, TB_LOSS, TB_MAX_MOVES, tb_probe_root_impl, tb_probe_wdl_impl, TB_PROMOTES_BISHOP, TB_PROMOTES_KNIGHT, TB_PROMOTES_NONE, TB_PROMOTES_QUEEN, TB_PROMOTES_ROOK, TB_RESULT_FAILED, TB_RESULT_FROM_MASK, TB_RESULT_FROM_SHIFT, TB_RESULT_PROMOTES_MASK, TB_RESULT_PROMOTES_SHIFT, TB_RESULT_TO_MASK, TB_RESULT_TO_SHIFT, TB_RESULT_WDL_MASK, TB_RESULT_WDL_SHIFT, TB_WIN};
23 |
24 |
25 | #[repr(u32)]
26 | #[derive(Debug, Eq, PartialEq, Copy, Clone)]
27 | pub enum TBResult {
28 | Loss = TB_LOSS,
29 | BlessedLoss = TB_BLESSED_LOSS,
30 | Draw = TB_DRAW,
31 | CursedWin = TB_CURSED_WIN,
32 | Win = TB_WIN,
33 | }
34 |
35 | impl TBResult {
36 | pub fn from_result(result: u32) -> TBResult {
37 | unsafe { transmute((result & TB_RESULT_WDL_MASK) >> TB_RESULT_WDL_SHIFT) }
38 | }
39 | }
40 |
41 | pub fn extract_move(result: u32) -> (i8, i8, Promotion) {
42 | let from = (result & TB_RESULT_FROM_MASK) >> TB_RESULT_FROM_SHIFT;
43 | let to = (result & TB_RESULT_TO_MASK) >> TB_RESULT_TO_SHIFT;
44 | let promotion = Promotion::from((result & TB_RESULT_PROMOTES_MASK) >> TB_RESULT_PROMOTES_SHIFT);
45 | (from as i8, to as i8, promotion)
46 | }
47 |
48 | pub fn is_failed_result(result: u32) -> bool {
49 | result == TB_RESULT_FAILED
50 | }
51 |
52 | #[repr(u32)]
53 | pub enum Promotion {
54 | Queen = TB_PROMOTES_QUEEN,
55 | Rook = TB_PROMOTES_ROOK,
56 | Bishop = TB_PROMOTES_BISHOP,
57 | Knight = TB_PROMOTES_KNIGHT,
58 | None = TB_PROMOTES_NONE,
59 | }
60 |
61 | impl From for Promotion {
62 | fn from(value: u32) -> Self {
63 | unsafe { transmute(value) }
64 | }
65 | }
66 |
67 | impl TBResult {
68 | pub fn invert(&self) -> TBResult {
69 | match self {
70 | TBResult::Loss => TBResult::Win,
71 | TBResult::BlessedLoss => TBResult::CursedWin,
72 | TBResult::Draw => TBResult::Draw,
73 | TBResult::CursedWin => TBResult::BlessedLoss,
74 | TBResult::Win => TBResult::Loss,
75 | }
76 | }
77 |
78 | fn score(&self) -> i32 {
79 | match self {
80 | TBResult::Loss => -1000,
81 | TBResult::BlessedLoss => -1,
82 | TBResult::Draw => 0,
83 | TBResult::CursedWin => 1,
84 | TBResult::Win => 1000,
85 | }
86 | }
87 | }
88 |
89 | impl PartialOrd for TBResult {
90 | fn partial_cmp(&self, other: &Self) -> Option {
91 | self.score().partial_cmp(&other.score())
92 | }
93 | }
94 |
95 | pub fn init(path: String) -> bool {
96 | unsafe {
97 | let c_path = CString::new(path).unwrap_or_default();
98 | tb_init(c_path.as_ptr())
99 | }
100 | }
101 |
102 | pub fn free() {
103 | unsafe {
104 | tb_free()
105 | }
106 | }
107 |
108 | pub fn probe_wdl(white: u64, black: u64, kings: u64, queens: u64, rooks: u64, bishops: u64, knights: u64, pawns: u64, ep: u16, turn: bool) -> Option {
109 | unsafe {
110 | let result = tb_probe_wdl_impl(white, black, kings, queens, rooks, bishops, knights, pawns, ep as c_uint, turn);
111 | if !is_failed_result(result) {
112 | Some(transmute(result))
113 | } else {
114 | None
115 | }
116 | }
117 | }
118 |
119 | pub fn probe_root(white: u64, black: u64, kings: u64, queens: u64, rooks: u64, bishops: u64, knights: u64, pawns: u64, rule50: u8, ep: u16, turn: bool) -> (u32, Vec) {
120 | unsafe {
121 | let mut moves: Vec = Vec::with_capacity(TB_MAX_MOVES as usize);
122 |
123 | let result = tb_probe_root_impl(white, black, kings, queens, rooks, bishops, knights, pawns,
124 | rule50 as c_uint, ep as c_uint, turn, moves.spare_capacity_mut().as_mut_ptr().cast());
125 |
126 | if !is_failed_result(result) {
127 | moves.set_len(TB_MAX_MOVES as usize);
128 | let count = moves.iter().position(|&m| m == TB_RESULT_FAILED).unwrap_or_default();
129 | moves.set_len(count);
130 | }
131 |
132 | (result, moves)
133 | }
134 | }
135 |
136 | pub fn max_piece_count() -> u32 {
137 | unsafe { TB_LARGEST }
138 | }
--------------------------------------------------------------------------------
/genmagics/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "genmagics"
3 | version = "1.0.0"
4 | edition = "2021"
5 |
6 | [dependencies]
7 | velvet = { path = "../engine", default-features = false }
--------------------------------------------------------------------------------
/gensets/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "gensets"
3 | version = "2.0.0"
4 | edition = "2021"
5 |
6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
7 |
8 | [dependencies]
9 | velvet = { path = "../engine", default-features = false }
10 | fathomrs = { path = "../fathomrs" }
11 |
12 | anyhow = "1.0.76"
13 | clap = { version = "4.3.11", features = ["derive"] }
14 | ctrlc = "3.4.1"
15 | glob = "0.3.1"
--------------------------------------------------------------------------------
/gensets/src/writer.rs:
--------------------------------------------------------------------------------
1 | /*
2 | * Velvet Chess Engine
3 | * Copyright (C) 2023 mhonert (https://github.com/mhonert)
4 | *
5 | * This program is free software: you can redistribute it and/or modify
6 | * it under the terms of the GNU General Public License as published by
7 | * the Free Software Foundation, either version 3 of the License, or
8 | * (at your option) any later version.
9 | *
10 | * This program is distributed in the hope that it will be useful,
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | * GNU General Public License for more details.
14 | *
15 | * You should have received a copy of the GNU General Public License
16 | * along with this program. If not, see .
17 | */
18 |
19 | use std::fs;
20 | use std::fs::{File};
21 | use std::io::{BufWriter, Write};
22 | use std::path::Path;
23 | use std::str::FromStr;
24 | use std::sync::Arc;
25 | use std::sync::atomic::{AtomicUsize, Ordering};
26 | use glob::glob;
27 | use velvet::nn::io::{write_i16, write_u16, write_u32, write_u8};
28 |
29 | const VERSION: u8 = 1;
30 |
31 | pub struct NextIDSource(AtomicUsize);
32 |
33 | impl NextIDSource {
34 | pub fn new() -> Self {
35 | let output_dir = "./data/out/";
36 | fs::create_dir_all(output_dir).expect("could not create output folder");
37 |
38 | let mut max_num = 0;
39 | for entry in glob(&format!("{}/*.bin", &output_dir)).expect("could not glob fen files from output dir") {
40 | match entry {
41 | Ok(path) => {
42 | let file_name = path.file_name().unwrap().to_string_lossy().to_string();
43 | let num_str = file_name.strip_prefix("test_pos_").unwrap().strip_suffix(".bin").unwrap();
44 | let num = usize::from_str(num_str).expect("could not extract set count from file name");
45 | max_num = max_num.max(num);
46 | }
47 | Err(e) => {
48 | eprintln!("Error: {:?}", e)
49 | }
50 | }
51 | }
52 |
53 | println!("Next ID: {}", max_num + 1);
54 | NextIDSource(AtomicUsize::new(max_num + 1))
55 | }
56 | }
57 |
58 | pub struct OutputWriter {
59 | id_source: Arc,
60 | output_dir: String,
61 | pos_count: usize,
62 | writer: BufWriter,
63 | }
64 |
65 | impl OutputWriter {
66 | pub fn new(id_source: Arc) -> Self {
67 | let output_dir = "./data/out/";
68 |
69 | let next_set = id_source.0.fetch_add(1, Ordering::Relaxed);
70 | let writer = next_file(output_dir, next_set);
71 | OutputWriter{id_source, output_dir: String::from(output_dir), pos_count: 0, writer}
72 | }
73 |
74 | pub fn add(&mut self, fen: String, result: i16, moves: Vec) {
75 | write_u8(&mut self.writer, fen.len() as u8).expect("could not write FEN len");
76 | write!(&mut self.writer, "{}", fen).expect("could not write FEN");
77 | write_i16(&mut self.writer, result).expect("could not write result");
78 | write_u16(&mut self.writer, moves.len() as u16).expect("could not write move count");
79 | for m in moves.iter() {
80 | write_u32(&mut self.writer, *m).expect("could not write move");
81 | }
82 |
83 | self.pos_count += moves.len();
84 |
85 | if self.pos_count >= 200_000 {
86 | self.pos_count = 0;
87 | let next_set = self.id_source.0.fetch_add(1, Ordering::Relaxed);
88 | self.writer = next_file(&self.output_dir, next_set);
89 | }
90 | }
91 |
92 | pub fn terminate(&mut self) {
93 | self.writer.flush().expect("could not flush output");
94 | }
95 | }
96 |
97 | fn next_file(path: &str, set_nr: usize) -> BufWriter {
98 | let file_name = format!("{}/test_pos_{}.bin", path, set_nr);
99 | if Path::new(&file_name).exists() {
100 | panic!("Output file already exists: {}", file_name);
101 | }
102 | let file = File::create(&file_name).expect("Could not create output file");
103 | let mut w = BufWriter::new(file);
104 | write_u8(&mut w, VERSION).expect("could not write version");
105 |
106 | w
107 | }
108 |
--------------------------------------------------------------------------------
/gputrainer/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "gputrainer"
3 | version = "3.0.0"
4 | edition = "2021"
5 |
6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
7 |
8 | [dependencies]
9 | velvet = { path = "../engine", default-features = false }
10 | fathomrs = { path = "../fathomrs" }
11 | traincommon = { path = "../traincommon" }
12 |
13 | env_logger = "0.10.1"
14 | log = "0.4.20"
15 | rand = "0.8.5"
16 | tch = "0.14.0"
17 | clap = { version = "4.4.11", features = ["derive"] }
18 |
--------------------------------------------------------------------------------
/gputrainer/src/layer.rs:
--------------------------------------------------------------------------------
1 | /*
2 | * Velvet Chess Engine
3 | * Copyright (C) 2023 mhonert (https://github.com/mhonert)
4 | *
5 | * This program is free software: you can redistribute it and/or modify
6 | * it under the terms of the GNU General Public License as published by
7 | * the Free Software Foundation, either version 3 of the License, or
8 | * (at your option) any later version.
9 | *
10 | * This program is distributed in the hope that it will be useful,
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | * GNU General Public License for more details.
14 | *
15 | * You should have received a copy of the GNU General Public License
16 | * along with this program. If not, see .
17 | */
18 |
19 | use crate::{Tensor};
20 | use std::borrow::Borrow;
21 | use tch::{IndexOp};
22 | use tch::nn::{Path};
23 | use tch::nn::init::DEFAULT_KAIMING_UNIFORM;
24 | use velvet::nn::{INPUTS};
25 |
26 | pub struct InputLayer {
27 | pub weights: Tensor,
28 | pub own_biases: Tensor,
29 | pub opp_biases: Tensor,
30 | }
31 |
32 | pub fn input_layer<'a, T: Borrow>>(
33 | vs: T,
34 | input_count: i64,
35 | output_count: i64,
36 | ) -> InputLayer {
37 | let vs = vs.borrow();
38 | let own_biases = vs.zeros("own_bias", &[output_count]);
39 | let opp_biases = vs.zeros("opp_bias", &[output_count]);
40 | let weights = vs.var("weight", &[output_count, input_count], DEFAULT_KAIMING_UNIFORM);
41 |
42 | tch::no_grad(|| {
43 | let _ = weights.i((.., INPUTS as i64..)).zero_();
44 | });
45 |
46 | InputLayer { weights, own_biases, opp_biases }
47 | }
48 |
49 | impl InputLayer {
50 | pub fn forward(&self, white_xs: &Tensor, black_xs: &Tensor, stms: &Tensor) -> Tensor {
51 | let white = white_xs.matmul(&self.weights.tr());
52 | let black = black_xs.matmul(&self.weights.tr());
53 |
54 | stms * Tensor::cat(&[&white + &self.own_biases, &black + &self.opp_biases], 1)
55 | + (1i32 - stms) * Tensor::cat(&[&black + &self.own_biases, &white + &self.opp_biases], 1)
56 | }
57 |
58 | pub fn copy_from(&mut self, weights: &Tensor, own_biases: &Tensor, opp_biases: &Tensor) {
59 | tch::no_grad(|| {
60 | self.weights.copy_(weights);
61 | self.own_biases.copy_(own_biases);
62 | self.opp_biases.copy_(opp_biases);
63 | });
64 | }
65 | }
66 |
67 | pub struct OutputLayer {
68 | weights: Tensor,
69 | biases: Tensor,
70 | }
71 |
72 | /// Creates a new output layer.
73 | pub fn output_layer<'a, T: Borrow>>(
74 | vs: T,
75 | input_count: i64,
76 | ) -> OutputLayer {
77 | let vs = vs.borrow();
78 |
79 | let weights = vs.var("weight", &[1, input_count], DEFAULT_KAIMING_UNIFORM);
80 | let biases = vs.zeros("bias", &[1]);
81 |
82 | OutputLayer { weights, biases }
83 | }
84 |
85 | impl OutputLayer {
86 | pub fn forward(&self, xs: &Tensor) -> Tensor {
87 | xs.linear(&self.weights, Some(&self.biases))
88 | }
89 |
90 | pub fn copy_from(&mut self, weights: &Tensor, biases: &Tensor) {
91 | tch::no_grad(|| {
92 | self.weights.copy_(weights);
93 | self.biases.copy_(biases);
94 | });
95 | }
96 | }
97 |
--------------------------------------------------------------------------------
/gputrainer/src/sets.rs:
--------------------------------------------------------------------------------
1 | /*
2 | * Velvet Chess Engine
3 | * Copyright (C) 2023 mhonert (https://github.com/mhonert)
4 | *
5 | * This program is free software: you can redistribute it and/or modify
6 | * it under the terms of the GNU General Public License as published by
7 | * the Free Software Foundation, either version 3 of the License, or
8 | * (at your option) any later version.
9 | *
10 | * This program is distributed in the hope that it will be useful,
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | * GNU General Public License for more details.
14 | *
15 | * You should have received a copy of the GNU General Public License
16 | * along with this program. If not, see .
17 | */
18 |
19 | use traincommon::sets::DataSamples;
20 |
21 | #[derive(Clone, Debug)]
22 | pub struct DataSample {
23 | pub wpov_inputs: Vec,
24 | pub bpov_inputs: Vec,
25 | pub result: f32,
26 | pub wtm: i8,
27 | pub piece_bucket: i8,
28 | }
29 |
30 | impl Default for DataSample {
31 | fn default() -> Self {
32 | DataSample { wpov_inputs: Vec::with_capacity(32), bpov_inputs: Vec::with_capacity(32), result: 0.0, wtm: 0, piece_bucket: 0 }
33 | }
34 | }
35 |
36 | pub struct GpuDataSamples(pub Vec);
37 |
38 | impl DataSamples for GpuDataSamples {
39 | fn init(&mut self, idx: usize, result: f32, stm: u8) {
40 | let sample = &mut self.0[idx];
41 | sample.wpov_inputs.clear();
42 | sample.bpov_inputs.clear();
43 | sample.result = result;
44 | sample.wtm = 1 - stm as i8;
45 | }
46 |
47 | fn add_wpov(&mut self, idx: usize, pos: u16) {
48 | self.0[idx].wpov_inputs.push(pos as i64);
49 | }
50 |
51 | fn add_bpov(&mut self, idx: usize, pos: u16) {
52 | self.0[idx].bpov_inputs.push(pos as i64);
53 | }
54 |
55 | fn finalize(&mut self, idx: usize) {
56 | let sample = &mut self.0[idx];
57 | sample.wpov_inputs.sort();
58 | sample.bpov_inputs.sort();
59 | }
60 | }
--------------------------------------------------------------------------------
/logo/velvet_logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mhonert/velvet-chess/b0e6c8f87527dbf258d4401bd886ca07ee4bc5fc/logo/velvet_logo.png
--------------------------------------------------------------------------------
/pgo/merged_avx512_linux.profdata:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mhonert/velvet-chess/b0e6c8f87527dbf258d4401bd886ca07ee4bc5fc/pgo/merged_avx512_linux.profdata
--------------------------------------------------------------------------------
/pytools/patch-verifier/Pipfile:
--------------------------------------------------------------------------------
1 | [[source]]
2 | url = "https://pypi.org/simple"
3 | verify_ssl = true
4 | name = "pypi"
5 |
6 | [packages]
7 |
8 | [dev-packages]
9 |
10 | [requires]
11 | python_version = "3.9"
12 |
--------------------------------------------------------------------------------
/pytools/patch-verifier/Pipfile.lock:
--------------------------------------------------------------------------------
1 | {
2 | "_meta": {
3 | "hash": {
4 | "sha256": "a36a5392bb1e8bbc06bfaa0761e52593cf2d83b486696bf54667ba8da616c839"
5 | },
6 | "pipfile-spec": 6,
7 | "requires": {
8 | "python_version": "3.9"
9 | },
10 | "sources": [
11 | {
12 | "name": "pypi",
13 | "url": "https://pypi.org/simple",
14 | "verify_ssl": true
15 | }
16 | ]
17 | },
18 | "default": {},
19 | "develop": {}
20 | }
21 |
--------------------------------------------------------------------------------
/pytools/patch-verifier/client.py:
--------------------------------------------------------------------------------
1 | import argparse
2 | import datetime
3 | import glob
4 | import io
5 | import json
6 | import logging as log
7 | import os
8 | import shutil
9 | import subprocess
10 | import sys
11 | import tarfile
12 | from pathlib import Path
13 | from typing import List
14 |
15 | from common import (parse_final_results, parse_final_results_file, parse_ongoing_results)
16 |
17 | # patch-verifier must be called from the repository root using the 'verify-patch' shell script
18 | BASE_DIR = 'pytools/patch-verifier/tmp'
19 |
20 |
21 | STC = {
22 | 'tc': '40/8+0.08',
23 | 'low_elo': 0,
24 | 'high_elo': 5
25 | }
26 |
27 | LTC = {
28 | 'tc': '40/50+0.5',
29 | 'low_elo': 0,
30 | 'high_elo': 5
31 | }
32 |
33 | QUICKFIX = {
34 | 'tc': '40/5+0.05',
35 | 'low_elo': -3,
36 | 'high_elo': 1
37 | }
38 |
39 |
40 | def main():
41 | log.basicConfig(stream=sys.stdout, level=log.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
42 |
43 | parser = argparse.ArgumentParser(description="Automatic Patch Verifier")
44 | subparsers = parser.add_subparsers(help='sub-command help', dest='command')
45 |
46 | upload_parser = subparsers.add_parser('upload', help='Uploads a new patch to the server for verification')
47 | upload_parser.add_argument('description', type=str, help='Description of the patch. Will be included in the commit message')
48 |
49 | skips = upload_parser.add_mutually_exclusive_group()
50 | skips.add_argument('--skip-ltc', help='Skips long time control verification', action='store_true')
51 | skips.add_argument('--skip-stc', help='Skips short time control verification', action='store_true')
52 | skips.add_argument('--quickfix', help='Quick verification for refactorings or minor bug-fixes', action='store_true')
53 |
54 | subparsers.add_parser('report', help='Fetches all available verification results')
55 |
56 | args = parser.parse_args()
57 |
58 | if args.command == 'upload':
59 | upload(args.description, args.skip_stc, args.skip_ltc, args.quickfix)
60 | elif args.command == 'report':
61 | report()
62 | else:
63 | parser.print_usage()
64 |
65 |
66 | def upload(description: str, skip_stc: bool, skip_ltc: bool, quickfix: bool):
67 | if skip_stc:
68 | log.info("Skipping STC verification")
69 |
70 | if skip_ltc:
71 | log.info("Skipping LTC verification")
72 |
73 | Path('%s/patches' % BASE_DIR).mkdir(parents=True, exist_ok=True)
74 |
75 | log.info('Upload patch "%s" to server ...' % description)
76 |
77 | patch_id = datetime.datetime.utcnow().strftime("%Y%m%d%H%M%S")
78 | log.info('Generated patch ID: "%s"' % patch_id)
79 |
80 | patch_dir = '%s/patches/%s' % (BASE_DIR, patch_id)
81 |
82 | log.info('Preparing Git patch')
83 | os.mkdir(patch_dir)
84 |
85 | try:
86 | with open('%s/%s.patch' % (patch_dir, patch_id), 'w') as patch_file:
87 | subprocess.run(['git', 'diff'], stdout=patch_file, check=True)
88 |
89 | with open('%s/%s.txt' % (patch_dir, patch_id), 'w') as description_file:
90 | description_file.write(description)
91 |
92 | with open('%s/%s.json' % (patch_dir, patch_id), 'w') as config_file:
93 | json.dump(create_config(skip_stc, skip_ltc, quickfix), config_file)
94 |
95 | log.info('Uploading to server')
96 | subprocess.run(['scp -q %s/%s.* server:chess/patch-verifier/inbox/' % (patch_dir, patch_id)], shell=True, check=True)
97 |
98 | except subprocess.CalledProcessError:
99 | upload_error(patch_dir, 'patch upload failed')
100 |
101 | log.info('Upload successful')
102 |
103 |
104 | def create_config(skip_stc, skip_ltc, quickfix):
105 | config = {}
106 | if quickfix:
107 | config['stc'] = QUICKFIX # Only perform a short time control verification
108 | return config
109 |
110 | if not skip_stc:
111 | config['stc'] = STC
112 |
113 | if not skip_ltc:
114 | config['ltc'] = LTC
115 |
116 | return config
117 |
118 |
119 | def report():
120 | log.info('Verification report')
121 | sync()
122 |
123 | log.info('Fetching latest results ...')
124 | subprocess.run(["rsync -az --delete --exclude='*.pgn' server:chess/patch-verifier/{inbox,work,accepted,rejected} %s" % BASE_DIR], shell=True, check=True)
125 |
126 | create_report()
127 |
128 |
129 | def sync():
130 | shutil.rmtree('%s/work' % BASE_DIR, ignore_errors=True)
131 | Path('%s' % BASE_DIR).mkdir(parents=True, exist_ok=True)
132 |
133 |
134 | def create_report():
135 | inbox = find_patch_ids('%s/inbox/' % BASE_DIR, '.patch')
136 | in_progress = find_patch_ids('%s/work/' % BASE_DIR, '.STC.result')
137 | for patch_id in find_patch_ids('%s/work/' % BASE_DIR, '.LTC.result'):
138 | if patch_id not in in_progress:
139 | in_progress.append(patch_id)
140 |
141 | accepted = find_patch_ids('%s/accepted/' % BASE_DIR, '.tar.gz')[-10:]
142 | rejected = find_patch_ids('%s/rejected/' % BASE_DIR, '.tar.gz')[-10:]
143 |
144 | for patch_id in in_progress:
145 | inbox.remove(patch_id)
146 |
147 | hl()
148 | if len(inbox) > 0:
149 | report_inbox(inbox)
150 | else:
151 | log.info("No patches waiting in Inbox")
152 |
153 | if len(accepted) > 0:
154 | hl()
155 | report_finished('accepted', accepted)
156 |
157 | if len(rejected) > 0:
158 | hl()
159 | report_finished('rejected', rejected)
160 |
161 | if len(in_progress) > 0:
162 | hl()
163 | report_in_progress(in_progress)
164 |
165 |
166 | def report_inbox(ids: List[str]):
167 | log.info("Patches waiting in Inbox:")
168 |
169 | for patch_id in ids:
170 | description = Path('%s/inbox/%s.txt' % (BASE_DIR, patch_id)).read_text()
171 | log.info(" -> %s - %s" % (patch_id, description))
172 |
173 |
174 | def report_in_progress(ids: List[str]):
175 | log.info("Currently running verification:")
176 | for patch_id in ids:
177 | description = Path('%s/inbox/%s.txt' % (BASE_DIR, patch_id)).read_text()
178 | log.info(" - %s - %s" % (patch_id, description))
179 | log.info("")
180 |
181 | if Path('%s/work/%s.STC.result' % (BASE_DIR, patch_id)).exists():
182 | report_ongoing_result('STC', patch_id)
183 | if Path('%s/work/%s.LTC.result' % (BASE_DIR, patch_id)).exists():
184 | report_ongoing_result('LTC', patch_id)
185 | hl()
186 |
187 |
188 | def report_finished(result: str, ids: List[str]):
189 | log.info("%s patches:" % result.capitalize())
190 | for patch_id in ids:
191 | with tarfile.open('%s/%s/%s.tar.gz' % (BASE_DIR, result, patch_id)) as tar:
192 | descr_file = io.TextIOWrapper(tar.extractfile('%s.txt' % patch_id))
193 | description = descr_file.read()
194 | log.info(" - %s - %s" % (patch_id, description))
195 |
196 | try:
197 | stc_file = io.TextIOWrapper(tar.extractfile('%s.STC.result' % patch_id))
198 | (stc_results, _) = parse_final_results_file(stc_file)
199 | log.info(' > STC: %s' % stc_results[0].strip())
200 | except KeyError:
201 | pass
202 |
203 | try:
204 | ltc_file = io.TextIOWrapper(tar.extractfile('%s.LTC.result' % patch_id))
205 | (ltc_results, _) = parse_final_results_file(ltc_file)
206 | log.info(' > LTC: %s' % ltc_results[0].strip())
207 | except KeyError:
208 | pass
209 |
210 | log.info("")
211 |
212 |
213 | def report_final_result(mode: str, patch_id: str):
214 | (results, _) = parse_final_results('%s/work/%s.%s.result' % (BASE_DIR, patch_id, mode))
215 | log.info('%s Elo result : %s' % (mode, results[0].strip()))
216 | log.info('%s SPRT result: %s' % (mode, results[1].strip()))
217 |
218 |
219 | def report_ongoing_result(mode: str, patch_id: str):
220 | (elo, sprt) = parse_ongoing_results('%s/work/%s.%s.result' % (BASE_DIR, patch_id, mode))
221 | log.info('%s Elo result : %s' % (mode, elo))
222 | log.info('%s SPRT result: %s' % (mode, sprt))
223 |
224 |
225 | def find_patch_ids(folder: str, file_ending: str) -> List[str]:
226 | patch_ids = []
227 | for file in sorted(glob.glob('%s/*%s' % (folder, file_ending))):
228 | patch_id = file.lstrip(folder).rstrip(file_ending)
229 | patch_ids.append(patch_id)
230 | return patch_ids
231 |
232 |
233 | def hl():
234 | log.info("___________________________________________________________________________________________________")
235 | log.info("")
236 |
237 |
238 | def upload_error(patch_dir: str, msg: str):
239 | print(' - %s' % msg)
240 | shutil.rmtree(patch_dir)
241 | exit(-1)
242 |
243 |
244 | main()
245 |
--------------------------------------------------------------------------------
/pytools/patch-verifier/common.py:
--------------------------------------------------------------------------------
1 | import logging as log
2 | from typing import List
3 |
4 |
5 | def parse_final_results(file_name: str) -> (List[str], bool):
6 | with open(file_name, 'r') as result_file:
7 | return parse_final_results_file(result_file)
8 |
9 |
10 | def parse_final_results_file(result_file) -> (List[str], bool):
11 | lines = result_file.readlines()[-3:]
12 |
13 | if "Finished match" not in lines[-1]:
14 | log.info(lines)
15 | raise RuntimeError("cutechess result output does not contain 'Finished match' line")
16 |
17 | if "H1 was accepted" in lines[-2]:
18 | return lines[:-1], True
19 | elif "H0 was accepted" in lines[-2]:
20 | return lines[:-1], False
21 | else:
22 | return lines[:-1], False
23 |
24 |
25 | def parse_ongoing_results(file_name: str) -> (str, str):
26 | with open(file_name, 'r') as result_file:
27 | previous_line = ''
28 | for line in reversed(result_file.readlines()):
29 | if line.startswith('Elo difference:'):
30 | return line.lstrip('Elo difference: ').strip(), previous_line.lstrip('SPRT: ').strip()
31 | previous_line = line
32 |
33 | return '-', '-'
34 |
35 |
--------------------------------------------------------------------------------
/rustfmt.toml:
--------------------------------------------------------------------------------
1 | max_width = 120
2 | use_small_heuristics = "Max"
3 | fn_args_layout = "Compressed"
4 |
--------------------------------------------------------------------------------
/selfplay/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "selfplay"
3 | version = "1.0.0"
4 | edition = "2021"
5 |
6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
7 |
8 | [dependencies]
9 | velvet = { path = "../engine" }
10 |
11 | rand = "0.8.5"
12 |
--------------------------------------------------------------------------------
/selfplay/src/lib.rs:
--------------------------------------------------------------------------------
1 | /*
2 | * Velvet Chess Engine
3 | * Copyright (C) 2024 mhonert (https://github.com/mhonert)
4 | *
5 | * This program is free software: you can redistribute it and/or modify
6 | * it under the terms of the GNU General Public License as published by
7 | * the Free Software Foundation, either version 3 of the License, or
8 | * (at your option) any later version.
9 | *
10 | * This program is distributed in the hope that it will be useful,
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | * GNU General Public License for more details.
14 | *
15 | * You should have received a copy of the GNU General Public License
16 | * along with this program. If not, see .
17 | */
18 |
19 | pub mod openings;
20 | pub mod pentanomial;
21 | pub mod selfplay;
--------------------------------------------------------------------------------
/selfplay/src/openings.rs:
--------------------------------------------------------------------------------
1 | /*
2 | * Velvet Chess Engine
3 | * Copyright (C) 2024 mhonert (https://github.com/mhonert)
4 | *
5 | * This program is free software: you can redistribute it and/or modify
6 | * it under the terms of the GNU General Public License as published by
7 | * the Free Software Foundation, either version 3 of the License, or
8 | * (at your option) any later version.
9 | *
10 | * This program is distributed in the hope that it will be useful,
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | * GNU General Public License for more details.
14 | *
15 | * You should have received a copy of the GNU General Public License
16 | * along with this program. If not, see .
17 | */
18 | use std::fs::File;
19 | use std::io::{BufRead, BufReader};
20 | use rand::prelude::ThreadRng;
21 | use rand::Rng;
22 |
23 | pub struct OpeningBook(Vec);
24 |
25 | impl OpeningBook {
26 | pub fn new(file_name: &str) -> OpeningBook {
27 | let file = File::open(file_name).expect("Could not open book file");
28 | let mut reader = BufReader::new(file);
29 |
30 | let mut openings = Vec::new();
31 |
32 | loop {
33 | let mut line = String::new();
34 |
35 | match reader.read_line(&mut line) {
36 | Ok(read) => if read == 0 {
37 | return OpeningBook(openings);
38 | },
39 |
40 | Err(e) => panic!("could not read line from opening book: {}", e)
41 | };
42 |
43 | openings.push(line.trim().to_string());
44 | }
45 | }
46 |
47 | // Get a random opening from the book
48 | pub fn get_random(&self) -> String {
49 | let mut rng = ThreadRng::default();
50 | let idx = rng.gen_range(0..self.0.len());
51 | self.0[idx].clone()
52 | }
53 | }
54 |
55 |
--------------------------------------------------------------------------------
/selfplay/src/pentanomial.rs:
--------------------------------------------------------------------------------
1 | /*
2 | * Velvet Chess Engine
3 | * Copyright (C) 2024 mhonert (https://github.com/mhonert)
4 | *
5 | * This program is free software: you can redistribute it and/or modify
6 | * it under the terms of the GNU General Public License as published by
7 | * the Free Software Foundation, either version 3 of the License, or
8 | * (at your option) any later version.
9 | *
10 | * This program is distributed in the hope that it will be useful,
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | * GNU General Public License for more details.
14 | *
15 | * You should have received a copy of the GNU General Public License
16 | * along with this program. If not, see .
17 | */
18 | use crate::selfplay::Outcome;
19 |
20 | #[derive(Copy, Clone, Default, Debug)]
21 | pub struct PentanomialCount {
22 | pub ll: usize, // Loss-Loss
23 | pub ld: usize, // Loss-Draw or Draw-Loss
24 | pub d2: usize, // Draw-Draw or Win-Loss/Loss-Win
25 | pub wd: usize, // Win-Draw or Draw-Win
26 | pub ww: usize, // Win-Win
27 | }
28 |
29 | impl PentanomialCount {
30 | pub fn add(&mut self, game_pair: (Outcome, Outcome)) {
31 | match game_pair {
32 | (Outcome::Win, Outcome::Win) => self.ww += 1,
33 | (Outcome::Win, Outcome::Loss) => self.d2 += 1,
34 | (Outcome::Win, Outcome::Draw) => self.wd += 1,
35 | (Outcome::Loss, Outcome::Win) => self.d2 += 1,
36 | (Outcome::Loss, Outcome::Loss) => self.ll += 1,
37 | (Outcome::Loss, Outcome::Draw) => self.ld += 1,
38 | (Outcome::Draw, Outcome::Win) => self.wd += 1,
39 | (Outcome::Draw, Outcome::Loss) => self.ld += 1,
40 | (Outcome::Draw, Outcome::Draw) => self.d2 += 1,
41 | }
42 | }
43 |
44 | pub fn clear(&mut self) {
45 | self.ll = 0;
46 | self.ld = 0;
47 | self.d2 = 0;
48 | self.wd = 0;
49 | self.ww = 0;
50 | }
51 |
52 | pub fn add_all(&mut self, counts: PentanomialCount) {
53 | self.ll += counts.ll;
54 | self.ld += counts.ld;
55 | self.d2 += counts.d2;
56 | self.wd += counts.wd;
57 | self.ww += counts.ww;
58 | }
59 |
60 | pub fn score(&self) -> f64 {
61 | self.ld as f64 * 0.25 + self.d2 as f64 * 0.5 + self.wd as f64 * 0.75 + self.ww as f64
62 | }
63 |
64 | pub fn total(&self) -> usize {
65 | self.ll + self.ld + self.d2 + self.wd + self.ww
66 | }
67 |
68 | pub fn gradient(&self) -> f64 {
69 | let total = self.total() as f64;
70 | (self.score() / total - 0.5) * 2.0
71 | }
72 | }
73 |
74 |
75 | #[derive(Default)]
76 | pub struct PentanomialModel {
77 | pub ll: f64, // Loss-Loss
78 | pub ld: f64, // Loss-Draw or Draw-Loss
79 | pub d2: f64, // Draw-Draw or Win-Loss/Loss-Win
80 | pub wd: f64, // Win-Draw or Draw-Win
81 | pub ww: f64, // Win-Win
82 | }
83 |
84 | impl PentanomialModel {
85 | pub fn score(&self) -> f64 {
86 | self.ld * 0.25 + self.d2 * 0.5 + self.wd * 0.75 + self.ww
87 | }
88 |
89 | pub fn deviation(&self, score: f64) -> PentanomialModel {
90 | PentanomialModel {
91 | ll: self.ll * (0.0 - score).powi(2),
92 | ld: self.ld * (0.25 - score).powi(2),
93 | d2: self.d2 * (0.5 - score).powi(2),
94 | wd: self.wd * (0.75 - score).powi(2),
95 | ww: self.ww * (1.0 - score).powi(2),
96 | }
97 | }
98 |
99 | pub fn total(&self) -> f64 {
100 | self.ll + self.ld + self.d2 + self.wd + self.ww
101 | }
102 | }
103 |
104 | impl From for PentanomialModel {
105 | fn from(counts: PentanomialCount) -> Self {
106 | let total = counts.total() as f64;
107 | PentanomialModel {
108 | ll: counts.ll as f64 / total,
109 | ld: counts.ld as f64 / total,
110 | d2: counts.d2 as f64 / total,
111 | wd: counts.wd as f64 / total,
112 | ww: counts.ww as f64 / total,
113 | }
114 | }
115 | }
--------------------------------------------------------------------------------
/selfplay/src/selfplay.rs:
--------------------------------------------------------------------------------
1 | /*
2 | * Velvet Chess Engine
3 | * Copyright (C) 2024 mhonert (https://github.com/mhonert)
4 | *
5 | * This program is free software: you can redistribute it and/or modify
6 | * it under the terms of the GNU General Public License as published by
7 | * the Free Software Foundation, either version 3 of the License, or
8 | * (at your option) any later version.
9 | *
10 | * This program is distributed in the hope that it will be useful,
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | * GNU General Public License for more details.
14 | *
15 | * You should have received a copy of the GNU General Public License
16 | * along with this program. If not, see .
17 | */
18 | use std::sync::{mpsc, Arc};
19 | use std::sync::atomic::{AtomicBool, AtomicU64};
20 | use std::sync::mpsc::{Receiver, Sender};
21 | use std::time::Instant;
22 | use velvet::board::Board;
23 | use velvet::engine::{LogLevel, Message};
24 | use velvet::fen::{create_from_fen, START_POS};
25 | use velvet::moves::{Move, NO_MOVE};
26 | use velvet::search::Search;
27 | use velvet::time_management::SearchLimits;
28 | use velvet::transposition_table::TranspositionTable;
29 |
30 | pub struct SearchControl {
31 | search: Search,
32 | limits: SearchLimits,
33 | time: i32,
34 | inc: i32,
35 | _tx: Sender,
36 | rx: Receiver,
37 | depths: usize,
38 | depth_count: usize,
39 | }
40 |
41 | impl SearchControl {
42 | pub fn new(time: i32, inc: i32) -> SearchControl {
43 | let mut board = create_from_fen(START_POS);
44 | board.reset_nn_eval();
45 |
46 | let (tx, rx) = mpsc::channel::();
47 |
48 | let search = Search::new(
49 | Arc::new(AtomicBool::new(true)),
50 | Arc::new(AtomicU64::new(0)),
51 | Arc::new(AtomicU64::new(0)),
52 | LogLevel::Error,
53 | SearchLimits::default(),
54 | TranspositionTable::new(16),
55 | board.clone(),
56 | false,
57 | );
58 |
59 | let limits = SearchLimits::new(None, None, Some(time), Some(inc), Some(inc), Some(inc), None, None, None).expect("Invalid search limits");
60 |
61 | SearchControl {
62 | search,
63 | limits,
64 | time,
65 | inc,
66 | _tx: tx,
67 | rx,
68 | depths: 0,
69 | depth_count: 0,
70 | }
71 | }
72 |
73 | pub fn reset(&mut self) {
74 | self.depths = 0;
75 | self.depth_count = 0;
76 | }
77 |
78 | pub fn new_game(&mut self, board: &Board, time: i32, inc: i32) {
79 | self.search.clear_tt();
80 | self.search.update(board, self.limits, false);
81 | self.time = time;
82 | self.inc = inc;
83 | self.depths = 0;
84 | self.depth_count = 0;
85 | }
86 |
87 | pub fn set_params(&mut self, params: &[(String, i16)]) {
88 | for (name, value) in params.iter() {
89 | if !self.search.set_param(name, *value) {
90 | panic!("Invalid feature option: {}", name);
91 | }
92 | }
93 | }
94 |
95 | // Returns the best move and a boolean indicating if the time was exceeded
96 | pub fn next_move(&mut self) -> (Move, bool) {
97 | let active_player = self.search.board.active_player();
98 |
99 | self.limits = SearchLimits::new(None, None, Some(self.time), Some(self.time), Some(self.inc), Some(self.inc), None, None, None).expect("Invalid search limits");
100 | self.limits.update(active_player, 20);
101 |
102 | self.search.update_limits(self.limits);
103 | let start = Instant::now();
104 | let (bm, _) = self.search.find_best_move_with_full_strength(Some(&self.rx), &[]);
105 | let duration = start.elapsed().as_millis() as i32;
106 | self.time -= duration;
107 | let time_loss = self.time < 0;
108 | self.time += self.inc;
109 |
110 | if self.search.max_reached_depth > 0 {
111 | self.depth_count += 1;
112 | self.depths += self.search.max_reached_depth;
113 | }
114 |
115 | (bm, time_loss)
116 | }
117 |
118 | pub fn perform_move(&mut self, m: Move) {
119 | self.search.board.perform_move(m);
120 | }
121 |
122 | pub fn avg_depth(&self) -> usize {
123 | if self.depth_count == 0 {
124 | return 0;
125 | }
126 | self.depths / self.depth_count
127 | }
128 | }
129 |
130 | #[derive(Clone, Copy, Debug)]
131 | pub enum Outcome {
132 | Win,
133 | Loss,
134 | Draw,
135 | }
136 |
137 | impl Outcome {
138 | pub fn invert(&self) -> Outcome {
139 | match self {
140 | Outcome::Win => Outcome::Loss,
141 | Outcome::Loss => Outcome::Win,
142 | Outcome::Draw => Outcome::Draw,
143 | }
144 | }
145 | }
146 |
147 | pub fn play_match(board: &mut Board, engines: &mut [&mut SearchControl; 2], time: i32, inc: i32) -> (Outcome, usize) {
148 | let mut time_losses = 0;
149 | loop {
150 | engines[0].new_game(board, time, inc);
151 | engines[1].new_game(board, time, inc);
152 |
153 | let mut i = 0;
154 | loop {
155 | let (bm, time_loss) = engines[i].next_move();
156 | if bm == NO_MOVE || time_loss {
157 | if time_loss {
158 | time_losses += 1;
159 | break;
160 | }
161 | return (if i == 0 { Outcome::Loss } else { Outcome::Win }, time_losses);
162 | }
163 |
164 | board.perform_move(bm);
165 | if board.is_insufficient_material_draw() || board.is_repetition_draw() || board.is_fifty_move_draw() {
166 | return (Outcome::Draw, time_losses);
167 | }
168 |
169 | engines[i].perform_move(bm);
170 | i = (i + 1) % 2;
171 | engines[i].perform_move(bm);
172 | }
173 | }
174 | }
175 |
176 |
--------------------------------------------------------------------------------
/sprt/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "sprt"
3 | version = "1.0.0"
4 | edition = "2021"
5 |
6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
7 |
8 | [dependencies]
9 | velvet = { path = "../engine" }
10 | selfplay = { path = "../selfplay" }
11 |
12 | rand = "0.8.5"
13 | core_affinity = "0.8.1"
14 | libm = "0.2.8"
15 | thread-priority = "1.1.0"
16 |
--------------------------------------------------------------------------------
/sprt/src/main.rs:
--------------------------------------------------------------------------------
1 | /*
2 | * Velvet Chess Engine
3 | * Copyright (C) 2025 mhonert (https://github.com/mhonert)
4 | *
5 | * This program is free software: you can redistribute it and/or modify
6 | * it under the terms of the GNU General Public License as published by
7 | * the Free Software Foundation, either version 3 of the License, or
8 | * (at your option) any later version.
9 | *
10 | * This program is distributed in the hope that it will be useful,
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | * GNU General Public License for more details.
14 | *
15 | * You should have received a copy of the GNU General Public License
16 | * along with this program. If not, see .
17 | */
18 | mod sprt;
19 |
20 | use std::env::args;
21 | use std::sync::{Arc};
22 | use core_affinity::CoreId;
23 | use thread_priority::*;
24 | use selfplay::openings::OpeningBook;
25 | use selfplay::pentanomial::PentanomialCount;
26 | use selfplay::selfplay::{play_match, SearchControl};
27 | use velvet::fen::{create_from_fen, read_fen, START_POS};
28 | use velvet::init::init;
29 | use crate::sprt::SprtState;
30 |
31 | const PAIRS: usize = 8;
32 |
33 | const SPRT_ELO_BOUNDS: (f64, f64) = (0.0, 3.0);
34 |
35 | const SPRT_ALPHA: f64 = 0.05;
36 | const SPRT_BETA: f64 = 0.05;
37 |
38 | fn main() {
39 | if args().len() < 3 {
40 | println!("Usage: sprt ");
41 | return;
42 | }
43 |
44 | init();
45 |
46 | let book_file = args().nth(1).expect("No book file parameter provided");
47 | let time = args().nth(2).expect("No base time parameter provided").parse::().expect("Invalid base time parameter") * 1000;
48 | let inc = time / 100;
49 |
50 | let params: Vec<(String, i16, i16)> = args().skip(3).map(|f| {
51 | let parts: Vec<&str> = f.split(',').collect();
52 | (parts[0].to_string(), parts[1].parse::().expect("Invalid parameter value A"), parts[2].parse::().expect("Invalid parameter value B"))
53 | }).collect();
54 |
55 | println!("Velvet SPRT Tool");
56 | println!(" - Testing Elo gain using TC {}+{} for params {:?} ", time as f64 / 1000.0, inc as f64 / 1000.0, params);
57 |
58 | let mut reserved_core_ids = Vec::new();
59 | let mut core_ids = core_affinity::get_core_ids().expect("Could not retrieve CPU core IDs");
60 |
61 | core_ids.sort();
62 |
63 | // Keep one "full" CPU core free
64 | // Assumes that HT is enabled and that there are two logical CPU cores per physical CPU core
65 | // (core_affinity library currently does not return the CPU core type)
66 | reserved_core_ids.push(core_ids.remove(0));
67 | reserved_core_ids.push(core_ids.remove(core_ids.len() / 2 + 1));
68 |
69 | println!(" - Using {} CPU cores", core_ids.len());
70 |
71 | // assert!(core_affinity::set_for_current(reserved_core_ids[0]), "could not set CPU core affinity");
72 |
73 | let openings = Arc::new(OpeningBook::new(&book_file));
74 |
75 | let state = Arc::new(SprtState::new(SPRT_ELO_BOUNDS.0, SPRT_ELO_BOUNDS.1));
76 |
77 | let handles: Vec<_> = core_ids.into_iter().map(|id| {
78 | let thread_state = state.clone();
79 | let thread_openings = openings.clone();
80 | let thread_params = params.clone();
81 |
82 | ThreadBuilder::default()
83 | .name(format!("Worker {:?}", id))
84 | .priority(ThreadPriority::Max)
85 | .spawn(move |result| {
86 | if let Err(e) = result {
87 | eprintln!("Could not set thread priority for worker thread running on {:?}: {}", id, e);
88 | }
89 | run_thread(id, thread_state.clone(), thread_openings.clone(), thread_params.clone(), time, inc);
90 | })
91 | .expect("could not spawn thread")
92 | }).collect();
93 |
94 | for handle in handles.into_iter() {
95 | handle.join().expect("could not join threads");
96 | }
97 | }
98 |
99 | fn run_thread(id: CoreId, state: Arc, openings: Arc, params: Vec<(String, i16, i16)>, time: i32, inc: i32) {
100 | if !core_affinity::set_for_current(id) {
101 | eprintln!("Could not set CPU core affinity for worker thread running on {:?}", id);
102 | }
103 |
104 | let mut engine_a = SearchControl::new(time, inc);
105 | let mut engine_b = SearchControl::new(time, inc);
106 |
107 | let mut board = create_from_fen(START_POS);
108 |
109 | let params_a = params.iter().map(|(name, a, _)| (name.clone(), *a)).collect::>();
110 | let params_b = params.iter().map(|(name, _, b)| (name.clone(), *b)).collect::>();
111 |
112 | engine_a.set_params(¶ms_a);
113 | engine_b.set_params(¶ms_b);
114 |
115 | while !state.stopped() {
116 | engine_a.reset();
117 | engine_b.reset();
118 |
119 | let mut p = PentanomialCount::default();
120 | let mut time_losses = 0;
121 |
122 | for _ in 0..PAIRS {
123 | let opening = openings.get_random();
124 |
125 | read_fen(&mut board, &opening).expect("Could not read FEN");
126 | let (first_result, add_time_losses) = play_match(&mut board, &mut [&mut engine_a, &mut engine_b], time, inc);
127 | if state.stopped() {
128 | return;
129 | }
130 | time_losses += add_time_losses;
131 |
132 | read_fen(&mut board, &opening).expect("Could not read FEN");
133 | let (second_result, add_time_losses) = play_match(&mut board, &mut [&mut engine_b, &mut engine_a], time, inc);
134 | if state.stopped() {
135 | return;
136 | }
137 | time_losses += add_time_losses;
138 |
139 | p.add((first_result, second_result.invert()));
140 | }
141 |
142 | let avg_depth = (engine_a.avg_depth() + engine_b.avg_depth()) / 2;
143 | state.update(&p, avg_depth, time_losses);
144 | }
145 | }
146 |
--------------------------------------------------------------------------------
/sprt/src/sprt.rs:
--------------------------------------------------------------------------------
1 | /*
2 | * Velvet Chess Engine
3 | * Copyright (C) 2024 mhonert (https://github.com/mhonert)
4 | *
5 | * This program is free software: you can redistribute it and/or modify
6 | * it under the terms of the GNU General Public License as published by
7 | * the Free Software Foundation, either version 3 of the License, or
8 | * (at your option) any later version.
9 | *
10 | * This program is distributed in the hope that it will be useful,
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | * GNU General Public License for more details.
14 | *
15 | * You should have received a copy of the GNU General Public License
16 | * along with this program. If not, see .
17 | */
18 | use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
19 | use selfplay::pentanomial::{PentanomialCount, PentanomialModel};
20 | use crate::{SPRT_ALPHA, SPRT_BETA};
21 |
22 | #[derive(Default)]
23 | pub struct SprtState {
24 | ll: AtomicUsize,
25 | ld: AtomicUsize,
26 | d2: AtomicUsize,
27 | wd: AtomicUsize,
28 | ww: AtomicUsize,
29 | avg_depths: AtomicUsize,
30 | depth_counts: AtomicUsize,
31 | time_losses: AtomicUsize,
32 | stopped: AtomicBool,
33 | elo0: f64,
34 | elo1: f64,
35 | upper_bound: f64,
36 | lower_bound: f64,
37 | }
38 |
39 | const STD_NORM_DEV_95: f64 = 1.959963984540054;
40 |
41 | impl SprtState {
42 | pub fn new(elo0: f64, elo1: f64) -> SprtState {
43 | SprtState {
44 | ll: AtomicUsize::new(0),
45 | ld: AtomicUsize::new(0),
46 | d2: AtomicUsize::new(0),
47 | wd: AtomicUsize::new(0),
48 | ww: AtomicUsize::new(0),
49 | avg_depths: AtomicUsize::new(0),
50 | depth_counts: AtomicUsize::new(0),
51 | time_losses: AtomicUsize::new(0),
52 | stopped: AtomicBool::new(false),
53 | elo0,
54 | elo1,
55 | upper_bound: ((1.0 - SPRT_BETA) / SPRT_ALPHA).ln(),
56 | lower_bound: (SPRT_BETA / (1.0 - SPRT_ALPHA)).ln(),
57 | }
58 | }
59 |
60 | pub fn update(&self, counts: &PentanomialCount, add_avg_depth: usize, time_losses: usize) -> bool {
61 | if self.stopped() {
62 | return true;
63 | }
64 |
65 | let total_time_losses = self.time_losses.fetch_add(time_losses, Ordering::Relaxed) + time_losses;
66 |
67 | let total_ll = self.ll.fetch_add(counts.ll, Ordering::Relaxed) + counts.ll;
68 | let total_ld = self.ld.fetch_add(counts.ld, Ordering::Relaxed) + counts.ld;
69 | let total_d2 = self.d2.fetch_add(counts.d2, Ordering::Relaxed) + counts.d2;
70 | let total_wd = self.wd.fetch_add(counts.wd, Ordering::Relaxed) + counts.wd;
71 | let total_ww = self.ww.fetch_add(counts.ww, Ordering::Relaxed) + counts.ww;
72 |
73 | let total = total_ll + total_ld + total_d2 + total_wd + total_ww;
74 |
75 | let p = PentanomialModel {
76 | ll: total_ll as f64 / total as f64,
77 | ld: total_ld as f64 / total as f64,
78 | d2: total_d2 as f64 / total as f64,
79 | wd: total_wd as f64 / total as f64,
80 | ww: total_ww as f64 / total as f64,
81 | };
82 |
83 | let score = p.score();
84 | let deviation = p.deviation(score);
85 | let variance = deviation.total();
86 | let pair_variance = variance / total as f64;
87 |
88 | let upper_bound = score + STD_NORM_DEV_95 * pair_variance.sqrt();
89 | let lower_bound = score - STD_NORM_DEV_95 * pair_variance.sqrt();
90 |
91 | let draw_ratio = p.d2;
92 |
93 | let elo_diff = score_to_norm_elo(score, variance);
94 | let elo_error = (score_to_norm_elo(upper_bound, variance) - score_to_norm_elo(lower_bound, variance)) / 2.0;
95 |
96 | let llr = self.llr(total as f64, variance, &p);
97 |
98 | let avg_depths = self.avg_depths.fetch_add(add_avg_depth, Ordering::Relaxed) + add_avg_depth;
99 | let depth_counts = self.depth_counts.fetch_add(1, Ordering::Relaxed) + 1;
100 | let avg_depth = avg_depths / depth_counts;
101 |
102 | println!("Norm. Elo: {:>6.2} (+/- {:>5.2}) / Draw ratio: {:>5.2}% / Avg. depth {:2} / Time losses: {:>5.3}% / Game pairs: {} / LLR: {:>5.2}",
103 | elo_diff, elo_error, draw_ratio * 100.0, avg_depth, ((total_time_losses * 100000) as f64 / total as f64) / 1000.0, total, llr);
104 |
105 | let mut stopped = false;
106 | if llr >= self.upper_bound {
107 | println!("----------------------------------------------");
108 | println!("> SPRT: Stopped (upper bound) -> H0 accepted ->");
109 | stopped = true;
110 | } else if llr <= self.lower_bound {
111 | println!("----------------------------------------------");
112 | println!("> SPRT: Stopped (lower bound) -> H1 accepted");
113 | stopped = true;
114 | }
115 |
116 | if stopped {
117 | self.stopped.store(stopped, Ordering::Relaxed);
118 | }
119 | stopped
120 | }
121 |
122 | fn llr(&self, total: f64, variance: f64, p: &PentanomialModel) -> f64 {
123 | if variance == 0.0 {
124 | return 0.0;
125 | }
126 | let score0 = norm_elo_to_score(self.elo0, variance);
127 | let score1 = norm_elo_to_score(self.elo1, variance);
128 |
129 | let deviation0 = p.deviation(score0);
130 | let variance0 = deviation0.total();
131 |
132 | let deviation1 = p.deviation(score1);
133 | let variance1 = deviation1.total();
134 |
135 | if variance0 == 0.0 || variance1 == 0.0 {
136 | return 0.0;
137 | }
138 |
139 | total * 0.5 * (variance0 / variance1).ln()
140 | }
141 |
142 | pub fn stopped(&self) -> bool {
143 | self.stopped.load(Ordering::Relaxed)
144 | }
145 | }
146 |
147 | fn score_to_norm_elo(score: f64, variance: f64) -> f64 {
148 | (score - 0.5) / (variance * 2.0).sqrt() * (400.0 / 10.0f64.ln())
149 | }
150 |
151 | fn norm_elo_to_score(nelo: f64, variance: f64) -> f64 {
152 | nelo * variance.sqrt() / (400.0 / 10.0f64.ln()) + 0.5
153 | }
154 |
--------------------------------------------------------------------------------
/tournament/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "tournament"
3 | version = "1.0.0"
4 | edition = "2021"
5 |
6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
7 |
8 | [dependencies]
9 | velvet = { path = "../engine" }
10 | selfplay = { path = "../selfplay" }
11 |
12 | rand = "0.8.5"
13 | core_affinity = "0.8.1"
14 | thread-priority = "1.1.0"
15 | anyhow = "1.0.89"
16 | serde = { version = "1.0.210", features = ["derive"] }
17 | toml = "0.8.19"
18 | chrono = "0.4.38"
19 |
20 | [target.'cfg(target_os = "linux")'.dependencies]
21 | libc = "0.2.159"
--------------------------------------------------------------------------------
/tournament/src/affinity.rs:
--------------------------------------------------------------------------------
1 | /*
2 | * Velvet Chess Engine
3 | * Copyright (C) 2024 mhonert (https://github.com/mhonert)
4 | *
5 | * This program is free software: you can redistribute it and/or modify
6 | * it under the terms of the GNU General Public License as published by
7 | * the Free Software Foundation, either version 3 of the License, or
8 | * (at your option) any later version.
9 | *
10 | * This program is distributed in the hope that it will be useful,
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | * GNU General Public License for more details.
14 | *
15 | * You should have received a copy of the GNU General Public License
16 | * along with this program. If not, see .
17 | */
18 | use core_affinity::CoreId;
19 |
20 |
21 | #[cfg(target_os = "linux")]
22 | pub fn pin_thread(cores: &[CoreId]) -> anyhow::Result<()> {
23 | if cores.is_empty() {
24 | return Ok(());
25 | }
26 | unsafe {
27 | let mut cpuset: libc::cpu_set_t = std::mem::zeroed();
28 | libc::CPU_ZERO(&mut cpuset);
29 | for &core in cores {
30 | libc::CPU_SET(core.id, &mut cpuset);
31 | }
32 |
33 | let result = libc::sched_setaffinity(0, size_of::(), &cpuset);
34 | if result != 0 {
35 | return Err(anyhow::anyhow!("Failed to set CPU affinity: {}", result));
36 | }
37 |
38 | Ok(())
39 | }
40 | }
41 |
42 | #[cfg(not(target_os = "linux"))]
43 | pub fn pin_thread(cores: &[CoreId]) -> anyhow::Result<()> {
44 | if cores.is_empty() {
45 | return Ok(());
46 | }
47 | if cores.len() > 1 {
48 | println!("Warning: CPU pinning to multiple cores is not supported on this platform, ignoring");
49 | return Ok(());
50 | }
51 |
52 | core_affinity::set_for_current(cores[0]);
53 | }
54 |
--------------------------------------------------------------------------------
/tournament/src/config.rs:
--------------------------------------------------------------------------------
1 | /*
2 | * Velvet Chess Engine
3 | * Copyright (C) 2024 mhonert (https://github.com/mhonert)
4 | *
5 | * This program is free software: you can redistribute it and/or modify
6 | * it under the terms of the GNU General Public License as published by
7 | * the Free Software Foundation, either version 3 of the License, or
8 | * (at your option) any later version.
9 | *
10 | * This program is distributed in the hope that it will be useful,
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | * GNU General Public License for more details.
14 | *
15 | * You should have received a copy of the GNU General Public License
16 | * along with this program. If not, see .
17 | */
18 | use std::collections::HashMap;
19 | use serde::Deserialize;
20 |
21 | #[derive(Clone, Debug, Deserialize)]
22 | pub struct EngineConfigs(pub HashMap);
23 |
24 | impl EngineConfigs {
25 | pub fn merge_default_options(&mut self, default_options: &HashMap) {
26 | self.0.iter_mut().for_each(|(_, e)| {
27 | e.merge_default_options(default_options);
28 | });
29 | }
30 | }
31 |
32 | #[derive(Clone, Debug, Deserialize)]
33 | pub struct EngineConfig {
34 | #[serde(skip)]
35 | pub id: u32,
36 |
37 | #[serde(skip)]
38 | pub name: String,
39 |
40 | pub cmd: String,
41 |
42 | #[serde(default)]
43 | pub options: HashMap,
44 |
45 | #[serde(default)]
46 | pub init_commands: Vec,
47 | }
48 |
49 | impl EngineConfig {
50 | pub fn merge_default_options(&mut self, default_options: &HashMap) {
51 | for (opt, val) in default_options.iter() {
52 | if !self.options.contains_key(opt) {
53 | self.options.insert(opt.clone(), val.clone());
54 | }
55 | }
56 | }
57 | }
58 |
59 | pub fn read_engine_configs(file_path: &String) -> anyhow::Result {
60 | let config_str = std::fs::read_to_string(file_path)?;
61 | let mut config: EngineConfigs = toml::from_str(&config_str)?;
62 |
63 | config.0.iter_mut().enumerate().for_each(|(i, (name, v))| {
64 | v.id = i as u32 + 1;
65 | v.name = name.clone();
66 | });
67 | Ok(config)
68 | }
69 |
70 | #[derive(Clone, Debug, Deserialize)]
71 | pub struct TournamentConfig {
72 | pub tc: f32,
73 |
74 | #[serde(skip)]
75 | pub inc: f32,
76 |
77 | #[serde(default)]
78 | pub engine_threads: u32,
79 |
80 | pub book: String,
81 | pub engines: String,
82 | pub challenger: String,
83 | pub opponents: Vec,
84 |
85 | #[serde(default)]
86 | pub default_options: HashMap,
87 | }
88 |
89 | pub fn read_tournament_config(file_path: String) -> anyhow::Result {
90 | let config_str = std::fs::read_to_string(file_path)?;
91 | let mut config: TournamentConfig = toml::from_str(&config_str)?;
92 |
93 | config.engine_threads = config.engine_threads.max(1);
94 | if config.engine_threads > 1 {
95 | config.default_options.insert("Threads".to_string(), config.engine_threads.to_string());
96 | }
97 |
98 | config.inc = config.tc / 100.0;
99 | Ok(config)
100 | }
--------------------------------------------------------------------------------
/tournament/src/pgn.rs:
--------------------------------------------------------------------------------
1 | /*
2 | * Velvet Chess Engine
3 | * Copyright (C) 2024 mhonert (https://github.com/mhonert)
4 | *
5 | * This program is free software: you can redistribute it and/or modify
6 | * it under the terms of the GNU General Public License as published by
7 | * the Free Software Foundation, either version 3 of the License, or
8 | * (at your option) any later version.
9 | *
10 | * This program is distributed in the hope that it will be useful,
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | * GNU General Public License for more details.
14 | *
15 | * You should have received a copy of the GNU General Public License
16 | * along with this program. If not, see .
17 | */
18 | use std::fs::File;
19 | use std::io::Write;
20 | use std::io::BufWriter;
21 | use chrono::{DateTime, Local};
22 | use selfplay::selfplay::Outcome;
23 |
24 | pub struct PgnWriter {
25 | writer: BufWriter,
26 | }
27 |
28 | impl PgnWriter {
29 | pub fn new(file_path: &str) -> anyhow::Result {
30 | let file = File::create(file_path)?;
31 | let writer = BufWriter::new(file);
32 |
33 | anyhow::Ok(PgnWriter { writer })
34 | }
35 |
36 | pub fn write_game(&mut self, game: PgnGame) -> anyhow::Result<()> {
37 | writeln!(self.writer, "[Event \"Velvet Test Gauntlet\"]")?;
38 | writeln!(self.writer, "[Site \"local\"]")?;
39 | writeln!(self.writer, "[Date \"{}\"]", chrono::Local::now().format("%Y.%m.%d"))?;
40 | writeln!(self.writer, "[Round \"{}\"]", game.round)?;
41 | writeln!(self.writer, "[White \"{}\"]", game.white)?;
42 | writeln!(self.writer, "[Black \"{}\"]", game.black)?;
43 |
44 | let result_str = match game.result {
45 | Outcome::Win => "1-0",
46 | Outcome::Loss => "0-1",
47 | Outcome::Draw => "1/2-1/2",
48 | };
49 |
50 | writeln!(self.writer, "[Result \"{}\"]", result_str)?;
51 |
52 | writeln!(self.writer, "[TimeControl \"{}+{}\"]", game.tc as f32 / 1000.0, game.inc as f32 / 1000.0)?;
53 | writeln!(self.writer, "[Time \"{}\"]", game.start_time.format("%H:%M:%S"))?;
54 | writeln!(self.writer, "[FEN \"{}\"]", game.opening)?;
55 |
56 | writeln!(self.writer)?;
57 |
58 | if (game.start_move_count as usize) % 2 == 0 {
59 | write!(self.writer, "{}... ", game.start_move_count / 2)?;
60 | }
61 |
62 | for (i, mv) in game.moves.iter().enumerate() {
63 | if i % 2 == 0 {
64 | write!(self.writer, "{}. ", game.start_move_count as usize + i / 2)?;
65 | }
66 | write!(self.writer, "{} ", mv)?;
67 | }
68 |
69 | writeln!(self.writer, "{}", result_str)?;
70 | writeln!(self.writer)?;
71 |
72 | anyhow::Ok(())
73 | }
74 |
75 | pub fn flush(&mut self) -> anyhow::Result<()> {
76 | self.writer.flush()?;
77 | anyhow::Ok(())
78 | }
79 | }
80 |
81 | pub struct PgnGame {
82 | pub start_time: DateTime,
83 | pub white: String,
84 | pub black: String,
85 | pub tc: i32,
86 | pub inc: i32,
87 | pub round: usize,
88 | pub opening: String,
89 | pub start_move_count: u16,
90 | pub result: Outcome,
91 | pub moves: Vec,
92 | }
93 |
94 | impl PgnGame {
95 | pub fn new(white: String, black: String, tc: i32, inc: i32, round: usize, opening: String, start_move_count: u16) -> PgnGame {
96 | PgnGame {
97 | start_time: Local::now(),
98 | white,
99 | black,
100 | tc,
101 | inc,
102 | round,
103 | opening,
104 | start_move_count,
105 | result: Outcome::Draw,
106 | moves: Vec::new(),
107 | }
108 | }
109 |
110 | pub fn add_move(&mut self, mv: &str) {
111 | self.moves.push(mv.to_string());
112 | }
113 |
114 | pub fn set_result(&mut self, result: Outcome) {
115 | self.result = result;
116 | }
117 | }
--------------------------------------------------------------------------------
/tournament/src/san.rs:
--------------------------------------------------------------------------------
1 | /*
2 | * Velvet Chess Engine
3 | * Copyright (C) 2024 mhonert (https://github.com/mhonert)
4 | *
5 | * This program is free software: you can redistribute it and/or modify
6 | * it under the terms of the GNU General Public License as published by
7 | * the Free Software Foundation, either version 3 of the License, or
8 | * (at your option) any later version.
9 | *
10 | * This program is distributed in the hope that it will be useful,
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | * GNU General Public License for more details.
14 | *
15 | * You should have received a copy of the GNU General Public License
16 | * along with this program. If not, see .
17 | */
18 | use velvet::moves::{Move, MoveType};
19 | use velvet::pieces::P;
20 |
21 | static PIECES: [char; 7] = ['_', 'P', 'N', 'B', 'R', 'Q', 'K'];
22 | static FILES: [char; 8] = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'];
23 | static RANKS: [char; 8] = ['1', '2', '3', '4', '5', '6', '7', '8'];
24 |
25 | // Convert a Move to a SAN (Standard Algebraic Notation) string
26 | pub fn move_to_san(m: Move, all_moves: &[Move], gives_check: bool) -> String {
27 | if matches!(m.move_type(), MoveType::KingKSCastling) {
28 | return "O-O".to_string();
29 | } else if matches!(m.move_type(), MoveType::KingQSCastling) {
30 | return "O-O-O".to_string();
31 | }
32 |
33 | let target_piece = m.move_type().piece_id();
34 | let source_piece = if m.move_type().is_promotion() { P } else { target_piece };
35 | let (start_file_required, start_rank_required) = determine_required_disambiguation(m, all_moves);
36 | let (start_file, start_rank) = get_file_rank(m.start());
37 | let (end_file, end_rank) = get_file_rank(m.end());
38 | let piece = PIECES[source_piece as usize];
39 |
40 | let mut san = String::new();
41 | if source_piece != P {
42 | san.push(piece);
43 | }
44 |
45 | if start_file_required || m.move_type().is_pawn_capture() {
46 | san.push(FILES[start_file as usize]);
47 | }
48 |
49 | if start_rank_required {
50 | san.push(RANKS[start_rank as usize]);
51 | }
52 |
53 | if m.move_type().is_capture() {
54 | san.push('x');
55 | }
56 |
57 | san.push(FILES[end_file as usize]);
58 | san.push(RANKS[end_rank as usize]);
59 |
60 | if m.is_promotion() {
61 | let promotion_piece = PIECES[target_piece as usize];
62 | san.push('=');
63 | san.push(promotion_piece);
64 | }
65 |
66 | if gives_check {
67 | san.push('+');
68 | }
69 |
70 | san
71 | }
72 |
73 | fn determine_required_disambiguation(m: Move, all_moves: &[Move]) -> (bool, bool) {
74 | if m.is_promotion() {
75 | return (false, false)
76 | }
77 |
78 | let start = m.start();
79 | let (start_file, start_rank) = get_file_rank(start);
80 |
81 | let end = m.end();
82 | let piece = m.move_type().piece_id();
83 | let mut file_required = false;
84 | let mut rank_required = false;
85 |
86 | for &om in all_moves {
87 | if om == m {
88 | continue;
89 | }
90 | if om.end() != end || om.move_type().piece_id() != piece {
91 | continue;
92 | }
93 |
94 | let (om_file, om_rank) = get_file_rank(om.start());
95 | if om_file != start_file {
96 | file_required = true;
97 | }
98 | if om_rank != start_rank {
99 | rank_required = true;
100 | }
101 | }
102 |
103 | (file_required, rank_required)
104 | }
105 |
106 | fn get_file_rank(pos: i8) -> (i8, i8) {
107 | (pos % 8, 7 - pos / 8)
108 | }
109 |
110 | #[cfg(test)]
111 | mod tests {
112 | use velvet::moves::MoveType;
113 | use super::*;
114 |
115 | #[test]
116 | fn test_move_to_san_pawn_move() {
117 | let m = Move::new(MoveType::PawnDoubleQuiet, pos("e2"), pos("e4"));
118 | let all_moves = vec![m];
119 |
120 | assert_eq!(move_to_san(m, &all_moves, false), "e4");
121 | }
122 |
123 | #[test]
124 | fn test_move_to_san_pawn_capture() {
125 | let m = Move::new(MoveType::PawnCapture, pos("e4"), pos("d5"));
126 | let all_moves = vec![m];
127 |
128 | assert_eq!(move_to_san(m, &all_moves, false), "exd5");
129 | }
130 |
131 | #[test]
132 | fn test_move_to_san_pawn_en_passant() {
133 | let m = Move::new(MoveType::PawnEnPassant, pos("e5"), pos("d6"));
134 | let all_moves = vec![m];
135 |
136 | assert_eq!(move_to_san(m, &all_moves, false), "exd6");
137 | }
138 |
139 | #[test]
140 | fn test_move_to_san_capture() {
141 | let m = Move::new(MoveType::RookCapture, pos("a7"), pos("a8"));
142 | let all_moves = vec![m];
143 |
144 | assert_eq!(move_to_san(m, &all_moves, false), "Rxa8");
145 | }
146 |
147 | #[test]
148 | fn test_move_to_san_rank_disambiguation() {
149 | let m1 = Move::new(MoveType::QueenQuiet, pos("h4"), pos("e1"));
150 | let m2 = Move::new(MoveType::QueenQuiet, pos("e4"), pos("e1"));
151 | let all_moves = vec![m1, m2];
152 |
153 | assert_eq!(move_to_san(m1, &all_moves, false), "Qhe1");
154 | }
155 |
156 | #[test]
157 | fn test_move_to_san_file_disambiguation() {
158 | let m1 = Move::new(MoveType::QueenQuiet, pos("h4"), pos("e1"));
159 | let m2 = Move::new(MoveType::QueenQuiet, pos("h1"), pos("e1"));
160 | let all_moves = vec![m1, m2];
161 |
162 | assert_eq!(move_to_san(m1, &all_moves, false), "Q4e1");
163 | }
164 |
165 | #[test]
166 | fn test_move_to_san_rank_file_disambiguation() {
167 | let m1 = Move::new(MoveType::QueenQuiet, pos("h4"), pos("e1"));
168 | let m2 = Move::new(MoveType::QueenQuiet, pos("e4"), pos("e1"));
169 | let m3 = Move::new(MoveType::QueenQuiet, pos("h1"), pos("e1"));
170 | let all_moves = vec![m1, m2, m3];
171 |
172 | assert_eq!(move_to_san(m1, &all_moves, false), "Qh4e1");
173 | }
174 |
175 | #[test]
176 | fn test_move_to_san_gives_check() {
177 | let m = Move::new(MoveType::QueenCapture, pos("h4"), pos("e1"));
178 | let all_moves = vec![m];
179 |
180 | assert_eq!(move_to_san(m, &all_moves, true), "Qxe1+");
181 | }
182 |
183 | #[test]
184 | fn test_move_to_san_promotion() {
185 | let m = Move::new(MoveType::QueenQuietPromotion, pos("a7"), pos("a8"));
186 | let all_moves = vec![m];
187 |
188 | assert_eq!(move_to_san(m, &all_moves, false), "a8=Q");
189 | }
190 |
191 | #[test]
192 | fn test_move_to_san_capture_promotion() {
193 | let m = Move::new(MoveType::QueenCapturePromotion, pos("a7"), pos("b8"));
194 | let all_moves = vec![m];
195 |
196 | assert_eq!(move_to_san(m, &all_moves, false), "axb8=Q");
197 | }
198 |
199 | #[test]
200 | fn test_move_to_san_promotion_with_queen_on_same_rank() {
201 | let m1 = Move::new(MoveType::QueenQuietPromotion, pos("a7"), pos("a8"));
202 | let m2 = Move::new(MoveType::QueenQuiet, pos("d8"), pos("a8"));
203 | let all_moves = vec![m1, m2];
204 |
205 | assert_eq!(move_to_san(m1, &all_moves, false), "a8=Q");
206 | }
207 |
208 | #[test]
209 | fn test_move_to_san_king_side_castling() {
210 | let m = Move::new(MoveType::KingKSCastling, pos("e1"), pos("g1"));
211 | let all_moves = vec![m];
212 |
213 | assert_eq!(move_to_san(m, &all_moves, false), "O-O");
214 | }
215 |
216 | #[test]
217 | fn test_move_to_san_queen_side_castling() {
218 | let m = Move::new(MoveType::KingQSCastling, pos("e1"), pos("c1"));
219 | let all_moves = vec![m];
220 |
221 | assert_eq!(move_to_san(m, &all_moves, false), "O-O-O");
222 | }
223 |
224 | fn pos(file_rank: &str) -> i8 {
225 | let file = file_rank.chars().next().unwrap();
226 | let rank = file_rank.chars().nth(1).unwrap();
227 |
228 | (8 - rank.to_digit(10).unwrap() as i8) * 8 + (file as i8 - 'a' as i8)
229 | }
230 | }
231 |
--------------------------------------------------------------------------------
/tournament/src/tournament.rs:
--------------------------------------------------------------------------------
1 | /*
2 | * Velvet Chess Engine
3 | * Copyright (C) 2024 mhonert (https://github.com/mhonert)
4 | *
5 | * This program is free software: you can redistribute it and/or modify
6 | * it under the terms of the GNU General Public License as published by
7 | * the Free Software Foundation, either version 3 of the License, or
8 | * (at your option) any later version.
9 | *
10 | * This program is distributed in the hope that it will be useful,
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | * GNU General Public License for more details.
14 | *
15 | * You should have received a copy of the GNU General Public License
16 | * along with this program. If not, see .
17 | */
18 | use std::collections::HashMap;
19 | use std::hash::BuildHasherDefault;
20 | use std::sync::{Arc, RwLock};
21 | use std::sync::atomic::AtomicBool;
22 | use selfplay::pentanomial::{PentanomialCount, PentanomialModel};
23 | use velvet::nn::io::FastHasher;
24 | use crate::config::{EngineConfig, EngineConfigs, TournamentConfig};
25 |
26 | pub struct TournamentState {
27 | stopped: AtomicBool,
28 | shared: RwLock,
29 | }
30 |
31 | impl TournamentState {
32 | pub fn new(tournament_config: &TournamentConfig, engine_configs: &EngineConfigs) -> anyhow::Result> {
33 | let mut shared_state = SharedState::default();
34 | for opponent in tournament_config.opponents.iter() {
35 | if let Some(config) = engine_configs.0.get(opponent) {
36 | shared_state.add_opponent(config.clone())?;
37 | } else {
38 | anyhow::bail!("Could not find engine config for opponent '{}'", opponent);
39 | }
40 | }
41 |
42 | anyhow::Ok(Arc::new(TournamentState {
43 | stopped: AtomicBool::new(false),
44 | shared: RwLock::new(shared_state),
45 | }))
46 | }
47 |
48 | pub fn stopped(&self) -> bool {
49 | self.stopped.load(std::sync::atomic::Ordering::Relaxed)
50 | }
51 |
52 | pub fn next_opponent(&self) -> Option<(EngineConfig, usize)> {
53 | self.shared.write().expect("Could not acquire write lock on shared state").next_opponent()
54 | }
55 |
56 | pub fn update(&self, opponent_id: u32, result: PentanomialCount) {
57 | self.shared.write().expect("Could not acquire write lock on shared state").update(opponent_id, result);
58 | }
59 | }
60 |
61 | type OpponentMap = HashMap>;
62 |
63 | #[derive(Default)]
64 | struct SharedState {
65 | finished_games: usize,
66 | started_pairs: usize,
67 | opponents: OpponentMap,
68 | }
69 |
70 | const STD_NORM_DEV_95: f64 = 1.959963984540054;
71 |
72 | impl SharedState {
73 | pub fn add_opponent(&mut self, config: EngineConfig) -> anyhow::Result<()> {
74 | let name = config.name.clone();
75 | if self.opponents.insert(config.id, Opponent::new(config)).is_some() {
76 | anyhow::bail!("Opponent {} already exists", name);
77 | }
78 |
79 | Ok(())
80 | }
81 |
82 | pub fn next_opponent(&mut self) -> Option<(EngineConfig, usize)> {
83 | if let Some(opponent) = self.opponents.values_mut().min_by_key(|opponent| opponent.matches()) {
84 | opponent.matches += 1;
85 | self.started_pairs += 1;
86 | return Some((opponent.config.clone(), self.started_pairs));
87 | }
88 |
89 | None
90 | }
91 |
92 | pub fn update(&mut self, opponent_id: u32, result: PentanomialCount) {
93 | let opponent = self.opponents.get_mut(&opponent_id).expect("Could not find opponent");
94 | opponent.results.add_all(result);
95 | self.finished_games += 2;
96 | if self.finished_games % 100 == 0 {
97 | self.print_results();
98 | }
99 | }
100 |
101 | fn print_results(&self) {
102 | println!("----------------------------------------------------------------------------------------------------");
103 | println!("Results after {} games:\n", self.finished_games);
104 |
105 | let longest_name = self.opponents.values().map(|opponent| opponent.config.name.len()).max().unwrap_or(0);
106 |
107 | let mut results = Vec::with_capacity(self.opponents.len());
108 |
109 | for (_, opponent) in self.opponents.iter() {
110 | let p = PentanomialModel::from(opponent.results);
111 | let total = opponent.results.total();
112 | if total == 0 {
113 | results.push((0, format!("{} No games finished yet", opponent.config.name)));
114 | continue;
115 | }
116 | let score = p.score();
117 | let deviation = p.deviation(score);
118 | let variance = deviation.total();
119 | let pair_variance = variance / total as f64;
120 |
121 | let upper_bound = score + STD_NORM_DEV_95 * pair_variance.sqrt();
122 | let lower_bound = score - STD_NORM_DEV_95 * pair_variance.sqrt();
123 |
124 | let draw_ratio = p.d2;
125 |
126 | let elo_diff = score_to_norm_elo(score, variance);
127 | let elo_error = score_to_norm_elo(upper_bound, variance) - score_to_norm_elo(lower_bound, variance);
128 |
129 | let name_with_padding = format!("{:width$}", opponent.config.name, width = longest_name);
130 |
131 | results.push((elo_diff as i32, format!("{}: {:>6.2} (+/- {:>5.2}) / {:>5.2}% / {} ({})",
132 | name_with_padding, elo_diff, elo_error, draw_ratio * 100.0, total * 2, opponent.matches * 2)));
133 | }
134 |
135 | results.sort_by_key(|(elo_diff, _)| -elo_diff);
136 | for (_, result) in results.iter() {
137 | println!(" - {}", result);
138 | }
139 | }
140 | }
141 |
142 | fn score_to_norm_elo(score: f64, variance: f64) -> f64 {
143 | (score - 0.5) / (variance * 2.0).sqrt() * (400.0 / 10.0f64.ln())
144 | }
145 |
146 | #[derive(Clone)]
147 | struct Opponent {
148 | config: EngineConfig,
149 | matches: usize,
150 | results: PentanomialCount,
151 | }
152 |
153 | impl Opponent {
154 | pub fn new(config: EngineConfig) -> Opponent {
155 | Opponent {
156 | config,
157 | matches: 0,
158 | results: PentanomialCount::default(),
159 | }
160 | }
161 |
162 | // matches returns the number of finished and ongoing matches
163 | pub fn matches(&self) -> usize {
164 | self.matches
165 | }
166 | }
--------------------------------------------------------------------------------
/tournament/src/uci_engine.rs:
--------------------------------------------------------------------------------
1 | /*
2 | * Velvet Chess Engine
3 | * Copyright (C) 2024 mhonert (https://github.com/mhonert)
4 | *
5 | * This program is free software: you can redistribute it and/or modify
6 | * it under the terms of the GNU General Public License as published by
7 | * the Free Software Foundation, either version 3 of the License, or
8 | * (at your option) any later version.
9 | *
10 | * This program is distributed in the hope that it will be useful,
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | * GNU General Public License for more details.
14 | *
15 | * You should have received a copy of the GNU General Public License
16 | * along with this program. If not, see .
17 | */
18 |
19 | use std::process::{Command, Stdio};
20 | use std::io::{BufRead, Write};
21 | use anyhow::bail;
22 | use crate::config::EngineConfig;
23 |
24 | pub struct UciEngine {
25 | config: EngineConfig,
26 | child: std::process::Child,
27 | debug_log: bool,
28 | }
29 |
30 | impl UciEngine {
31 | pub fn start(config: &EngineConfig) -> anyhow::Result {
32 | let child = Command::new(&config.cmd)
33 | .stdin(Stdio::piped())
34 | .stdout(Stdio::piped())
35 | .spawn()?;
36 |
37 | anyhow::Ok(UciEngine {
38 | config: config.clone(),
39 | child,
40 | debug_log: false,
41 | })
42 | }
43 |
44 | pub fn init(&mut self) -> anyhow::Result<()> {
45 | for cmd in &self.config.init_commands.clone() {
46 | self.send_command(cmd)?;
47 | }
48 |
49 | self.send_command("uci")?;
50 | self.expect_response("uciok")?;
51 |
52 | for (opt, val) in &self.config.options.clone() {
53 | self.send_command(&format!("setoption name {} value {}", opt, val))?;
54 | }
55 |
56 | self.ready()?;
57 |
58 | anyhow::Ok(())
59 | }
60 |
61 | pub fn ready(&mut self) -> anyhow::Result<()> {
62 | self.send_command("isready")?;
63 | self.expect_response("readyok")?;
64 |
65 | anyhow::Ok(())
66 | }
67 |
68 | pub fn uci_newgame(&mut self) -> anyhow::Result<()> {
69 | self.send_command("ucinewgame")?;
70 |
71 | anyhow::Ok(())
72 | }
73 | pub fn go(&mut self, opening: &str, wtime: i32, btime: i32, inc: i32, moves: &String) -> anyhow::Result {
74 | let position = if moves.is_empty() {
75 | format!("position fen {}", opening)
76 | } else {
77 | format!("position fen {} moves {}", opening, moves)
78 | };
79 |
80 | self.send_command(&position)?;
81 | self.send_command(&format!("go wtime {} winc {} btime {} binc {}", wtime, inc, btime, inc))?;
82 |
83 | let response = self.expect_response("bestmove")?;
84 |
85 | // Extract best move from response
86 | // The best move appears after the "bestmove" token, which can appear anywhere in the response
87 | let parts: Vec<&str> = response.split_whitespace().collect();
88 | if let Some(best_move) = parts.iter().skip_while(|&&x| x != "bestmove").nth(1) {
89 | Ok(best_move.to_string())
90 | } else {
91 | bail!("Could not extract best move from response");
92 | }
93 | }
94 |
95 | pub fn quit(&mut self) -> anyhow::Result<()> {
96 | self.send_command("quit")?;
97 | self.child.wait()?;
98 |
99 | anyhow::Ok(())
100 | }
101 |
102 | fn send_command(&mut self, cmd: &str) -> anyhow::Result<()> {
103 | if let Some(stdin) = self.child.stdin.as_mut() {
104 | if self.debug_log {
105 | println!("{} w > {}", self.config.name, cmd);
106 | }
107 | writeln!(stdin, "{}", cmd)?;
108 | anyhow::Ok(())
109 | } else {
110 | bail!("Could not get stdin handle for engine process");
111 | }
112 | }
113 |
114 | fn expect_response(&mut self, expected_response: &str) -> anyhow::Result {
115 | if let Some(stdout) = self.child.stdout.as_mut() {
116 | let reader = std::io::BufReader::new(stdout);
117 | for line in reader.lines() {
118 | let line = line?;
119 | if self.debug_log {
120 | println!("{} r < {}", self.config.name, line);
121 | }
122 | if line.contains(expected_response) {
123 | return anyhow::Ok(line);
124 | }
125 | }
126 | bail!("Expected response '{}' not received", expected_response);
127 | } else {
128 | bail!("Could not get stdout handle for engine process");
129 | }
130 | }
131 |
132 | pub fn name(&self) -> String {
133 | self.config.name.clone()
134 | }
135 | }
136 |
137 | impl Drop for UciEngine {
138 | fn drop(&mut self) {
139 | if let Err(e) = self.quit() {
140 | eprintln!("Could not quit engine process: {}", e);
141 | if let Err(e) = self.child.kill() {
142 | eprintln!("Could not kill engine process: {}", e);
143 | }
144 | }
145 | }
146 | }
147 |
--------------------------------------------------------------------------------
/traincommon/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "traincommon"
3 | version = "2.0.0"
4 | edition = "2021"
5 |
6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
7 |
8 | [dependencies]
9 | velvet = { path = "../engine", default-features = false }
10 | fathomrs = { path = "../fathomrs" }
11 |
12 | lz4_flex = { version = "0.11.1", default-features = false, features = ["frame"] }
13 | rand = "0.8.5"
14 |
--------------------------------------------------------------------------------
/traincommon/src/idsource.rs:
--------------------------------------------------------------------------------
1 | /*
2 | * Velvet Chess Engine
3 | * Copyright (C) 2023 mhonert (https://github.com/mhonert)
4 | *
5 | * This program is free software: you can redistribute it and/or modify
6 | * it under the terms of the GNU General Public License as published by
7 | * the Free Software Foundation, either version 3 of the License, or
8 | * (at your option) any later version.
9 | *
10 | * This program is distributed in the hope that it will be useful,
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | * GNU General Public License for more details.
14 | *
15 | * You should have received a copy of the GNU General Public License
16 | * along with this program. If not, see .
17 | */
18 |
19 | use rand::prelude::SliceRandom;
20 | use rand::RngCore;
21 |
22 | pub struct IDSource {
23 | ids: Vec,
24 | min_id: usize,
25 | max_id: usize,
26 | batch_id_count: usize,
27 | epoch: usize,
28 | }
29 |
30 | impl IDSource {
31 | pub fn new(rng: &mut dyn RngCore, min_id: usize, max_id: usize, batch_id_count: usize) -> Self {
32 | IDSource { ids: shuffled_ids(rng, min_id, max_id), min_id, max_id, batch_id_count, epoch: 1 }
33 | }
34 |
35 | pub fn next_batch(&mut self, rng: &mut dyn RngCore) -> (usize, Vec) {
36 | if self.ids.len() < self.batch_id_count {
37 | self.ids.append(&mut shuffled_ids(rng, self.min_id, self.max_id));
38 | self.epoch += 1;
39 | }
40 | (self.epoch, self.ids.drain(self.ids.len() - self.batch_id_count..).collect())
41 | }
42 |
43 | pub fn per_batch_count(&self) -> usize {
44 | self.batch_id_count
45 | }
46 | }
47 |
48 | fn shuffled_ids(rng: &mut dyn RngCore, min: usize, max: usize) -> Vec {
49 | let mut ids: Vec = (min..=max).collect();
50 | ids.shuffle(rng);
51 | ids
52 | }
53 |
--------------------------------------------------------------------------------
/traincommon/src/lib.rs:
--------------------------------------------------------------------------------
1 | /*
2 | * Velvet Chess Engine
3 | * Copyright (C) 2023 mhonert (https://github.com/mhonert)
4 | *
5 | * This program is free software: you can redistribute it and/or modify
6 | * it under the terms of the GNU General Public License as published by
7 | * the Free Software Foundation, either version 3 of the License, or
8 | * (at your option) any later version.
9 | *
10 | * This program is distributed in the hope that it will be useful,
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | * GNU General Public License for more details.
14 | *
15 | * You should have received a copy of the GNU General Public License
16 | * along with this program. If not, see .
17 | */
18 |
19 | pub mod idsource;
20 | pub mod sets;
--------------------------------------------------------------------------------
/tuner/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "tuner"
3 | version = "1.0.0"
4 | edition = "2021"
5 |
6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
7 |
8 | [dependencies]
9 | velvet = { path = "../engine" }
10 | selfplay = { path = "../selfplay" }
11 |
12 | rand = "0.8.5"
13 | core_affinity = "0.8.1"
14 | thread-priority = "1.1.0"
15 |
--------------------------------------------------------------------------------
/verify-patch:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | python3 ./pytools/patch-verifier/client.py "$@"
3 |
--------------------------------------------------------------------------------