Compare commits
3 Commits
b36f5b0b84
...
a5476ccdd7
| Author | SHA1 | Date | |
|---|---|---|---|
| a5476ccdd7 | |||
| 145bc948dd | |||
| 4131c93f99 |
@ -4,6 +4,8 @@
|
||||
|
||||
The project owner is learning Zig. Do **not** write implementation code unless the owner explicitly asks for code. Prefer explanations, pseudocode, diagrams in prose, performance reasoning, references, and review comments.
|
||||
|
||||
The project owner owns the chess implementation. Do **not** modify files under `src/chess/` unless the owner explicitly asks for changes there. For UI/graphics work, adapt the caller-side code instead of changing chess logic or chess tests. If a requested graphics/UI change appears to require a chess implementation change, stop and explain the blocker instead of modifying `src/chess/`.
|
||||
|
||||
## Required research standard
|
||||
|
||||
For factual claims about algorithms, chess rules/standards, numerical methods, graphics, Zig behavior, compiler behavior, or performance:
|
||||
|
||||
BIN
assets/pieces/png/Chess_bdt45.png
Normal file
|
After Width: | Height: | Size: 4.6 KiB |
BIN
assets/pieces/png/Chess_blt45.png
Normal file
|
After Width: | Height: | Size: 7.8 KiB |
BIN
assets/pieces/png/Chess_kdt45.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
assets/pieces/png/Chess_klt45.png
Normal file
|
After Width: | Height: | Size: 9.5 KiB |
BIN
assets/pieces/png/Chess_ndt45.png
Normal file
|
After Width: | Height: | Size: 6.5 KiB |
BIN
assets/pieces/png/Chess_nlt45.png
Normal file
|
After Width: | Height: | Size: 8.7 KiB |
BIN
assets/pieces/png/Chess_pdt45.png
Normal file
|
After Width: | Height: | Size: 2.9 KiB |
BIN
assets/pieces/png/Chess_plt45.png
Normal file
|
After Width: | Height: | Size: 5.4 KiB |
BIN
assets/pieces/png/Chess_qdt45.png
Normal file
|
After Width: | Height: | Size: 9.9 KiB |
BIN
assets/pieces/png/Chess_qlt45.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
assets/pieces/png/Chess_rdt45.png
Normal file
|
After Width: | Height: | Size: 2.4 KiB |
BIN
assets/pieces/png/Chess_rlt45.png
Normal file
|
After Width: | Height: | Size: 2.6 KiB |
BIN
assets/pieces/raw/Chess_bdt45.rgba
Normal file
BIN
assets/pieces/raw/Chess_blt45.rgba
Normal file
BIN
assets/pieces/raw/Chess_kdt45.rgba
Normal file
BIN
assets/pieces/raw/Chess_klt45.rgba
Normal file
BIN
assets/pieces/raw/Chess_ndt45.rgba
Normal file
BIN
assets/pieces/raw/Chess_nlt45.rgba
Normal file
BIN
assets/pieces/raw/Chess_pdt45.rgba
Normal file
BIN
assets/pieces/raw/Chess_plt45.rgba
Normal file
BIN
assets/pieces/raw/Chess_qdt45.rgba
Normal file
BIN
assets/pieces/raw/Chess_qlt45.rgba
Normal file
BIN
assets/pieces/raw/Chess_rdt45.rgba
Normal file
BIN
assets/pieces/raw/Chess_rlt45.rgba
Normal file
12
assets/pieces/svg/Chess_bdt45.svg
Normal file
@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="45" height="45">
|
||||
<g style="opacity:1; fill:none; fill-rule:evenodd; fill-opacity:1; stroke:#000000; stroke-width:1.5; stroke-linecap:round; stroke-linejoin:round; stroke-miterlimit:4; stroke-dasharray:none; stroke-opacity:1;" transform="translate(0,0.6)">
|
||||
<g style="fill:#000000; stroke:#000000; stroke-linecap:butt;">
|
||||
<path d="M 9,36 C 12.39,35.03 19.11,36.43 22.5,34 C 25.89,36.43 32.61,35.03 36,36 C 36,36 37.65,36.54 39,38 C 38.32,38.97 37.35,38.99 36,38.5 C 32.61,37.53 25.89,38.96 22.5,37.5 C 19.11,38.96 12.39,37.53 9,38.5 C 7.65,38.99 6.68,38.97 6,38 C 7.35,36.54 9,36 9,36 z"/>
|
||||
<path d="M 15,32 C 17.5,34.5 27.5,34.5 30,32 C 30.5,30.5 30,30 30,30 C 30,27.5 27.5,26 27.5,26 C 33,24.5 33.5,14.5 22.5,10.5 C 11.5,14.5 12,24.5 17.5,26 C 17.5,26 15,27.5 15,30 C 15,30 14.5,30.5 15,32 z"/>
|
||||
<path d="M 25 8 A 2.5 2.5 0 1 1 20,8 A 2.5 2.5 0 1 1 25 8 z"/>
|
||||
</g>
|
||||
<path d="M 17.5,26 L 27.5,26 M 15,30 L 30,30 M 22.5,15.5 L 22.5,20.5 M 20,18 L 25,18" style="fill:none; stroke:#ffffff; stroke-linejoin:miter;"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
12
assets/pieces/svg/Chess_blt45.svg
Normal file
@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="45" height="45">
|
||||
<g style="opacity:1; fill:none; fill-rule:evenodd; fill-opacity:1; stroke:#000000; stroke-width:1.5; stroke-linecap:round; stroke-linejoin:round; stroke-miterlimit:4; stroke-dasharray:none; stroke-opacity:1;" transform="translate(0,0.6)">
|
||||
<g style="fill:#ffffff; stroke:#000000; stroke-linecap:butt;">
|
||||
<path d="M 9,36 C 12.39,35.03 19.11,36.43 22.5,34 C 25.89,36.43 32.61,35.03 36,36 C 36,36 37.65,36.54 39,38 C 38.32,38.97 37.35,38.99 36,38.5 C 32.61,37.53 25.89,38.96 22.5,37.5 C 19.11,38.96 12.39,37.53 9,38.5 C 7.65,38.99 6.68,38.97 6,38 C 7.35,36.54 9,36 9,36 z"/>
|
||||
<path d="M 15,32 C 17.5,34.5 27.5,34.5 30,32 C 30.5,30.5 30,30 30,30 C 30,27.5 27.5,26 27.5,26 C 33,24.5 33.5,14.5 22.5,10.5 C 11.5,14.5 12,24.5 17.5,26 C 17.5,26 15,27.5 15,30 C 15,30 14.5,30.5 15,32 z"/>
|
||||
<path d="M 25 8 A 2.5 2.5 0 1 1 20,8 A 2.5 2.5 0 1 1 25 8 z"/>
|
||||
</g>
|
||||
<path d="M 17.5,26 L 27.5,26 M 15,30 L 30,30 M 22.5,15.5 L 22.5,20.5 M 20,18 L 25,18" style="fill:none; stroke:#000000; stroke-linejoin:miter;"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
12
assets/pieces/svg/Chess_kdt45.svg
Normal file
@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="45" height="45">
|
||||
<g style="fill:none; fill-opacity:1; fill-rule:evenodd; stroke:#000000; stroke-width:1.5; stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4; stroke-dasharray:none; stroke-opacity:1;">
|
||||
<path d="M 22.5,11.63 L 22.5,6" style="fill:none; stroke:#000000; stroke-linejoin:miter;" id="path6570"/>
|
||||
<path d="M 22.5,25 C 22.5,25 27,17.5 25.5,14.5 C 25.5,14.5 24.5,12 22.5,12 C 20.5,12 19.5,14.5 19.5,14.5 C 18,17.5 22.5,25 22.5,25" style="fill:#000000;fill-opacity:1; stroke-linecap:butt; stroke-linejoin:miter;"/>
|
||||
<path d="M 12.5,37 C 18,40.5 27,40.5 32.5,37 L 32.5,30 C 32.5,30 41.5,25.5 38.5,19.5 C 34.5,13 25,16 22.5,23.5 L 22.5,27 L 22.5,23.5 C 20,16 10.5,13 6.5,19.5 C 3.5,25.5 12.5,30 12.5,30 L 12.5,37" style="fill:#000000; stroke:#000000;"/>
|
||||
<path d="M 20,8 L 25,8" style="fill:none; stroke:#000000; stroke-linejoin:miter;"/>
|
||||
<path d="M 32,29.5 C 32,29.5 40.5,25.5 38.03,19.85 C 34.15,14 25,18 22.5,24.5 L 22.5,26.6 L 22.5,24.5 C 20,18 10.85,14 6.97,19.85 C 4.5,25.5 13,29.5 13,29.5" style="fill:none; stroke:#ffffff;"/>
|
||||
<path d="M 12.5,30 C 18,27 27,27 32.5,30 M 12.5,33.5 C 18,30.5 27,30.5 32.5,33.5 M 12.5,37 C 18,34 27,34 32.5,37" style="fill:none; stroke:#ffffff;"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
9
assets/pieces/svg/Chess_klt45.svg
Normal file
@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="45" height="45">
|
||||
<g fill="none" fill-rule="evenodd" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5">
|
||||
<path stroke-linejoin="miter" d="M22.5 11.63V6M20 8h5"/>
|
||||
<path fill="#fff" stroke-linecap="butt" stroke-linejoin="miter" d="M22.5 25s4.5-7.5 3-10.5c0 0-1-2.5-3-2.5s-3 2.5-3 2.5c-1.5 3 3 10.5 3 10.5"/>
|
||||
<path fill="#fff" d="M12.5 37c5.5 3.5 14.5 3.5 20 0v-7s9-4.5 6-10.5c-4-6.5-13.5-3.5-16 4V27v-3.5c-2.5-7.5-12-10.5-16-4-3 6 6 10.5 6 10.5v7"/>
|
||||
<path d="M12.5 30c5.5-3 14.5-3 20 0m-20 3.5c5.5-3 14.5-3 20 0m-20 3.5c5.5-3 14.5-3 20 0"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 700 B |
22
assets/pieces/svg/Chess_ndt45.svg
Normal file
@ -0,0 +1,22 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="45" height="45">
|
||||
<g style="opacity:1; fill:none; fill-opacity:1; fill-rule:evenodd; stroke:#000000; stroke-width:1.5; stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4; stroke-dasharray:none; stroke-opacity:1;" transform="translate(0,0.3)">
|
||||
<path
|
||||
d="M 22,10 C 32.5,11 38.5,18 38,39 L 15,39 C 15,30 25,32.5 23,18"
|
||||
style="fill:#000000; stroke:#000000;" />
|
||||
<path
|
||||
d="M 24,18 C 24.38,20.91 18.45,25.37 16,27 C 13,29 13.18,31.34 11,31 C 9.958,30.06 12.41,27.96 11,28 C 10,28 11.19,29.23 10,30 C 9,30 5.997,31 6,26 C 6,24 12,14 12,14 C 12,14 13.89,12.1 14,10.5 C 13.27,9.506 13.5,8.5 13.5,7.5 C 14.5,6.5 16.5,10 16.5,10 L 18.5,10 C 18.5,10 19.28,8.008 21,7 C 22,7 22,10 22,10"
|
||||
style="fill:#000000; stroke:#000000;" />
|
||||
<path
|
||||
d="M 9.5 25.5 A 0.5 0.5 0 1 1 8.5,25.5 A 0.5 0.5 0 1 1 9.5 25.5 z"
|
||||
style="fill:#ffffff; stroke:#ffffff;" />
|
||||
<path
|
||||
d="M 15 15.5 A 0.5 1.5 0 1 1 14,15.5 A 0.5 1.5 0 1 1 15 15.5 z"
|
||||
transform="matrix(0.866,0.5,-0.5,0.866,9.693,-5.173)"
|
||||
style="fill:#ffffff; stroke:#ffffff;" />
|
||||
<path
|
||||
d="M 24.55,10.4 L 24.1,11.85 L 24.6,12 C 27.75,13 30.25,14.49 32.5,18.75 C 34.75,23.01 35.75,29.06 35.25,39 L 35.2,39.5 L 37.45,39.5 L 37.5,39 C 38,28.94 36.62,22.15 34.25,17.66 C 31.88,13.17 28.46,11.02 25.06,10.5 L 24.55,10.4 z "
|
||||
style="fill:#ffffff; stroke:none;" />
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
19
assets/pieces/svg/Chess_nlt45.svg
Normal file
@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="45" height="45">
|
||||
<g style="opacity:1; fill:none; fill-opacity:1; fill-rule:evenodd; stroke:#000000; stroke-width:1.5; stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4; stroke-dasharray:none; stroke-opacity:1;" transform="translate(0,0.3)">
|
||||
<path
|
||||
d="M 22,10 C 32.5,11 38.5,18 38,39 L 15,39 C 15,30 25,32.5 23,18"
|
||||
style="fill:#ffffff; stroke:#000000;" />
|
||||
<path
|
||||
d="M 24,18 C 24.38,20.91 18.45,25.37 16,27 C 13,29 13.18,31.34 11,31 C 9.958,30.06 12.41,27.96 11,28 C 10,28 11.19,29.23 10,30 C 9,30 5.997,31 6,26 C 6,24 12,14 12,14 C 12,14 13.89,12.1 14,10.5 C 13.27,9.506 13.5,8.5 13.5,7.5 C 14.5,6.5 16.5,10 16.5,10 L 18.5,10 C 18.5,10 19.28,8.008 21,7 C 22,7 22,10 22,10"
|
||||
style="fill:#ffffff; stroke:#000000;" />
|
||||
<path
|
||||
d="M 9.5 25.5 A 0.5 0.5 0 1 1 8.5,25.5 A 0.5 0.5 0 1 1 9.5 25.5 z"
|
||||
style="fill:#000000; stroke:#000000;" />
|
||||
<path
|
||||
d="M 15 15.5 A 0.5 1.5 0 1 1 14,15.5 A 0.5 1.5 0 1 1 15 15.5 z"
|
||||
transform="matrix(0.866,0.5,-0.5,0.866,9.693,-5.173)"
|
||||
style="fill:#000000; stroke:#000000;" />
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
5
assets/pieces/svg/Chess_pdt45.svg
Normal file
@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="45" height="45">
|
||||
<path d="m 22.5,9 c -2.21,0 -4,1.79 -4,4 0,0.89 0.29,1.71 0.78,2.38 C 17.33,16.5 16,18.59 16,21 c 0,2.03 0.94,3.84 2.41,5.03 C 15.41,27.09 11,31.58 11,39.5 H 34 C 34,31.58 29.59,27.09 26.59,26.03 28.06,24.84 29,23.03 29,21 29,18.59 27.67,16.5 25.72,15.38 26.21,14.71 26.5,13.89 26.5,13 c 0,-2.21 -1.79,-4 -4,-4 z" style="opacity:1; fill:#000000; fill-opacity:1; fill-rule:nonzero; stroke:#000000; stroke-width:1.5; stroke-linecap:round; stroke-linejoin:miter; stroke-miterlimit:4; stroke-dasharray:none; stroke-opacity:1;"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 766 B |
5
assets/pieces/svg/Chess_plt45.svg
Normal file
@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="45" height="45">
|
||||
<path d="m 22.5,9 c -2.21,0 -4,1.79 -4,4 0,0.89 0.29,1.71 0.78,2.38 C 17.33,16.5 16,18.59 16,21 c 0,2.03 0.94,3.84 2.41,5.03 C 15.41,27.09 11,31.58 11,39.5 H 34 C 34,31.58 29.59,27.09 26.59,26.03 28.06,24.84 29,23.03 29,21 29,18.59 27.67,16.5 25.72,15.38 26.21,14.71 26.5,13.89 26.5,13 c 0,-2.21 -1.79,-4 -4,-4 z" style="opacity:1; fill:#ffffff; fill-opacity:1; fill-rule:nonzero; stroke:#000000; stroke-width:1.5; stroke-linecap:round; stroke-linejoin:miter; stroke-miterlimit:4; stroke-dasharray:none; stroke-opacity:1;"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 766 B |
27
assets/pieces/svg/Chess_qdt45.svg
Normal file
@ -0,0 +1,27 @@
|
||||
<?xml version="1.0" encoding="utf-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
|
||||
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="45"
|
||||
height="45">
|
||||
<g style="fill:#000000;stroke:#000000;stroke-width:1.5; stroke-linecap:round;stroke-linejoin:round">
|
||||
|
||||
<path d="M 9,26 C 17.5,24.5 30,24.5 36,26 L 38.5,13.5 L 31,25 L 30.7,10.9 L 25.5,24.5 L 22.5,10 L 19.5,24.5 L 14.3,10.9 L 14,25 L 6.5,13.5 L 9,26 z"
|
||||
style="stroke-linecap:butt;fill:#000000" />
|
||||
<path d="m 9,26 c 0,2 1.5,2 2.5,4 1,1.5 1,1 0.5,3.5 -1.5,1 -1,2.5 -1,2.5 -1.5,1.5 0,2.5 0,2.5 6.5,1 16.5,1 23,0 0,0 1.5,-1 0,-2.5 0,0 0.5,-1.5 -1,-2.5 -0.5,-2.5 -0.5,-2 0.5,-3.5 1,-2 2.5,-2 2.5,-4 -8.5,-1.5 -18.5,-1.5 -27,0 z" />
|
||||
<path d="M 11.5,30 C 15,29 30,29 33.5,30" />
|
||||
<path d="m 12,33.5 c 6,-1 15,-1 21,0" />
|
||||
<circle cx="6" cy="12" r="2" />
|
||||
<circle cx="14" cy="9" r="2" />
|
||||
<circle cx="22.5" cy="8" r="2" />
|
||||
<circle cx="31" cy="9" r="2" />
|
||||
<circle cx="39" cy="12" r="2" />
|
||||
<path d="M 11,38.5 A 35,35 1 0 0 34,38.5"
|
||||
style="fill:none; stroke:#000000;stroke-linecap:butt;" />
|
||||
<g style="fill:none; stroke:#ffffff;">
|
||||
<path d="M 11,29 A 35,35 1 0 1 34,29" />
|
||||
<path d="M 12.5,31.5 L 32.5,31.5" />
|
||||
<path d="M 11.5,34.5 A 35,35 1 0 0 33.5,34.5" />
|
||||
<path d="M 10.5,37.5 A 35,35 1 0 0 34.5,37.5" />
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
15
assets/pieces/svg/Chess_qlt45.svg
Normal file
@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="45" height="45">
|
||||
<g style="fill:#ffffff;stroke:#000000;stroke-width:1.5;stroke-linejoin:round">
|
||||
<path d="M 9,26 C 17.5,24.5 30,24.5 36,26 L 38.5,13.5 L 31,25 L 30.7,10.9 L 25.5,24.5 L 22.5,10 L 19.5,24.5 L 14.3,10.9 L 14,25 L 6.5,13.5 L 9,26 z"/>
|
||||
<path d="M 9,26 C 9,28 10.5,28 11.5,30 C 12.5,31.5 12.5,31 12,33.5 C 10.5,34.5 11,36 11,36 C 9.5,37.5 11,38.5 11,38.5 C 17.5,39.5 27.5,39.5 34,38.5 C 34,38.5 35.5,37.5 34,36 C 34,36 34.5,34.5 33,33.5 C 32.5,31 32.5,31.5 33.5,30 C 34.5,28 36,28 36,26 C 27.5,24.5 17.5,24.5 9,26 z"/>
|
||||
<path d="M 11.5,30 C 15,29 30,29 33.5,30" style="fill:none"/>
|
||||
<path d="M 12,33.5 C 18,32.5 27,32.5 33,33.5" style="fill:none"/>
|
||||
<circle cx="6" cy="12" r="2" />
|
||||
<circle cx="14" cy="9" r="2" />
|
||||
<circle cx="22.5" cy="8" r="2" />
|
||||
<circle cx="31" cy="9" r="2" />
|
||||
<circle cx="39" cy="12" r="2" />
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
39
assets/pieces/svg/Chess_rdt45.svg
Normal file
@ -0,0 +1,39 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="45" height="45">
|
||||
<g style="opacity:1; fill:#000000; fill-opacity:1; fill-rule:evenodd; stroke:#000000; stroke-width:1.5; stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4; stroke-dasharray:none; stroke-opacity:1;" transform="translate(0,0.3)">
|
||||
<path
|
||||
d="M 9,39 L 36,39 L 36,36 L 9,36 L 9,39 z "
|
||||
style="stroke-linecap:butt;" />
|
||||
<path
|
||||
d="M 12.5,32 L 14,29.5 L 31,29.5 L 32.5,32 L 12.5,32 z "
|
||||
style="stroke-linecap:butt;" />
|
||||
<path
|
||||
d="M 12,36 L 12,32 L 33,32 L 33,36 L 12,36 z "
|
||||
style="stroke-linecap:butt;" />
|
||||
<path
|
||||
d="M 14,29.5 L 14,16.5 L 31,16.5 L 31,29.5 L 14,29.5 z "
|
||||
style="stroke-linecap:butt;stroke-linejoin:miter;" />
|
||||
<path
|
||||
d="M 14,16.5 L 11,14 L 34,14 L 31,16.5 L 14,16.5 z "
|
||||
style="stroke-linecap:butt;" />
|
||||
<path
|
||||
d="M 11,14 L 11,9 L 15,9 L 15,11 L 20,11 L 20,9 L 25,9 L 25,11 L 30,11 L 30,9 L 34,9 L 34,14 L 11,14 z "
|
||||
style="stroke-linecap:butt;" />
|
||||
<path
|
||||
d="M 12,35.5 L 33,35.5 L 33,35.5"
|
||||
style="fill:none; stroke:#ffffff; stroke-width:1; stroke-linejoin:miter;" />
|
||||
<path
|
||||
d="M 13,31.5 L 32,31.5"
|
||||
style="fill:none; stroke:#ffffff; stroke-width:1; stroke-linejoin:miter;" />
|
||||
<path
|
||||
d="M 14,29.5 L 31,29.5"
|
||||
style="fill:none; stroke:#ffffff; stroke-width:1; stroke-linejoin:miter;" />
|
||||
<path
|
||||
d="M 14,16.5 L 31,16.5"
|
||||
style="fill:none; stroke:#ffffff; stroke-width:1; stroke-linejoin:miter;" />
|
||||
<path
|
||||
d="M 11,14 L 34,14"
|
||||
style="fill:none; stroke:#ffffff; stroke-width:1; stroke-linejoin:miter;" />
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.8 KiB |
25
assets/pieces/svg/Chess_rlt45.svg
Normal file
@ -0,0 +1,25 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="45" height="45">
|
||||
<g style="opacity:1; fill:#ffffff; fill-opacity:1; fill-rule:evenodd; stroke:#000000; stroke-width:1.5; stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4; stroke-dasharray:none; stroke-opacity:1;" transform="translate(0,0.3)">
|
||||
<path
|
||||
d="M 9,39 L 36,39 L 36,36 L 9,36 L 9,39 z "
|
||||
style="stroke-linecap:butt;" />
|
||||
<path
|
||||
d="M 12,36 L 12,32 L 33,32 L 33,36 L 12,36 z "
|
||||
style="stroke-linecap:butt;" />
|
||||
<path
|
||||
d="M 11,14 L 11,9 L 15,9 L 15,11 L 20,11 L 20,9 L 25,9 L 25,11 L 30,11 L 30,9 L 34,9 L 34,14"
|
||||
style="stroke-linecap:butt;" />
|
||||
<path
|
||||
d="M 34,14 L 31,17 L 14,17 L 11,14" />
|
||||
<path
|
||||
d="M 31,17 L 31,29.5 L 14,29.5 L 14,17"
|
||||
style="stroke-linecap:butt; stroke-linejoin:miter;" />
|
||||
<path
|
||||
d="M 31,29.5 L 32.5,32 L 12.5,32 L 14,29.5" />
|
||||
<path
|
||||
d="M 11,14 L 34,14"
|
||||
style="fill:none; stroke:#000000; stroke-linejoin:miter;" />
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
70
build.zig
@ -53,6 +53,22 @@ pub fn build(b: *std.Build) void {
|
||||
const frag_spv = frag_cmd.addOutputFileArg("square.frag.spv");
|
||||
frag_cmd.addFileArg(b.path("shaders/square.frag"));
|
||||
|
||||
const piece_vert_cmd = b.addSystemCommand(&.{
|
||||
"glslc",
|
||||
"--target-env=vulkan1.2",
|
||||
"-o",
|
||||
});
|
||||
const piece_vert_spv = piece_vert_cmd.addOutputFileArg("piece.vert.spv");
|
||||
piece_vert_cmd.addFileArg(b.path("shaders/piece.vert"));
|
||||
|
||||
const piece_frag_cmd = b.addSystemCommand(&.{
|
||||
"glslc",
|
||||
"--target-env=vulkan1.2",
|
||||
"-o",
|
||||
});
|
||||
const piece_frag_spv = piece_frag_cmd.addOutputFileArg("piece.frag.spv");
|
||||
piece_frag_cmd.addFileArg(b.path("shaders/piece.frag"));
|
||||
|
||||
exe.root_module.addAnonymousImport("square_vertex_shader", .{
|
||||
.root_source_file = vert_spv,
|
||||
});
|
||||
@ -61,11 +77,65 @@ pub fn build(b: *std.Build) void {
|
||||
.root_source_file = frag_spv,
|
||||
});
|
||||
|
||||
exe.root_module.addAnonymousImport("piece_vertex_shader", .{
|
||||
.root_source_file = piece_vert_spv,
|
||||
});
|
||||
|
||||
exe.root_module.addAnonymousImport("piece_fragment_shader", .{
|
||||
.root_source_file = piece_frag_spv,
|
||||
});
|
||||
|
||||
const piece_assets = [_]struct { name: []const u8, path: []const u8 }{
|
||||
.{ .name = "white_pawn_rgba", .path = "assets/pieces/raw/Chess_plt45.rgba" },
|
||||
.{ .name = "white_knight_rgba", .path = "assets/pieces/raw/Chess_nlt45.rgba" },
|
||||
.{ .name = "white_bishop_rgba", .path = "assets/pieces/raw/Chess_blt45.rgba" },
|
||||
.{ .name = "white_rook_rgba", .path = "assets/pieces/raw/Chess_rlt45.rgba" },
|
||||
.{ .name = "white_queen_rgba", .path = "assets/pieces/raw/Chess_qlt45.rgba" },
|
||||
.{ .name = "white_king_rgba", .path = "assets/pieces/raw/Chess_klt45.rgba" },
|
||||
.{ .name = "black_pawn_rgba", .path = "assets/pieces/raw/Chess_pdt45.rgba" },
|
||||
.{ .name = "black_knight_rgba", .path = "assets/pieces/raw/Chess_ndt45.rgba" },
|
||||
.{ .name = "black_bishop_rgba", .path = "assets/pieces/raw/Chess_bdt45.rgba" },
|
||||
.{ .name = "black_rook_rgba", .path = "assets/pieces/raw/Chess_rdt45.rgba" },
|
||||
.{ .name = "black_queen_rgba", .path = "assets/pieces/raw/Chess_qdt45.rgba" },
|
||||
.{ .name = "black_king_rgba", .path = "assets/pieces/raw/Chess_kdt45.rgba" },
|
||||
};
|
||||
|
||||
for (piece_assets) |asset| {
|
||||
exe.root_module.addAnonymousImport(asset.name, .{
|
||||
.root_source_file = b.path(asset.path),
|
||||
});
|
||||
}
|
||||
|
||||
b.installArtifact(exe);
|
||||
|
||||
const run_cmd = b.addRunArtifact(exe);
|
||||
run_cmd.step.dependOn(b.getInstallStep());
|
||||
if (b.args) |args| {
|
||||
run_cmd.addArgs(args);
|
||||
}
|
||||
|
||||
const run_step = b.step("run", "Run zig-chess");
|
||||
run_step.dependOn(&run_cmd.step);
|
||||
|
||||
const magic_numbers_exe = b.addExecutable(.{
|
||||
.name = "magic-numbers",
|
||||
.root_module = b.createModule(.{
|
||||
.root_source_file = b.path("tools/magic_numbers.zig"),
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
}),
|
||||
});
|
||||
|
||||
const install_magic_numbers = b.addInstallArtifact(magic_numbers_exe, .{});
|
||||
|
||||
const magic_numbers_step = b.step("magic-numbers", "Build the magic bitboard number utility");
|
||||
magic_numbers_step.dependOn(&install_magic_numbers.step);
|
||||
|
||||
const run_magic_numbers_cmd = b.addRunArtifact(magic_numbers_exe);
|
||||
if (b.args) |args| {
|
||||
run_magic_numbers_cmd.addArgs(args);
|
||||
}
|
||||
|
||||
const run_magic_numbers_step = b.step("run-magic-numbers", "Run the magic bitboard number utility");
|
||||
run_magic_numbers_step.dependOn(&run_magic_numbers_cmd.step);
|
||||
}
|
||||
|
||||
388
docs/magic-bitboards-design.md
Normal file
@ -0,0 +1,388 @@
|
||||
# Magic Bitboard Design Guide for a Zig Chess Engine
|
||||
|
||||
## Overview
|
||||
|
||||
This document describes the implementation strategy for sliding-piece move generation using Magic Bitboards in Zig.
|
||||
|
||||
The goal is:
|
||||
|
||||
- Extremely fast rook/bishop/queen move generation
|
||||
- All move tables precomputed at startup or compile time
|
||||
- Efficient runtime lookups using:
|
||||
- Bit masks
|
||||
- Bitwise operations
|
||||
- Multiplication by magic numbers
|
||||
- Table indexing
|
||||
|
||||
This document assumes:
|
||||
|
||||
- Board representation uses 64-bit bitboards (`u64`)
|
||||
- One bitboard per piece type
|
||||
- Separate white and black occupancy bitboards
|
||||
- Additional metadata stored separately, such as castling and en passant
|
||||
|
||||
References for follow-up:
|
||||
|
||||
- Chess Programming Wiki, “Magic Bitboards”: https://www.chessprogramming.org/Magic_Bitboards
|
||||
- Chess Programming Wiki, “Sliding Piece Attacks”: https://www.chessprogramming.org/Sliding_Piece_Attacks
|
||||
- Chess Programming Wiki, “Bitboards”: https://www.chessprogramming.org/Bitboards
|
||||
|
||||
## 1. Board Representation
|
||||
|
||||
Use one `u64` per piece type.
|
||||
|
||||
Example:
|
||||
|
||||
```zig
|
||||
const Board = struct {
|
||||
white_pawns: u64,
|
||||
white_knights: u64,
|
||||
white_bishops: u64,
|
||||
white_rooks: u64,
|
||||
white_queens: u64,
|
||||
white_king: u64,
|
||||
|
||||
black_pawns: u64,
|
||||
black_knights: u64,
|
||||
black_bishops: u64,
|
||||
black_rooks: u64,
|
||||
black_queens: u64,
|
||||
black_king: u64,
|
||||
|
||||
white_occ: u64,
|
||||
black_occ: u64,
|
||||
all_occ: u64,
|
||||
};
|
||||
```
|
||||
|
||||
## 2. Square Numbering
|
||||
|
||||
Recommended:
|
||||
|
||||
```text
|
||||
A1 = 0
|
||||
B1 = 1
|
||||
...
|
||||
H1 = 7
|
||||
|
||||
A2 = 8
|
||||
...
|
||||
H8 = 63
|
||||
```
|
||||
|
||||
This layout aligns naturally with white perspective, rank/file math, and bit shifting.
|
||||
|
||||
## 3. Sliding Piece Basics
|
||||
|
||||
Magic bitboards are only needed for:
|
||||
|
||||
- Rooks
|
||||
- Bishops
|
||||
- Queens
|
||||
|
||||
Knights, kings, and pawns can use precomputed attack masks or direct bit operations.
|
||||
|
||||
## 4. Relevant Occupancy Masks
|
||||
|
||||
Each rook/bishop square has a relevant occupancy mask.
|
||||
|
||||
This mask contains only squares that can block movement.
|
||||
|
||||
Board edges are excluded because edge blockers do not affect how far the slider can move; they create redundant occupancy states.
|
||||
|
||||
Example: rook on D4.
|
||||
|
||||
Relevant mask includes:
|
||||
|
||||
```text
|
||||
D5 D6 D7
|
||||
D3 D2
|
||||
C4 B4
|
||||
E4 F4 G4
|
||||
```
|
||||
|
||||
Not included:
|
||||
|
||||
```text
|
||||
D8
|
||||
D1
|
||||
A4
|
||||
H4
|
||||
```
|
||||
|
||||
## 5. Runtime Occupancy Extraction
|
||||
|
||||
At runtime:
|
||||
|
||||
```zig
|
||||
const blockers = board.all_occ & rook_masks[square];
|
||||
```
|
||||
|
||||
This isolates only blockers relevant to the rook.
|
||||
|
||||
## 6. Magic Bitboard Lookup Formula
|
||||
|
||||
Core formula:
|
||||
|
||||
```zig
|
||||
index = (blockers * magic) >> shift;
|
||||
```
|
||||
|
||||
Where:
|
||||
|
||||
- `blockers` = masked occupancy
|
||||
- `magic` = precomputed magic number
|
||||
- `shift` = reduces result to table index size
|
||||
|
||||
## 7. Why Multiplication Works
|
||||
|
||||
The multiplication mixes blocker bits into a pseudo-random pattern.
|
||||
|
||||
A good magic number guarantees that every possible blocker configuration maps to a usable table index. In practice, collisions are allowed only when the colliding occupancies produce the same attack bitboard.
|
||||
|
||||
## 8. Why Shifting Is Needed
|
||||
|
||||
Multiplication produces a 64-bit result. We only need enough bits to index the move table.
|
||||
|
||||
Example:
|
||||
|
||||
```text
|
||||
4096 occupancy states -> need 12 bits
|
||||
shift = 64 - 12
|
||||
```
|
||||
|
||||
Formula:
|
||||
|
||||
```zig
|
||||
index = (blockers *% magic) >> (64 - relevant_bits);
|
||||
```
|
||||
|
||||
Use Zig wrapping multiplication (`*%`) for magic indexing.
|
||||
|
||||
## 9. Move Tables
|
||||
|
||||
Each square has attack entries:
|
||||
|
||||
```zig
|
||||
rook_attacks[64][N]
|
||||
bishop_attacks[64][N]
|
||||
```
|
||||
|
||||
Where `N` depends on occupancy combinations.
|
||||
|
||||
Typical upper bounds:
|
||||
|
||||
- Rook: up to 4096 entries per square
|
||||
- Bishop: up to 512 entries per square
|
||||
|
||||
Each entry stores a `u64` attack bitboard.
|
||||
|
||||
## 10. Runtime Move Generation
|
||||
|
||||
Rook:
|
||||
|
||||
```zig
|
||||
const blockers = board.all_occ & rook_masks[square];
|
||||
const index = (blockers *% rook_magics[square]) >> rook_shifts[square];
|
||||
const moves = rook_attacks[square][index];
|
||||
```
|
||||
|
||||
Bishop:
|
||||
|
||||
```zig
|
||||
const blockers = board.all_occ & bishop_masks[square];
|
||||
const index = (blockers *% bishop_magics[square]) >> bishop_shifts[square];
|
||||
const moves = bishop_attacks[square][index];
|
||||
```
|
||||
|
||||
Queen:
|
||||
|
||||
```zig
|
||||
const queen_moves = rook_moves | bishop_moves;
|
||||
```
|
||||
|
||||
Use OR (`|`), not AND (`&`).
|
||||
|
||||
## 11. Friendly Piece Removal
|
||||
|
||||
Sliding attacks include friendly occupied squares. Remove illegal destinations:
|
||||
|
||||
```zig
|
||||
const legal_moves = attacks & ~friendly_occ;
|
||||
```
|
||||
|
||||
## 12. Captures
|
||||
|
||||
Captures naturally remain in the move set because sliding stops on enemy blockers and includes the enemy blocker square.
|
||||
|
||||
To isolate captures:
|
||||
|
||||
```zig
|
||||
const captures = legal_moves & enemy_occ;
|
||||
```
|
||||
|
||||
## 13. Generating Relevant Occupancy Masks
|
||||
|
||||
Rook rays:
|
||||
|
||||
- North
|
||||
- South
|
||||
- East
|
||||
- West
|
||||
|
||||
Bishop rays:
|
||||
|
||||
- North-east
|
||||
- North-west
|
||||
- South-east
|
||||
- South-west
|
||||
|
||||
For magic relevant occupancy masks, exclude edge squares.
|
||||
|
||||
## 14. Generating All Occupancy Variations
|
||||
|
||||
If a mask has `n` relevant bits, then occupancy count is:
|
||||
|
||||
```text
|
||||
2^n
|
||||
```
|
||||
|
||||
Example:
|
||||
|
||||
```text
|
||||
12 relevant bits -> 4096 occupancy states
|
||||
```
|
||||
|
||||
Generate every occupancy subset.
|
||||
|
||||
## 15. Generating Attack Tables
|
||||
|
||||
For each occupancy subset:
|
||||
|
||||
1. Simulate sliding movement.
|
||||
2. Stop when a blocker is encountered.
|
||||
3. Include the blocker square.
|
||||
4. Save resulting move bitboard.
|
||||
|
||||
## 16. Magic Number Generation
|
||||
|
||||
Magic generation is brute force.
|
||||
|
||||
Algorithm for a square:
|
||||
|
||||
1. Generate all occupancies.
|
||||
2. Generate all attacks.
|
||||
3. Pick random `u64` candidate magic.
|
||||
4. Test `(blockers *% magic) >> shift`.
|
||||
5. Ensure every occupancy maps without harmful collision.
|
||||
6. If collision is harmful, reject magic and try another.
|
||||
|
||||
## 17. Zig-Specific Caveats
|
||||
|
||||
Use wrapping multiplication:
|
||||
|
||||
```zig
|
||||
const index = (blockers *% magic) >> shift;
|
||||
```
|
||||
|
||||
Normal multiplication may trap in safety-checked modes if overflow occurs.
|
||||
|
||||
## 18. Zig Integer Types
|
||||
|
||||
Use explicit types everywhere:
|
||||
|
||||
```zig
|
||||
u64
|
||||
u32
|
||||
usize
|
||||
```
|
||||
|
||||
Avoid implicit casts.
|
||||
|
||||
## 19. Compile-Time Generation
|
||||
|
||||
Zig can generate tables at compile time. Runtime generation is also useful while learning and debugging.
|
||||
|
||||
Potential workflow:
|
||||
|
||||
1. Runtime utility searches for magics and prints/generated tables.
|
||||
2. Generated constants are reviewed.
|
||||
3. Final engine uses embedded constants and precomputed attack tables.
|
||||
|
||||
## 20. Memory Usage
|
||||
|
||||
Approximate attack table RAM usage:
|
||||
|
||||
| Table | Size |
|
||||
| --- | ---: |
|
||||
| Rook attacks | ~800 KB |
|
||||
| Bishop attacks | ~40 KB |
|
||||
| Total | <1 MB |
|
||||
|
||||
## 21. Suggested Development Order
|
||||
|
||||
### Phase 1
|
||||
|
||||
Implement:
|
||||
|
||||
- square numbering
|
||||
- bitboard helpers
|
||||
- occupancy masks
|
||||
|
||||
### Phase 2
|
||||
|
||||
Implement:
|
||||
|
||||
- rook ray generation
|
||||
- bishop ray generation
|
||||
|
||||
### Phase 3
|
||||
|
||||
Implement:
|
||||
|
||||
- occupancy subset generation
|
||||
- attack generation
|
||||
|
||||
### Phase 4
|
||||
|
||||
Implement:
|
||||
|
||||
- brute-force magic finder
|
||||
|
||||
### Phase 5
|
||||
|
||||
Implement:
|
||||
|
||||
- runtime lookup system
|
||||
|
||||
### Phase 6
|
||||
|
||||
Validate against known positions/FENs.
|
||||
|
||||
## 22. Validation Tests
|
||||
|
||||
Verify:
|
||||
|
||||
- edge squares
|
||||
- center squares
|
||||
- empty board
|
||||
- fully blocked board
|
||||
- single blocker
|
||||
- captures
|
||||
- friendly blockers
|
||||
- queen union correctness
|
||||
|
||||
## 23. Final Runtime Formula Summary
|
||||
|
||||
```zig
|
||||
const blockers = board.all_occ & mask;
|
||||
const index = (blockers *% magic) >> shift;
|
||||
const attacks = attack_table[index];
|
||||
const legal_moves = attacks & ~friendly_occ;
|
||||
```
|
||||
|
||||
Queen:
|
||||
|
||||
```zig
|
||||
const queen_moves = rook_moves | bishop_moves;
|
||||
```
|
||||
448
docs/uci-engine-architecture-plan.md
Normal file
@ -0,0 +1,448 @@
|
||||
# UCI Engine Architecture Plan
|
||||
|
||||
## Goal
|
||||
|
||||
Support two families of executables:
|
||||
|
||||
1. **GUI/UI executable**
|
||||
- Renders and tracks games.
|
||||
- Lets humans play/edit/replay games.
|
||||
- Can let people play against bots.
|
||||
- Can spawn bot/engine executables as subprocesses on demand.
|
||||
- Speaks UCI to engine subprocesses.
|
||||
- Can optionally use a user-selected external UCI engine, such as Stockfish, for playback analysis.
|
||||
- Eventually stores games in a database.
|
||||
|
||||
2. **Bot/engine executables**
|
||||
- No GUI.
|
||||
- Implement chess engine logic.
|
||||
- Speak UCI over stdin/stdout.
|
||||
- Can be launched by the GUI.
|
||||
- Can also run as long-lived processes connected to external services such as lichess.org through a service adapter.
|
||||
|
||||
## Important terminology
|
||||
|
||||
In UCI terminology, the **engine** is the side that implements the UCI command loop over stdin/stdout.
|
||||
|
||||
The GUI/app is the **UCI controller/client**:
|
||||
|
||||
```text
|
||||
GUI/controller
|
||||
├── starts engine process
|
||||
├── writes UCI commands to engine stdin
|
||||
├── reads UCI responses from engine stdout
|
||||
└── applies bestmove to the current game
|
||||
```
|
||||
|
||||
The engine executable is the **UCI engine/server-like process**:
|
||||
|
||||
```text
|
||||
Engine
|
||||
├── reads commands from stdin
|
||||
├── maintains/searches position
|
||||
├── writes info/bestmove responses to stdout
|
||||
└── exits on quit
|
||||
```
|
||||
|
||||
Avoid naming the GUI-side wrapper a "server" in code. Prefer names like:
|
||||
|
||||
```text
|
||||
UciEngineClient
|
||||
UciController
|
||||
EngineProcess
|
||||
```
|
||||
|
||||
## Desired long-term process architecture
|
||||
|
||||
```text
|
||||
┌─────────────────────┐
|
||||
│ zig-chess-gui │
|
||||
│ │
|
||||
│ - Vulkan UI │
|
||||
│ - game tracking │
|
||||
│ - human input │
|
||||
│ - playback │
|
||||
│ - database later │
|
||||
│ - UCI client │
|
||||
└──────────┬──────────┘
|
||||
│ UCI stdin/stdout
|
||||
▼
|
||||
┌─────────────────────┐
|
||||
│ zig-chess-engine-* │
|
||||
│ │
|
||||
│ - evaluation │
|
||||
│ - search │
|
||||
│ - move selection │
|
||||
│ - UCI command loop │
|
||||
└─────────────────────┘
|
||||
```
|
||||
|
||||
Future external-service architecture:
|
||||
|
||||
```text
|
||||
┌─────────────────────┐
|
||||
│ lichess-adapter │
|
||||
│ │
|
||||
│ - lichess API │
|
||||
│ - account/game I/O │
|
||||
│ - UCI client │
|
||||
└──────────┬──────────┘
|
||||
│ UCI stdin/stdout or socket/process bridge
|
||||
▼
|
||||
┌─────────────────────┐
|
||||
│ zig-chess-engine-* │
|
||||
└─────────────────────┘
|
||||
```
|
||||
|
||||
Key design principle:
|
||||
|
||||
```text
|
||||
GUI ───────────────┐
|
||||
├── UCI ── engine executable
|
||||
Lichess adapter ───┘
|
||||
```
|
||||
|
||||
The engine should not know whether it is being used by the GUI, a test harness, or a lichess adapter.
|
||||
|
||||
## Suggested source layout
|
||||
|
||||
Possible future layout:
|
||||
|
||||
```text
|
||||
src/
|
||||
├── chess/ // board/game/FEN/PGN/legal moves
|
||||
├── main.zig // current GUI entry point, or later src/gui/main.zig
|
||||
├── uci/
|
||||
│ ├── protocol.zig // shared UCI parse/format types
|
||||
│ ├── client.zig // GUI/service side: controls engine process
|
||||
│ └── server.zig // engine side: stdin/stdout command loop
|
||||
├── engine/
|
||||
│ ├── engine.zig // common engine interface
|
||||
│ ├── eval.zig // static evaluation
|
||||
│ ├── search.zig // search algorithms
|
||||
│ └── perft.zig // move-generator validation tooling
|
||||
└── bots/
|
||||
├── random.zig // random/legal-move bot executable
|
||||
├── material.zig // material-eval bot executable
|
||||
└── search.zig // stronger search bot executable
|
||||
```
|
||||
|
||||
This layout can evolve. The important boundary is that engine executables communicate with outside controllers through UCI.
|
||||
|
||||
## Build targets
|
||||
|
||||
Eventually `build.zig` should produce multiple executables, for example:
|
||||
|
||||
```text
|
||||
zig-chess-gui
|
||||
zig-chess-random-bot
|
||||
zig-chess-material-bot
|
||||
zig-chess-search-bot
|
||||
```
|
||||
|
||||
The GUI target links rendering/UI code.
|
||||
|
||||
The engine targets should avoid linking Vulkan/windowing code.
|
||||
|
||||
## Analysis engine policy
|
||||
|
||||
The GUI should support an optional external UCI engine for real-time analysis during game playback.
|
||||
|
||||
Primary intended use case:
|
||||
|
||||
```text
|
||||
Playback mode
|
||||
├── user steps through game
|
||||
├── GUI sends current position to analysis engine
|
||||
├── analysis engine returns eval/PV/best line
|
||||
└── GUI displays analysis to the user
|
||||
```
|
||||
|
||||
This analysis engine is separate from project bot engines:
|
||||
|
||||
- It is for user-facing analysis only.
|
||||
- It must not be used by project bot engines as their search/evaluation implementation.
|
||||
- Project bots should have their own evaluation/search logic.
|
||||
- The GUI should treat the analysis engine as just another external UCI process.
|
||||
|
||||
### Stockfish licensing policy
|
||||
|
||||
Stockfish is GPL-3.0 licensed. Because this project is intended to be MIT licensed, do **not** bundle Stockfish directly into the executable or source distribution by default.
|
||||
|
||||
Preferred approaches:
|
||||
|
||||
1. **User-provided engine path**
|
||||
- User installs Stockfish or another UCI engine separately.
|
||||
- GUI stores/configures the path.
|
||||
- GUI launches it as an external process.
|
||||
|
||||
2. **Optional download flow**
|
||||
- GUI can offer to download Stockfish or guide the user through downloading it.
|
||||
- The download should be explicit and optional.
|
||||
- The UI should clearly identify the engine and its license before download/use.
|
||||
- Keep downloaded engine files outside the MIT-licensed source tree/release bundle unless licensing obligations are intentionally handled.
|
||||
|
||||
3. **Generic UCI analysis engine support**
|
||||
- Do not hard-code Stockfish as the only option.
|
||||
- Any compatible UCI engine path should work.
|
||||
|
||||
### Analysis engine UI/config needs
|
||||
|
||||
Eventually the GUI should provide:
|
||||
|
||||
- analysis engine executable path
|
||||
- enable/disable analysis
|
||||
- analysis depth or movetime
|
||||
- optional thread/hash settings via UCI options
|
||||
- current eval display
|
||||
- principal variation display
|
||||
- best move display
|
||||
- start/stop analysis when stepping through playback
|
||||
|
||||
Suggested app-side abstraction:
|
||||
|
||||
```text
|
||||
AnalysisEngine
|
||||
├── engine_process / UciEngineClient
|
||||
├── executable_path
|
||||
├── enabled
|
||||
├── depth or movetime limit
|
||||
├── latest_score
|
||||
├── latest_pv
|
||||
└── latest_bestmove
|
||||
```
|
||||
|
||||
## UCI protocol responsibilities
|
||||
|
||||
### GUI/client side
|
||||
|
||||
The GUI should be able to:
|
||||
|
||||
- Spawn an engine executable.
|
||||
- Send UCI commands.
|
||||
- Read engine output asynchronously or incrementally.
|
||||
- Track engine readiness.
|
||||
- Track available engine options.
|
||||
- Send current game position.
|
||||
- Request a move/search.
|
||||
- Stop a search.
|
||||
- Shut down the engine process cleanly.
|
||||
|
||||
Common commands sent by GUI:
|
||||
|
||||
```text
|
||||
uci
|
||||
isready
|
||||
ucinewgame
|
||||
position startpos
|
||||
position startpos moves e2e4 e7e5
|
||||
position fen <fen fields> moves ...
|
||||
go depth 5
|
||||
go movetime 1000
|
||||
stop
|
||||
quit
|
||||
```
|
||||
|
||||
Common responses parsed by GUI:
|
||||
|
||||
```text
|
||||
id name <name>
|
||||
id author <author>
|
||||
option name <name> type <type> ...
|
||||
uciok
|
||||
readyok
|
||||
info depth 3 score cp 20 nodes 1234 time 10 pv e2e4 e7e5
|
||||
bestmove e2e4
|
||||
bestmove e2e4 ponder e7e5
|
||||
```
|
||||
|
||||
### Engine/server side
|
||||
|
||||
Each engine executable should:
|
||||
|
||||
- Read one line at a time from stdin.
|
||||
- Parse UCI commands.
|
||||
- Maintain an internal position.
|
||||
- Search when given `go`.
|
||||
- Print `bestmove ...` when done.
|
||||
- Print `info ...` optionally during search.
|
||||
- Respond to `stop` promptly.
|
||||
- Exit on `quit`.
|
||||
|
||||
## First milestone: minimal UCI engine executable
|
||||
|
||||
Implement an engine executable that supports only:
|
||||
|
||||
Input:
|
||||
|
||||
```text
|
||||
uci
|
||||
isready
|
||||
quit
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
```text
|
||||
id name ZigChess Random Bot
|
||||
id author WayfinderAK
|
||||
uciok
|
||||
readyok
|
||||
```
|
||||
|
||||
No search or move generation required yet.
|
||||
|
||||
Purpose:
|
||||
|
||||
- Prove executable separation.
|
||||
- Prove stdin/stdout loop.
|
||||
- Prove GUI/test harness can launch and communicate with engine.
|
||||
|
||||
## Second milestone: GUI-side engine process wrapper
|
||||
|
||||
Implement a UI/application-side wrapper that can:
|
||||
|
||||
- Start an engine executable by path.
|
||||
- Send `uci`.
|
||||
- Wait for `uciok`.
|
||||
- Send `isready`.
|
||||
- Wait for `readyok`.
|
||||
- Send `quit` on shutdown.
|
||||
|
||||
Suggested name:
|
||||
|
||||
```zig
|
||||
UciEngineClient
|
||||
```
|
||||
|
||||
or:
|
||||
|
||||
```zig
|
||||
EngineProcess
|
||||
```
|
||||
|
||||
This code belongs on the GUI/controller side, not inside the engine bot.
|
||||
|
||||
## Third milestone: position and bestmove
|
||||
|
||||
Add UCI support for:
|
||||
|
||||
```text
|
||||
position startpos
|
||||
position startpos moves e2e4 e7e5
|
||||
position fen <fen> moves ...
|
||||
go depth 1
|
||||
bestmove <move>
|
||||
```
|
||||
|
||||
For the first engine, `go depth 1` can simply pick the first legal move or a random legal move.
|
||||
|
||||
Move format is UCI long algebraic coordinate format:
|
||||
|
||||
```text
|
||||
e2e4
|
||||
e7e8q
|
||||
e1g1
|
||||
```
|
||||
|
||||
This is different from SAN/PGN notation.
|
||||
|
||||
## Fourth milestone: reusable protocol module
|
||||
|
||||
Create shared parse/format helpers for UCI text:
|
||||
|
||||
```text
|
||||
src/uci/protocol.zig
|
||||
```
|
||||
|
||||
Potential responsibilities:
|
||||
|
||||
- Parse GUI-to-engine commands.
|
||||
- Parse engine-to-GUI responses.
|
||||
- Format moves as UCI coordinate moves.
|
||||
- Parse UCI coordinate moves into from/to/promotion.
|
||||
- Format `position ...` commands from a `Game` move list.
|
||||
|
||||
Keep protocol parsing separate from subprocess management and separate from search/evaluation.
|
||||
|
||||
## Fifth milestone: stronger engines
|
||||
|
||||
Once the protocol boundary works:
|
||||
|
||||
- random legal move engine
|
||||
- material evaluation engine
|
||||
- shallow search engine
|
||||
- alpha-beta search engine
|
||||
- improved move ordering
|
||||
- transposition table
|
||||
- time management
|
||||
|
||||
Each stronger engine can be its own executable or selected by options/config.
|
||||
|
||||
## Future lichess adapter
|
||||
|
||||
Do not put lichess-specific code inside the engine.
|
||||
|
||||
Instead, later create a separate adapter/service that:
|
||||
|
||||
- talks to lichess.org APIs
|
||||
- manages authentication/account events
|
||||
- receives games/moves from lichess
|
||||
- launches or connects to an engine executable
|
||||
- sends positions/search commands via UCI
|
||||
- sends engine moves back to lichess
|
||||
|
||||
This keeps engines reusable by:
|
||||
|
||||
- GUI
|
||||
- lichess adapter
|
||||
- local match runner
|
||||
- tournament harness
|
||||
- tests/benchmarks
|
||||
|
||||
## Future playback analysis milestone
|
||||
|
||||
After the generic UCI client exists, add analysis support to playback mode:
|
||||
|
||||
1. Let the user configure an external UCI engine path.
|
||||
2. Start that engine as an analysis process.
|
||||
3. Send `uci` / `isready` handshake.
|
||||
4. When playback cursor changes, send:
|
||||
|
||||
```text
|
||||
position fen <current playback FEN>
|
||||
go depth <n>
|
||||
```
|
||||
|
||||
or:
|
||||
|
||||
```text
|
||||
position fen <current playback FEN>
|
||||
go movetime <ms>
|
||||
```
|
||||
|
||||
5. Parse `info` lines for score and PV.
|
||||
6. Display the latest analysis in the UI.
|
||||
7. Send `stop` when the user moves to another ply or disables analysis.
|
||||
8. Send `quit` when closing the app or changing engine path.
|
||||
|
||||
Keep this separate from bot-vs-human play. The analysis engine is an advisor for the user, not the implementation of project bots.
|
||||
|
||||
## References
|
||||
|
||||
Primary UCI reference:
|
||||
|
||||
- Stefan Meyer-Kahlen, **Universal Chess Interface (UCI)** protocol, April 2006. Commonly distributed as `uci.txt` and mirrored by chess-programming resources. Covers `uci`, `isready`, `position`, `go`, `stop`, `quit`, `info`, `bestmove`, and engine options.
|
||||
|
||||
Stockfish licensing/reference:
|
||||
|
||||
- Official Stockfish repository: https://github.com/official-stockfish/Stockfish
|
||||
- Stockfish `Copying.txt`: GNU General Public License v3.0, https://github.com/official-stockfish/Stockfish/blob/master/Copying.txt
|
||||
- Because the project goal is MIT licensing, Stockfish should be user-provided or optionally downloaded rather than bundled by default.
|
||||
|
||||
Useful related notation distinction:
|
||||
|
||||
- UCI moves are coordinate moves such as `e2e4` or `e7e8q`.
|
||||
- PGN/SAN moves are display/game-record notation such as `e4`, `Nf3`, `O-O`, `Qxf7#`.
|
||||
- The GUI move list should display PGN/SAN.
|
||||
- Engine communication should use UCI coordinate moves.
|
||||
382
docs/ui-move-list-playback-plan.md
Normal file
@ -0,0 +1,382 @@
|
||||
# UI Move List and Playback Mode Plan
|
||||
|
||||
## Goal
|
||||
|
||||
Add UI support for:
|
||||
|
||||
- A fixed-size move list panel on the left side of the board.
|
||||
- Two move columns: White move and Black move, with turn number on the left.
|
||||
- Standard PGN/SAN notation display.
|
||||
- Scroll support when the move list is longer than the visible panel.
|
||||
- A move-list button that copies the current game as PGN to the clipboard.
|
||||
- A playback-only vertical analysis bar between the board and move list.
|
||||
- A board-orientation control that can render from White's perspective or Black's perspective.
|
||||
- A new playback mode where pieces cannot be moved, and the user can step through a loaded game.
|
||||
- In edit mode, a UI control to choose whose move is next.
|
||||
- Mode-specific paste behavior:
|
||||
- Edit mode: `Ctrl+V` pastes FEN.
|
||||
- Playback mode: `Ctrl+V` pastes PGN.
|
||||
- Play mode: no FEN/PGN paste.
|
||||
|
||||
## Collaboration rule
|
||||
|
||||
The assistant should manage only UI/application-side changes for this feature.
|
||||
|
||||
Do **not** edit files under `src/chess/` unless explicitly requested.
|
||||
|
||||
When UI work needs chess-layer functionality, stop and describe the required chess API/data shape so the project owner can implement it.
|
||||
|
||||
## Current state
|
||||
|
||||
- `BoardState` owns the current chess position.
|
||||
- `src/chess/game.zig` has a basic `Game` framework with:
|
||||
- `initial_state`
|
||||
- `state`
|
||||
- `moves`
|
||||
- `time_control`
|
||||
- `clocks`
|
||||
- `status`
|
||||
- UI currently uses `current_state` directly in `src/main.zig`.
|
||||
- Existing modes are:
|
||||
- `play`
|
||||
- `edit`
|
||||
- Existing paste behavior parses clipboard as FEN regardless of mode.
|
||||
- FEN copy is available via `Ctrl+C`.
|
||||
- Desired copy behavior: `Ctrl+C` should continue to copy FEN in every mode. PGN copy should use an explicit move-list button, not replace `Ctrl+C`.
|
||||
|
||||
## Desired architecture
|
||||
|
||||
Eventually, UI should treat `Game` as the game/session model:
|
||||
|
||||
```text
|
||||
UI/App
|
||||
├── current mode: play | edit | playback
|
||||
├── active Game
|
||||
├── playback cursor / ply index
|
||||
├── move list scroll offset
|
||||
├── board orientation: white perspective | black perspective
|
||||
├── analysis display state for playback mode
|
||||
└── rendering state
|
||||
```
|
||||
|
||||
The chess layer should provide enough data for the UI to display moves and reconstruct positions, but the UI should not implement chess notation rules itself.
|
||||
|
||||
## Stage 0: Board orientation and edit-side-to-move controls
|
||||
|
||||
### UI work
|
||||
|
||||
- Add app/UI state for board orientation:
|
||||
|
||||
```text
|
||||
white perspective: current rendering
|
||||
black perspective: visually flipped board, with White pieces/ranks at the top and Black pieces/ranks at the bottom
|
||||
```
|
||||
|
||||
- This must be a purely visual transform.
|
||||
- Keep the same chess square numbering and logical coordinates:
|
||||
- `a1` is still square `0`
|
||||
- White still starts on ranks 1 and 2 logically
|
||||
- Black still starts on ranks 7 and 8 logically
|
||||
- Update all board visual mappings consistently:
|
||||
- square rendering
|
||||
- piece rendering
|
||||
- coordinate labels
|
||||
- mouse hit-testing
|
||||
- hover/selection/check/checkmate highlights
|
||||
- valid/legal move dots
|
||||
- dragging source/target display
|
||||
- promotion popup placement
|
||||
- Add a UI control to toggle orientation, probably near the mode buttons or move list panel.
|
||||
- In edit mode, add a control to choose whose move is next:
|
||||
- White to move
|
||||
- Black to move
|
||||
- Changing side-to-move in edit mode should update only the current board state's active color/turn field and then rebuild rendering.
|
||||
- In non-edit modes, the side-to-move selector should either be hidden or disabled.
|
||||
|
||||
### Chess-layer needs
|
||||
|
||||
No chess-rule changes are required for board orientation. It is visual only.
|
||||
|
||||
For edit-side-to-move, the UI needs mutable access to the current position's active color. If `BoardState.turn` remains public, no new chess API is required. If you later make it private, add a small setter such as:
|
||||
|
||||
```zig
|
||||
pub fn setTurn(self: *BoardState, color: piece.Color) void
|
||||
```
|
||||
|
||||
## Stage 1: Add playback mode UI shell
|
||||
|
||||
### UI work
|
||||
|
||||
- Add `playback` to the app/input mode enum.
|
||||
- Add a third mode button, likely labeled `VIEW` or `PGN`/`REPLAY`.
|
||||
- In playback mode:
|
||||
- ignore board clicks/drags for moving pieces
|
||||
- no palette editing
|
||||
- no legal-move highlight generation
|
||||
- Keep reset behavior available or decide whether reset resets the active game/playback.
|
||||
|
||||
### Chess-layer needs
|
||||
|
||||
None.
|
||||
|
||||
## Stage 2: Make paste mode-specific
|
||||
|
||||
### UI work
|
||||
|
||||
Change clipboard paste behavior:
|
||||
|
||||
- If mode is `.edit`:
|
||||
- parse clipboard as FEN
|
||||
- replace current board state/game position
|
||||
- If mode is `.playback`:
|
||||
- send clipboard text to a future PGN import function
|
||||
- for now, log that PGN paste is not yet implemented if chess API is missing
|
||||
- If mode is `.play`:
|
||||
- ignore paste or log debug message
|
||||
|
||||
### Chess-layer needs
|
||||
|
||||
Eventually needed:
|
||||
|
||||
```zig
|
||||
pub fn loadPgn(allocator: std.mem.Allocator, pgn_text: []const u8) !Game
|
||||
```
|
||||
|
||||
or equivalent parse API.
|
||||
|
||||
For stage 2, this can be stubbed from the UI side with no chess changes.
|
||||
|
||||
## Stage 3: Move list panel with placeholder/static data
|
||||
|
||||
### UI work
|
||||
|
||||
- Define a fixed rectangle left of the board.
|
||||
- Render panel background and border.
|
||||
- Render column headers, for example:
|
||||
|
||||
```text
|
||||
# White Black
|
||||
```
|
||||
|
||||
- Add a visible PGN copy button inside or attached to the move list panel.
|
||||
- Clicking it should copy the current game PGN to the clipboard once chess-layer PGN export exists.
|
||||
- Until PGN export exists, the button can be visually present but log that export is not implemented.
|
||||
- Render rows using temporary/static strings or currently available move placeholders.
|
||||
- Establish layout constants:
|
||||
- panel left/right/top/bottom
|
||||
- row height
|
||||
- turn-number column x
|
||||
- white column x
|
||||
- black column x
|
||||
- max visible rows
|
||||
|
||||
### Chess-layer needs
|
||||
|
||||
None for placeholder rendering.
|
||||
|
||||
## Stage 4: Record/display move notation from played games
|
||||
|
||||
### UI work
|
||||
|
||||
- Replace direct `current_state.move(...)` calls with `game.makeMove(...)` once `Game` is wired into `main.zig`.
|
||||
- Read `game.moves` and draw move rows.
|
||||
- Update the move list after every played move.
|
||||
|
||||
### Chess-layer needs
|
||||
|
||||
The `Game` move history should expose display notation for each ply.
|
||||
|
||||
Suggested shape:
|
||||
|
||||
```zig
|
||||
pub const MoveRecord = struct {
|
||||
from: bitboard.Square,
|
||||
to: bitboard.Square,
|
||||
promotion: ?piece.PieceType,
|
||||
san: []const u8, // or fixed buffer / owned slice
|
||||
time_remaining: u32,
|
||||
};
|
||||
```
|
||||
|
||||
If avoiding heap allocation in each move record, possible alternatives:
|
||||
|
||||
```zig
|
||||
san: [16]u8,
|
||||
san_len: u8,
|
||||
```
|
||||
|
||||
The chess layer should generate SAN before mutating the board, because SAN depends on the pre-move position.
|
||||
|
||||
Needed API could be:
|
||||
|
||||
```zig
|
||||
pub fn makeMove(self: *Game, allocator: std.mem.Allocator, from: Square, to: Square, promotion: ?PieceType) !void
|
||||
```
|
||||
|
||||
or:
|
||||
|
||||
```zig
|
||||
pub fn formatSanMove(allocator: std.mem.Allocator, state_before: BoardState, from: Square, to: Square, promotion: ?PieceType) ![]u8
|
||||
```
|
||||
|
||||
## Stage 5: Playback analysis bar
|
||||
|
||||
### UI work
|
||||
|
||||
- Add a vertical analysis bar between the board and the move list panel.
|
||||
- The analysis bar should be visible only in playback mode.
|
||||
- The bar is divided into a White section and a Black section.
|
||||
- When analysis is equal, the White/Black divide should be centered.
|
||||
- When White is winning, the White portion grows and the divide moves toward Black's side.
|
||||
- When Black is winning, the Black portion grows and the divide moves toward White's side.
|
||||
- The bar is purely display; it should not affect board state or move legality.
|
||||
- Initial placeholder behavior can render the bar at 50/50 until analysis data exists.
|
||||
- Later, map engine evaluation scores to a display fraction.
|
||||
|
||||
Suggested visual mapping:
|
||||
|
||||
```text
|
||||
score = 0.00 pawns -> 50% White / 50% Black
|
||||
score > 0, White better -> White section larger
|
||||
score < 0, Black better -> Black section larger
|
||||
```
|
||||
|
||||
Use a bounded/nonlinear mapping so very large evaluations do not immediately consume the whole bar. For example, later UI code could map centipawns to a normalized value with a clamp or sigmoid-like curve. Exact mapping can be tuned visually.
|
||||
|
||||
### Chess/engine-layer needs
|
||||
|
||||
No chess-layer changes are needed for placeholder rendering.
|
||||
|
||||
Eventually, playback analysis needs external UCI analysis data:
|
||||
|
||||
```text
|
||||
score cp <centipawns>
|
||||
score mate <moves>
|
||||
pv <principal variation moves>
|
||||
```
|
||||
|
||||
The UI only needs a normalized analysis value or raw score from the UCI analysis client.
|
||||
|
||||
## Stage 6: Scrolling move list
|
||||
|
||||
### UI work
|
||||
|
||||
- Track `move_list_scroll_row` or similar state in `main.zig`.
|
||||
- Add mouse wheel handling when cursor is over the move panel.
|
||||
- Clamp scroll offset between `0` and `max(0, total_rows - visible_rows)`.
|
||||
- Optional later: visual scrollbar.
|
||||
|
||||
### Chess-layer needs
|
||||
|
||||
None.
|
||||
|
||||
## Stage 7: Playback cursor and stepping
|
||||
|
||||
### UI work
|
||||
|
||||
- Add `playback_ply_index` app state.
|
||||
- In playback mode:
|
||||
- Right arrow: advance one ply
|
||||
- Left arrow: go back one ply
|
||||
- Home: first position
|
||||
- End: final position
|
||||
- Render the board for the current playback ply, not necessarily the final game state.
|
||||
- Highlight or mark the current row/ply in the move list.
|
||||
|
||||
### Chess-layer needs
|
||||
|
||||
Need a way to reconstruct board state at an arbitrary ply.
|
||||
|
||||
Possible API:
|
||||
|
||||
```zig
|
||||
pub fn stateAtPly(self: *const Game, ply_index: usize) board.BoardState
|
||||
```
|
||||
|
||||
or, if allocation/errors are involved:
|
||||
|
||||
```zig
|
||||
pub fn replayToPly(self: *const Game, ply_index: usize) !board.BoardState
|
||||
```
|
||||
|
||||
This can replay from `initial_state` through the first `ply_index` moves.
|
||||
|
||||
## Stage 8: PGN paste/import and PGN copy/export
|
||||
|
||||
### UI work
|
||||
|
||||
- In playback mode, `Ctrl+V` gets clipboard text and passes it to chess PGN parser.
|
||||
- Keep `Ctrl+C` as FEN copy in every mode.
|
||||
- Add click handling for the move-list PGN copy button.
|
||||
- When the PGN copy button is clicked, copy exported PGN to the clipboard.
|
||||
- On success:
|
||||
- replace active game with parsed game
|
||||
- set mode to playback or remain playback
|
||||
- set playback cursor to final ply or start; choose behavior intentionally
|
||||
- reset move-list scroll
|
||||
- On failure:
|
||||
- log a warning
|
||||
- keep current game unchanged
|
||||
|
||||
### Chess-layer needs
|
||||
|
||||
PGN parser that returns a `Game` with move history and final state, plus PGN export for the current game.
|
||||
|
||||
Suggested API:
|
||||
|
||||
```zig
|
||||
pub fn initFromPgn(allocator: std.mem.Allocator, pgn_text: []const u8, time_control: TimeControl) !Game
|
||||
```
|
||||
|
||||
or:
|
||||
|
||||
```zig
|
||||
pub fn parsePgn(allocator: std.mem.Allocator, pgn_text: []const u8) !Game
|
||||
```
|
||||
|
||||
Suggested export API:
|
||||
|
||||
```zig
|
||||
pub fn formatPgn(allocator: std.mem.Allocator, game: Game) ![]u8
|
||||
```
|
||||
|
||||
or as a method:
|
||||
|
||||
```zig
|
||||
pub fn formatPgn(self: *const Game, allocator: std.mem.Allocator) ![]u8
|
||||
```
|
||||
|
||||
Parser should understand at least:
|
||||
|
||||
- move numbers: `1.`, `1...`
|
||||
- SAN moves
|
||||
- castling: `O-O`, `O-O-O`
|
||||
- captures: `x`
|
||||
- promotion: `=Q`, `=R`, `=B`, `=N`
|
||||
- check/checkmate suffixes: `+`, `#`
|
||||
- game termination markers: `1-0`, `0-1`, `1/2-1/2`, `*`
|
||||
- comments and tags can be added later if desired
|
||||
|
||||
Reference: Steven J. Edwards, *Portable Game Notation Specification and Implementation Guide*, especially movetext/SAN sections.
|
||||
|
||||
## Stage 9: Optional polish
|
||||
|
||||
- Auto-scroll move list to latest move in play mode.
|
||||
- Click a move row in playback mode to jump to that ply.
|
||||
- Show current move highlight.
|
||||
- Add scrollbar.
|
||||
- Show numeric eval next to the analysis bar.
|
||||
- Show best line/PV next to or below the move list.
|
||||
- Add richer PGN copy/export options, such as including/excluding tag pairs.
|
||||
- Display game result in the move panel.
|
||||
- Show time remaining per move later if clocks are implemented.
|
||||
|
||||
## Immediate next action
|
||||
|
||||
Start with Stage 0, then Stage 1 and Stage 2 in UI-only files:
|
||||
|
||||
- `src/board_input.zig`
|
||||
- `src/text_render.zig`
|
||||
- `src/main.zig`
|
||||
|
||||
Do not modify `src/chess/` for these stages.
|
||||
11
shaders/piece.frag
Normal file
@ -0,0 +1,11 @@
|
||||
#version 450
|
||||
|
||||
layout(set = 0, binding = 0) uniform sampler2D piece_texture;
|
||||
|
||||
layout(location = 0) in vec4 frag_color;
|
||||
layout(location = 1) in vec2 frag_uv;
|
||||
layout(location = 0) out vec4 out_color;
|
||||
|
||||
void main() {
|
||||
out_color = texture(piece_texture, frag_uv) * frag_color;
|
||||
}
|
||||
14
shaders/piece.vert
Normal file
@ -0,0 +1,14 @@
|
||||
#version 450
|
||||
|
||||
layout(location = 0) in vec2 in_position;
|
||||
layout(location = 1) in vec4 in_color;
|
||||
layout(location = 2) in vec2 in_uv;
|
||||
|
||||
layout(location = 0) out vec4 frag_color;
|
||||
layout(location = 1) out vec2 frag_uv;
|
||||
|
||||
void main() {
|
||||
gl_Position = vec4(in_position, 0.0, 1.0);
|
||||
frag_color = in_color;
|
||||
frag_uv = in_uv;
|
||||
}
|
||||
@ -1,7 +1,8 @@
|
||||
#version 450
|
||||
|
||||
layout(location = 0) in vec4 frag_color;
|
||||
layout(location = 0) out vec4 out_color;
|
||||
|
||||
void main() {
|
||||
out_color = vec4(0.1, 0.8, 1.0, 1.0);
|
||||
out_color = frag_color;
|
||||
}
|
||||
|
||||
@ -1,7 +1,11 @@
|
||||
#version 450
|
||||
|
||||
layout(location=0) in vec2 in_position;
|
||||
layout(location = 0) in vec2 in_position;
|
||||
layout(location = 1) in vec4 in_color;
|
||||
|
||||
layout(location = 0) out vec4 frag_color;
|
||||
|
||||
void main() {
|
||||
gl_Position = vec4(in_position, 0.0, 1.0);
|
||||
frag_color = in_color;
|
||||
}
|
||||
|
||||
18
src/assets.zig
Normal file
@ -0,0 +1,18 @@
|
||||
pub const piece_width = 256;
|
||||
pub const piece_height = 256;
|
||||
pub const piece_channels = 4;
|
||||
pub const piece_byte_len = piece_width * piece_height * piece_channels;
|
||||
|
||||
pub const white_pawn_rgba = @embedFile("white_pawn_rgba");
|
||||
pub const white_knight_rgba = @embedFile("white_knight_rgba");
|
||||
pub const white_bishop_rgba = @embedFile("white_bishop_rgba");
|
||||
pub const white_rook_rgba = @embedFile("white_rook_rgba");
|
||||
pub const white_queen_rgba = @embedFile("white_queen_rgba");
|
||||
pub const white_king_rgba = @embedFile("white_king_rgba");
|
||||
|
||||
pub const black_pawn_rgba = @embedFile("black_pawn_rgba");
|
||||
pub const black_knight_rgba = @embedFile("black_knight_rgba");
|
||||
pub const black_bishop_rgba = @embedFile("black_bishop_rgba");
|
||||
pub const black_rook_rgba = @embedFile("black_rook_rgba");
|
||||
pub const black_queen_rgba = @embedFile("black_queen_rgba");
|
||||
pub const black_king_rgba = @embedFile("black_king_rgba");
|
||||
516
src/board_input.zig
Normal file
@ -0,0 +1,516 @@
|
||||
const std = @import("std");
|
||||
|
||||
const chess_board = @import("chess/board.zig");
|
||||
const bitboard = @import("chess/bitboard.zig");
|
||||
const geometry = @import("geometry.zig");
|
||||
const piece = @import("chess/piece.zig");
|
||||
const piece_render = @import("piece_render.zig");
|
||||
|
||||
pub const SquareCoord = struct {
|
||||
file: u3,
|
||||
rank: u3,
|
||||
};
|
||||
|
||||
pub const MoveRequest = struct {
|
||||
from: bitboard.Square,
|
||||
to: bitboard.Square,
|
||||
};
|
||||
|
||||
pub const ClickResult = union(enum) {
|
||||
none,
|
||||
selected: SquareCoord,
|
||||
cleared,
|
||||
move_requested: MoveRequest,
|
||||
};
|
||||
|
||||
pub const InputMode = enum {
|
||||
play,
|
||||
edit,
|
||||
};
|
||||
|
||||
pub const ModeButton = enum {
|
||||
play,
|
||||
edit,
|
||||
reset,
|
||||
};
|
||||
|
||||
pub const PendingDragState = struct {
|
||||
source: SquareCoord,
|
||||
encoded: u4,
|
||||
start_ndc: [2]f32,
|
||||
previous_selection: ?SquareCoord,
|
||||
};
|
||||
|
||||
pub const DragState = struct {
|
||||
source: SquareCoord,
|
||||
encoded: u4,
|
||||
cursor_ndc: [2]f32,
|
||||
previous_selection: ?SquareCoord,
|
||||
};
|
||||
|
||||
pub const InteractionState = struct {
|
||||
selection: SelectionState = .{},
|
||||
selected_palette_piece: ?u4 = null,
|
||||
pending_drag: ?PendingDragState = null,
|
||||
current_drag: ?DragState = null,
|
||||
};
|
||||
|
||||
pub const PressResult = union(enum) {
|
||||
none,
|
||||
mode_changed: InputMode,
|
||||
reset_requested,
|
||||
palette_changed,
|
||||
edit_place: struct { square: SquareCoord, encoded: u4 },
|
||||
edit_clear: SquareCoord,
|
||||
play_selected: bitboard.Square,
|
||||
play_cleared,
|
||||
play_move_requested: MoveRequest,
|
||||
};
|
||||
|
||||
pub const DragUpdateResult = union(enum) {
|
||||
none,
|
||||
drag_started: bitboard.Square,
|
||||
drag_moved,
|
||||
};
|
||||
|
||||
pub const ReleaseResult = union(enum) {
|
||||
none,
|
||||
click_without_drag,
|
||||
drag_cancelled: ?bitboard.Square,
|
||||
drag_selected: bitboard.Square,
|
||||
drag_cleared,
|
||||
drag_move_requested: MoveRequest,
|
||||
};
|
||||
|
||||
fn squareCoordEql(a: SquareCoord, b: SquareCoord) bool {
|
||||
return a.file == b.file and a.rank == b.rank;
|
||||
}
|
||||
|
||||
pub fn coordToSquare(coord: SquareCoord) bitboard.Square {
|
||||
return @intCast((@as(u6, coord.rank) * 8) + @as(u6, coord.file));
|
||||
}
|
||||
|
||||
pub fn squareToCoord(square: bitboard.Square) SquareCoord {
|
||||
return .{
|
||||
.file = @intCast(square % 8),
|
||||
.rank = @intCast(square / 8),
|
||||
};
|
||||
}
|
||||
|
||||
pub const SelectionState = struct {
|
||||
selected: ?SquareCoord = null,
|
||||
|
||||
pub fn handleClick(
|
||||
self: *SelectionState,
|
||||
state: chess_board.BoardState,
|
||||
maybe_square: ?SquareCoord,
|
||||
) ClickResult {
|
||||
const square = maybe_square orelse {
|
||||
self.selected = null;
|
||||
return .cleared;
|
||||
};
|
||||
|
||||
const clicked_piece = state.getSquare(coordToSquare(square));
|
||||
const clicked_color = piece.colorOf(clicked_piece);
|
||||
|
||||
if (self.selected) |from| {
|
||||
if (squareCoordEql(from, square)) {
|
||||
self.selected = null;
|
||||
return .cleared;
|
||||
}
|
||||
|
||||
if (clicked_color == state.turn) {
|
||||
self.selected = square;
|
||||
return .{ .selected = square };
|
||||
}
|
||||
|
||||
self.selected = null;
|
||||
return .{ .move_requested = .{
|
||||
.from = coordToSquare(from),
|
||||
.to = coordToSquare(square),
|
||||
} };
|
||||
}
|
||||
|
||||
if (clicked_color != state.turn) {
|
||||
return .none;
|
||||
}
|
||||
|
||||
self.selected = square;
|
||||
return .{ .selected = square };
|
||||
}
|
||||
};
|
||||
|
||||
fn ndcNearlyEqual(a: [2]f32, b: [2]f32) bool {
|
||||
const epsilon: f32 = 0.002;
|
||||
return @abs(a[0] - b[0]) < epsilon and @abs(a[1] - b[1]) < epsilon;
|
||||
}
|
||||
|
||||
fn hasDraggedFarEnough(start: [2]f32, current: [2]f32) bool {
|
||||
const threshold: f32 = 0.025;
|
||||
const dx = current[0] - start[0];
|
||||
const dy = current[1] - start[1];
|
||||
return (dx * dx + dy * dy) >= threshold * threshold;
|
||||
}
|
||||
|
||||
pub fn hoveredSquareForState(_: InteractionState, _: InputMode, cursor_square: ?SquareCoord) ?SquareCoord {
|
||||
return cursor_square;
|
||||
}
|
||||
|
||||
pub fn handleDragUpdate(state: *InteractionState, cursor_ndc: ?[2]f32) DragUpdateResult {
|
||||
const ndc = cursor_ndc orelse return .none;
|
||||
|
||||
if (state.pending_drag) |pending| {
|
||||
if (hasDraggedFarEnough(pending.start_ndc, ndc)) {
|
||||
state.current_drag = .{
|
||||
.source = pending.source,
|
||||
.encoded = pending.encoded,
|
||||
.cursor_ndc = ndc,
|
||||
.previous_selection = pending.previous_selection,
|
||||
};
|
||||
state.pending_drag = null;
|
||||
state.selection.selected = null;
|
||||
return .{ .drag_started = coordToSquare(state.current_drag.?.source) };
|
||||
}
|
||||
}
|
||||
|
||||
if (state.current_drag) |*drag| {
|
||||
if (!ndcNearlyEqual(drag.cursor_ndc, ndc)) {
|
||||
drag.cursor_ndc = ndc;
|
||||
return .drag_moved;
|
||||
}
|
||||
}
|
||||
|
||||
return .none;
|
||||
}
|
||||
|
||||
pub fn handleMousePress(
|
||||
input: *InteractionState,
|
||||
board_state: chess_board.BoardState,
|
||||
mode: InputMode,
|
||||
mode_button: ?ModeButton,
|
||||
palette_piece: ?u4,
|
||||
cursor_square: ?SquareCoord,
|
||||
cursor_ndc: ?[2]f32,
|
||||
) PressResult {
|
||||
if (mode_button) |button| {
|
||||
if (button == .reset) return .reset_requested;
|
||||
|
||||
const new_mode: InputMode = switch (button) {
|
||||
.play => .play,
|
||||
.edit => .edit,
|
||||
.reset => unreachable,
|
||||
};
|
||||
if (new_mode != mode) {
|
||||
input.selected_palette_piece = null;
|
||||
input.selection.selected = null;
|
||||
input.pending_drag = null;
|
||||
input.current_drag = null;
|
||||
return .{ .mode_changed = new_mode };
|
||||
}
|
||||
return .none;
|
||||
}
|
||||
|
||||
if (mode == .edit) {
|
||||
if (palette_piece) |encoded_piece| {
|
||||
input.selected_palette_piece = if (input.selected_palette_piece != null and input.selected_palette_piece.? == encoded_piece) null else encoded_piece;
|
||||
input.selection.selected = null;
|
||||
input.pending_drag = null;
|
||||
input.current_drag = null;
|
||||
return .palette_changed;
|
||||
}
|
||||
|
||||
const square = cursor_square orelse return .none;
|
||||
if (input.selected_palette_piece) |encoded_piece| {
|
||||
return .{ .edit_place = .{ .square = square, .encoded = encoded_piece } };
|
||||
}
|
||||
return .{ .edit_clear = square };
|
||||
}
|
||||
|
||||
const square = cursor_square orelse return .none;
|
||||
const ndc = cursor_ndc orelse return .none;
|
||||
const encoded = board_state.getSquare(coordToSquare(square));
|
||||
const previous_selection = input.selection.selected;
|
||||
|
||||
if (piece.colorOf(encoded) == board_state.turn) {
|
||||
input.pending_drag = .{
|
||||
.source = square,
|
||||
.encoded = encoded,
|
||||
.start_ndc = ndc,
|
||||
.previous_selection = previous_selection,
|
||||
};
|
||||
}
|
||||
|
||||
return switch (input.selection.handleClick(board_state, cursor_square)) {
|
||||
.none => .none,
|
||||
.cleared => .play_cleared,
|
||||
.selected => |selected| .{ .play_selected = coordToSquare(selected) },
|
||||
.move_requested => |move| .{ .play_move_requested = move },
|
||||
};
|
||||
}
|
||||
|
||||
pub fn handleMouseRelease(input: *InteractionState, cursor_square: ?SquareCoord) ReleaseResult {
|
||||
if (input.pending_drag != null) {
|
||||
input.pending_drag = null;
|
||||
return .click_without_drag;
|
||||
}
|
||||
|
||||
const drag = input.current_drag orelse return .none;
|
||||
input.current_drag = null;
|
||||
|
||||
if (cursor_square) |target| {
|
||||
if (squareCoordEql(drag.source, target)) {
|
||||
if (drag.previous_selection) |previous| {
|
||||
if (squareCoordEql(previous, drag.source)) {
|
||||
input.selection.selected = null;
|
||||
return .drag_cleared;
|
||||
}
|
||||
}
|
||||
input.selection.selected = drag.source;
|
||||
return .{ .drag_selected = coordToSquare(drag.source) };
|
||||
}
|
||||
|
||||
input.selection.selected = null;
|
||||
return .{ .drag_move_requested = .{ .from = coordToSquare(drag.source), .to = coordToSquare(target) } };
|
||||
}
|
||||
|
||||
input.selection.selected = drag.previous_selection;
|
||||
return .{ .drag_cancelled = if (drag.previous_selection) |selected| coordToSquare(selected) else null };
|
||||
}
|
||||
|
||||
pub fn screenToNdc(
|
||||
mouse_x: f64,
|
||||
mouse_y: f64,
|
||||
window_width: u32,
|
||||
window_height: u32,
|
||||
) ?[2]f32 {
|
||||
if (window_width == 0 or window_height == 0) return null;
|
||||
|
||||
const w: f64 = @floatFromInt(window_width);
|
||||
const h: f64 = @floatFromInt(window_height);
|
||||
|
||||
return .{
|
||||
@floatCast((mouse_x / w) * 2.0 - 1.0),
|
||||
@floatCast(1.0 - (mouse_y / h) * 2.0),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn screenToBoardSquare(
|
||||
mouse_x: f64,
|
||||
mouse_y: f64,
|
||||
window_width: u32,
|
||||
window_height: u32,
|
||||
board_rect: geometry.BoardRect,
|
||||
) ?SquareCoord {
|
||||
const ndc = screenToNdc(mouse_x, mouse_y, window_width, window_height) orelse return null;
|
||||
const ndc_x = ndc[0];
|
||||
const ndc_y = ndc[1];
|
||||
|
||||
const right = board_rect.left + board_rect.width;
|
||||
const top = board_rect.bottom + board_rect.height;
|
||||
|
||||
if (ndc_x < board_rect.left or ndc_x >= right) return null;
|
||||
if (ndc_y < board_rect.bottom or ndc_y >= top) return null;
|
||||
|
||||
const local_x = (ndc_x - board_rect.left) / board_rect.width;
|
||||
const local_y = (ndc_y - board_rect.bottom) / board_rect.height;
|
||||
|
||||
const file_float = local_x * 8.0;
|
||||
const rank_float = local_y * 8.0;
|
||||
|
||||
const file_int: u32 = @intFromFloat(@floor(file_float));
|
||||
const rank_int: u32 = @intFromFloat(@floor(rank_float));
|
||||
|
||||
if (file_int >= 8 or rank_int >= 8) return null;
|
||||
|
||||
return .{
|
||||
.file = @intCast(file_int),
|
||||
.rank = @intCast(rank_int),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn screenToPromotionPiece(
|
||||
mouse_x: f64,
|
||||
mouse_y: f64,
|
||||
window_width: u32,
|
||||
window_height: u32,
|
||||
board_rect: geometry.BoardRect,
|
||||
square: bitboard.Square,
|
||||
) ?piece.PieceType {
|
||||
const ndc = screenToNdc(mouse_x, mouse_y, window_width, window_height) orelse return null;
|
||||
const popup_rect = piece_render.promotionPopupRectForSquare(board_rect, square);
|
||||
const right = popup_rect.left + popup_rect.width;
|
||||
const top = popup_rect.bottom + popup_rect.height;
|
||||
|
||||
if (ndc[0] < popup_rect.left or ndc[0] >= right) return null;
|
||||
if (ndc[1] < popup_rect.bottom or ndc[1] >= top) return null;
|
||||
|
||||
const local_x = (ndc[0] - popup_rect.left) / popup_rect.width;
|
||||
const index_float = local_x * @as(f32, @floatFromInt(piece_render.promotion_piece_types.len));
|
||||
const index: usize = @intFromFloat(@floor(index_float));
|
||||
if (index >= piece_render.promotion_piece_types.len) return null;
|
||||
return piece_render.promotion_piece_types[index];
|
||||
}
|
||||
|
||||
pub fn screenToModeButton(
|
||||
mouse_x: f64,
|
||||
mouse_y: f64,
|
||||
window_width: u32,
|
||||
window_height: u32,
|
||||
) ?ModeButton {
|
||||
const ndc = screenToNdc(mouse_x, mouse_y, window_width, window_height) orelse return null;
|
||||
const x = ndc[0];
|
||||
const y = ndc[1];
|
||||
|
||||
const button_w: f32 = 0.16;
|
||||
const button_h: f32 = 0.08;
|
||||
const reset_button_w: f32 = 0.22;
|
||||
const gap: f32 = 0.025;
|
||||
const top: f32 = 0.96;
|
||||
const bottom = top - button_h;
|
||||
const play_left: f32 = -0.95;
|
||||
const edit_left = play_left + button_w + gap;
|
||||
const reset_left = edit_left + button_w + gap;
|
||||
|
||||
if (y < bottom or y >= top) return null;
|
||||
if (x >= play_left and x < play_left + button_w) return .play;
|
||||
if (x >= edit_left and x < edit_left + button_w) return .edit;
|
||||
if (x >= reset_left and x < reset_left + reset_button_w) return .reset;
|
||||
return null;
|
||||
}
|
||||
|
||||
pub fn screenToPalettePiece(
|
||||
mouse_x: f64,
|
||||
mouse_y: f64,
|
||||
window_width: u32,
|
||||
window_height: u32,
|
||||
board_rect: geometry.BoardRect,
|
||||
) ?u4 {
|
||||
const ndc = screenToNdc(mouse_x, mouse_y, window_width, window_height) orelse return null;
|
||||
const ndc_x = ndc[0];
|
||||
const ndc_y = ndc[1];
|
||||
|
||||
const palette_rect = piece_render.paletteRectForBoard(board_rect);
|
||||
const right = palette_rect.left + palette_rect.width;
|
||||
const top = palette_rect.bottom + palette_rect.height;
|
||||
|
||||
if (ndc_x < palette_rect.left or ndc_x >= right) return null;
|
||||
if (ndc_y < palette_rect.bottom or ndc_y >= top) return null;
|
||||
|
||||
const local_y = (top - ndc_y) / palette_rect.height;
|
||||
const index_float = local_y * @as(f32, @floatFromInt(piece_render.palette_entries.len));
|
||||
const index: usize = @intFromFloat(@floor(index_float));
|
||||
|
||||
if (index >= piece_render.palette_entries.len) return null;
|
||||
return piece_render.palette_entries[index].encoded;
|
||||
}
|
||||
|
||||
fn screenPointForBoardCoord(
|
||||
board_rect: geometry.BoardRect,
|
||||
window_width: u32,
|
||||
window_height: u32,
|
||||
board_x: f32,
|
||||
board_y: f32,
|
||||
) [2]f64 {
|
||||
const ndc = geometry.boardToNdc(board_rect, board_x, board_y);
|
||||
const w: f64 = @floatFromInt(window_width);
|
||||
const h: f64 = @floatFromInt(window_height);
|
||||
|
||||
return .{
|
||||
((@as(f64, ndc[0]) + 1.0) / 2.0) * w,
|
||||
((1.0 - @as(f64, ndc[1])) / 2.0) * h,
|
||||
};
|
||||
}
|
||||
|
||||
test "screenToBoardSquare maps board corners and center squares" {
|
||||
const board_rect = geometry.boardRectForExtent(800, 600);
|
||||
|
||||
const a1 = screenPointForBoardCoord(board_rect, 800, 600, 0.5, 0.5);
|
||||
try std.testing.expectEqual(SquareCoord{ .file = 0, .rank = 0 }, screenToBoardSquare(a1[0], a1[1], 800, 600, board_rect).?);
|
||||
|
||||
const h8 = screenPointForBoardCoord(board_rect, 800, 600, 7.5, 7.5);
|
||||
try std.testing.expectEqual(SquareCoord{ .file = 7, .rank = 7 }, screenToBoardSquare(h8[0], h8[1], 800, 600, board_rect).?);
|
||||
|
||||
const e4 = screenPointForBoardCoord(board_rect, 800, 600, 4.5, 3.5);
|
||||
try std.testing.expectEqual(SquareCoord{ .file = 4, .rank = 3 }, screenToBoardSquare(e4[0], e4[1], 800, 600, board_rect).?);
|
||||
}
|
||||
|
||||
test "screenToBoardSquare returns null outside board" {
|
||||
const board_rect = geometry.boardRectForExtent(800, 600);
|
||||
|
||||
try std.testing.expectEqual(null, screenToBoardSquare(0, 0, 800, 600, board_rect));
|
||||
try std.testing.expectEqual(null, screenToBoardSquare(799, 599, 800, 600, board_rect));
|
||||
try std.testing.expectEqual(null, screenToBoardSquare(400, 300, 0, 600, board_rect));
|
||||
}
|
||||
|
||||
test "screenToPalettePiece maps palette cells to encoded pieces" {
|
||||
const board_rect = geometry.boardRectForExtent(800, 600);
|
||||
const palette_rect = piece_render.paletteRectForBoard(board_rect);
|
||||
const cell_height = palette_rect.height / @as(f32, @floatFromInt(piece_render.palette_entries.len));
|
||||
|
||||
const top_cell_ndc_x = palette_rect.left + palette_rect.width / 2.0;
|
||||
const top_cell_ndc_y = palette_rect.bottom + palette_rect.height - cell_height / 2.0;
|
||||
const window_x = ((@as(f64, top_cell_ndc_x) + 1.0) / 2.0) * 800.0;
|
||||
const window_y = ((1.0 - @as(f64, top_cell_ndc_y)) / 2.0) * 600.0;
|
||||
|
||||
try std.testing.expectEqual(piece.encode(.white, .king), screenToPalettePiece(window_x, window_y, 800, 600, board_rect).?);
|
||||
}
|
||||
|
||||
test "SelectionState selects side-to-move piece and requests move on second click" {
|
||||
var state = chess_board.BoardState.empty();
|
||||
state.turn = .white;
|
||||
state.setSquare(12, piece.encode(.white, .pawn));
|
||||
|
||||
var selection = SelectionState{};
|
||||
|
||||
try std.testing.expectEqual(ClickResult.none, selection.handleClick(state, .{ .file = 0, .rank = 0 }));
|
||||
try std.testing.expectEqual(null, selection.selected);
|
||||
|
||||
try std.testing.expectEqual(ClickResult{ .selected = .{ .file = 4, .rank = 1 } }, selection.handleClick(state, .{ .file = 4, .rank = 1 }));
|
||||
try std.testing.expectEqual(SquareCoord{ .file = 4, .rank = 1 }, selection.selected.?);
|
||||
|
||||
try std.testing.expectEqual(
|
||||
ClickResult{ .move_requested = .{ .from = 12, .to = 28 } },
|
||||
selection.handleClick(state, .{ .file = 4, .rank = 3 }),
|
||||
);
|
||||
try std.testing.expectEqual(null, selection.selected);
|
||||
}
|
||||
|
||||
test "SelectionState ignores pieces that are not side to move" {
|
||||
var state = chess_board.BoardState.empty();
|
||||
state.turn = .white;
|
||||
state.setSquare(52, piece.encode(.black, .pawn));
|
||||
|
||||
var selection = SelectionState{};
|
||||
|
||||
try std.testing.expectEqual(ClickResult.none, selection.handleClick(state, .{ .file = 4, .rank = 6 }));
|
||||
try std.testing.expectEqual(null, selection.selected);
|
||||
}
|
||||
|
||||
test "SelectionState deselects when selected square is clicked again" {
|
||||
var state = chess_board.BoardState.empty();
|
||||
state.turn = .white;
|
||||
state.setSquare(12, piece.encode(.white, .pawn));
|
||||
|
||||
var selection = SelectionState{};
|
||||
|
||||
_ = selection.handleClick(state, .{ .file = 4, .rank = 1 });
|
||||
try std.testing.expectEqual(SquareCoord{ .file = 4, .rank = 1 }, selection.selected.?);
|
||||
|
||||
try std.testing.expectEqual(ClickResult.cleared, selection.handleClick(state, .{ .file = 4, .rank = 1 }));
|
||||
try std.testing.expectEqual(null, selection.selected);
|
||||
}
|
||||
|
||||
test "SelectionState switches selection when same-color piece is clicked" {
|
||||
var state = chess_board.BoardState.empty();
|
||||
state.turn = .white;
|
||||
state.setSquare(12, piece.encode(.white, .pawn));
|
||||
state.setSquare(6, piece.encode(.white, .knight));
|
||||
|
||||
var selection = SelectionState{};
|
||||
|
||||
_ = selection.handleClick(state, .{ .file = 4, .rank = 1 });
|
||||
try std.testing.expectEqual(SquareCoord{ .file = 4, .rank = 1 }, selection.selected.?);
|
||||
|
||||
try std.testing.expectEqual(ClickResult{ .selected = .{ .file = 6, .rank = 0 } }, selection.handleClick(state, .{ .file = 6, .rank = 0 }));
|
||||
try std.testing.expectEqual(SquareCoord{ .file = 6, .rank = 0 }, selection.selected.?);
|
||||
}
|
||||
13
src/chess/bitboard.zig
Normal file
@ -0,0 +1,13 @@
|
||||
const std = @import("std");
|
||||
|
||||
pub const Bitboard = u64;
|
||||
pub const Square = u6;
|
||||
|
||||
pub fn bit(square: Square) Bitboard {
|
||||
return @as(Bitboard, 1) << square;
|
||||
}
|
||||
|
||||
test "bit returns a bitboard with one square set" {
|
||||
try std.testing.expectEqual(@as(Bitboard, 1), bit(0));
|
||||
try std.testing.expectEqual(@as(Bitboard, 1) << 63, bit(63));
|
||||
}
|
||||
1943
src/chess/board.zig
Normal file
628
src/chess/fen.zig
Normal file
@ -0,0 +1,628 @@
|
||||
const std = @import("std");
|
||||
|
||||
const piece = @import("piece.zig");
|
||||
const board = @import("board.zig");
|
||||
|
||||
pub fn parseFen(fen: []const u8) !board.BoardState {
|
||||
var state = board.BoardState.empty();
|
||||
var it = std.mem.splitScalar(u8, fen, ' ');
|
||||
var field_index: usize = 0;
|
||||
|
||||
while (it.next()) |field| : (field_index += 1) {
|
||||
if (field_index >= 6) return error.InvalidFenFieldCount;
|
||||
|
||||
switch (field_index) {
|
||||
0 => try parseBoardPlacement(&state, field),
|
||||
1 => try parseTurn(&state, field),
|
||||
2 => try parseCastleRights(&state, field),
|
||||
3 => try parseEnPassant(&state, field),
|
||||
4 => try parseHalfmove(&state, field),
|
||||
5 => try parseFullmove(&state, field),
|
||||
else => unreachable,
|
||||
}
|
||||
}
|
||||
if (field_index != 6) return error.InvalidFenFieldCount;
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
pub fn parseBoardPlacement(state: *board.BoardState, placement: []const u8) !void {
|
||||
var it = std.mem.splitScalar(u8, placement, '/');
|
||||
|
||||
var rank_count: u4 = 0;
|
||||
while (it.next()) |part| {
|
||||
if (rank_count >= 8) return error.InvalidRankCount;
|
||||
|
||||
const rank: u3 = @intCast(7 - rank_count);
|
||||
var file: u4 = 0;
|
||||
|
||||
for (part) |c| {
|
||||
switch (c) {
|
||||
'1'...'9' => {
|
||||
file += @intCast(c - '0');
|
||||
if (file > 8) return error.InvalidRankWidth;
|
||||
},
|
||||
'P',
|
||||
'N',
|
||||
'B',
|
||||
'R',
|
||||
'Q',
|
||||
'K',
|
||||
'p',
|
||||
'n',
|
||||
'b',
|
||||
'r',
|
||||
'q',
|
||||
'k',
|
||||
=> {
|
||||
if (file >= 8) return error.InvalidRankWidth;
|
||||
const p = try piece.fromFENChar(c);
|
||||
state.setSquare((@as(u6, rank) * 8) + file, p);
|
||||
file += 1;
|
||||
},
|
||||
else => return error.InvalidFenCharacter,
|
||||
}
|
||||
}
|
||||
|
||||
if (file != 8) return error.InvalidRankWidth;
|
||||
rank_count += 1;
|
||||
}
|
||||
|
||||
if (rank_count != 8) return error.InvalidRankCount;
|
||||
}
|
||||
|
||||
pub fn parseTurn(state: *board.BoardState, turn_string: []const u8) !void {
|
||||
if (turn_string.len != 1) return error.InvalidTurnChar;
|
||||
const turn_char = turn_string[0];
|
||||
if (turn_char == 'w') {
|
||||
state.turn = piece.Color.white;
|
||||
} else if (turn_char == 'b') {
|
||||
state.turn = piece.Color.black;
|
||||
} else {
|
||||
return error.InvalidTurnChar;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parseHalfmove(state: *board.BoardState, halfmove_string: []const u8) !void {
|
||||
state.halfmove = try std.fmt.parseInt(u8, halfmove_string, 10);
|
||||
}
|
||||
|
||||
pub fn parseFullmove(state: *board.BoardState, fullmove_string: []const u8) !void {
|
||||
const fullmove = try std.fmt.parseInt(u32, fullmove_string, 10);
|
||||
if (fullmove == 0) return error.InvalidFullmoveNumber;
|
||||
state.fullmove = fullmove;
|
||||
}
|
||||
|
||||
pub fn parseCastleRights(state: *board.BoardState, castle_string: []const u8) !void {
|
||||
if (std.mem.eql(u8, castle_string, "")) return error.InvalidCastlingRights;
|
||||
if (std.mem.eql(u8, castle_string, "-")) {
|
||||
state.castle_rights = 0;
|
||||
return;
|
||||
}
|
||||
var rights: u4 = 0;
|
||||
var last_order: u3 = 0;
|
||||
for (castle_string) |c| {
|
||||
const bit: u4 = switch (c) {
|
||||
'K' => 0b1000,
|
||||
'Q' => 0b0100,
|
||||
'k' => 0b0010,
|
||||
'q' => 0b0001,
|
||||
else => return error.InvalidCastlingRights,
|
||||
};
|
||||
const order: u3 = switch (c) {
|
||||
'K' => 1,
|
||||
'Q' => 2,
|
||||
'k' => 3,
|
||||
'q' => 4,
|
||||
else => unreachable,
|
||||
};
|
||||
if (order <= last_order) {
|
||||
return error.InvalidCastlingRights;
|
||||
}
|
||||
if ((rights & bit) != 0) {
|
||||
return error.InvalidCastlingRights;
|
||||
}
|
||||
rights |= bit;
|
||||
last_order = order;
|
||||
}
|
||||
state.castle_rights = rights;
|
||||
}
|
||||
|
||||
pub fn parseEnPassant(state: *board.BoardState, ep_string: []const u8) !void {
|
||||
if (std.mem.eql(u8, ep_string, "-")) {
|
||||
state.en_passant = 0;
|
||||
return;
|
||||
}
|
||||
if (ep_string.len != 2) return error.InvalidSquare;
|
||||
const ep_square = try board.parseSquareFromAlgebraic(ep_string);
|
||||
if (ep_string[1] != '3' and ep_string[1] != '6') return error.InvalidEnPassantSquare;
|
||||
|
||||
state.en_passant = (1 << 6) | @as(u7, ep_square);
|
||||
}
|
||||
|
||||
fn pieceToFenChar(encoded: u4) !u8 {
|
||||
const color = piece.colorOf(encoded) orelse return error.InvalidPiece;
|
||||
|
||||
return switch (piece.typeOf(encoded)) {
|
||||
.pawn => if (color == .white) 'P' else 'p',
|
||||
.knight => if (color == .white) 'N' else 'n',
|
||||
.bishop => if (color == .white) 'B' else 'b',
|
||||
.rook => if (color == .white) 'R' else 'r',
|
||||
.queen => if (color == .white) 'Q' else 'q',
|
||||
.king => if (color == .white) 'K' else 'k',
|
||||
.none => error.InvalidPiece,
|
||||
};
|
||||
}
|
||||
|
||||
fn appendBoardPlacement(allocator: std.mem.Allocator, out: *std.ArrayList(u8), state: board.BoardState) !void {
|
||||
var rank: u4 = 8;
|
||||
while (rank > 0) {
|
||||
rank -= 1;
|
||||
var empty_count: u4 = 0;
|
||||
|
||||
var file: u4 = 0;
|
||||
while (file < 8) : (file += 1) {
|
||||
const square_index: u6 = @intCast((@as(u6, @intCast(rank)) * 8) + @as(u6, @intCast(file)));
|
||||
const square = state.getSquare(square_index);
|
||||
if (square == 0) {
|
||||
empty_count += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (empty_count > 0) {
|
||||
try out.append(allocator, '0' + @as(u8, @intCast(empty_count)));
|
||||
empty_count = 0;
|
||||
}
|
||||
|
||||
try out.append(allocator, try pieceToFenChar(square));
|
||||
}
|
||||
|
||||
if (empty_count > 0) {
|
||||
try out.append(allocator, '0' + @as(u8, @intCast(empty_count)));
|
||||
}
|
||||
|
||||
if (rank > 0) try out.append(allocator, '/');
|
||||
}
|
||||
}
|
||||
|
||||
fn appendCastleRights(allocator: std.mem.Allocator, out: *std.ArrayList(u8), rights: u4) !void {
|
||||
if (rights == 0) {
|
||||
try out.append(allocator, '-');
|
||||
return;
|
||||
}
|
||||
|
||||
if ((rights & 0b1000) != 0) try out.append(allocator, 'K');
|
||||
if ((rights & 0b0100) != 0) try out.append(allocator, 'Q');
|
||||
if ((rights & 0b0010) != 0) try out.append(allocator, 'k');
|
||||
if ((rights & 0b0001) != 0) try out.append(allocator, 'q');
|
||||
}
|
||||
|
||||
fn appendEnPassant(allocator: std.mem.Allocator, out: *std.ArrayList(u8), en_passant: u7) !void {
|
||||
const valid = (en_passant & (@as(u7, 1) << 6)) != 0;
|
||||
if (!valid) {
|
||||
try out.append(allocator, '-');
|
||||
return;
|
||||
}
|
||||
|
||||
const square = en_passant & 0b111111;
|
||||
const file: u8 = @intCast(square % 8);
|
||||
const rank: u8 = @intCast(square / 8);
|
||||
|
||||
try out.append(allocator, 'a' + file);
|
||||
try out.append(allocator, '1' + rank);
|
||||
}
|
||||
|
||||
pub fn formatFen(allocator: std.mem.Allocator, state: board.BoardState) ![]u8 {
|
||||
var out: std.ArrayList(u8) = .empty;
|
||||
errdefer out.deinit(allocator);
|
||||
|
||||
try appendBoardPlacement(allocator, &out, state);
|
||||
try out.append(allocator, ' ');
|
||||
|
||||
try out.append(allocator, if (state.turn == .white) 'w' else 'b');
|
||||
try out.append(allocator, ' ');
|
||||
|
||||
try appendCastleRights(allocator, &out, state.castle_rights);
|
||||
try out.append(allocator, ' ');
|
||||
|
||||
try appendEnPassant(allocator, &out, state.en_passant);
|
||||
try out.append(allocator, ' ');
|
||||
|
||||
var halfmove_buf: [3]u8 = undefined;
|
||||
const halfmove = try std.fmt.bufPrint(&halfmove_buf, "{}", .{state.halfmove});
|
||||
try out.appendSlice(allocator, halfmove);
|
||||
try out.append(allocator, ' ');
|
||||
|
||||
var fullmove_buf: [10]u8 = undefined;
|
||||
const fullmove = try std.fmt.bufPrint(&fullmove_buf, "{}", .{state.fullmove});
|
||||
try out.appendSlice(allocator, fullmove);
|
||||
|
||||
return try out.toOwnedSlice(allocator);
|
||||
}
|
||||
|
||||
fn expectStartingBoardPieces(state: board.BoardState) !void {
|
||||
try std.testing.expectEqual(piece.encode(.black, .rook), state.getSquare(56));
|
||||
try std.testing.expectEqual(piece.encode(.black, .knight), state.getSquare(57));
|
||||
try std.testing.expectEqual(piece.encode(.black, .bishop), state.getSquare(58));
|
||||
try std.testing.expectEqual(piece.encode(.black, .queen), state.getSquare(59));
|
||||
try std.testing.expectEqual(piece.encode(.black, .king), state.getSquare(60));
|
||||
try std.testing.expectEqual(piece.encode(.black, .bishop), state.getSquare(61));
|
||||
try std.testing.expectEqual(piece.encode(.black, .knight), state.getSquare(62));
|
||||
try std.testing.expectEqual(piece.encode(.black, .rook), state.getSquare(63));
|
||||
|
||||
for (8..16) |square| {
|
||||
try std.testing.expectEqual(piece.encode(.white, .pawn), state.getSquare(@intCast(square)));
|
||||
}
|
||||
for (16..48) |square| {
|
||||
try std.testing.expectEqual(@as(u4, 0), state.getSquare(@intCast(square)));
|
||||
}
|
||||
for (48..56) |square| {
|
||||
try std.testing.expectEqual(piece.encode(.black, .pawn), state.getSquare(@intCast(square)));
|
||||
}
|
||||
|
||||
try std.testing.expectEqual(piece.encode(.white, .rook), state.getSquare(0));
|
||||
try std.testing.expectEqual(piece.encode(.white, .knight), state.getSquare(1));
|
||||
try std.testing.expectEqual(piece.encode(.white, .bishop), state.getSquare(2));
|
||||
try std.testing.expectEqual(piece.encode(.white, .queen), state.getSquare(3));
|
||||
try std.testing.expectEqual(piece.encode(.white, .king), state.getSquare(4));
|
||||
try std.testing.expectEqual(piece.encode(.white, .bishop), state.getSquare(5));
|
||||
try std.testing.expectEqual(piece.encode(.white, .knight), state.getSquare(6));
|
||||
try std.testing.expectEqual(piece.encode(.white, .rook), state.getSquare(7));
|
||||
}
|
||||
|
||||
test "parseBoardPlacement parses starting position" {
|
||||
var state = board.BoardState.empty();
|
||||
try parseBoardPlacement(
|
||||
&state,
|
||||
"rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR",
|
||||
);
|
||||
|
||||
try expectStartingBoardPieces(state);
|
||||
}
|
||||
|
||||
test "parseBoardPlacement rejects invalid rank counts" {
|
||||
var state = board.BoardState.empty();
|
||||
|
||||
try std.testing.expectError(
|
||||
error.InvalidRankCount,
|
||||
parseBoardPlacement(&state, "8/8/8/8/8/8/8"),
|
||||
);
|
||||
|
||||
try std.testing.expectError(
|
||||
error.InvalidRankCount,
|
||||
parseBoardPlacement(&state, "8/8/8/8/8/8/8/8/8"),
|
||||
);
|
||||
}
|
||||
|
||||
test "parseBoardPlacement rejects ranks that are not exactly eight squares" {
|
||||
var state = board.BoardState.empty();
|
||||
|
||||
try std.testing.expectError(
|
||||
error.InvalidRankWidth,
|
||||
parseBoardPlacement(&state, "7/8/8/8/8/8/8/8"),
|
||||
);
|
||||
|
||||
try std.testing.expectError(
|
||||
error.InvalidRankWidth,
|
||||
parseBoardPlacement(&state, "9/8/8/8/8/8/8/8"),
|
||||
);
|
||||
|
||||
try std.testing.expectError(
|
||||
error.InvalidRankWidth,
|
||||
parseBoardPlacement(&state, "8P/8/8/8/8/8/8/8"),
|
||||
);
|
||||
}
|
||||
|
||||
test "parseTurn accepts white and black active colors" {
|
||||
var state = board.BoardState.empty();
|
||||
|
||||
try parseTurn(&state, "b");
|
||||
try std.testing.expectEqual(piece.Color.black, state.turn);
|
||||
|
||||
try parseTurn(&state, "w");
|
||||
try std.testing.expectEqual(piece.Color.white, state.turn);
|
||||
}
|
||||
|
||||
test "parseTurn rejects invalid active color" {
|
||||
var state = board.BoardState.empty();
|
||||
|
||||
try std.testing.expectError(error.InvalidTurnChar, parseTurn(&state, "x"));
|
||||
try std.testing.expectError(error.InvalidTurnChar, parseTurn(&state, "white"));
|
||||
}
|
||||
|
||||
test "parseHalfmove parses decimal halfmove clock" {
|
||||
var state = board.BoardState.empty();
|
||||
|
||||
try parseHalfmove(&state, "42");
|
||||
try std.testing.expectEqual(@as(u8, 42), state.halfmove);
|
||||
}
|
||||
|
||||
test "parseHalfmove rejects invalid or overflowing values" {
|
||||
var state = board.BoardState.empty();
|
||||
|
||||
try std.testing.expectError(error.InvalidCharacter, parseHalfmove(&state, "abc"));
|
||||
try std.testing.expectError(error.Overflow, parseHalfmove(&state, "256"));
|
||||
}
|
||||
|
||||
test "parseFullmove parses decimal fullmove number" {
|
||||
var state = board.BoardState.empty();
|
||||
|
||||
try parseFullmove(&state, "1");
|
||||
try std.testing.expectEqual(@as(u32, 1), state.fullmove);
|
||||
|
||||
try parseFullmove(&state, "300");
|
||||
try std.testing.expectEqual(@as(u32, 300), state.fullmove);
|
||||
}
|
||||
|
||||
test "parseFullmove rejects zero and invalid values" {
|
||||
var state = board.BoardState.empty();
|
||||
|
||||
try std.testing.expectError(error.InvalidFullmoveNumber, parseFullmove(&state, "0"));
|
||||
try std.testing.expectError(error.InvalidCharacter, parseFullmove(&state, "abc"));
|
||||
}
|
||||
|
||||
test "parseCastleRights parses no castling rights" {
|
||||
var state = board.BoardState.empty();
|
||||
|
||||
try parseCastleRights(&state, "-");
|
||||
try std.testing.expectEqual(@as(u4, 0b0000), state.castle_rights);
|
||||
}
|
||||
|
||||
test "parseCastleRights parses individual and combined rights" {
|
||||
var state = board.BoardState.empty();
|
||||
|
||||
try parseCastleRights(&state, "K");
|
||||
try std.testing.expectEqual(@as(u4, 0b1000), state.castle_rights);
|
||||
|
||||
try parseCastleRights(&state, "Q");
|
||||
try std.testing.expectEqual(@as(u4, 0b0100), state.castle_rights);
|
||||
|
||||
try parseCastleRights(&state, "k");
|
||||
try std.testing.expectEqual(@as(u4, 0b0010), state.castle_rights);
|
||||
|
||||
try parseCastleRights(&state, "q");
|
||||
try std.testing.expectEqual(@as(u4, 0b0001), state.castle_rights);
|
||||
|
||||
try parseCastleRights(&state, "KQkq");
|
||||
try std.testing.expectEqual(@as(u4, 0b1111), state.castle_rights);
|
||||
|
||||
try parseCastleRights(&state, "Kq");
|
||||
try std.testing.expectEqual(@as(u4, 0b1001), state.castle_rights);
|
||||
|
||||
try parseCastleRights(&state, "kq");
|
||||
try std.testing.expectEqual(@as(u4, 0b0011), state.castle_rights);
|
||||
}
|
||||
|
||||
test "parseCastleRights rejects invalid castling rights" {
|
||||
var state = board.BoardState.empty();
|
||||
|
||||
try std.testing.expectError(error.InvalidCastlingRights, parseCastleRights(&state, ""));
|
||||
try std.testing.expectError(error.InvalidCastlingRights, parseCastleRights(&state, "K-"));
|
||||
try std.testing.expectError(error.InvalidCastlingRights, parseCastleRights(&state, "A"));
|
||||
try std.testing.expectError(error.InvalidCastlingRights, parseCastleRights(&state, "KK"));
|
||||
try std.testing.expectError(error.InvalidCastlingRights, parseCastleRights(&state, "qK"));
|
||||
}
|
||||
|
||||
test "parseEnPassant parses no en passant target" {
|
||||
var state = board.BoardState.empty();
|
||||
|
||||
try parseEnPassant(&state, "-");
|
||||
try std.testing.expectEqual(@as(u7, 0), state.en_passant);
|
||||
}
|
||||
|
||||
test "parseEnPassant stores syntactically valid target even when not capturable" {
|
||||
var state = board.BoardState.empty();
|
||||
|
||||
state.turn = .black;
|
||||
try parseEnPassant(&state, "e3");
|
||||
try std.testing.expectEqual(@as(u7, 84), state.en_passant);
|
||||
|
||||
state.turn = .white;
|
||||
try parseEnPassant(&state, "e6");
|
||||
try std.testing.expectEqual(@as(u7, 108), state.en_passant);
|
||||
}
|
||||
|
||||
test "parseEnPassant stores rank 6 target capturable by white pawn" {
|
||||
var state = board.BoardState.empty();
|
||||
state.turn = .white;
|
||||
state.setSquare((@as(u6, @intCast(4)) * 8) + @as(u6, @intCast(3)), piece.encode(.white, .pawn)); // d5 can capture e6
|
||||
|
||||
try parseEnPassant(&state, "e6");
|
||||
try std.testing.expectEqual(@as(u7, 108), state.en_passant);
|
||||
}
|
||||
|
||||
test "parseEnPassant stores rank 3 target capturable by black pawn" {
|
||||
var state = board.BoardState.empty();
|
||||
state.turn = .black;
|
||||
state.setSquare((@as(u6, @intCast(3)) * 8) + @as(u6, @intCast(5)), piece.encode(.black, .pawn)); // f4 can capture e3
|
||||
|
||||
try parseEnPassant(&state, "e3");
|
||||
try std.testing.expectEqual(@as(u7, 84), state.en_passant);
|
||||
}
|
||||
|
||||
test "parseEnPassant handles capturable edge-file targets" {
|
||||
var white_state = board.BoardState.empty();
|
||||
white_state.turn = .white;
|
||||
white_state.setSquare((@as(u6, @intCast(4)) * 8) + @as(u6, @intCast(1)), piece.encode(.white, .pawn)); // b5 can capture a6
|
||||
|
||||
try parseEnPassant(&white_state, "a6");
|
||||
try std.testing.expectEqual(@as(u7, 104), white_state.en_passant);
|
||||
|
||||
var black_state = board.BoardState.empty();
|
||||
black_state.turn = .black;
|
||||
black_state.setSquare((@as(u6, @intCast(3)) * 8) + @as(u6, @intCast(6)), piece.encode(.black, .pawn)); // g4 can capture h3
|
||||
|
||||
try parseEnPassant(&black_state, "h3");
|
||||
try std.testing.expectEqual(@as(u7, 87), black_state.en_passant);
|
||||
}
|
||||
|
||||
test "parseEnPassant stores target even if adjacent pawn is wrong color" {
|
||||
var state = board.BoardState.empty();
|
||||
state.turn = .white;
|
||||
state.setSquare((@as(u6, @intCast(4)) * 8) + @as(u6, @intCast(3)), piece.encode(.black, .pawn));
|
||||
|
||||
try parseEnPassant(&state, "e6");
|
||||
try std.testing.expectEqual(@as(u7, 108), state.en_passant);
|
||||
}
|
||||
|
||||
test "parseEnPassant rejects invalid target squares" {
|
||||
var state = board.BoardState.empty();
|
||||
|
||||
try std.testing.expectError(error.InvalidSquare, parseEnPassant(&state, ""));
|
||||
try std.testing.expectError(error.InvalidSquare, parseEnPassant(&state, "e"));
|
||||
try std.testing.expectError(error.InvalidSquare, parseEnPassant(&state, "i3"));
|
||||
try std.testing.expectError(error.InvalidSquare, parseEnPassant(&state, "e9"));
|
||||
}
|
||||
|
||||
test "parseEnPassant rejects target squares outside ranks 3 and 6" {
|
||||
var state = board.BoardState.empty();
|
||||
|
||||
try std.testing.expectError(error.InvalidEnPassantSquare, parseEnPassant(&state, "e2"));
|
||||
try std.testing.expectError(error.InvalidEnPassantSquare, parseEnPassant(&state, "e4"));
|
||||
try std.testing.expectError(error.InvalidEnPassantSquare, parseEnPassant(&state, "e5"));
|
||||
try std.testing.expectError(error.InvalidEnPassantSquare, parseEnPassant(&state, "e7"));
|
||||
}
|
||||
|
||||
test "parseFen parses starting position" {
|
||||
const state = try parseFen("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1");
|
||||
|
||||
try expectStartingBoardPieces(state);
|
||||
try std.testing.expectEqual(piece.Color.white, state.turn);
|
||||
try std.testing.expectEqual(@as(u4, 0b1111), state.castle_rights);
|
||||
try std.testing.expectEqual(@as(u7, 0), state.en_passant);
|
||||
try std.testing.expectEqual(@as(u8, 0), state.halfmove);
|
||||
try std.testing.expectEqual(@as(u32, 1), state.fullmove);
|
||||
}
|
||||
|
||||
test "parseFen parses capturable en passant position" {
|
||||
const state = try parseFen("8/8/8/3Pp3/8/8/8/8 w - e6 0 1");
|
||||
|
||||
try std.testing.expectEqual(piece.Color.white, state.turn);
|
||||
try std.testing.expectEqual(@as(u4, 0), state.castle_rights);
|
||||
try std.testing.expectEqual(@as(u7, 108), state.en_passant);
|
||||
try std.testing.expectEqual(piece.encode(.white, .pawn), state.getSquare(35));
|
||||
try std.testing.expectEqual(piece.encode(.black, .pawn), state.getSquare(36));
|
||||
}
|
||||
|
||||
test "parseFen preserves non-capturable en passant target" {
|
||||
const state = try parseFen("r1bqkbnr/pppp1ppp/2n5/4p3/3P4/5N2/PPP2PPP/RNBQKB1R w KQkq e3 4 5");
|
||||
|
||||
try std.testing.expectEqual(piece.Color.white, state.turn);
|
||||
try std.testing.expectEqual(@as(u4, 0b1111), state.castle_rights);
|
||||
try std.testing.expectEqual(@as(u7, 84), state.en_passant);
|
||||
try std.testing.expectEqual(@as(u8, 4), state.halfmove);
|
||||
try std.testing.expectEqual(@as(u32, 5), state.fullmove);
|
||||
try std.testing.expectEqual(piece.encode(.white, .pawn), state.getSquare(27));
|
||||
try std.testing.expectEqual(piece.encode(.black, .pawn), state.getSquare(36));
|
||||
}
|
||||
|
||||
test "parseFen rejects invalid field counts" {
|
||||
try std.testing.expectError(
|
||||
error.InvalidFenFieldCount,
|
||||
parseFen("8/8/8/8/8/8/8/8 w - - 0"),
|
||||
);
|
||||
|
||||
try std.testing.expectError(
|
||||
error.InvalidFenFieldCount,
|
||||
parseFen("8/8/8/8/8/8/8/8 w - - 0 1 extra"),
|
||||
);
|
||||
}
|
||||
|
||||
test "formatFen formats starting position" {
|
||||
const expected = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1";
|
||||
const state = try parseFen(expected);
|
||||
|
||||
const actual = try formatFen(std.testing.allocator, state);
|
||||
defer std.testing.allocator.free(actual);
|
||||
|
||||
try std.testing.expectEqualStrings(expected, actual);
|
||||
}
|
||||
|
||||
test "formatFen formats capturable en passant target" {
|
||||
const expected = "8/8/8/3Pp3/8/8/8/8 w - e6 0 1";
|
||||
const state = try parseFen(expected);
|
||||
|
||||
const actual = try formatFen(std.testing.allocator, state);
|
||||
defer std.testing.allocator.free(actual);
|
||||
|
||||
try std.testing.expectEqualStrings(expected, actual);
|
||||
}
|
||||
|
||||
test "formatFen preserves non-capturable en passant target" {
|
||||
const input = "r1bqkbnr/pppp1ppp/2n5/4p3/3P4/5N2/PPP2PPP/RNBQKB1R w KQkq e3 4 5";
|
||||
const expected = "r1bqkbnr/pppp1ppp/2n5/4p3/3P4/5N2/PPP2PPP/RNBQKB1R w KQkq e3 4 5";
|
||||
const state = try parseFen(input);
|
||||
|
||||
const actual = try formatFen(std.testing.allocator, state);
|
||||
defer std.testing.allocator.free(actual);
|
||||
|
||||
try std.testing.expectEqualStrings(expected, actual);
|
||||
}
|
||||
|
||||
test "formatFen includes promoted white pieces" {
|
||||
const cases = [_]struct {
|
||||
promotion_type: piece.PieceType,
|
||||
expected: []const u8,
|
||||
}{
|
||||
.{ .promotion_type = .queen, .expected = "4Q3/8/8/8/8/8/8/8 b - - 0 1" },
|
||||
.{ .promotion_type = .rook, .expected = "4R3/8/8/8/8/8/8/8 b - - 0 1" },
|
||||
.{ .promotion_type = .bishop, .expected = "4B3/8/8/8/8/8/8/8 b - - 0 1" },
|
||||
.{ .promotion_type = .knight, .expected = "4N3/8/8/8/8/8/8/8 b - - 0 1" },
|
||||
};
|
||||
|
||||
for (cases) |case| {
|
||||
var state = board.BoardState.empty();
|
||||
state.fullmove = 1;
|
||||
state.setSquare(52, piece.encode(.white, .pawn));
|
||||
|
||||
state.move(52, 60, case.promotion_type);
|
||||
|
||||
const actual = try formatFen(std.testing.allocator, state);
|
||||
defer std.testing.allocator.free(actual);
|
||||
|
||||
try std.testing.expectEqualStrings(case.expected, actual);
|
||||
}
|
||||
}
|
||||
|
||||
test "formatFen includes promoted black pieces" {
|
||||
const cases = [_]struct {
|
||||
promotion_type: piece.PieceType,
|
||||
expected: []const u8,
|
||||
}{
|
||||
.{ .promotion_type = .queen, .expected = "8/8/8/8/8/8/8/3q4 w - - 0 8" },
|
||||
.{ .promotion_type = .rook, .expected = "8/8/8/8/8/8/8/3r4 w - - 0 8" },
|
||||
.{ .promotion_type = .bishop, .expected = "8/8/8/8/8/8/8/3b4 w - - 0 8" },
|
||||
.{ .promotion_type = .knight, .expected = "8/8/8/8/8/8/8/3n4 w - - 0 8" },
|
||||
};
|
||||
|
||||
for (cases) |case| {
|
||||
var state = board.BoardState.empty();
|
||||
state.turn = .black;
|
||||
state.fullmove = 7;
|
||||
state.setSquare(11, piece.encode(.black, .pawn));
|
||||
|
||||
state.move(11, 3, case.promotion_type);
|
||||
|
||||
const actual = try formatFen(std.testing.allocator, state);
|
||||
defer std.testing.allocator.free(actual);
|
||||
|
||||
try std.testing.expectEqualStrings(case.expected, actual);
|
||||
}
|
||||
}
|
||||
|
||||
test "formatFen formats black turn no castling and larger counters" {
|
||||
var state = board.BoardState.empty();
|
||||
state.turn = .black;
|
||||
state.castle_rights = 0;
|
||||
state.halfmove = 42;
|
||||
state.fullmove = 300;
|
||||
state.setSquare((@as(u6, @intCast(0)) * 8) + @as(u6, @intCast(4)), piece.encode(.white, .king));
|
||||
state.setSquare((@as(u6, @intCast(7)) * 8) + @as(u6, @intCast(4)), piece.encode(.black, .king));
|
||||
state.setSquare((@as(u6, @intCast(0)) * 8) + @as(u6, @intCast(0)), piece.encode(.white, .rook));
|
||||
state.setSquare((@as(u6, @intCast(7)) * 8) + @as(u6, @intCast(7)), piece.encode(.black, .rook));
|
||||
|
||||
const actual = try formatFen(std.testing.allocator, state);
|
||||
defer std.testing.allocator.free(actual);
|
||||
|
||||
try std.testing.expectEqualStrings("4k2r/8/8/8/8/8/8/R3K3 b - - 42 300", actual);
|
||||
}
|
||||
134
src/chess/game.zig
Normal file
@ -0,0 +1,134 @@
|
||||
const std = @import("std");
|
||||
|
||||
const board = @import("board.zig");
|
||||
const piece = @import("piece.zig");
|
||||
const bitboard = @import("bitboard.zig");
|
||||
const fen = @import("fen.zig");
|
||||
|
||||
const Game = struct {
|
||||
initial_state: board.BoardState,
|
||||
state: board.BoardState,
|
||||
moves: std.ArrayList(MoveRecord),
|
||||
time_control: TimeControl,
|
||||
clocks: [2]u32,
|
||||
status: GameStatus,
|
||||
|
||||
pub fn deinit(self: *Game, allocator: std.mem.Allocator) void {
|
||||
self.moves.deinit(allocator);
|
||||
}
|
||||
|
||||
pub fn makeMove(self: *Game, from: bitboard.Square, to: bitboard.Square, promotion: ?piece.PieceType) void {
|
||||
self.state.move(from, to, promotion);
|
||||
}
|
||||
|
||||
pub fn legalMoves(self: *Game, square: bitboard.Square) bitboard.Bitboard {
|
||||
if (!self.state.isCheckmate(self.state.turn) and !self.state.isStalemate(self.state.turn)) {
|
||||
return self.state.getLegalMoves(square);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
};
|
||||
|
||||
const MoveRecord = struct {
|
||||
from: u6,
|
||||
to: u6,
|
||||
promotion: u4,
|
||||
time_remaining: u32,
|
||||
captured_piece: u4,
|
||||
previous_castling_rights: u4,
|
||||
previous_en_passant: u7,
|
||||
previous_halfmove: u8,
|
||||
previous_fullmove: u32,
|
||||
};
|
||||
|
||||
const TimeControl = enum {
|
||||
tentwo,
|
||||
fiveone,
|
||||
};
|
||||
|
||||
const GameStatus = enum {
|
||||
not_started,
|
||||
playing,
|
||||
check,
|
||||
stalemate,
|
||||
checkmate_black,
|
||||
checkmate_white,
|
||||
};
|
||||
|
||||
pub fn initStartingPosition(time_control: TimeControl) Game {
|
||||
const state = board.BoardState.initStartingPosition();
|
||||
return .{
|
||||
.initial_state = state,
|
||||
.state = state,
|
||||
.moves = .empty,
|
||||
.time_control = time_control,
|
||||
.status = .not_started,
|
||||
.clocks = [_]u32{0} ** 2,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn initFromFen(fen_string: []const u8, time_control: TimeControl) !Game {
|
||||
const state = try fen.parseFen(fen_string);
|
||||
return .{
|
||||
.initial_state = state,
|
||||
.state = state,
|
||||
.moves = .empty,
|
||||
.time_control = time_control,
|
||||
.status = .not_started,
|
||||
.clocks = [_]u32{0} ** 2,
|
||||
};
|
||||
}
|
||||
|
||||
test "initStartingPosition creates independent current and initial states" {
|
||||
var game = initStartingPosition(.tentwo);
|
||||
defer game.deinit(std.testing.allocator);
|
||||
|
||||
const expected = board.BoardState.initStartingPosition();
|
||||
try std.testing.expectEqualDeep(expected, game.initial_state);
|
||||
try std.testing.expectEqualDeep(expected, game.state);
|
||||
try std.testing.expectEqual(@as(usize, 0), game.moves.items.len);
|
||||
try std.testing.expectEqual(TimeControl.tentwo, game.time_control);
|
||||
try std.testing.expectEqual(GameStatus.not_started, game.status);
|
||||
try std.testing.expectEqual(@as(u32, 0), game.clocks[0]);
|
||||
try std.testing.expectEqual(@as(u32, 0), game.clocks[1]);
|
||||
|
||||
game.makeMove(12, 28, null); // e2-e4
|
||||
|
||||
try std.testing.expectEqualDeep(expected, game.initial_state);
|
||||
try std.testing.expectEqual(piece.encode(.white, .pawn), game.state.getSquare(28));
|
||||
try std.testing.expectEqual(@as(u4, 0), game.state.getSquare(12));
|
||||
}
|
||||
|
||||
test "initFromFen stores parsed position as initial and current state" {
|
||||
const fen_string = "7k/5Q2/6K1/8/8/8/8/8 b - - 0 1";
|
||||
var game = try initFromFen(fen_string, .fiveone);
|
||||
defer game.deinit(std.testing.allocator);
|
||||
|
||||
const expected = try fen.parseFen(fen_string);
|
||||
try std.testing.expectEqualDeep(expected, game.initial_state);
|
||||
try std.testing.expectEqualDeep(expected, game.state);
|
||||
try std.testing.expectEqual(@as(usize, 0), game.moves.items.len);
|
||||
try std.testing.expectEqual(TimeControl.fiveone, game.time_control);
|
||||
try std.testing.expectEqual(GameStatus.not_started, game.status);
|
||||
}
|
||||
|
||||
test "legalMoves returns legal destinations while game is active" {
|
||||
var game = initStartingPosition(.tentwo);
|
||||
defer game.deinit(std.testing.allocator);
|
||||
|
||||
const e2: bitboard.Square = 12;
|
||||
const e3: bitboard.Square = 20;
|
||||
const e4: bitboard.Square = 28;
|
||||
|
||||
try std.testing.expectEqual(bitboard.bit(e3) | bitboard.bit(e4), game.legalMoves(e2));
|
||||
}
|
||||
|
||||
test "legalMoves returns no destinations after checkmate or stalemate" {
|
||||
var mate_game = try initFromFen("rnb1kbnr/pppp1ppp/8/4p3/6Pq/5P2/PPPPP2P/RNBQKBNR w KQkq - 1 3", .tentwo);
|
||||
defer mate_game.deinit(std.testing.allocator);
|
||||
try std.testing.expectEqual(@as(bitboard.Bitboard, 0), mate_game.legalMoves(4)); // white king e1
|
||||
|
||||
var stalemate_game = try initFromFen("7k/5Q2/6K1/8/8/8/8/8 b - - 0 1", .tentwo);
|
||||
defer stalemate_game.deinit(std.testing.allocator);
|
||||
try std.testing.expectEqual(@as(bitboard.Bitboard, 0), stalemate_game.legalMoves(63)); // black king h8
|
||||
}
|
||||
74137
src/chess/generated_magics.zig
Normal file
25
src/chess/magic.zig
Normal file
@ -0,0 +1,25 @@
|
||||
const bitboard = @import("bitboard.zig");
|
||||
const generated = @import("generated_magics.zig");
|
||||
|
||||
pub const Bitboard = bitboard.Bitboard;
|
||||
pub const Square = bitboard.Square;
|
||||
|
||||
pub fn rookAttacks(square: Square, all_occ: Bitboard) Bitboard {
|
||||
const info = generated.rook_magic_info[square];
|
||||
const blockers = all_occ & info.mask;
|
||||
const shift_amount: u6 = @intCast(info.shift);
|
||||
const index: usize = @intCast((blockers *% info.magic) >> shift_amount);
|
||||
return generated.rook_attacks[square][index];
|
||||
}
|
||||
|
||||
pub fn bishopAttacks(square: Square, all_occ: Bitboard) Bitboard {
|
||||
const info = generated.bishop_magic_info[square];
|
||||
const blockers = all_occ & info.mask;
|
||||
const shift_amount: u6 = @intCast(info.shift);
|
||||
const index: usize = @intCast((blockers *% info.magic) >> shift_amount);
|
||||
return generated.bishop_attacks[square][index];
|
||||
}
|
||||
|
||||
pub fn queenAttacks(square: Square, all_occ: Bitboard) Bitboard {
|
||||
return rookAttacks(square, all_occ) | bishopAttacks(square, all_occ);
|
||||
}
|
||||
81
src/chess/piece.zig
Normal file
@ -0,0 +1,81 @@
|
||||
const std = @import("std");
|
||||
|
||||
pub const Color = enum(u1) {
|
||||
black = 0,
|
||||
white = 1,
|
||||
};
|
||||
|
||||
pub const PieceType = enum(u3) {
|
||||
none = 0,
|
||||
pawn = 1,
|
||||
knight = 2,
|
||||
bishop = 3,
|
||||
rook = 4,
|
||||
queen = 5,
|
||||
king = 6,
|
||||
};
|
||||
|
||||
pub fn encode(color: Color, piece_type: PieceType) u4 {
|
||||
if (piece_type == .none) return 0;
|
||||
return (@as(u4, @intFromEnum(color)) << 3) | @as(u4, @intFromEnum(piece_type));
|
||||
}
|
||||
|
||||
pub fn typeOf(encoded: u4) PieceType {
|
||||
const type_bits: u3 = @intCast(encoded & 0b111);
|
||||
return @enumFromInt(type_bits);
|
||||
}
|
||||
|
||||
pub fn colorOf(encoded: u4) ?Color {
|
||||
if (typeOf(encoded) == .none) return null;
|
||||
const color_bit: u1 = @intCast(encoded >> 3);
|
||||
return @enumFromInt(color_bit);
|
||||
}
|
||||
|
||||
pub fn fromFENChar(ch: u8) !u4 {
|
||||
switch (ch) {
|
||||
'P' => return encode(.white, .pawn),
|
||||
'N' => return encode(.white, .knight),
|
||||
'B' => return encode(.white, .bishop),
|
||||
'R' => return encode(.white, .rook),
|
||||
'Q' => return encode(.white, .queen),
|
||||
'K' => return encode(.white, .king),
|
||||
'p' => return encode(.black, .pawn),
|
||||
'n' => return encode(.black, .knight),
|
||||
'b' => return encode(.black, .bishop),
|
||||
'r' => return encode(.black, .rook),
|
||||
'q' => return encode(.black, .queen),
|
||||
'k' => return encode(.black, .king),
|
||||
else => return error.InvalidPieceType,
|
||||
}
|
||||
}
|
||||
|
||||
test "encode pieces uses low bits for type and bit 3 for color" {
|
||||
try std.testing.expectEqual(@as(u4, 1), encode(.black, .pawn));
|
||||
try std.testing.expectEqual(@as(u4, 6), encode(.black, .king));
|
||||
try std.testing.expectEqual(@as(u4, 9), encode(.white, .pawn));
|
||||
try std.testing.expectEqual(@as(u4, 14), encode(.white, .king));
|
||||
try std.testing.expectEqual(@as(u4, 0), encode(.white, .none));
|
||||
try std.testing.expectEqual(@as(u4, 10), encode(.white, .knight));
|
||||
try std.testing.expectEqual(@as(u4, 3), encode(.black, .bishop));
|
||||
}
|
||||
|
||||
test "fromFENChar encodes white and black pieces" {
|
||||
try std.testing.expectEqual(encode(.white, .pawn), fromFENChar('P'));
|
||||
try std.testing.expectEqual(encode(.black, .king), fromFENChar('k'));
|
||||
}
|
||||
|
||||
test "fromFENChar rejects invalid character" {
|
||||
try std.testing.expectError(error.InvalidPieceType, fromFENChar('x'));
|
||||
}
|
||||
|
||||
test "typeOf returns correct type" {
|
||||
try std.testing.expectEqual(.pawn, typeOf(9));
|
||||
try std.testing.expectEqual(.king, typeOf(6));
|
||||
try std.testing.expectEqual(.none, typeOf(0));
|
||||
}
|
||||
|
||||
test "colorOf returns correct color" {
|
||||
try std.testing.expectEqual(null, colorOf(0));
|
||||
try std.testing.expectEqual(.white, colorOf(14));
|
||||
try std.testing.expectEqual(.black, colorOf(3));
|
||||
}
|
||||
252
src/geometry.zig
@ -1,12 +1,250 @@
|
||||
const std = @import("std");
|
||||
|
||||
const board = @import("chess/board.zig");
|
||||
const piece = @import("chess/piece.zig");
|
||||
|
||||
pub const Vertex = extern struct {
|
||||
position: [2]f32,
|
||||
color: [4]f32,
|
||||
uv: [2]f32,
|
||||
};
|
||||
|
||||
const square_vertices = [_]Vertex{
|
||||
.{ .position = .{ -0.5, -0.5 } },
|
||||
.{ .position = .{ 0.5, -0.5 } },
|
||||
.{ .position = .{ 0.5, 0.5 } },
|
||||
.{ .position = .{ -0.5, -0.5 } },
|
||||
.{ .position = .{ 0.5, 0.5 } },
|
||||
.{ .position = .{ -0.5, 0.5 } },
|
||||
pub const BoardRect = struct {
|
||||
left: f32,
|
||||
bottom: f32,
|
||||
width: f32,
|
||||
height: f32,
|
||||
};
|
||||
|
||||
pub const Light = [4]f32{ 0.85, 0.75, 0.60, 1.0 };
|
||||
pub const Dark = [4]f32{ 0.35, 0.20, 0.10, 1.0 };
|
||||
pub const White = [4]f32{ 1.0, 1.0, 1.0, 1.0 };
|
||||
|
||||
pub fn boardRectForExtent(width: u32, height: u32) BoardRect {
|
||||
return boardRectForExtentWithPalette(width, height, true);
|
||||
}
|
||||
|
||||
pub fn boardRectForExtentWithPalette(width: u32, height: u32, show_palette: bool) BoardRect {
|
||||
const window_w: f32 = @floatFromInt(width);
|
||||
const window_h: f32 = @floatFromInt(height);
|
||||
|
||||
const margin_px: f32 = 32.0;
|
||||
const palette_gap_px: f32 = if (show_palette) 16.0 else 0.0;
|
||||
const palette_width_ratio: f32 = if (show_palette) 1.0 / 12.0 else 0.0;
|
||||
const menu_area_px: f32 = 80.0;
|
||||
const fen_gap_ratio: f32 = 1.0 / 18.0;
|
||||
const fen_glyph_height_ratio: f32 = 7.0 / 95.0;
|
||||
const fen_area_ratio = fen_gap_ratio + fen_glyph_height_ratio;
|
||||
|
||||
const available_w = @max(1.0, window_w - (2.0 * margin_px) - palette_gap_px);
|
||||
const available_h = @max(1.0, window_h - (2.0 * margin_px) - menu_area_px);
|
||||
|
||||
const board_from_width = available_w / (1.0 + palette_width_ratio);
|
||||
const board_from_height = available_h / (1.0 + fen_area_ratio);
|
||||
const board_size_px = @min(board_from_width, board_from_height);
|
||||
const palette_width_px = board_size_px * palette_width_ratio;
|
||||
const group_width_px = board_size_px + palette_gap_px + palette_width_px;
|
||||
|
||||
const board_left_px = (window_w - group_width_px) / 2.0;
|
||||
const board_bottom_px = margin_px + (board_size_px * fen_area_ratio);
|
||||
const max_board_top_px = window_h - margin_px - menu_area_px;
|
||||
const board_top_px = @min(board_bottom_px + board_size_px, max_board_top_px);
|
||||
const adjusted_board_size_px = @max(1.0, board_top_px - board_bottom_px);
|
||||
|
||||
return .{
|
||||
.left = (board_left_px / window_w) * 2.0 - 1.0,
|
||||
.bottom = (board_bottom_px / window_h) * 2.0 - 1.0,
|
||||
.width = (adjusted_board_size_px / window_w) * 2.0,
|
||||
.height = (adjusted_board_size_px / window_h) * 2.0,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn appendQuad(
|
||||
vertices: *std.ArrayList(Vertex),
|
||||
allocator: std.mem.Allocator,
|
||||
bottom_left: [2]f32,
|
||||
bottom_right: [2]f32,
|
||||
top_right: [2]f32,
|
||||
top_left: [2]f32,
|
||||
color: [4]f32,
|
||||
) !void {
|
||||
try appendTexturedQuad(
|
||||
vertices,
|
||||
allocator,
|
||||
bottom_left,
|
||||
bottom_right,
|
||||
top_right,
|
||||
top_left,
|
||||
color,
|
||||
.{ 0.0, 0.0 },
|
||||
.{ 0.0, 0.0 },
|
||||
.{ 0.0, 0.0 },
|
||||
.{ 0.0, 0.0 },
|
||||
);
|
||||
}
|
||||
|
||||
pub fn appendTexturedQuad(
|
||||
vertices: *std.ArrayList(Vertex),
|
||||
allocator: std.mem.Allocator,
|
||||
bottom_left: [2]f32,
|
||||
bottom_right: [2]f32,
|
||||
top_right: [2]f32,
|
||||
top_left: [2]f32,
|
||||
color: [4]f32,
|
||||
bottom_left_uv: [2]f32,
|
||||
bottom_right_uv: [2]f32,
|
||||
top_right_uv: [2]f32,
|
||||
top_left_uv: [2]f32,
|
||||
) !void {
|
||||
try vertices.append(allocator, .{
|
||||
.position = bottom_left,
|
||||
.color = color,
|
||||
.uv = bottom_left_uv,
|
||||
});
|
||||
try vertices.append(allocator, .{
|
||||
.position = bottom_right,
|
||||
.color = color,
|
||||
.uv = bottom_right_uv,
|
||||
});
|
||||
try vertices.append(allocator, .{
|
||||
.position = top_right,
|
||||
.color = color,
|
||||
.uv = top_right_uv,
|
||||
});
|
||||
|
||||
try vertices.append(allocator, .{
|
||||
.position = bottom_left,
|
||||
.color = color,
|
||||
.uv = bottom_left_uv,
|
||||
});
|
||||
try vertices.append(allocator, .{
|
||||
.position = top_right,
|
||||
.color = color,
|
||||
.uv = top_right_uv,
|
||||
});
|
||||
try vertices.append(allocator, .{
|
||||
.position = top_left,
|
||||
.color = color,
|
||||
.uv = top_left_uv,
|
||||
});
|
||||
}
|
||||
|
||||
pub fn appendChessboard(
|
||||
vertices: *std.ArrayList(Vertex),
|
||||
board_rect: BoardRect,
|
||||
allocator: std.mem.Allocator,
|
||||
) !void {
|
||||
var rank: usize = 0;
|
||||
while (rank < 8) : (rank += 1) {
|
||||
var file: usize = 0;
|
||||
while (file < 8) : (file += 1) {
|
||||
const color = if ((rank + file) % 2 == 0) Dark else Light;
|
||||
|
||||
const x0: f32 = @floatFromInt(file);
|
||||
const x1: f32 = @floatFromInt(file + 1);
|
||||
const y0: f32 = @floatFromInt(rank);
|
||||
const y1: f32 = @floatFromInt(rank + 1);
|
||||
|
||||
try appendQuad(
|
||||
vertices,
|
||||
allocator,
|
||||
boardToNdc(board_rect, x0, y0),
|
||||
boardToNdc(board_rect, x1, y0),
|
||||
boardToNdc(board_rect, x1, y1),
|
||||
boardToNdc(board_rect, x0, y1),
|
||||
color,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn appendPieceQuad(
|
||||
vertices: *std.ArrayList(Vertex),
|
||||
board_rect: BoardRect,
|
||||
allocator: std.mem.Allocator,
|
||||
file: f32,
|
||||
rank: f32,
|
||||
) !void {
|
||||
try appendPieceQuadWithColor(vertices, board_rect, allocator, file, rank, White);
|
||||
}
|
||||
|
||||
pub fn appendPieceQuadWithColor(
|
||||
vertices: *std.ArrayList(Vertex),
|
||||
board_rect: BoardRect,
|
||||
allocator: std.mem.Allocator,
|
||||
file: f32,
|
||||
rank: f32,
|
||||
color: [4]f32,
|
||||
) !void {
|
||||
const inset: f32 = 0.08;
|
||||
const x0 = file + inset;
|
||||
const x1 = file + 1.0 - inset;
|
||||
const y0 = rank + inset;
|
||||
const y1 = rank + 1.0 - inset;
|
||||
|
||||
try appendTexturedQuad(
|
||||
vertices,
|
||||
allocator,
|
||||
boardToNdc(board_rect, x0, y0),
|
||||
boardToNdc(board_rect, x1, y0),
|
||||
boardToNdc(board_rect, x1, y1),
|
||||
boardToNdc(board_rect, x0, y1),
|
||||
color,
|
||||
.{ 0.0, 1.0 },
|
||||
.{ 1.0, 1.0 },
|
||||
.{ 1.0, 0.0 },
|
||||
.{ 0.0, 0.0 },
|
||||
);
|
||||
}
|
||||
|
||||
pub fn appendPieceQuadCenteredAtNdc(
|
||||
vertices: *std.ArrayList(Vertex),
|
||||
allocator: std.mem.Allocator,
|
||||
center: [2]f32,
|
||||
size: [2]f32,
|
||||
color: [4]f32,
|
||||
) !void {
|
||||
const half_w = size[0] / 2.0;
|
||||
const half_h = size[1] / 2.0;
|
||||
const x0 = center[0] - half_w;
|
||||
const x1 = center[0] + half_w;
|
||||
const y0 = center[1] - half_h;
|
||||
const y1 = center[1] + half_h;
|
||||
|
||||
try appendTexturedQuad(
|
||||
vertices,
|
||||
allocator,
|
||||
.{ x0, y0 },
|
||||
.{ x1, y0 },
|
||||
.{ x1, y1 },
|
||||
.{ x0, y1 },
|
||||
color,
|
||||
.{ 0.0, 1.0 },
|
||||
.{ 1.0, 1.0 },
|
||||
.{ 1.0, 0.0 },
|
||||
.{ 0.0, 0.0 },
|
||||
);
|
||||
}
|
||||
|
||||
pub fn appendPiecesFromBoard(
|
||||
vertices: *std.ArrayList(Vertex),
|
||||
board_rect: BoardRect,
|
||||
allocator: std.mem.Allocator,
|
||||
state: board.BoardState,
|
||||
) !void {
|
||||
for (0..8) |rank| {
|
||||
for (0..8) |file| {
|
||||
const p = state.getSquare(@intCast((rank * 8) + file));
|
||||
if (piece.typeOf(p) != piece.PieceType.none) {
|
||||
try appendPieceQuad(vertices, board_rect, allocator, @floatFromInt(file), @floatFromInt(rank));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn boardToNdc(rect: BoardRect, x: f32, y: f32) [2]f32 {
|
||||
return .{
|
||||
rect.left + (x / 8.0) * rect.width,
|
||||
rect.bottom + (y / 8.0) * rect.height,
|
||||
};
|
||||
}
|
||||
|
||||
1331
src/main.zig
477
src/piece_render.zig
Normal file
@ -0,0 +1,477 @@
|
||||
const std = @import("std");
|
||||
|
||||
const board = @import("chess/board.zig");
|
||||
const fen = @import("chess/fen.zig");
|
||||
const bitboard = @import("chess/bitboard.zig");
|
||||
const geometry = @import("geometry.zig");
|
||||
const piece = @import("chess/piece.zig");
|
||||
|
||||
pub const PieceGroup = enum(u5) {
|
||||
white_pawn,
|
||||
white_knight,
|
||||
white_bishop,
|
||||
white_rook,
|
||||
white_queen,
|
||||
white_king,
|
||||
black_pawn,
|
||||
black_knight,
|
||||
black_bishop,
|
||||
black_rook,
|
||||
black_queen,
|
||||
black_king,
|
||||
dragged_white_pawn,
|
||||
dragged_white_knight,
|
||||
dragged_white_bishop,
|
||||
dragged_white_rook,
|
||||
dragged_white_queen,
|
||||
dragged_white_king,
|
||||
dragged_black_pawn,
|
||||
dragged_black_knight,
|
||||
dragged_black_bishop,
|
||||
dragged_black_rook,
|
||||
dragged_black_queen,
|
||||
dragged_black_king,
|
||||
};
|
||||
|
||||
pub const PieceGroupCount = 24;
|
||||
|
||||
pub const PaletteEntry = struct {
|
||||
group: PieceGroup,
|
||||
encoded: u4,
|
||||
};
|
||||
|
||||
pub const promotion_piece_types = [_]piece.PieceType{ .queen, .rook, .bishop, .knight };
|
||||
|
||||
pub const palette_entries = [_]PaletteEntry{
|
||||
.{ .group = .white_king, .encoded = piece.encode(.white, .king) },
|
||||
.{ .group = .white_queen, .encoded = piece.encode(.white, .queen) },
|
||||
.{ .group = .white_rook, .encoded = piece.encode(.white, .rook) },
|
||||
.{ .group = .white_bishop, .encoded = piece.encode(.white, .bishop) },
|
||||
.{ .group = .white_knight, .encoded = piece.encode(.white, .knight) },
|
||||
.{ .group = .white_pawn, .encoded = piece.encode(.white, .pawn) },
|
||||
.{ .group = .black_king, .encoded = piece.encode(.black, .king) },
|
||||
.{ .group = .black_queen, .encoded = piece.encode(.black, .queen) },
|
||||
.{ .group = .black_rook, .encoded = piece.encode(.black, .rook) },
|
||||
.{ .group = .black_bishop, .encoded = piece.encode(.black, .bishop) },
|
||||
.{ .group = .black_knight, .encoded = piece.encode(.black, .knight) },
|
||||
.{ .group = .black_pawn, .encoded = piece.encode(.black, .pawn) },
|
||||
};
|
||||
|
||||
pub fn paletteRectForBoard(board_rect: geometry.BoardRect) geometry.BoardRect {
|
||||
const gap = board_rect.width / 48.0;
|
||||
return .{
|
||||
.left = board_rect.left + board_rect.width + gap,
|
||||
.bottom = board_rect.bottom,
|
||||
.width = board_rect.width / @as(f32, @floatFromInt(palette_entries.len)),
|
||||
.height = board_rect.height,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn promotionPopupRectForSquare(board_rect: geometry.BoardRect, square: bitboard.Square) geometry.BoardRect {
|
||||
const cell_width = (board_rect.width / 8.0) * 0.65;
|
||||
const cell_height = (board_rect.height / 8.0) * 0.65;
|
||||
const width = cell_width * @as(f32, @floatFromInt(promotion_piece_types.len));
|
||||
const height = cell_height;
|
||||
const center = geometry.boardToNdc(
|
||||
board_rect,
|
||||
@as(f32, @floatFromInt(square % 8)) + 0.5,
|
||||
@as(f32, @floatFromInt(square / 8)) + 0.5,
|
||||
);
|
||||
const board_right = board_rect.left + board_rect.width;
|
||||
const board_top = board_rect.bottom + board_rect.height;
|
||||
const unclamped_left = center[0] - (width / 2.0);
|
||||
const unclamped_bottom = center[1] - (height / 2.0);
|
||||
|
||||
return .{
|
||||
.left = @max(board_rect.left, @min(unclamped_left, board_right - width)),
|
||||
.bottom = @max(board_rect.bottom, @min(unclamped_bottom, board_top - height)),
|
||||
.width = width,
|
||||
.height = height,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn promotionChoiceRectForIndex(board_rect: geometry.BoardRect, square: bitboard.Square, index: usize) geometry.BoardRect {
|
||||
const popup_rect = promotionPopupRectForSquare(board_rect, square);
|
||||
const cell_width = popup_rect.width / @as(f32, @floatFromInt(promotion_piece_types.len));
|
||||
return .{
|
||||
.left = popup_rect.left + (@as(f32, @floatFromInt(index)) * cell_width),
|
||||
.bottom = popup_rect.bottom,
|
||||
.width = cell_width,
|
||||
.height = popup_rect.height,
|
||||
};
|
||||
}
|
||||
|
||||
pub const PieceVertexGroups = struct {
|
||||
groups: [PieceGroupCount]std.ArrayList(geometry.Vertex),
|
||||
|
||||
pub fn init() PieceVertexGroups {
|
||||
return .{
|
||||
.groups = [_]std.ArrayList(geometry.Vertex){.empty} ** PieceGroupCount,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: *PieceVertexGroups, allocator: std.mem.Allocator) void {
|
||||
for (&self.groups) |*vertex_group| {
|
||||
vertex_group.deinit(allocator);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn group(self: *PieceVertexGroups, piece_group: PieceGroup) *std.ArrayList(geometry.Vertex) {
|
||||
return &self.groups[@intFromEnum(piece_group)];
|
||||
}
|
||||
|
||||
pub fn constGroup(self: *const PieceVertexGroups, piece_group: PieceGroup) *const std.ArrayList(geometry.Vertex) {
|
||||
return &self.groups[@intFromEnum(piece_group)];
|
||||
}
|
||||
};
|
||||
|
||||
pub fn pieceGroupFromEncoded(encoded: u4) ?PieceGroup {
|
||||
return pieceGroupFromEncodedWithOffset(encoded, false);
|
||||
}
|
||||
|
||||
fn draggedPieceGroupFromEncoded(encoded: u4) ?PieceGroup {
|
||||
return pieceGroupFromEncodedWithOffset(encoded, true);
|
||||
}
|
||||
|
||||
fn pieceGroupFromEncodedWithOffset(encoded: u4, dragged: bool) ?PieceGroup {
|
||||
if (encoded == 0) return null;
|
||||
|
||||
const color = piece.colorOf(encoded) orelse return null;
|
||||
const piece_type = piece.typeOf(encoded);
|
||||
|
||||
return switch (color) {
|
||||
.white => switch (piece_type) {
|
||||
.pawn => if (dragged) .dragged_white_pawn else .white_pawn,
|
||||
.knight => if (dragged) .dragged_white_knight else .white_knight,
|
||||
.bishop => if (dragged) .dragged_white_bishop else .white_bishop,
|
||||
.rook => if (dragged) .dragged_white_rook else .white_rook,
|
||||
.queen => if (dragged) .dragged_white_queen else .white_queen,
|
||||
.king => if (dragged) .dragged_white_king else .white_king,
|
||||
.none => null,
|
||||
},
|
||||
.black => switch (piece_type) {
|
||||
.pawn => if (dragged) .dragged_black_pawn else .black_pawn,
|
||||
.knight => if (dragged) .dragged_black_knight else .black_knight,
|
||||
.bishop => if (dragged) .dragged_black_bishop else .black_bishop,
|
||||
.rook => if (dragged) .dragged_black_rook else .black_rook,
|
||||
.queen => if (dragged) .dragged_black_queen else .black_queen,
|
||||
.king => if (dragged) .dragged_black_king else .black_king,
|
||||
.none => null,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
fn appendSidewaysPieceQuad(
|
||||
vertices: *std.ArrayList(geometry.Vertex),
|
||||
board_rect: geometry.BoardRect,
|
||||
allocator: std.mem.Allocator,
|
||||
file: f32,
|
||||
rank: f32,
|
||||
) !void {
|
||||
const inset: f32 = 0.08;
|
||||
const x0 = file + inset;
|
||||
const x1 = file + 1.0 - inset;
|
||||
const y0 = rank + inset;
|
||||
const y1 = rank + 1.0 - inset;
|
||||
|
||||
try geometry.appendTexturedQuad(
|
||||
vertices,
|
||||
allocator,
|
||||
geometry.boardToNdc(board_rect, x1, y0),
|
||||
geometry.boardToNdc(board_rect, x1, y1),
|
||||
geometry.boardToNdc(board_rect, x0, y1),
|
||||
geometry.boardToNdc(board_rect, x0, y0),
|
||||
geometry.White,
|
||||
.{ 0.0, 1.0 },
|
||||
.{ 1.0, 1.0 },
|
||||
.{ 1.0, 0.0 },
|
||||
.{ 0.0, 0.0 },
|
||||
);
|
||||
}
|
||||
|
||||
fn appendPieceQuadInRect(
|
||||
vertices: *std.ArrayList(geometry.Vertex),
|
||||
allocator: std.mem.Allocator,
|
||||
rect: geometry.BoardRect,
|
||||
) !void {
|
||||
const inset_x = rect.width * 0.08;
|
||||
const inset_y = rect.height * 0.08;
|
||||
const x0 = rect.left + inset_x;
|
||||
const x1 = rect.left + rect.width - inset_x;
|
||||
const y0 = rect.bottom + inset_y;
|
||||
const y1 = rect.bottom + rect.height - inset_y;
|
||||
|
||||
try geometry.appendTexturedQuad(
|
||||
vertices,
|
||||
allocator,
|
||||
.{ x0, y0 },
|
||||
.{ x1, y0 },
|
||||
.{ x1, y1 },
|
||||
.{ x0, y1 },
|
||||
geometry.White,
|
||||
.{ 0.0, 1.0 },
|
||||
.{ 1.0, 1.0 },
|
||||
.{ 1.0, 0.0 },
|
||||
.{ 0.0, 0.0 },
|
||||
);
|
||||
}
|
||||
|
||||
pub fn appendPiecesGroupedFromBoard(
|
||||
groups: *PieceVertexGroups,
|
||||
board_rect: geometry.BoardRect,
|
||||
allocator: std.mem.Allocator,
|
||||
state: board.BoardState,
|
||||
) !void {
|
||||
try appendPiecesGroupedFromBoardExcept(groups, board_rect, allocator, state, null);
|
||||
}
|
||||
|
||||
pub fn appendPiecesGroupedFromBoardExcept(
|
||||
groups: *PieceVertexGroups,
|
||||
board_rect: geometry.BoardRect,
|
||||
allocator: std.mem.Allocator,
|
||||
state: board.BoardState,
|
||||
except_square: ?@import("board_input.zig").SquareCoord,
|
||||
) !void {
|
||||
try appendPiecesGroupedFromBoardExceptWithGameOver(groups, board_rect, allocator, state, except_square, null);
|
||||
}
|
||||
|
||||
pub fn appendPiecesGroupedFromBoardExceptWithGameOver(
|
||||
groups: *PieceVertexGroups,
|
||||
board_rect: geometry.BoardRect,
|
||||
allocator: std.mem.Allocator,
|
||||
state: board.BoardState,
|
||||
except_square: ?@import("board_input.zig").SquareCoord,
|
||||
losing_king_square: ?@import("board_input.zig").SquareCoord,
|
||||
) !void {
|
||||
for (0..8) |rank| {
|
||||
for (0..8) |file| {
|
||||
if (except_square) |except| {
|
||||
if (except.file == file and except.rank == rank) continue;
|
||||
}
|
||||
|
||||
const encoded = state.getSquare(@intCast((rank * 8) + file));
|
||||
const piece_group = pieceGroupFromEncoded(encoded) orelse continue;
|
||||
|
||||
if (losing_king_square) |losing| {
|
||||
if (losing.file == file and losing.rank == rank) {
|
||||
try appendSidewaysPieceQuad(
|
||||
groups.group(piece_group),
|
||||
board_rect,
|
||||
allocator,
|
||||
@as(f32, @floatFromInt(file)),
|
||||
@as(f32, @floatFromInt(rank)),
|
||||
);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
try geometry.appendPieceQuad(
|
||||
groups.group(piece_group),
|
||||
board_rect,
|
||||
allocator,
|
||||
@as(f32, @floatFromInt(file)),
|
||||
@as(f32, @floatFromInt(rank)),
|
||||
);
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn appendPieceGhostAtSquare(
|
||||
groups: *PieceVertexGroups,
|
||||
board_rect: geometry.BoardRect,
|
||||
allocator: std.mem.Allocator,
|
||||
encoded: u4,
|
||||
square: @import("board_input.zig").SquareCoord,
|
||||
) !void {
|
||||
const piece_group = pieceGroupFromEncoded(encoded) orelse return;
|
||||
try geometry.appendPieceQuadWithColor(
|
||||
groups.group(piece_group),
|
||||
board_rect,
|
||||
allocator,
|
||||
@floatFromInt(square.file),
|
||||
@floatFromInt(square.rank),
|
||||
.{ 1.0, 1.0, 1.0, 0.35 },
|
||||
);
|
||||
}
|
||||
|
||||
pub fn appendDraggedPiece(
|
||||
groups: *PieceVertexGroups,
|
||||
board_rect: geometry.BoardRect,
|
||||
allocator: std.mem.Allocator,
|
||||
encoded: u4,
|
||||
center_ndc: [2]f32,
|
||||
) !void {
|
||||
const piece_group = draggedPieceGroupFromEncoded(encoded) orelse return;
|
||||
try geometry.appendPieceQuadCenteredAtNdc(
|
||||
groups.group(piece_group),
|
||||
allocator,
|
||||
center_ndc,
|
||||
.{ board_rect.width / 8.0 * 0.84, board_rect.height / 8.0 * 0.84 },
|
||||
geometry.White,
|
||||
);
|
||||
}
|
||||
|
||||
pub fn appendPalettePiecesGrouped(
|
||||
groups: *PieceVertexGroups,
|
||||
board_rect: geometry.BoardRect,
|
||||
allocator: std.mem.Allocator,
|
||||
) !void {
|
||||
const palette_rect = paletteRectForBoard(board_rect);
|
||||
const cell_height = palette_rect.height / @as(f32, @floatFromInt(palette_entries.len));
|
||||
|
||||
for (palette_entries, 0..) |entry, i| {
|
||||
const y = palette_rect.bottom + (@as(f32, @floatFromInt(palette_entries.len - 1 - i)) * cell_height);
|
||||
const cell_rect = geometry.BoardRect{
|
||||
.left = palette_rect.left,
|
||||
.bottom = y,
|
||||
.width = palette_rect.width,
|
||||
.height = cell_height,
|
||||
};
|
||||
try appendPieceQuadInRect(groups.group(entry.group), allocator, cell_rect);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn appendPromotionPiecesGrouped(
|
||||
groups: *PieceVertexGroups,
|
||||
board_rect: geometry.BoardRect,
|
||||
allocator: std.mem.Allocator,
|
||||
color: piece.Color,
|
||||
square: bitboard.Square,
|
||||
) !void {
|
||||
for (promotion_piece_types, 0..) |piece_type, i| {
|
||||
const encoded = piece.encode(color, piece_type);
|
||||
const piece_group = pieceGroupFromEncoded(encoded) orelse continue;
|
||||
const cell_rect = promotionChoiceRectForIndex(board_rect, square, i);
|
||||
const piece_rect = geometry.BoardRect{
|
||||
.left = cell_rect.left + (cell_rect.width * 0.25),
|
||||
.bottom = cell_rect.bottom + (cell_rect.height * 0.25),
|
||||
.width = cell_rect.width * 0.5,
|
||||
.height = cell_rect.height * 0.5,
|
||||
};
|
||||
try appendPieceQuadInRect(groups.group(piece_group), allocator, piece_rect);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn paletteIndexForEncoded(encoded: u4) ?usize {
|
||||
for (palette_entries, 0..) |entry, i| {
|
||||
if (entry.encoded == encoded) return i;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
fn expectGroupVertexCount(groups: *const PieceVertexGroups, piece_group: PieceGroup, expected: usize) !void {
|
||||
try std.testing.expectEqual(expected, groups.constGroup(piece_group).items.len);
|
||||
}
|
||||
|
||||
test "pieceGroupFromEncoded maps encoded pieces to render groups" {
|
||||
try std.testing.expectEqual(PieceGroup.white_pawn, pieceGroupFromEncoded(piece.encode(.white, .pawn)).?);
|
||||
try std.testing.expectEqual(PieceGroup.white_knight, pieceGroupFromEncoded(piece.encode(.white, .knight)).?);
|
||||
try std.testing.expectEqual(PieceGroup.white_bishop, pieceGroupFromEncoded(piece.encode(.white, .bishop)).?);
|
||||
try std.testing.expectEqual(PieceGroup.white_rook, pieceGroupFromEncoded(piece.encode(.white, .rook)).?);
|
||||
try std.testing.expectEqual(PieceGroup.white_queen, pieceGroupFromEncoded(piece.encode(.white, .queen)).?);
|
||||
try std.testing.expectEqual(PieceGroup.white_king, pieceGroupFromEncoded(piece.encode(.white, .king)).?);
|
||||
|
||||
try std.testing.expectEqual(PieceGroup.black_pawn, pieceGroupFromEncoded(piece.encode(.black, .pawn)).?);
|
||||
try std.testing.expectEqual(PieceGroup.black_knight, pieceGroupFromEncoded(piece.encode(.black, .knight)).?);
|
||||
try std.testing.expectEqual(PieceGroup.black_bishop, pieceGroupFromEncoded(piece.encode(.black, .bishop)).?);
|
||||
try std.testing.expectEqual(PieceGroup.black_rook, pieceGroupFromEncoded(piece.encode(.black, .rook)).?);
|
||||
try std.testing.expectEqual(PieceGroup.black_queen, pieceGroupFromEncoded(piece.encode(.black, .queen)).?);
|
||||
try std.testing.expectEqual(PieceGroup.black_king, pieceGroupFromEncoded(piece.encode(.black, .king)).?);
|
||||
|
||||
try std.testing.expectEqual(null, pieceGroupFromEncoded(0));
|
||||
}
|
||||
|
||||
test "appendPiecesGroupedFromBoard leaves all groups empty for empty board" {
|
||||
const state = board.BoardState.empty();
|
||||
const board_rect = geometry.boardRectForExtent(800, 600);
|
||||
|
||||
var groups = PieceVertexGroups.init();
|
||||
defer groups.deinit(std.testing.allocator);
|
||||
|
||||
try appendPiecesGroupedFromBoard(&groups, board_rect, std.testing.allocator, state);
|
||||
|
||||
for (groups.groups) |vertex_group| {
|
||||
try std.testing.expectEqual(@as(usize, 0), vertex_group.items.len);
|
||||
}
|
||||
}
|
||||
|
||||
test "appendPalettePiecesGrouped adds one piece to each piece group" {
|
||||
const board_rect = geometry.boardRectForExtent(800, 600);
|
||||
|
||||
var groups = PieceVertexGroups.init();
|
||||
defer groups.deinit(std.testing.allocator);
|
||||
|
||||
try appendPalettePiecesGrouped(&groups, board_rect, std.testing.allocator);
|
||||
|
||||
for (groups.groups, 0..) |vertex_group, i| {
|
||||
const group: PieceGroup = @enumFromInt(@as(u5, @intCast(i)));
|
||||
const is_dragged_group = @intFromEnum(group) >= @intFromEnum(PieceGroup.dragged_white_pawn);
|
||||
const expected: usize = if (is_dragged_group) 0 else 6;
|
||||
try std.testing.expectEqual(expected, vertex_group.items.len);
|
||||
}
|
||||
}
|
||||
|
||||
test "appendDraggedPiece uses dragged overlay group" {
|
||||
const board_rect = geometry.boardRectForExtent(800, 600);
|
||||
|
||||
var groups = PieceVertexGroups.init();
|
||||
defer groups.deinit(std.testing.allocator);
|
||||
|
||||
try appendDraggedPiece(&groups, board_rect, std.testing.allocator, piece.encode(.white, .queen), .{ 0.0, 0.0 });
|
||||
|
||||
try expectGroupVertexCount(&groups, .white_queen, 0);
|
||||
try expectGroupVertexCount(&groups, .dragged_white_queen, 6);
|
||||
}
|
||||
|
||||
test "appendPiecesGroupedFromBoardExceptWithGameOver renders sideways losing king" {
|
||||
var state = board.BoardState.empty();
|
||||
state.setSquare(63, piece.encode(.black, .king));
|
||||
state.setSquare(45, piece.encode(.white, .king));
|
||||
const board_rect = geometry.boardRectForExtent(800, 600);
|
||||
|
||||
var groups = PieceVertexGroups.init();
|
||||
defer groups.deinit(std.testing.allocator);
|
||||
|
||||
try appendPiecesGroupedFromBoardExceptWithGameOver(
|
||||
&groups,
|
||||
board_rect,
|
||||
std.testing.allocator,
|
||||
state,
|
||||
null,
|
||||
.{ .file = 7, .rank = 7 },
|
||||
);
|
||||
|
||||
try expectGroupVertexCount(&groups, .white_king, 6);
|
||||
try expectGroupVertexCount(&groups, .black_king, 6);
|
||||
}
|
||||
|
||||
test "appendPiecesGroupedFromBoard groups starting position vertices by piece" {
|
||||
const state = try fen.parseFen("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1");
|
||||
const board_rect = geometry.boardRectForExtent(800, 600);
|
||||
|
||||
var groups = PieceVertexGroups.init();
|
||||
defer groups.deinit(std.testing.allocator);
|
||||
|
||||
try appendPiecesGroupedFromBoard(&groups, board_rect, std.testing.allocator, state);
|
||||
|
||||
try expectGroupVertexCount(&groups, .white_pawn, 8 * 6);
|
||||
try expectGroupVertexCount(&groups, .white_knight, 2 * 6);
|
||||
try expectGroupVertexCount(&groups, .white_bishop, 2 * 6);
|
||||
try expectGroupVertexCount(&groups, .white_rook, 2 * 6);
|
||||
try expectGroupVertexCount(&groups, .white_queen, 1 * 6);
|
||||
try expectGroupVertexCount(&groups, .white_king, 1 * 6);
|
||||
|
||||
try expectGroupVertexCount(&groups, .black_pawn, 8 * 6);
|
||||
try expectGroupVertexCount(&groups, .black_knight, 2 * 6);
|
||||
try expectGroupVertexCount(&groups, .black_bishop, 2 * 6);
|
||||
try expectGroupVertexCount(&groups, .black_rook, 2 * 6);
|
||||
try expectGroupVertexCount(&groups, .black_queen, 1 * 6);
|
||||
try expectGroupVertexCount(&groups, .black_king, 1 * 6);
|
||||
|
||||
var total_vertices: usize = 0;
|
||||
for (groups.groups) |vertex_group| {
|
||||
total_vertices += vertex_group.items.len;
|
||||
}
|
||||
try std.testing.expectEqual(@as(usize, 32 * 6), total_vertices);
|
||||
}
|
||||
585
src/text_render.zig
Normal file
@ -0,0 +1,585 @@
|
||||
const std = @import("std");
|
||||
|
||||
const board_input = @import("board_input.zig");
|
||||
const bitboard = @import("chess/bitboard.zig");
|
||||
const chess_board = @import("chess/board.zig");
|
||||
const geometry = @import("geometry.zig");
|
||||
const piece_render = @import("piece_render.zig");
|
||||
|
||||
const Glyph = [7]u5;
|
||||
|
||||
pub const TextStyle = struct {
|
||||
pixel_size: f32,
|
||||
pixel_aspect: f32 = 1.0,
|
||||
color: [4]f32,
|
||||
};
|
||||
|
||||
fn glyphForChar(ch: u8) ?Glyph {
|
||||
return switch (ch) {
|
||||
'0' => .{ 0b01110, 0b10001, 0b10011, 0b10101, 0b11001, 0b10001, 0b01110 },
|
||||
'1' => .{ 0b00100, 0b01100, 0b00100, 0b00100, 0b00100, 0b00100, 0b01110 },
|
||||
'2' => .{ 0b01110, 0b10001, 0b00001, 0b00010, 0b00100, 0b01000, 0b11111 },
|
||||
'3' => .{ 0b11110, 0b00001, 0b00001, 0b01110, 0b00001, 0b00001, 0b11110 },
|
||||
'4' => .{ 0b00010, 0b00110, 0b01010, 0b10010, 0b11111, 0b00010, 0b00010 },
|
||||
'5' => .{ 0b11111, 0b10000, 0b10000, 0b11110, 0b00001, 0b00001, 0b11110 },
|
||||
'6' => .{ 0b01110, 0b10000, 0b10000, 0b11110, 0b10001, 0b10001, 0b01110 },
|
||||
'7' => .{ 0b11111, 0b00001, 0b00010, 0b00100, 0b01000, 0b01000, 0b01000 },
|
||||
'8' => .{ 0b01110, 0b10001, 0b10001, 0b01110, 0b10001, 0b10001, 0b01110 },
|
||||
'9' => .{ 0b01110, 0b10001, 0b10001, 0b01111, 0b00001, 0b00001, 0b01110 },
|
||||
|
||||
'a' => .{ 0b00000, 0b00000, 0b01110, 0b00001, 0b01111, 0b10001, 0b01111 },
|
||||
'b' => .{ 0b10000, 0b10000, 0b10110, 0b11001, 0b10001, 0b10001, 0b11110 },
|
||||
'c' => .{ 0b00000, 0b00000, 0b01111, 0b10000, 0b10000, 0b10000, 0b01111 },
|
||||
'd' => .{ 0b00001, 0b00001, 0b01101, 0b10011, 0b10001, 0b10001, 0b01111 },
|
||||
'e' => .{ 0b00000, 0b00000, 0b01110, 0b10001, 0b11111, 0b10000, 0b01110 },
|
||||
'f' => .{ 0b00110, 0b01000, 0b01000, 0b11100, 0b01000, 0b01000, 0b01000 },
|
||||
'g' => .{ 0b00000, 0b01111, 0b10001, 0b10001, 0b01111, 0b00001, 0b01110 },
|
||||
'i' => .{ 0b00100, 0b00000, 0b01100, 0b00100, 0b00100, 0b00100, 0b01110 },
|
||||
'h' => .{ 0b10000, 0b10000, 0b10110, 0b11001, 0b10001, 0b10001, 0b10001 },
|
||||
'k' => .{ 0b10000, 0b10010, 0b10100, 0b11000, 0b10100, 0b10010, 0b10001 },
|
||||
'l' => .{ 0b01100, 0b00100, 0b00100, 0b00100, 0b00100, 0b00100, 0b01110 },
|
||||
'n' => .{ 0b00000, 0b00000, 0b10110, 0b11001, 0b10001, 0b10001, 0b10001 },
|
||||
'p' => .{ 0b00000, 0b00000, 0b11110, 0b10001, 0b11110, 0b10000, 0b10000 },
|
||||
'q' => .{ 0b00000, 0b00000, 0b01101, 0b10011, 0b01111, 0b00001, 0b00001 },
|
||||
'r' => .{ 0b00000, 0b00000, 0b10110, 0b11001, 0b10000, 0b10000, 0b10000 },
|
||||
't' => .{ 0b01000, 0b01000, 0b11100, 0b01000, 0b01000, 0b01001, 0b00110 },
|
||||
'w' => .{ 0b00000, 0b00000, 0b10001, 0b10001, 0b10101, 0b10101, 0b01010 },
|
||||
'y' => .{ 0b00000, 0b10001, 0b10001, 0b01111, 0b00001, 0b00001, 0b01110 },
|
||||
|
||||
'A' => .{ 0b01110, 0b10001, 0b10001, 0b11111, 0b10001, 0b10001, 0b10001 },
|
||||
'B' => .{ 0b11110, 0b10001, 0b10001, 0b11110, 0b10001, 0b10001, 0b11110 },
|
||||
'D' => .{ 0b11110, 0b10001, 0b10001, 0b10001, 0b10001, 0b10001, 0b11110 },
|
||||
'E' => .{ 0b11111, 0b10000, 0b10000, 0b11110, 0b10000, 0b10000, 0b11111 },
|
||||
'I' => .{ 0b11111, 0b00100, 0b00100, 0b00100, 0b00100, 0b00100, 0b11111 },
|
||||
'K' => .{ 0b10001, 0b10010, 0b10100, 0b11000, 0b10100, 0b10010, 0b10001 },
|
||||
'L' => .{ 0b10000, 0b10000, 0b10000, 0b10000, 0b10000, 0b10000, 0b11111 },
|
||||
'N' => .{ 0b10001, 0b11001, 0b10101, 0b10011, 0b10001, 0b10001, 0b10001 },
|
||||
'P' => .{ 0b11110, 0b10001, 0b10001, 0b11110, 0b10000, 0b10000, 0b10000 },
|
||||
'Q' => .{ 0b01110, 0b10001, 0b10001, 0b10001, 0b10101, 0b10010, 0b01101 },
|
||||
'R' => .{ 0b11110, 0b10001, 0b10001, 0b11110, 0b10100, 0b10010, 0b10001 },
|
||||
'S' => .{ 0b01111, 0b10000, 0b10000, 0b01110, 0b00001, 0b00001, 0b11110 },
|
||||
'T' => .{ 0b11111, 0b00100, 0b00100, 0b00100, 0b00100, 0b00100, 0b00100 },
|
||||
'Y' => .{ 0b10001, 0b10001, 0b01010, 0b00100, 0b00100, 0b00100, 0b00100 },
|
||||
|
||||
'#' => .{ 0b01010, 0b01010, 0b11111, 0b01010, 0b11111, 0b01010, 0b01010 },
|
||||
'-' => .{ 0b00000, 0b00000, 0b00000, 0b11111, 0b00000, 0b00000, 0b00000 },
|
||||
'/' => .{ 0b00001, 0b00010, 0b00010, 0b00100, 0b01000, 0b01000, 0b10000 },
|
||||
else => null,
|
||||
};
|
||||
}
|
||||
|
||||
fn appendGlyph(
|
||||
vertices: *std.ArrayList(geometry.Vertex),
|
||||
allocator: std.mem.Allocator,
|
||||
glyph: Glyph,
|
||||
x: f32,
|
||||
y: f32,
|
||||
style: TextStyle,
|
||||
) !void {
|
||||
for (glyph, 0..) |row_bits, row| {
|
||||
for (0..5) |col| {
|
||||
const shift: u3 = @intCast(4 - col);
|
||||
if (((row_bits >> shift) & 1) == 0) continue;
|
||||
|
||||
const pixel_width = style.pixel_size * style.pixel_aspect;
|
||||
const x0 = x + (@as(f32, @floatFromInt(col)) * pixel_width);
|
||||
const y0 = y + (@as(f32, @floatFromInt(6 - row)) * style.pixel_size);
|
||||
const x1 = x0 + pixel_width;
|
||||
const y1 = y0 + style.pixel_size;
|
||||
|
||||
try geometry.appendQuad(
|
||||
vertices,
|
||||
allocator,
|
||||
.{ x0, y0 },
|
||||
.{ x1, y0 },
|
||||
.{ x1, y1 },
|
||||
.{ x0, y1 },
|
||||
style.color,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn appendText(
|
||||
vertices: *std.ArrayList(geometry.Vertex),
|
||||
allocator: std.mem.Allocator,
|
||||
text: []const u8,
|
||||
x: f32,
|
||||
y: f32,
|
||||
style: TextStyle,
|
||||
) !void {
|
||||
var cursor_x = x;
|
||||
const advance = style.pixel_size * style.pixel_aspect * 6.0;
|
||||
|
||||
for (text) |ch| {
|
||||
if (ch == ' ') {
|
||||
cursor_x += advance;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (glyphForChar(ch)) |glyph| {
|
||||
try appendGlyph(vertices, allocator, glyph, cursor_x, y, style);
|
||||
}
|
||||
|
||||
cursor_x += advance;
|
||||
}
|
||||
}
|
||||
|
||||
const SelectedHighlightColor = [4]f32{ 0.12, 0.55, 0.12, 0.48 };
|
||||
const HoverHighlightColor = [4]f32{ 0.45, 0.90, 0.45, 0.38 };
|
||||
|
||||
fn appendSquareOverlay(
|
||||
vertices: *std.ArrayList(geometry.Vertex),
|
||||
allocator: std.mem.Allocator,
|
||||
board_rect: geometry.BoardRect,
|
||||
square: board_input.SquareCoord,
|
||||
color: [4]f32,
|
||||
) !void {
|
||||
const file: f32 = @floatFromInt(square.file);
|
||||
const rank: f32 = @floatFromInt(square.rank);
|
||||
|
||||
try geometry.appendQuad(
|
||||
vertices,
|
||||
allocator,
|
||||
geometry.boardToNdc(board_rect, file, rank),
|
||||
geometry.boardToNdc(board_rect, file + 1.0, rank),
|
||||
geometry.boardToNdc(board_rect, file + 1.0, rank + 1.0),
|
||||
geometry.boardToNdc(board_rect, file, rank + 1.0),
|
||||
color,
|
||||
);
|
||||
}
|
||||
|
||||
pub fn appendSelectedSquareHighlight(
|
||||
vertices: *std.ArrayList(geometry.Vertex),
|
||||
allocator: std.mem.Allocator,
|
||||
board_rect: geometry.BoardRect,
|
||||
selected: ?board_input.SquareCoord,
|
||||
) !void {
|
||||
const square = selected orelse return;
|
||||
try appendSquareOverlay(vertices, allocator, board_rect, square, SelectedHighlightColor);
|
||||
}
|
||||
|
||||
pub fn appendPalettePieceHighlight(
|
||||
vertices: *std.ArrayList(geometry.Vertex),
|
||||
allocator: std.mem.Allocator,
|
||||
board_rect: geometry.BoardRect,
|
||||
selected_piece: ?u4,
|
||||
) !void {
|
||||
const encoded = selected_piece orelse return;
|
||||
const index = piece_render.paletteIndexForEncoded(encoded) orelse return;
|
||||
const palette_rect = piece_render.paletteRectForBoard(board_rect);
|
||||
const cell_height = palette_rect.height / @as(f32, @floatFromInt(piece_render.palette_entries.len));
|
||||
const y = palette_rect.bottom + (@as(f32, @floatFromInt(piece_render.palette_entries.len - 1 - index)) * cell_height);
|
||||
|
||||
try geometry.appendQuad(
|
||||
vertices,
|
||||
allocator,
|
||||
.{ palette_rect.left, y },
|
||||
.{ palette_rect.left + palette_rect.width, y },
|
||||
.{ palette_rect.left + palette_rect.width, y + cell_height },
|
||||
.{ palette_rect.left, y + cell_height },
|
||||
.{ 0.45, 0.90, 0.45, 0.38 },
|
||||
);
|
||||
}
|
||||
|
||||
pub fn appendHoveredSquareHighlight(
|
||||
vertices: *std.ArrayList(geometry.Vertex),
|
||||
allocator: std.mem.Allocator,
|
||||
board_rect: geometry.BoardRect,
|
||||
hovered: ?board_input.SquareCoord,
|
||||
) !void {
|
||||
const square = hovered orelse return;
|
||||
try appendSquareOverlay(vertices, allocator, board_rect, square, HoverHighlightColor);
|
||||
}
|
||||
|
||||
pub fn appendCheckBorder(
|
||||
vertices: *std.ArrayList(geometry.Vertex),
|
||||
allocator: std.mem.Allocator,
|
||||
board_rect: geometry.BoardRect,
|
||||
checked: ?board_input.SquareCoord,
|
||||
) !void {
|
||||
const square = checked orelse return;
|
||||
const file: f32 = @floatFromInt(square.file);
|
||||
const rank: f32 = @floatFromInt(square.rank);
|
||||
const thickness: f32 = 0.08;
|
||||
const color = [4]f32{ 1.0, 0.05, 0.05, 0.85 };
|
||||
|
||||
try geometry.appendQuad(vertices, allocator, geometry.boardToNdc(board_rect, file, rank), geometry.boardToNdc(board_rect, file + 1.0, rank), geometry.boardToNdc(board_rect, file + 1.0, rank + thickness), geometry.boardToNdc(board_rect, file, rank + thickness), color);
|
||||
try geometry.appendQuad(vertices, allocator, geometry.boardToNdc(board_rect, file, rank + 1.0 - thickness), geometry.boardToNdc(board_rect, file + 1.0, rank + 1.0 - thickness), geometry.boardToNdc(board_rect, file + 1.0, rank + 1.0), geometry.boardToNdc(board_rect, file, rank + 1.0), color);
|
||||
try geometry.appendQuad(vertices, allocator, geometry.boardToNdc(board_rect, file, rank), geometry.boardToNdc(board_rect, file + thickness, rank), geometry.boardToNdc(board_rect, file + thickness, rank + 1.0), geometry.boardToNdc(board_rect, file, rank + 1.0), color);
|
||||
try geometry.appendQuad(vertices, allocator, geometry.boardToNdc(board_rect, file + 1.0 - thickness, rank), geometry.boardToNdc(board_rect, file + 1.0, rank), geometry.boardToNdc(board_rect, file + 1.0, rank + 1.0), geometry.boardToNdc(board_rect, file + 1.0 - thickness, rank + 1.0), color);
|
||||
}
|
||||
|
||||
pub fn appendCheckmateMarker(
|
||||
vertices: *std.ArrayList(geometry.Vertex),
|
||||
allocator: std.mem.Allocator,
|
||||
board_rect: geometry.BoardRect,
|
||||
winning_king: ?board_input.SquareCoord,
|
||||
) !void {
|
||||
const square = winning_king orelse return;
|
||||
const square_w = board_rect.width / 8.0;
|
||||
const square_h = board_rect.height / 8.0;
|
||||
const pixel_size = @min(square_w, square_h) * 0.045;
|
||||
const glyph_w = pixel_size * 5.0;
|
||||
const glyph_h = pixel_size * 7.0;
|
||||
const padding_x = square_w * 0.10;
|
||||
const padding_y = square_h * 0.10;
|
||||
const top_right = geometry.boardToNdc(
|
||||
board_rect,
|
||||
@as(f32, @floatFromInt(square.file)) + 1.0,
|
||||
@as(f32, @floatFromInt(square.rank)) + 1.0,
|
||||
);
|
||||
|
||||
try appendText(
|
||||
vertices,
|
||||
allocator,
|
||||
"#",
|
||||
top_right[0] - padding_x - glyph_w,
|
||||
top_right[1] - padding_y - glyph_h,
|
||||
.{ .pixel_size = pixel_size, .color = .{ 1.0, 0.92, 0.18, 1.0 } },
|
||||
);
|
||||
}
|
||||
|
||||
pub fn appendValidMoveDots(
|
||||
vertices: *std.ArrayList(geometry.Vertex),
|
||||
allocator: std.mem.Allocator,
|
||||
board_rect: geometry.BoardRect,
|
||||
state: chess_board.BoardState,
|
||||
valid_moves: bitboard.Bitboard,
|
||||
) !void {
|
||||
const square_w = board_rect.width / 8.0;
|
||||
const square_h = board_rect.height / 8.0;
|
||||
const radius_x = square_w * 0.11;
|
||||
const radius_y = square_h * 0.11;
|
||||
const color = SelectedHighlightColor;
|
||||
const segments = 48;
|
||||
|
||||
var moves = valid_moves;
|
||||
while (moves != 0) {
|
||||
const move_square: bitboard.Square = @intCast(@ctz(moves));
|
||||
moves &= moves - 1;
|
||||
const file: u3 = @intCast(move_square % 8);
|
||||
const rank: u3 = @intCast(move_square / 8);
|
||||
if (state.getSquare(@intCast((@as(u6, rank) * 8) + @as(u6, file))) != 0) {
|
||||
const x0 = @as(f32, @floatFromInt(file));
|
||||
const x1 = x0 + 1.0;
|
||||
const y0 = @as(f32, @floatFromInt(rank));
|
||||
const y1 = y0 + 1.0;
|
||||
const thickness: f32 = 0.07;
|
||||
|
||||
try geometry.appendQuad(vertices, allocator, geometry.boardToNdc(board_rect, x0, y0), geometry.boardToNdc(board_rect, x1, y0), geometry.boardToNdc(board_rect, x1, y0 + thickness), geometry.boardToNdc(board_rect, x0, y0 + thickness), color);
|
||||
try geometry.appendQuad(vertices, allocator, geometry.boardToNdc(board_rect, x0, y1 - thickness), geometry.boardToNdc(board_rect, x1, y1 - thickness), geometry.boardToNdc(board_rect, x1, y1), geometry.boardToNdc(board_rect, x0, y1), color);
|
||||
try geometry.appendQuad(vertices, allocator, geometry.boardToNdc(board_rect, x0, y0), geometry.boardToNdc(board_rect, x0 + thickness, y0), geometry.boardToNdc(board_rect, x0 + thickness, y1), geometry.boardToNdc(board_rect, x0, y1), color);
|
||||
try geometry.appendQuad(vertices, allocator, geometry.boardToNdc(board_rect, x1 - thickness, y0), geometry.boardToNdc(board_rect, x1, y0), geometry.boardToNdc(board_rect, x1, y1), geometry.boardToNdc(board_rect, x1 - thickness, y1), color);
|
||||
} else {
|
||||
const center = geometry.boardToNdc(
|
||||
board_rect,
|
||||
@as(f32, @floatFromInt(file)) + 0.5,
|
||||
@as(f32, @floatFromInt(rank)) + 0.5,
|
||||
);
|
||||
|
||||
var i: usize = 0;
|
||||
while (i < segments) : (i += 1) {
|
||||
const angle0 = (@as(f32, @floatFromInt(i)) / @as(f32, @floatFromInt(segments))) * std.math.tau;
|
||||
const angle1 = (@as(f32, @floatFromInt(i + 1)) / @as(f32, @floatFromInt(segments))) * std.math.tau;
|
||||
const p0 = [2]f32{ center[0] + (@cos(angle0) * radius_x), center[1] + (@sin(angle0) * radius_y) };
|
||||
const p1 = [2]f32{ center[0] + (@cos(angle1) * radius_x), center[1] + (@sin(angle1) * radius_y) };
|
||||
|
||||
try vertices.append(allocator, .{ .position = center, .color = color, .uv = .{ 0.0, 0.0 } });
|
||||
try vertices.append(allocator, .{ .position = p0, .color = color, .uv = .{ 0.0, 0.0 } });
|
||||
try vertices.append(allocator, .{ .position = p1, .color = color, .uv = .{ 0.0, 0.0 } });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn appendRectBorder(
|
||||
vertices: *std.ArrayList(geometry.Vertex),
|
||||
allocator: std.mem.Allocator,
|
||||
rect: geometry.BoardRect,
|
||||
thickness: f32,
|
||||
color: [4]f32,
|
||||
) !void {
|
||||
const x0 = rect.left;
|
||||
const x1 = rect.left + rect.width;
|
||||
const y0 = rect.bottom;
|
||||
const y1 = rect.bottom + rect.height;
|
||||
|
||||
try geometry.appendQuad(vertices, allocator, .{ x0, y0 }, .{ x1, y0 }, .{ x1, y0 + thickness }, .{ x0, y0 + thickness }, color);
|
||||
try geometry.appendQuad(vertices, allocator, .{ x0, y1 - thickness }, .{ x1, y1 - thickness }, .{ x1, y1 }, .{ x0, y1 }, color);
|
||||
try geometry.appendQuad(vertices, allocator, .{ x0, y0 }, .{ x0 + thickness, y0 }, .{ x0 + thickness, y1 }, .{ x0, y1 }, color);
|
||||
try geometry.appendQuad(vertices, allocator, .{ x1 - thickness, y0 }, .{ x1, y0 }, .{ x1, y1 }, .{ x1 - thickness, y1 }, color);
|
||||
}
|
||||
|
||||
pub fn appendPromotionPopup(
|
||||
vertices: *std.ArrayList(geometry.Vertex),
|
||||
allocator: std.mem.Allocator,
|
||||
board_rect: geometry.BoardRect,
|
||||
square: bitboard.Square,
|
||||
) !void {
|
||||
const popup_rect = piece_render.promotionPopupRectForSquare(board_rect, square);
|
||||
try geometry.appendQuad(
|
||||
vertices,
|
||||
allocator,
|
||||
.{ popup_rect.left, popup_rect.bottom },
|
||||
.{ popup_rect.left + popup_rect.width, popup_rect.bottom },
|
||||
.{ popup_rect.left + popup_rect.width, popup_rect.bottom + popup_rect.height },
|
||||
.{ popup_rect.left, popup_rect.bottom + popup_rect.height },
|
||||
.{ 0.04, 0.04, 0.04, 0.98 },
|
||||
);
|
||||
|
||||
for (piece_render.promotion_piece_types, 0..) |_, i| {
|
||||
const cell = piece_render.promotionChoiceRectForIndex(board_rect, square, i);
|
||||
const border_color = geometry.White;
|
||||
const thickness = board_rect.width / 220.0;
|
||||
try appendRectBorder(vertices, allocator, cell, thickness, border_color);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
pub fn appendModeMenu(
|
||||
vertices: *std.ArrayList(geometry.Vertex),
|
||||
allocator: std.mem.Allocator,
|
||||
edit_mode: bool,
|
||||
) !void {
|
||||
const button_w: f32 = 0.16;
|
||||
const button_h: f32 = 0.08;
|
||||
const reset_button_w: f32 = 0.22;
|
||||
const gap: f32 = 0.025;
|
||||
const top: f32 = 0.96;
|
||||
const bottom = top - button_h;
|
||||
const play_left: f32 = -0.95;
|
||||
const edit_left = play_left + button_w + gap;
|
||||
const reset_left = edit_left + button_w + gap;
|
||||
const inactive = [4]f32{ 0.18, 0.18, 0.18, 1.0 };
|
||||
const active = [4]f32{ 0.25, 0.45, 0.25, 1.0 };
|
||||
const reset_color = [4]f32{ 0.25, 0.25, 0.35, 1.0 };
|
||||
|
||||
try geometry.appendQuad(
|
||||
vertices,
|
||||
allocator,
|
||||
.{ play_left, bottom },
|
||||
.{ play_left + button_w, bottom },
|
||||
.{ play_left + button_w, top },
|
||||
.{ play_left, top },
|
||||
if (edit_mode) inactive else active,
|
||||
);
|
||||
try geometry.appendQuad(
|
||||
vertices,
|
||||
allocator,
|
||||
.{ edit_left, bottom },
|
||||
.{ edit_left + button_w, bottom },
|
||||
.{ edit_left + button_w, top },
|
||||
.{ edit_left, top },
|
||||
if (edit_mode) active else inactive,
|
||||
);
|
||||
try geometry.appendQuad(
|
||||
vertices,
|
||||
allocator,
|
||||
.{ reset_left, bottom },
|
||||
.{ reset_left + reset_button_w, bottom },
|
||||
.{ reset_left + reset_button_w, top },
|
||||
.{ reset_left, top },
|
||||
reset_color,
|
||||
);
|
||||
|
||||
const pixel_size: f32 = button_h / 13.0;
|
||||
const y = bottom + (button_h - pixel_size * 7.0) / 2.0;
|
||||
try appendText(vertices, allocator, "PLAY", play_left + 0.018, y, .{ .pixel_size = pixel_size, .color = geometry.White });
|
||||
try appendText(vertices, allocator, "EDIT", edit_left + 0.018, y, .{ .pixel_size = pixel_size, .color = geometry.White });
|
||||
try appendText(vertices, allocator, "RESET", reset_left + 0.018, y, .{ .pixel_size = pixel_size, .color = geometry.White });
|
||||
}
|
||||
|
||||
pub fn appendBoardCoordinateLabels(
|
||||
vertices: *std.ArrayList(geometry.Vertex),
|
||||
allocator: std.mem.Allocator,
|
||||
board_rect: geometry.BoardRect,
|
||||
) !void {
|
||||
const square_w = board_rect.width / 8.0;
|
||||
const square_h = board_rect.height / 8.0;
|
||||
const square_size = if (square_w < square_h) square_w else square_h;
|
||||
const pixel_size = square_size / 36.0;
|
||||
const padding = square_size / 24.0;
|
||||
const glyph_w = pixel_size * 5.0;
|
||||
const glyph_h = pixel_size * 7.0;
|
||||
|
||||
const files = "abcdefgh";
|
||||
for (files, 0..) |file_ch, file| {
|
||||
const is_dark = (file % 2) == 0;
|
||||
const color = if (is_dark) geometry.Light else geometry.Dark;
|
||||
const x = board_rect.left + (@as(f32, @floatFromInt(file + 1)) * square_w) - padding - glyph_w;
|
||||
const y = board_rect.bottom + padding;
|
||||
|
||||
try appendText(vertices, allocator, files[file .. file + 1], x, y, .{
|
||||
.pixel_size = pixel_size,
|
||||
.color = color,
|
||||
});
|
||||
|
||||
_ = file_ch;
|
||||
}
|
||||
|
||||
const ranks = "12345678";
|
||||
for (ranks, 0..) |rank_ch, rank| {
|
||||
const is_dark = (rank % 2) == 0;
|
||||
const color = if (is_dark) geometry.Light else geometry.Dark;
|
||||
const x = board_rect.left + padding;
|
||||
const y = board_rect.bottom + (@as(f32, @floatFromInt(rank + 1)) * square_h) - padding - glyph_h;
|
||||
|
||||
try appendText(vertices, allocator, ranks[rank .. rank + 1], x, y, .{
|
||||
.pixel_size = pixel_size,
|
||||
.color = color,
|
||||
});
|
||||
|
||||
_ = rank_ch;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn appendFenText(
|
||||
vertices: *std.ArrayList(geometry.Vertex),
|
||||
allocator: std.mem.Allocator,
|
||||
board_rect: geometry.BoardRect,
|
||||
fen_text: []const u8,
|
||||
) !void {
|
||||
const max_text_width = board_rect.width;
|
||||
const pixel_aspect = board_rect.width / board_rect.height;
|
||||
const natural_pixel_size = board_rect.height / 95.0;
|
||||
const fit_pixel_size = max_text_width / (@as(f32, @floatFromInt(fen_text.len)) * 6.0 * pixel_aspect);
|
||||
const pixel_size = if (fit_pixel_size < natural_pixel_size) fit_pixel_size else natural_pixel_size;
|
||||
const gap = board_rect.height / 18.0;
|
||||
const y = board_rect.bottom - gap - (pixel_size * 7.0);
|
||||
|
||||
try appendText(vertices, allocator, fen_text, board_rect.left, y, .{
|
||||
.pixel_size = pixel_size,
|
||||
.pixel_aspect = pixel_aspect,
|
||||
.color = geometry.White,
|
||||
});
|
||||
}
|
||||
|
||||
test "appendText appends vertices for supported glyph pixels" {
|
||||
var vertices: std.ArrayList(geometry.Vertex) = .empty;
|
||||
defer vertices.deinit(std.testing.allocator);
|
||||
|
||||
try appendText(&vertices, std.testing.allocator, "1", 0.0, 0.0, .{
|
||||
.pixel_size = 0.01,
|
||||
.color = geometry.White,
|
||||
});
|
||||
|
||||
try std.testing.expect(vertices.items.len > 0);
|
||||
try std.testing.expectEqual(@as(usize, 0), vertices.items.len % 6);
|
||||
}
|
||||
|
||||
test "appendSelectedSquareHighlight appends one quad when selected" {
|
||||
var vertices: std.ArrayList(geometry.Vertex) = .empty;
|
||||
defer vertices.deinit(std.testing.allocator);
|
||||
|
||||
try appendSelectedSquareHighlight(
|
||||
&vertices,
|
||||
std.testing.allocator,
|
||||
geometry.boardRectForExtent(800, 600),
|
||||
.{ .file = 4, .rank = 1 },
|
||||
);
|
||||
|
||||
try std.testing.expectEqual(@as(usize, 6), vertices.items.len);
|
||||
}
|
||||
|
||||
test "appendSelectedSquareHighlight appends nothing without selection" {
|
||||
var vertices: std.ArrayList(geometry.Vertex) = .empty;
|
||||
defer vertices.deinit(std.testing.allocator);
|
||||
|
||||
try appendSelectedSquareHighlight(
|
||||
&vertices,
|
||||
std.testing.allocator,
|
||||
geometry.boardRectForExtent(800, 600),
|
||||
null,
|
||||
);
|
||||
|
||||
try std.testing.expectEqual(@as(usize, 0), vertices.items.len);
|
||||
}
|
||||
|
||||
test "appendHoveredSquareHighlight appends one quad when hovered" {
|
||||
var vertices: std.ArrayList(geometry.Vertex) = .empty;
|
||||
defer vertices.deinit(std.testing.allocator);
|
||||
|
||||
try appendHoveredSquareHighlight(
|
||||
&vertices,
|
||||
std.testing.allocator,
|
||||
geometry.boardRectForExtent(800, 600),
|
||||
.{ .file = 2, .rank = 2 },
|
||||
);
|
||||
|
||||
try std.testing.expectEqual(@as(usize, 6), vertices.items.len);
|
||||
}
|
||||
|
||||
test "appendCheckBorder appends four border quads when checked" {
|
||||
var vertices: std.ArrayList(geometry.Vertex) = .empty;
|
||||
defer vertices.deinit(std.testing.allocator);
|
||||
|
||||
try appendCheckBorder(
|
||||
&vertices,
|
||||
std.testing.allocator,
|
||||
geometry.boardRectForExtent(800, 600),
|
||||
.{ .file = 4, .rank = 0 },
|
||||
);
|
||||
|
||||
try std.testing.expectEqual(@as(usize, 4 * 6), vertices.items.len);
|
||||
}
|
||||
|
||||
test "appendCheckBorder appends nothing without checked square" {
|
||||
var vertices: std.ArrayList(geometry.Vertex) = .empty;
|
||||
defer vertices.deinit(std.testing.allocator);
|
||||
|
||||
try appendCheckBorder(
|
||||
&vertices,
|
||||
std.testing.allocator,
|
||||
geometry.boardRectForExtent(800, 600),
|
||||
null,
|
||||
);
|
||||
|
||||
try std.testing.expectEqual(@as(usize, 0), vertices.items.len);
|
||||
}
|
||||
|
||||
test "appendCheckmateMarker appends hash glyph when winner is present" {
|
||||
var vertices: std.ArrayList(geometry.Vertex) = .empty;
|
||||
defer vertices.deinit(std.testing.allocator);
|
||||
|
||||
try appendCheckmateMarker(
|
||||
&vertices,
|
||||
std.testing.allocator,
|
||||
geometry.boardRectForExtent(800, 600),
|
||||
.{ .file = 5, .rank = 5 },
|
||||
);
|
||||
|
||||
try std.testing.expect(vertices.items.len > 0);
|
||||
}
|
||||
|
||||
test "appendCheckmateMarker appends nothing without winner" {
|
||||
var vertices: std.ArrayList(geometry.Vertex) = .empty;
|
||||
defer vertices.deinit(std.testing.allocator);
|
||||
|
||||
try appendCheckmateMarker(
|
||||
&vertices,
|
||||
std.testing.allocator,
|
||||
geometry.boardRectForExtent(800, 600),
|
||||
null,
|
||||
);
|
||||
|
||||
try std.testing.expectEqual(@as(usize, 0), vertices.items.len);
|
||||
}
|
||||
|
||||
test "appendValidMoveDots appends one circular triangle fan per move" {
|
||||
var vertices: std.ArrayList(geometry.Vertex) = .empty;
|
||||
defer vertices.deinit(std.testing.allocator);
|
||||
|
||||
const state = chess_board.BoardState.empty();
|
||||
const moves = bitboard.bit(20) | bitboard.bit(28);
|
||||
|
||||
try appendValidMoveDots(
|
||||
&vertices,
|
||||
std.testing.allocator,
|
||||
geometry.boardRectForExtent(800, 600),
|
||||
state,
|
||||
moves,
|
||||
);
|
||||
|
||||
try std.testing.expectEqual(@as(usize, 2 * 48 * 3), vertices.items.len);
|
||||
}
|
||||
@ -15,6 +15,15 @@ pub const VertexBufferContext = struct {
|
||||
ldc.vkd.freeMemory(ldc.device, self.memory, null);
|
||||
}
|
||||
};
|
||||
pub const BufferContext = struct {
|
||||
buffer: vk.Buffer,
|
||||
memory: vk.DeviceMemory,
|
||||
|
||||
pub fn destroy(self: *const BufferContext, ldc: *const device.LogicalDeviceContext) void {
|
||||
ldc.vkd.destroyBuffer(ldc.device, self.buffer, null);
|
||||
ldc.vkd.freeMemory(ldc.device, self.memory, null);
|
||||
}
|
||||
};
|
||||
|
||||
pub fn initVertexBuffer(
|
||||
vc: context.VulkanContext,
|
||||
@ -22,50 +31,24 @@ pub fn initVertexBuffer(
|
||||
vertices: []const geometry.Vertex,
|
||||
) !VertexBufferContext {
|
||||
const buffer_size: vk.DeviceSize = @intCast(@sizeOf(geometry.Vertex) * vertices.len);
|
||||
|
||||
const buffer_create_info = vk.BufferCreateInfo{
|
||||
.size = buffer_size,
|
||||
.usage = .{
|
||||
.vertex_buffer_bit = true,
|
||||
},
|
||||
.sharing_mode = .exclusive,
|
||||
const usage: vk.BufferUsageFlags = .{
|
||||
.vertex_buffer_bit = true,
|
||||
};
|
||||
const memory_properties: vk.MemoryPropertyFlags = .{
|
||||
.host_visible_bit = true,
|
||||
.host_coherent_bit = true,
|
||||
};
|
||||
|
||||
const buffer = try ldc.vkd.createBuffer(ldc.device, &buffer_create_info, null);
|
||||
errdefer ldc.vkd.destroyBuffer(ldc.device, buffer, null);
|
||||
|
||||
const memory_requirements = ldc.vkd.getBufferMemoryRequirements(
|
||||
ldc.device,
|
||||
buffer,
|
||||
);
|
||||
const memory_index = try findMemoryType(
|
||||
vc,
|
||||
ldc,
|
||||
memory_requirements.memory_type_bits,
|
||||
.{
|
||||
.host_visible_bit = true,
|
||||
.host_coherent_bit = true,
|
||||
},
|
||||
);
|
||||
|
||||
const alloc_info = vk.MemoryAllocateInfo{
|
||||
.allocation_size = memory_requirements.size,
|
||||
.memory_type_index = memory_index,
|
||||
};
|
||||
|
||||
const memory = try ldc.vkd.allocateMemory(ldc.device, &alloc_info, null);
|
||||
errdefer ldc.vkd.freeMemory(ldc.device, memory, null);
|
||||
|
||||
try ldc.vkd.bindBufferMemory(ldc.device, buffer, memory, 0);
|
||||
const buffer_context = try createBuffer(vc, ldc, buffer_size, usage, memory_properties);
|
||||
|
||||
const mapped = try ldc.vkd.mapMemory(
|
||||
ldc.device,
|
||||
memory,
|
||||
buffer_context.memory,
|
||||
0,
|
||||
buffer_size,
|
||||
.{},
|
||||
);
|
||||
defer ldc.vkd.unmapMemory(ldc.device, memory);
|
||||
defer ldc.vkd.unmapMemory(ldc.device, buffer_context.memory);
|
||||
|
||||
const dst: [*]u8 = @ptrCast(mapped);
|
||||
const dst_slice = dst[0..buffer_size];
|
||||
@ -74,13 +57,13 @@ pub fn initVertexBuffer(
|
||||
std.mem.copyForwards(u8, dst_slice, src_bytes);
|
||||
|
||||
return .{
|
||||
.buffer = buffer,
|
||||
.memory = memory,
|
||||
.buffer = buffer_context.buffer,
|
||||
.memory = buffer_context.memory,
|
||||
.vertex_count = @intCast(vertices.len),
|
||||
};
|
||||
}
|
||||
|
||||
fn findMemoryType(
|
||||
pub fn findMemoryType(
|
||||
vc: context.VulkanContext,
|
||||
ldc: device.LogicalDeviceContext,
|
||||
type_filter: u32,
|
||||
@ -107,3 +90,83 @@ fn findMemoryType(
|
||||
|
||||
return error.NoSuitableMemoryType;
|
||||
}
|
||||
|
||||
pub fn createBuffer(
|
||||
vc: context.VulkanContext,
|
||||
ldc: device.LogicalDeviceContext,
|
||||
size: vk.DeviceSize,
|
||||
usage: vk.BufferUsageFlags,
|
||||
properties: vk.MemoryPropertyFlags,
|
||||
) !BufferContext {
|
||||
const buffer_create_info = vk.BufferCreateInfo{
|
||||
.size = size,
|
||||
.usage = usage,
|
||||
.sharing_mode = .exclusive,
|
||||
};
|
||||
|
||||
const buffer = try ldc.vkd.createBuffer(ldc.device, &buffer_create_info, null);
|
||||
errdefer ldc.vkd.destroyBuffer(ldc.device, buffer, null);
|
||||
|
||||
const memory_requirements = ldc.vkd.getBufferMemoryRequirements(
|
||||
ldc.device,
|
||||
buffer,
|
||||
);
|
||||
const memory_index = try findMemoryType(
|
||||
vc,
|
||||
ldc,
|
||||
memory_requirements.memory_type_bits,
|
||||
properties,
|
||||
);
|
||||
|
||||
const alloc_info = vk.MemoryAllocateInfo{
|
||||
.allocation_size = memory_requirements.size,
|
||||
.memory_type_index = memory_index,
|
||||
};
|
||||
|
||||
const memory = try ldc.vkd.allocateMemory(ldc.device, &alloc_info, null);
|
||||
errdefer ldc.vkd.freeMemory(ldc.device, memory, null);
|
||||
|
||||
try ldc.vkd.bindBufferMemory(ldc.device, buffer, memory, 0);
|
||||
|
||||
return .{
|
||||
.buffer = buffer,
|
||||
.memory = memory,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn initStagingBuffer(
|
||||
vc: context.VulkanContext,
|
||||
ldc: device.LogicalDeviceContext,
|
||||
bytes: []const u8,
|
||||
) !BufferContext {
|
||||
const buffer_size: vk.DeviceSize = @intCast(bytes.len);
|
||||
|
||||
const buffer_context = try createBuffer(
|
||||
vc,
|
||||
ldc,
|
||||
buffer_size,
|
||||
.{
|
||||
.transfer_src_bit = true,
|
||||
},
|
||||
.{
|
||||
.host_visible_bit = true,
|
||||
.host_coherent_bit = true,
|
||||
},
|
||||
);
|
||||
|
||||
const mapped = try ldc.vkd.mapMemory(
|
||||
ldc.device,
|
||||
buffer_context.memory,
|
||||
0,
|
||||
buffer_size,
|
||||
.{},
|
||||
);
|
||||
defer ldc.vkd.unmapMemory(ldc.device, buffer_context.memory);
|
||||
|
||||
const dst: [*]u8 = @ptrCast(mapped);
|
||||
const dst_slice = dst[0..buffer_size];
|
||||
|
||||
std.mem.copyForwards(u8, dst_slice, bytes);
|
||||
|
||||
return buffer_context;
|
||||
}
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
const std = @import("std");
|
||||
const log = std.log.scoped(.graphics);
|
||||
const vk = @import("vulkan");
|
||||
|
||||
const device = @import("device.zig");
|
||||
@ -7,6 +8,8 @@ const render_pass = @import("render_pass.zig");
|
||||
const swapchain = @import("swapchain.zig");
|
||||
const pipeline = @import("pipeline.zig");
|
||||
const buffer = @import("buffer.zig");
|
||||
const descriptors = @import("descriptors.zig");
|
||||
const piece_render = @import("../piece_render.zig");
|
||||
|
||||
pub const CommandContext = struct {
|
||||
command_pool: vk.CommandPool,
|
||||
@ -19,13 +22,8 @@ pub const CommandContext = struct {
|
||||
}
|
||||
};
|
||||
|
||||
pub fn initCommandBuffers(
|
||||
pub fn initCommandPoolOnly(
|
||||
ldc: device.LogicalDeviceContext,
|
||||
render_pass_context: render_pass.RenderPassContext,
|
||||
framebuffer_context: framebuffers.FramebufferContext,
|
||||
pipeline_context: pipeline.PipelineContext,
|
||||
swapchain_context: swapchain.SwapchainContext,
|
||||
vertex_buffer_context: buffer.VertexBufferContext,
|
||||
allocator: std.mem.Allocator,
|
||||
) !CommandContext {
|
||||
const command_pool_create_info = vk.CommandPoolCreateInfo{
|
||||
@ -41,32 +39,56 @@ pub fn initCommandBuffers(
|
||||
null,
|
||||
);
|
||||
errdefer ldc.vkd.destroyCommandPool(ldc.device, command_pool, null);
|
||||
std.debug.print("created command pool\n", .{});
|
||||
|
||||
const command_buffers = try allocator.alloc(vk.CommandBuffer, framebuffer_context.framebuffers.len);
|
||||
const command_buffers = try allocator.alloc(vk.CommandBuffer, 0);
|
||||
errdefer allocator.free(command_buffers);
|
||||
|
||||
const command_buffer_allocate_info = vk.CommandBufferAllocateInfo{
|
||||
return .{
|
||||
.command_pool = command_pool,
|
||||
.command_buffers = command_buffers,
|
||||
.allocator = allocator,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn initCommandBuffers(
|
||||
ldc: device.LogicalDeviceContext,
|
||||
render_pass_context: render_pass.RenderPassContext,
|
||||
framebuffer_context: framebuffers.FramebufferContext,
|
||||
board_pipeline_context: pipeline.PipelineContext,
|
||||
piece_pipeline_context: pipeline.PipelineContext,
|
||||
descriptor_contexts: [piece_render.PieceGroupCount]?descriptors.DescriptorContext,
|
||||
swapchain_context: swapchain.SwapchainContext,
|
||||
board_vertex_buffer_context: buffer.VertexBufferContext,
|
||||
piece_vertex_buffers: [piece_render.PieceGroupCount]?buffer.VertexBufferContext,
|
||||
allocator: std.mem.Allocator,
|
||||
) !CommandContext {
|
||||
var command_context = try initCommandPoolOnly(ldc, allocator);
|
||||
errdefer command_context.destroy(&ldc);
|
||||
log.debug("created command pool", .{});
|
||||
|
||||
allocator.free(command_context.command_buffers);
|
||||
command_context.command_buffers = try allocator.alloc(vk.CommandBuffer, framebuffer_context.framebuffers.len);
|
||||
errdefer allocator.free(command_context.command_buffers);
|
||||
|
||||
const command_buffer_allocate_info = vk.CommandBufferAllocateInfo{
|
||||
.command_pool = command_context.command_pool,
|
||||
.level = .primary,
|
||||
.command_buffer_count = @intCast(command_buffers.len),
|
||||
.command_buffer_count = @intCast(command_context.command_buffers.len),
|
||||
};
|
||||
|
||||
try ldc.vkd.allocateCommandBuffers(
|
||||
ldc.device,
|
||||
&command_buffer_allocate_info,
|
||||
command_buffers.ptr,
|
||||
command_context.command_buffers.ptr,
|
||||
);
|
||||
|
||||
std.debug.print("allocated command buffers: {}\n", .{command_buffers.len});
|
||||
log.debug("allocated command buffers: {}", .{command_context.command_buffers.len});
|
||||
|
||||
for (command_buffers, 0..) |command_buffer, i| {
|
||||
for (command_context.command_buffers, 0..) |command_buffer, i| {
|
||||
const begin_info = vk.CommandBufferBeginInfo{};
|
||||
|
||||
try ldc.vkd.beginCommandBuffer(command_buffer, &begin_info);
|
||||
|
||||
// This is the only "drawing" currently happening: begin a render pass
|
||||
// and clear the swapchain image. The embedded shaders are not used yet.
|
||||
const clear_color = vk.ClearValue{
|
||||
.color = .{
|
||||
.float_32 = .{ 0.02, 0.02, 0.08, 1.0 },
|
||||
@ -93,36 +115,107 @@ pub fn initCommandBuffers(
|
||||
ldc.vkd.cmdBindPipeline(
|
||||
command_buffer,
|
||||
.graphics,
|
||||
pipeline_context.graphics_pipeline,
|
||||
board_pipeline_context.graphics_pipeline,
|
||||
);
|
||||
|
||||
const vertex_buffers = [_]vk.Buffer{
|
||||
vertex_buffer_context.buffer,
|
||||
};
|
||||
|
||||
const offsets = [_]vk.DeviceSize{
|
||||
0,
|
||||
};
|
||||
const board_vertex_buffers = [_]vk.Buffer{board_vertex_buffer_context.buffer};
|
||||
const board_offsets = [_]vk.DeviceSize{0};
|
||||
|
||||
ldc.vkd.cmdBindVertexBuffers(
|
||||
command_buffer,
|
||||
0,
|
||||
&vertex_buffers,
|
||||
&offsets,
|
||||
&board_vertex_buffers,
|
||||
&board_offsets,
|
||||
);
|
||||
|
||||
ldc.vkd.cmdDraw(command_buffer, vertex_buffer_context.vertex_count, 1, 0, 0);
|
||||
ldc.vkd.cmdDraw(command_buffer, board_vertex_buffer_context.vertex_count, 1, 0, 0);
|
||||
|
||||
ldc.vkd.cmdBindPipeline(
|
||||
command_buffer,
|
||||
.graphics,
|
||||
piece_pipeline_context.graphics_pipeline,
|
||||
);
|
||||
|
||||
for (piece_vertex_buffers, descriptor_contexts) |maybe_piece_buffer, maybe_descriptor_context| {
|
||||
const piece_buffer = maybe_piece_buffer orelse continue;
|
||||
const piece_descriptor_context = maybe_descriptor_context orelse continue;
|
||||
|
||||
const descriptor_sets = [_]vk.DescriptorSet{piece_descriptor_context.descriptor_set};
|
||||
ldc.vkd.cmdBindDescriptorSets(
|
||||
command_buffer,
|
||||
.graphics,
|
||||
piece_pipeline_context.pipeline_layout,
|
||||
0,
|
||||
&descriptor_sets,
|
||||
null,
|
||||
);
|
||||
|
||||
const piece_buffers = [_]vk.Buffer{piece_buffer.buffer};
|
||||
const piece_offsets = [_]vk.DeviceSize{0};
|
||||
|
||||
ldc.vkd.cmdBindVertexBuffers(
|
||||
command_buffer,
|
||||
0,
|
||||
&piece_buffers,
|
||||
&piece_offsets,
|
||||
);
|
||||
|
||||
ldc.vkd.cmdDraw(command_buffer, piece_buffer.vertex_count, 1, 0, 0);
|
||||
}
|
||||
|
||||
ldc.vkd.cmdEndRenderPass(command_buffer);
|
||||
|
||||
try ldc.vkd.endCommandBuffer(command_buffer);
|
||||
}
|
||||
|
||||
std.debug.print("recorded command buffers\n", .{});
|
||||
log.debug("recorded command buffers", .{});
|
||||
|
||||
return .{
|
||||
.command_pool = command_pool,
|
||||
.command_buffers = command_buffers,
|
||||
.allocator = allocator,
|
||||
};
|
||||
return command_context;
|
||||
}
|
||||
|
||||
pub fn beginSingleTimeCommands(
|
||||
ldc: device.LogicalDeviceContext,
|
||||
command_context: CommandContext,
|
||||
) !vk.CommandBuffer {
|
||||
const command_buffer_allocate_info = vk.CommandBufferAllocateInfo{
|
||||
.command_pool = command_context.command_pool,
|
||||
.level = .primary,
|
||||
.command_buffer_count = 1,
|
||||
};
|
||||
|
||||
var command_buffer: vk.CommandBuffer = undefined;
|
||||
try ldc.vkd.allocateCommandBuffers(
|
||||
ldc.device,
|
||||
&command_buffer_allocate_info,
|
||||
@ptrCast(&command_buffer),
|
||||
);
|
||||
|
||||
const begin_info = vk.CommandBufferBeginInfo{
|
||||
.flags = .{
|
||||
.one_time_submit_bit = true,
|
||||
},
|
||||
};
|
||||
|
||||
try ldc.vkd.beginCommandBuffer(command_buffer, &begin_info);
|
||||
|
||||
return command_buffer;
|
||||
}
|
||||
|
||||
pub fn endSingleTimeCommands(
|
||||
ldc: device.LogicalDeviceContext,
|
||||
command_context: CommandContext,
|
||||
command_buffer: vk.CommandBuffer,
|
||||
) !void {
|
||||
try ldc.vkd.endCommandBuffer(command_buffer);
|
||||
|
||||
const command_buffers = [_]vk.CommandBuffer{command_buffer};
|
||||
|
||||
const submit_info = vk.SubmitInfo{
|
||||
.command_buffer_count = command_buffers.len,
|
||||
.p_command_buffers = &command_buffers,
|
||||
};
|
||||
|
||||
try ldc.vkd.queueSubmit(ldc.graphics_queue, &[_]vk.SubmitInfo{submit_info}, .null_handle);
|
||||
try ldc.vkd.queueWaitIdle(ldc.graphics_queue);
|
||||
ldc.vkd.freeCommandBuffers(ldc.device, command_context.command_pool, &command_buffers);
|
||||
}
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
const std = @import("std");
|
||||
const log = std.log.scoped(.graphics);
|
||||
const glfw = @import("zglfw");
|
||||
const vk = @import("vulkan");
|
||||
|
||||
@ -16,9 +17,9 @@ pub fn initInstance(name: [:0]const u8) !VulkanContext {
|
||||
const base = vk.BaseWrapper.load(glfw.getInstanceProcAddress);
|
||||
const required_extensions = try glfw.getRequiredInstanceExtensions();
|
||||
|
||||
std.debug.print("Required instance extensions:\n", .{});
|
||||
log.debug("Required instance extensions:", .{});
|
||||
for (required_extensions) |extension| {
|
||||
std.debug.print(" {s}\n", .{extension});
|
||||
log.debug(" {s}", .{extension});
|
||||
}
|
||||
const app_info = vk.ApplicationInfo{
|
||||
.p_application_name = name,
|
||||
@ -35,7 +36,7 @@ pub fn initInstance(name: [:0]const u8) !VulkanContext {
|
||||
};
|
||||
|
||||
const instance = try base.createInstance(&instance_create_info, null);
|
||||
std.debug.print("Created Vulkan Instance\n", .{});
|
||||
log.debug("Created Vulkan Instance", .{});
|
||||
|
||||
const vki = vk.InstanceWrapper.load(instance, base.dispatch.vkGetInstanceProcAddr.?);
|
||||
|
||||
|
||||
91
src/vulkan/descriptors.zig
Normal file
@ -0,0 +1,91 @@
|
||||
const vk = @import("vulkan");
|
||||
|
||||
const device = @import("device.zig");
|
||||
const pipeline = @import("pipeline.zig");
|
||||
const texture = @import("texture.zig");
|
||||
|
||||
pub const DescriptorContext = struct {
|
||||
descriptor_pool: vk.DescriptorPool,
|
||||
descriptor_set: vk.DescriptorSet,
|
||||
|
||||
pub fn destroy(self: *const DescriptorContext, ldc: *const device.LogicalDeviceContext) void {
|
||||
ldc.vkd.destroyDescriptorPool(ldc.device, self.descriptor_pool, null);
|
||||
}
|
||||
};
|
||||
|
||||
pub fn initTextureDescriptor(
|
||||
ldc: device.LogicalDeviceContext,
|
||||
pipeline_context: pipeline.PipelineContext,
|
||||
texture_context: texture.TextureContext,
|
||||
) !DescriptorContext {
|
||||
const pool_size = vk.DescriptorPoolSize{
|
||||
.type = .combined_image_sampler,
|
||||
.descriptor_count = 1,
|
||||
};
|
||||
|
||||
const pool_sizes = [_]vk.DescriptorPoolSize{pool_size};
|
||||
|
||||
const pool_info = vk.DescriptorPoolCreateInfo{
|
||||
.max_sets = 1,
|
||||
.pool_size_count = pool_sizes.len,
|
||||
.p_pool_sizes = &pool_sizes,
|
||||
};
|
||||
|
||||
const descriptor_pool = try ldc.vkd.createDescriptorPool(
|
||||
ldc.device,
|
||||
&pool_info,
|
||||
null,
|
||||
);
|
||||
errdefer ldc.vkd.destroyDescriptorPool(ldc.device, descriptor_pool, null);
|
||||
|
||||
const set_layouts = [_]vk.DescriptorSetLayout{
|
||||
pipeline_context.descriptor_set_layout,
|
||||
};
|
||||
|
||||
const alloc_info = vk.DescriptorSetAllocateInfo{
|
||||
.descriptor_pool = descriptor_pool,
|
||||
.descriptor_set_count = set_layouts.len,
|
||||
.p_set_layouts = &set_layouts,
|
||||
};
|
||||
|
||||
var descriptor_sets: [1]vk.DescriptorSet = undefined;
|
||||
try ldc.vkd.allocateDescriptorSets(
|
||||
ldc.device,
|
||||
&alloc_info,
|
||||
&descriptor_sets,
|
||||
);
|
||||
|
||||
const descriptor_set = descriptor_sets[0];
|
||||
|
||||
const image_info = vk.DescriptorImageInfo{
|
||||
.image_layout = .shader_read_only_optimal,
|
||||
.image_view = texture_context.image_view,
|
||||
.sampler = texture_context.sampler,
|
||||
};
|
||||
|
||||
const image_infos = [_]vk.DescriptorImageInfo{image_info};
|
||||
|
||||
const descriptor_write = vk.WriteDescriptorSet{
|
||||
.dst_set = descriptor_set,
|
||||
.dst_binding = 0,
|
||||
.dst_array_element = 0,
|
||||
.descriptor_count = 1,
|
||||
.descriptor_type = .combined_image_sampler,
|
||||
.p_image_info = &image_infos,
|
||||
.p_buffer_info = undefined,
|
||||
.p_texel_buffer_view = undefined,
|
||||
};
|
||||
|
||||
const descriptor_writes = [_]vk.WriteDescriptorSet{descriptor_write};
|
||||
|
||||
ldc.vkd.updateDescriptorSets(
|
||||
ldc.device,
|
||||
&descriptor_writes,
|
||||
null,
|
||||
);
|
||||
|
||||
return .{
|
||||
.descriptor_pool = descriptor_pool,
|
||||
.descriptor_set = descriptor_set,
|
||||
};
|
||||
}
|
||||
@ -1,4 +1,5 @@
|
||||
const std = @import("std");
|
||||
const log = std.log.scoped(.graphics);
|
||||
const vk = @import("vulkan");
|
||||
|
||||
const context = @import("context.zig");
|
||||
@ -41,11 +42,11 @@ pub fn initLogicalDevice(
|
||||
|
||||
const device = try vc.vki.createDevice(physical_device, &device_create_info, null);
|
||||
errdefer vc.vki.destroyDevice(device, null);
|
||||
std.debug.print("created logical device\n", .{});
|
||||
log.debug("created logical device", .{});
|
||||
|
||||
const vkd = vk.DeviceWrapper.load(device, vc.vki.dispatch.vkGetDeviceProcAddr.?);
|
||||
const graphics_queue = vkd.getDeviceQueue(device, graphics_queue_family_index, 0);
|
||||
std.debug.print("retrieved graphics queue\n", .{});
|
||||
log.debug("retrieved graphics queue", .{});
|
||||
|
||||
return .{
|
||||
.physical_device = physical_device,
|
||||
@ -61,11 +62,11 @@ pub fn debugPhysicalGPUs(
|
||||
physical_devices: []vk.PhysicalDevice,
|
||||
surface: vk.SurfaceKHR,
|
||||
) !void {
|
||||
std.debug.print("physical devices: {}\n", .{physical_devices.len});
|
||||
log.debug("physical devices: {}", .{physical_devices.len});
|
||||
|
||||
for (physical_devices, 0..) |physical_device, i| {
|
||||
const props = vc.vki.getPhysicalDeviceProperties(physical_device);
|
||||
std.debug.print("device {}: {s}\n", .{ i, std.mem.sliceTo(&props.device_name, 0) });
|
||||
log.debug("device {}: {s}", .{ i, std.mem.sliceTo(&props.device_name, 0) });
|
||||
const queue_families = try vc.vki.getPhysicalDeviceQueueFamilyPropertiesAlloc(
|
||||
physical_device,
|
||||
std.heap.page_allocator,
|
||||
@ -83,8 +84,8 @@ pub fn debugPhysicalGPUs(
|
||||
surface,
|
||||
);
|
||||
|
||||
std.debug.print(
|
||||
" queue {}: count={}, graphics={}, compute={}, transfer={}, present={}\n",
|
||||
log.debug(
|
||||
" queue {}: count={}, graphics={}, compute={}, transfer={}, present={}",
|
||||
.{
|
||||
queue_index,
|
||||
queue_family.queue_count,
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
const std = @import("std");
|
||||
const log = std.log.scoped(.graphics);
|
||||
const vk = @import("vulkan");
|
||||
|
||||
const commands = @import("commands.zig");
|
||||
@ -33,7 +34,7 @@ pub fn drawFrame(
|
||||
|
||||
const image_index = image_result.image_index;
|
||||
const should_recreate_after_present = image_result.result == .suboptimal_khr;
|
||||
std.debug.print("Acquired swapchain image: {}\n", .{image_index});
|
||||
log.debug("Acquired swapchain image: {}", .{image_index});
|
||||
|
||||
try ldc.vkd.resetFences(ldc.device, &wait_fences);
|
||||
|
||||
@ -76,7 +77,7 @@ pub fn drawFrame(
|
||||
error.OutOfDateKHR => return .Recreate,
|
||||
else => return err,
|
||||
};
|
||||
std.debug.print("Presented one frame\n", .{});
|
||||
log.debug("Presented one frame", .{});
|
||||
|
||||
if (should_recreate_after_present or present_result == .suboptimal_khr) {
|
||||
return FrameResult.Recreate;
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
const std = @import("std");
|
||||
const log = std.log.scoped(.graphics);
|
||||
const vk = @import("vulkan");
|
||||
|
||||
const device = @import("device.zig");
|
||||
@ -53,7 +54,7 @@ pub fn initFramebuffers(
|
||||
created_framebuffer_count += 1;
|
||||
}
|
||||
|
||||
std.debug.print("created framebuffers: {}\n", .{framebuffers.len});
|
||||
log.debug("created framebuffers: {}", .{framebuffers.len});
|
||||
|
||||
return .{
|
||||
.framebuffers = framebuffers,
|
||||
|
||||
@ -7,10 +7,14 @@ const geometry = @import("../geometry.zig");
|
||||
pub const PipelineContext = struct {
|
||||
graphics_pipeline: vk.Pipeline,
|
||||
pipeline_layout: vk.PipelineLayout,
|
||||
descriptor_set_layout: vk.DescriptorSetLayout = .null_handle,
|
||||
|
||||
pub fn destroy(self: *const PipelineContext, ldc: *const device.LogicalDeviceContext) void {
|
||||
ldc.vkd.destroyPipeline(ldc.device, self.graphics_pipeline, null);
|
||||
ldc.vkd.destroyPipelineLayout(ldc.device, self.pipeline_layout, null);
|
||||
if (self.descriptor_set_layout != .null_handle) {
|
||||
ldc.vkd.destroyDescriptorSetLayout(ldc.device, self.descriptor_set_layout, null);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@ -31,8 +35,112 @@ pub fn createShaderModule(ldc: device.LogicalDeviceContext, allocator: std.mem.A
|
||||
return try ldc.vkd.createShaderModule(ldc.device, &create_info, null);
|
||||
}
|
||||
|
||||
pub fn initPipelineContext(ldc: device.LogicalDeviceContext, extent: vk.Extent2D, render_pass: vk.RenderPass, vert_spv: []const u8, frag_spv: []const u8, allocator: std.mem.Allocator) !PipelineContext {
|
||||
const pipeline_layout_create_info = vk.PipelineLayoutCreateInfo{};
|
||||
pub fn initBoardPipelineContext(ldc: device.LogicalDeviceContext, extent: vk.Extent2D, render_pass: vk.RenderPass, vert_spv: []const u8, frag_spv: []const u8, allocator: std.mem.Allocator) !PipelineContext {
|
||||
const attribute_descriptions = [_]vk.VertexInputAttributeDescription{
|
||||
.{
|
||||
.location = 0,
|
||||
.binding = 0,
|
||||
.format = .r32g32_sfloat,
|
||||
.offset = @offsetOf(geometry.Vertex, "position"),
|
||||
},
|
||||
.{
|
||||
.location = 1,
|
||||
.binding = 0,
|
||||
.format = .r32g32b32a32_sfloat,
|
||||
.offset = @offsetOf(geometry.Vertex, "color"),
|
||||
},
|
||||
};
|
||||
|
||||
return try initPipelineContext(
|
||||
ldc,
|
||||
extent,
|
||||
render_pass,
|
||||
vert_spv,
|
||||
frag_spv,
|
||||
allocator,
|
||||
null,
|
||||
&attribute_descriptions,
|
||||
);
|
||||
}
|
||||
|
||||
pub fn initPiecePipelineContext(ldc: device.LogicalDeviceContext, extent: vk.Extent2D, render_pass: vk.RenderPass, vert_spv: []const u8, frag_spv: []const u8, allocator: std.mem.Allocator) !PipelineContext {
|
||||
const sampler_layout_binding = vk.DescriptorSetLayoutBinding{
|
||||
.binding = 0,
|
||||
.descriptor_type = .combined_image_sampler,
|
||||
.descriptor_count = 1,
|
||||
.stage_flags = .{
|
||||
.fragment_bit = true,
|
||||
},
|
||||
.p_immutable_samplers = null,
|
||||
};
|
||||
|
||||
const descriptor_set_layout_bindings = [_]vk.DescriptorSetLayoutBinding{
|
||||
sampler_layout_binding,
|
||||
};
|
||||
|
||||
const descriptor_set_layout_create_info = vk.DescriptorSetLayoutCreateInfo{
|
||||
.binding_count = descriptor_set_layout_bindings.len,
|
||||
.p_bindings = &descriptor_set_layout_bindings,
|
||||
};
|
||||
|
||||
const descriptor_set_layout = try ldc.vkd.createDescriptorSetLayout(
|
||||
ldc.device,
|
||||
&descriptor_set_layout_create_info,
|
||||
null,
|
||||
);
|
||||
errdefer ldc.vkd.destroyDescriptorSetLayout(ldc.device, descriptor_set_layout, null);
|
||||
|
||||
const descriptor_set_layouts = [_]vk.DescriptorSetLayout{
|
||||
descriptor_set_layout,
|
||||
};
|
||||
|
||||
const attribute_descriptions = [_]vk.VertexInputAttributeDescription{
|
||||
.{
|
||||
.location = 0,
|
||||
.binding = 0,
|
||||
.format = .r32g32_sfloat,
|
||||
.offset = @offsetOf(geometry.Vertex, "position"),
|
||||
},
|
||||
.{
|
||||
.location = 1,
|
||||
.binding = 0,
|
||||
.format = .r32g32b32a32_sfloat,
|
||||
.offset = @offsetOf(geometry.Vertex, "color"),
|
||||
},
|
||||
.{
|
||||
.location = 2,
|
||||
.binding = 0,
|
||||
.format = .r32g32_sfloat,
|
||||
.offset = @offsetOf(geometry.Vertex, "uv"),
|
||||
},
|
||||
};
|
||||
|
||||
return try initPipelineContext(
|
||||
ldc,
|
||||
extent,
|
||||
render_pass,
|
||||
vert_spv,
|
||||
frag_spv,
|
||||
allocator,
|
||||
&descriptor_set_layouts,
|
||||
&attribute_descriptions,
|
||||
);
|
||||
}
|
||||
|
||||
fn initPipelineContext(
|
||||
ldc: device.LogicalDeviceContext,
|
||||
extent: vk.Extent2D,
|
||||
render_pass: vk.RenderPass,
|
||||
vert_spv: []const u8,
|
||||
frag_spv: []const u8,
|
||||
allocator: std.mem.Allocator,
|
||||
descriptor_set_layouts: ?[]const vk.DescriptorSetLayout,
|
||||
attribute_descriptions: []const vk.VertexInputAttributeDescription,
|
||||
) !PipelineContext {
|
||||
const pipeline_layout_create_info = vk.PipelineLayoutCreateInfo{
|
||||
.set_layout_count = if (descriptor_set_layouts) |layouts| @intCast(layouts.len) else 0,
|
||||
.p_set_layouts = if (descriptor_set_layouts) |layouts| layouts.ptr else null,
|
||||
};
|
||||
|
||||
const pipeline_layout = try ldc.vkd.createPipelineLayout(
|
||||
ldc.device,
|
||||
@ -70,18 +178,11 @@ pub fn initPipelineContext(ldc: device.LogicalDeviceContext, extent: vk.Extent2D
|
||||
.input_rate = .vertex,
|
||||
};
|
||||
|
||||
const attribute_description = vk.VertexInputAttributeDescription{
|
||||
.location = 0,
|
||||
.binding = 0,
|
||||
.format = .r32g32_sfloat,
|
||||
.offset = @offsetOf(geometry.Vertex, "position"),
|
||||
};
|
||||
|
||||
const vertex_input_info = vk.PipelineVertexInputStateCreateInfo{
|
||||
.vertex_binding_description_count = 1,
|
||||
.p_vertex_binding_descriptions = @ptrCast(&binding_description),
|
||||
.vertex_attribute_description_count = 1,
|
||||
.p_vertex_attribute_descriptions = @ptrCast(&attribute_description),
|
||||
.vertex_attribute_description_count = @intCast(attribute_descriptions.len),
|
||||
.p_vertex_attribute_descriptions = attribute_descriptions.ptr,
|
||||
};
|
||||
|
||||
const input_assembly = vk.PipelineInputAssemblyStateCreateInfo{
|
||||
@ -91,9 +192,9 @@ pub fn initPipelineContext(ldc: device.LogicalDeviceContext, extent: vk.Extent2D
|
||||
|
||||
const viewport = vk.Viewport{
|
||||
.x = 0.0,
|
||||
.y = 0.0,
|
||||
.y = @floatFromInt(extent.height),
|
||||
.width = @floatFromInt(extent.width),
|
||||
.height = @floatFromInt(extent.height),
|
||||
.height = -@as(f32, @floatFromInt(extent.height)),
|
||||
.min_depth = 0.0,
|
||||
.max_depth = 1.0,
|
||||
};
|
||||
@ -115,9 +216,7 @@ pub fn initPipelineContext(ldc: device.LogicalDeviceContext, extent: vk.Extent2D
|
||||
.rasterizer_discard_enable = .false,
|
||||
.polygon_mode = .fill,
|
||||
.line_width = 1.0,
|
||||
.cull_mode = .{
|
||||
.back_bit = true,
|
||||
},
|
||||
.cull_mode = .{},
|
||||
.front_face = .clockwise,
|
||||
.depth_bias_enable = .false,
|
||||
.depth_bias_constant_factor = 0.0,
|
||||
@ -141,9 +240,9 @@ pub fn initPipelineContext(ldc: device.LogicalDeviceContext, extent: vk.Extent2D
|
||||
.b_bit = true,
|
||||
.a_bit = true,
|
||||
},
|
||||
.blend_enable = .false,
|
||||
.src_color_blend_factor = .one,
|
||||
.dst_color_blend_factor = .zero,
|
||||
.blend_enable = .true,
|
||||
.src_color_blend_factor = .src_alpha,
|
||||
.dst_color_blend_factor = .one_minus_src_alpha,
|
||||
.color_blend_op = .add,
|
||||
.src_alpha_blend_factor = .one,
|
||||
.dst_alpha_blend_factor = .zero,
|
||||
@ -176,10 +275,7 @@ pub fn initPipelineContext(ldc: device.LogicalDeviceContext, extent: vk.Extent2D
|
||||
.base_pipeline_index = -1,
|
||||
};
|
||||
|
||||
const pipeline_infos = [_]vk.GraphicsPipelineCreateInfo{
|
||||
pipeline_info,
|
||||
};
|
||||
|
||||
const pipeline_infos = [_]vk.GraphicsPipelineCreateInfo{pipeline_info};
|
||||
var pipelines: [1]vk.Pipeline = undefined;
|
||||
|
||||
_ = try ldc.vkd.createGraphicsPipelines(
|
||||
@ -193,5 +289,6 @@ pub fn initPipelineContext(ldc: device.LogicalDeviceContext, extent: vk.Extent2D
|
||||
return .{
|
||||
.graphics_pipeline = pipelines[0],
|
||||
.pipeline_layout = pipeline_layout,
|
||||
.descriptor_set_layout = if (descriptor_set_layouts) |layouts| layouts[0] else .null_handle,
|
||||
};
|
||||
}
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
const std = @import("std");
|
||||
const log = std.log.scoped(.graphics);
|
||||
const vk = @import("vulkan");
|
||||
|
||||
const device = @import("device.zig");
|
||||
@ -59,7 +60,7 @@ pub fn initRenderPass(ldc: device.LogicalDeviceContext, format: vk.Format) !Rend
|
||||
};
|
||||
|
||||
const render_pass = try ldc.vkd.createRenderPass(ldc.device, &render_pass_create_info, null);
|
||||
std.debug.print("created render pass\n", .{});
|
||||
log.debug("created render pass", .{});
|
||||
|
||||
return .{ .render_pass = render_pass };
|
||||
}
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
const std = @import("std");
|
||||
const log = std.log.scoped(.graphics);
|
||||
const glfw = @import("zglfw");
|
||||
const vk = @import("vulkan");
|
||||
|
||||
@ -30,6 +31,7 @@ pub fn initSwapchain(
|
||||
ldc: device.LogicalDeviceContext,
|
||||
surface: vk.SurfaceKHR,
|
||||
window: *glfw.Window,
|
||||
old_swapchain: vk.SwapchainKHR,
|
||||
allocator: std.mem.Allocator,
|
||||
) !SwapchainContext {
|
||||
const surface_caps = try vc.vki.getPhysicalDeviceSurfaceCapabilitiesKHR(
|
||||
@ -37,16 +39,16 @@ pub fn initSwapchain(
|
||||
surface,
|
||||
);
|
||||
|
||||
std.debug.print(
|
||||
"surface current extent: {}x{}\n",
|
||||
log.debug(
|
||||
"surface current extent: {}x{}",
|
||||
.{
|
||||
surface_caps.current_extent.width,
|
||||
surface_caps.current_extent.height,
|
||||
},
|
||||
);
|
||||
|
||||
std.debug.print(
|
||||
"surface min/max image count: {}/{}\n",
|
||||
log.debug(
|
||||
"surface min/max image count: {}/{}",
|
||||
.{
|
||||
surface_caps.min_image_count,
|
||||
surface_caps.max_image_count,
|
||||
@ -100,16 +102,16 @@ pub fn initSwapchain(
|
||||
chosen_image_count = surface_caps.max_image_count;
|
||||
}
|
||||
|
||||
std.debug.print(
|
||||
"chosen swapchain format={any}, color_space={any}\n",
|
||||
log.debug(
|
||||
"chosen swapchain format={any}, color_space={any}",
|
||||
.{ chosen_surface_format.format, chosen_surface_format.color_space },
|
||||
);
|
||||
std.debug.print("chosen present mode={any}\n", .{chosen_present_mode});
|
||||
std.debug.print(
|
||||
"chosen extent={}x{}\n",
|
||||
log.debug("chosen present mode={any}", .{chosen_present_mode});
|
||||
log.debug(
|
||||
"chosen extent={}x{}",
|
||||
.{ chosen_extent.width, chosen_extent.height },
|
||||
);
|
||||
std.debug.print("chosen image count={}\n", .{chosen_image_count});
|
||||
log.debug("chosen image count={}", .{chosen_image_count});
|
||||
|
||||
const swapchain_create_info = vk.SwapchainCreateInfoKHR{
|
||||
.surface = surface,
|
||||
@ -128,11 +130,12 @@ pub fn initSwapchain(
|
||||
},
|
||||
.present_mode = chosen_present_mode,
|
||||
.clipped = .true,
|
||||
.old_swapchain = old_swapchain,
|
||||
};
|
||||
|
||||
const swapchain = try ldc.vkd.createSwapchainKHR(ldc.device, &swapchain_create_info, null);
|
||||
errdefer ldc.vkd.destroySwapchainKHR(ldc.device, swapchain, null);
|
||||
std.debug.print("created swapchain\n", .{});
|
||||
log.debug("created swapchain", .{});
|
||||
|
||||
const swapchain_images = try ldc.vkd.getSwapchainImagesAllocKHR(
|
||||
ldc.device,
|
||||
@ -140,7 +143,7 @@ pub fn initSwapchain(
|
||||
allocator,
|
||||
);
|
||||
errdefer allocator.free(swapchain_images);
|
||||
std.debug.print("swapchain images: {}\n", .{swapchain_images.len});
|
||||
log.debug("swapchain images: {}", .{swapchain_images.len});
|
||||
|
||||
const swapchain_image_views = try allocator.alloc(vk.ImageView, swapchain_images.len);
|
||||
errdefer allocator.free(swapchain_image_views);
|
||||
@ -182,7 +185,7 @@ pub fn initSwapchain(
|
||||
created_image_view_count += 1;
|
||||
}
|
||||
|
||||
std.debug.print("created swapchain image views: {}\n", .{swapchain_image_views.len});
|
||||
log.debug("created swapchain image views: {}", .{swapchain_image_views.len});
|
||||
|
||||
return .{
|
||||
.swapchain = swapchain,
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
const std = @import("std");
|
||||
const log = std.log.scoped(.graphics);
|
||||
const vk = @import("vulkan");
|
||||
|
||||
const device = @import("device.zig");
|
||||
@ -44,7 +45,7 @@ pub fn initSyncObjects(ldc: device.LogicalDeviceContext) !SyncContext {
|
||||
null,
|
||||
);
|
||||
|
||||
std.debug.print("created synchronization objects\n", .{});
|
||||
log.debug("created synchronization objects", .{});
|
||||
|
||||
return .{
|
||||
.image_available_semaphore = image_available_semaphore,
|
||||
|
||||
293
src/vulkan/texture.zig
Normal file
@ -0,0 +1,293 @@
|
||||
const vk = @import("vulkan");
|
||||
|
||||
const context = @import("context.zig");
|
||||
const device = @import("device.zig");
|
||||
const buffer = @import("buffer.zig");
|
||||
const commands = @import("commands.zig");
|
||||
const assets = @import("../assets.zig");
|
||||
|
||||
pub const TextureContext = struct {
|
||||
image: vk.Image,
|
||||
memory: vk.DeviceMemory,
|
||||
image_view: vk.ImageView,
|
||||
sampler: vk.Sampler,
|
||||
|
||||
pub fn destroy(self: *const TextureContext, ldc: *const device.LogicalDeviceContext) void {
|
||||
ldc.vkd.destroySampler(ldc.device, self.sampler, null);
|
||||
ldc.vkd.destroyImageView(ldc.device, self.image_view, null);
|
||||
ldc.vkd.destroyImage(ldc.device, self.image, null);
|
||||
ldc.vkd.freeMemory(ldc.device, self.memory, null);
|
||||
}
|
||||
};
|
||||
|
||||
pub fn createImage(
|
||||
vc: context.VulkanContext,
|
||||
ldc: device.LogicalDeviceContext,
|
||||
width: u32,
|
||||
height: u32,
|
||||
format: vk.Format,
|
||||
tiling: vk.ImageTiling,
|
||||
usage: vk.ImageUsageFlags,
|
||||
properties: vk.MemoryPropertyFlags,
|
||||
) !TextureContext {
|
||||
const image_create_info = vk.ImageCreateInfo{
|
||||
.image_type = .@"2d",
|
||||
.extent = .{
|
||||
.width = width,
|
||||
.height = height,
|
||||
.depth = 1,
|
||||
},
|
||||
.mip_levels = 1,
|
||||
.array_layers = 1,
|
||||
.format = format,
|
||||
.tiling = tiling,
|
||||
.initial_layout = .undefined,
|
||||
.usage = usage,
|
||||
.samples = .{ .@"1_bit" = true },
|
||||
.sharing_mode = .exclusive,
|
||||
};
|
||||
|
||||
const image = try ldc.vkd.createImage(ldc.device, &image_create_info, null);
|
||||
errdefer ldc.vkd.destroyImage(ldc.device, image, null);
|
||||
|
||||
const memory_requirements = ldc.vkd.getImageMemoryRequirements(ldc.device, image);
|
||||
const memory_type_index = try buffer.findMemoryType(
|
||||
vc,
|
||||
ldc,
|
||||
memory_requirements.memory_type_bits,
|
||||
properties,
|
||||
);
|
||||
|
||||
const alloc_info = vk.MemoryAllocateInfo{
|
||||
.allocation_size = memory_requirements.size,
|
||||
.memory_type_index = memory_type_index,
|
||||
};
|
||||
|
||||
const memory = try ldc.vkd.allocateMemory(ldc.device, &alloc_info, null);
|
||||
errdefer ldc.vkd.freeMemory(ldc.device, memory, null);
|
||||
|
||||
try ldc.vkd.bindImageMemory(ldc.device, image, memory, 0);
|
||||
|
||||
const image_view = try createImageView(ldc, image, format);
|
||||
errdefer ldc.vkd.destroyImageView(ldc.device, image_view, null);
|
||||
|
||||
const sampler = try createTextureSampler(ldc);
|
||||
errdefer ldc.vkd.destroySampler(ldc.device, sampler, null);
|
||||
|
||||
return .{
|
||||
.image = image,
|
||||
.memory = memory,
|
||||
.image_view = image_view,
|
||||
.sampler = sampler,
|
||||
};
|
||||
}
|
||||
|
||||
fn createImageView(
|
||||
ldc: device.LogicalDeviceContext,
|
||||
image: vk.Image,
|
||||
format: vk.Format,
|
||||
) !vk.ImageView {
|
||||
const view_create_info = vk.ImageViewCreateInfo{
|
||||
.image = image,
|
||||
.view_type = .@"2d",
|
||||
.format = format,
|
||||
.components = .{
|
||||
.r = .identity,
|
||||
.g = .identity,
|
||||
.b = .identity,
|
||||
.a = .identity,
|
||||
},
|
||||
.subresource_range = .{
|
||||
.aspect_mask = .{
|
||||
.color_bit = true,
|
||||
},
|
||||
.base_mip_level = 0,
|
||||
.level_count = 1,
|
||||
.base_array_layer = 0,
|
||||
.layer_count = 1,
|
||||
},
|
||||
};
|
||||
|
||||
return try ldc.vkd.createImageView(
|
||||
ldc.device,
|
||||
&view_create_info,
|
||||
null,
|
||||
);
|
||||
}
|
||||
|
||||
fn createTextureSampler(ldc: device.LogicalDeviceContext) !vk.Sampler {
|
||||
const sampler_create_info = vk.SamplerCreateInfo{
|
||||
.mag_filter = .linear,
|
||||
.min_filter = .linear,
|
||||
.mipmap_mode = .linear,
|
||||
.address_mode_u = .clamp_to_edge,
|
||||
.address_mode_v = .clamp_to_edge,
|
||||
.address_mode_w = .clamp_to_edge,
|
||||
.mip_lod_bias = 0.0,
|
||||
.anisotropy_enable = .false,
|
||||
.max_anisotropy = 1.0,
|
||||
.compare_enable = .false,
|
||||
.compare_op = .always,
|
||||
.min_lod = 0.0,
|
||||
.max_lod = 0.0,
|
||||
.border_color = .int_opaque_black,
|
||||
.unnormalized_coordinates = .false,
|
||||
};
|
||||
|
||||
return try ldc.vkd.createSampler(
|
||||
ldc.device,
|
||||
&sampler_create_info,
|
||||
null,
|
||||
);
|
||||
}
|
||||
|
||||
pub fn transitionImageLayout(
|
||||
ldc: device.LogicalDeviceContext,
|
||||
command_context: commands.CommandContext,
|
||||
image: vk.Image,
|
||||
old_layout: vk.ImageLayout,
|
||||
new_layout: vk.ImageLayout,
|
||||
) !void {
|
||||
const command_buffer = try commands.beginSingleTimeCommands(ldc, command_context);
|
||||
|
||||
var barrier = vk.ImageMemoryBarrier{
|
||||
.src_access_mask = .{},
|
||||
.dst_access_mask = .{},
|
||||
.old_layout = old_layout,
|
||||
.new_layout = new_layout,
|
||||
.src_queue_family_index = vk.QUEUE_FAMILY_IGNORED,
|
||||
.dst_queue_family_index = vk.QUEUE_FAMILY_IGNORED,
|
||||
.image = image,
|
||||
.subresource_range = .{
|
||||
.aspect_mask = .{ .color_bit = true },
|
||||
.base_mip_level = 0,
|
||||
.level_count = 1,
|
||||
.base_array_layer = 0,
|
||||
.layer_count = 1,
|
||||
},
|
||||
};
|
||||
|
||||
var source_stage: vk.PipelineStageFlags = undefined;
|
||||
var destination_stage: vk.PipelineStageFlags = undefined;
|
||||
|
||||
if (old_layout == .undefined and new_layout == .transfer_dst_optimal) {
|
||||
barrier.src_access_mask = .{};
|
||||
barrier.dst_access_mask = .{ .transfer_write_bit = true };
|
||||
|
||||
source_stage = .{ .top_of_pipe_bit = true };
|
||||
destination_stage = .{ .transfer_bit = true };
|
||||
} else if (old_layout == .transfer_dst_optimal and new_layout == .shader_read_only_optimal) {
|
||||
barrier.src_access_mask = .{ .transfer_write_bit = true };
|
||||
barrier.dst_access_mask = .{ .shader_read_bit = true };
|
||||
|
||||
source_stage = .{ .transfer_bit = true };
|
||||
destination_stage = .{ .fragment_shader_bit = true };
|
||||
} else {
|
||||
return error.UnsupportedLayoutTransition;
|
||||
}
|
||||
|
||||
const image_barriers = [_]vk.ImageMemoryBarrier{barrier};
|
||||
|
||||
ldc.vkd.cmdPipelineBarrier(
|
||||
command_buffer,
|
||||
source_stage,
|
||||
destination_stage,
|
||||
.{},
|
||||
null,
|
||||
null,
|
||||
&image_barriers,
|
||||
);
|
||||
|
||||
try commands.endSingleTimeCommands(ldc, command_context, command_buffer);
|
||||
}
|
||||
|
||||
pub fn copyBufferToImage(
|
||||
ldc: device.LogicalDeviceContext,
|
||||
command_context: commands.CommandContext,
|
||||
source_buffer: vk.Buffer,
|
||||
destination_image: vk.Image,
|
||||
width: u32,
|
||||
height: u32,
|
||||
) !void {
|
||||
const command_buffer = try commands.beginSingleTimeCommands(ldc, command_context);
|
||||
|
||||
const region = vk.BufferImageCopy{
|
||||
.buffer_offset = 0,
|
||||
.buffer_row_length = 0,
|
||||
.buffer_image_height = 0,
|
||||
.image_subresource = .{
|
||||
.aspect_mask = .{ .color_bit = true },
|
||||
.mip_level = 0,
|
||||
.base_array_layer = 0,
|
||||
.layer_count = 1,
|
||||
},
|
||||
.image_offset = .{ .x = 0, .y = 0, .z = 0 },
|
||||
.image_extent = .{
|
||||
.width = width,
|
||||
.height = height,
|
||||
.depth = 1,
|
||||
},
|
||||
};
|
||||
|
||||
const regions = [_]vk.BufferImageCopy{region};
|
||||
|
||||
ldc.vkd.cmdCopyBufferToImage(
|
||||
command_buffer,
|
||||
source_buffer,
|
||||
destination_image,
|
||||
.transfer_dst_optimal,
|
||||
®ions,
|
||||
);
|
||||
|
||||
try commands.endSingleTimeCommands(ldc, command_context, command_buffer);
|
||||
}
|
||||
|
||||
pub fn initTextureFromRgba(
|
||||
vc: context.VulkanContext,
|
||||
ldc: device.LogicalDeviceContext,
|
||||
command_context: commands.CommandContext,
|
||||
asset: []const u8,
|
||||
width: u32,
|
||||
height: u32,
|
||||
format: vk.Format,
|
||||
tiling: vk.ImageTiling,
|
||||
usage: vk.ImageUsageFlags,
|
||||
properties: vk.MemoryPropertyFlags,
|
||||
) !TextureContext {
|
||||
const staging = try buffer.initStagingBuffer(vc, ldc, asset);
|
||||
defer staging.destroy(&ldc);
|
||||
|
||||
const texture = try createImage(
|
||||
vc,
|
||||
ldc,
|
||||
width,
|
||||
height,
|
||||
format,
|
||||
tiling,
|
||||
usage,
|
||||
properties,
|
||||
);
|
||||
try transitionImageLayout(
|
||||
ldc,
|
||||
command_context,
|
||||
texture.image,
|
||||
.undefined,
|
||||
.transfer_dst_optimal,
|
||||
);
|
||||
try copyBufferToImage(
|
||||
ldc,
|
||||
command_context,
|
||||
staging.buffer,
|
||||
texture.image,
|
||||
width,
|
||||
height,
|
||||
);
|
||||
try transitionImageLayout(
|
||||
ldc,
|
||||
command_context,
|
||||
texture.image,
|
||||
.transfer_dst_optimal,
|
||||
.shader_read_only_optimal,
|
||||
);
|
||||
return texture;
|
||||
}
|
||||
@ -1,4 +1,5 @@
|
||||
const std = @import("std");
|
||||
const log = std.log.scoped(.graphics);
|
||||
const glfw = @import("zglfw");
|
||||
|
||||
pub fn initWindow(x: c_int, y: c_int, name: [:0]const u8) !*glfw.Window {
|
||||
@ -14,15 +15,15 @@ pub fn initWindow(x: c_int, y: c_int, name: [:0]const u8) !*glfw.Window {
|
||||
null,
|
||||
);
|
||||
|
||||
std.debug.print("GLFW platform: {any}\n", .{glfw.getPlatform()});
|
||||
std.debug.print("Vulkan supported by GLFW: {}\n", .{glfw.isVulkanSupported()});
|
||||
log.debug("GLFW platform: {any}", .{glfw.getPlatform()});
|
||||
log.debug("Vulkan supported by GLFW: {}", .{glfw.isVulkanSupported()});
|
||||
|
||||
const size = window.getSize();
|
||||
const fb_size = window.getFramebufferSize();
|
||||
|
||||
std.debug.print("Window size: {}x{}\n", .{ size[0], size[1] });
|
||||
std.debug.print("Framebuffer size: {}x{}\n", .{ fb_size[0], fb_size[1] });
|
||||
std.debug.print("Window visible: {}\n", .{window.getAttribute(.visible)});
|
||||
log.debug("Window size: {}x{}", .{ size[0], size[1] });
|
||||
log.debug("Framebuffer size: {}x{}", .{ fb_size[0], fb_size[1] });
|
||||
log.debug("Window visible: {}", .{window.getAttribute(.visible)});
|
||||
|
||||
return window;
|
||||
}
|
||||
|
||||
835
tools/magic_numbers.zig
Normal file
@ -0,0 +1,835 @@
|
||||
const std = @import("std");
|
||||
|
||||
const Bitboard = u64;
|
||||
const Square = u6;
|
||||
const rank_1_mask: u64 = 0x0000_0000_0000_00FF;
|
||||
const file_a_mask: u64 = 0x0101_0101_0101_0101;
|
||||
|
||||
const SlidingPiece = enum {
|
||||
bishop,
|
||||
rook,
|
||||
|
||||
fn parse(text: []const u8) ?SlidingPiece {
|
||||
if (std.mem.eql(u8, text, "bishop")) return .bishop;
|
||||
if (std.mem.eql(u8, text, "rook")) return .rook;
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const RookAttackTableSize = 4096;
|
||||
const BishopAttackTableSize = 512;
|
||||
|
||||
const MagicInfo = struct {
|
||||
mask: Bitboard,
|
||||
magic: Bitboard,
|
||||
shift: u7,
|
||||
};
|
||||
|
||||
const Config = struct {
|
||||
piece: SlidingPiece = .rook,
|
||||
square: ?Square = null,
|
||||
seed: u64 = 0x9e37_79b9_7f4a_7c15,
|
||||
attempts: u64 = 1_000_000,
|
||||
output_path: ?[]const u8 = null,
|
||||
};
|
||||
|
||||
pub fn main(init: std.process.Init) !void {
|
||||
var args = try std.process.Args.Iterator.initAllocator(init.minimal.args, std.heap.page_allocator);
|
||||
defer args.deinit();
|
||||
|
||||
_ = args.next(); // executable path
|
||||
|
||||
var config = Config{};
|
||||
while (args.next()) |arg| {
|
||||
if (std.mem.eql(u8, arg, "--help") or std.mem.eql(u8, arg, "-h")) {
|
||||
printUsage();
|
||||
return;
|
||||
} else if (std.mem.eql(u8, arg, "--piece")) {
|
||||
const value = args.next() orelse return error.MissingPieceValue;
|
||||
config.piece = SlidingPiece.parse(value) orelse return error.InvalidPiece;
|
||||
} else if (std.mem.eql(u8, arg, "--square")) {
|
||||
const value = args.next() orelse return error.MissingSquareValue;
|
||||
const parsed = try std.fmt.parseUnsigned(u8, value, 10);
|
||||
if (parsed >= 64) return error.InvalidSquare;
|
||||
config.square = @intCast(parsed);
|
||||
} else if (std.mem.eql(u8, arg, "--seed")) {
|
||||
const value = args.next() orelse return error.MissingSeedValue;
|
||||
config.seed = try std.fmt.parseUnsigned(u64, value, 0);
|
||||
} else if (std.mem.eql(u8, arg, "--attempts")) {
|
||||
const value = args.next() orelse return error.MissingAttemptsValue;
|
||||
config.attempts = try std.fmt.parseUnsigned(u64, value, 10);
|
||||
} else if (std.mem.eql(u8, arg, "--output")) {
|
||||
config.output_path = args.next() orelse return error.MissingOutputValue;
|
||||
} else {
|
||||
std.log.err("unknown argument: {s}", .{arg});
|
||||
printUsage();
|
||||
return error.UnknownArgument;
|
||||
}
|
||||
}
|
||||
|
||||
std.debug.print("magic number utility scaffold\n", .{});
|
||||
std.debug.print("piece: {s}\n", .{@tagName(config.piece)});
|
||||
if (config.square) |square| {
|
||||
std.debug.print("square: {}\n", .{square});
|
||||
} else {
|
||||
std.debug.print("square: all\n", .{});
|
||||
}
|
||||
std.debug.print("seed: 0x{x}\n", .{config.seed});
|
||||
std.debug.print("attempts: {}\n\n", .{config.attempts});
|
||||
|
||||
std.debug.print(
|
||||
"TODO: add occupancy-mask generation, attack-table generation, and random magic search here.\n" ++
|
||||
"This tool intentionally does not import src/chess/ so it can evolve independently from chess implementation code.\n",
|
||||
.{},
|
||||
);
|
||||
|
||||
printBitboard(getQueenAttackMask(0));
|
||||
|
||||
var gpa = std.heap.DebugAllocator(.{}){};
|
||||
defer _ = gpa.deinit();
|
||||
|
||||
const allocator = gpa.allocator();
|
||||
|
||||
if (config.output_path) |output_path| {
|
||||
const rook_magic_info = try allocator.create([64]MagicInfo);
|
||||
defer allocator.destroy(rook_magic_info);
|
||||
const bishop_magic_info = try allocator.create([64]MagicInfo);
|
||||
defer allocator.destroy(bishop_magic_info);
|
||||
const rook_attacks = try allocator.create([64][RookAttackTableSize]Bitboard);
|
||||
defer allocator.destroy(rook_attacks);
|
||||
const bishop_attacks = try allocator.create([64][BishopAttackTableSize]Bitboard);
|
||||
defer allocator.destroy(bishop_attacks);
|
||||
|
||||
rook_magic_info.* = .{@as(MagicInfo, .{ .mask = 0, .magic = 0, .shift = 0 })} ** 64;
|
||||
bishop_magic_info.* = .{@as(MagicInfo, .{ .mask = 0, .magic = 0, .shift = 0 })} ** 64;
|
||||
rook_attacks.* = .{.{0} ** RookAttackTableSize} ** 64;
|
||||
bishop_attacks.* = .{.{0} ** BishopAttackTableSize} ** 64;
|
||||
|
||||
var prng = std.Random.DefaultPrng.init(config.seed);
|
||||
const random = prng.random();
|
||||
|
||||
for (0..64) |square_index| {
|
||||
const square: Square = @intCast(square_index);
|
||||
try generateRookMagicNumber(square, random, rook_magic_info, rook_attacks, allocator, config.attempts);
|
||||
try generateBishopMagicNumber(square, random, bishop_magic_info, bishop_attacks, allocator, config.attempts);
|
||||
std.debug.print("generated magic tables for square {}\n", .{square_index});
|
||||
}
|
||||
|
||||
try writeGeneratedMagicTables(
|
||||
output_path,
|
||||
rook_magic_info,
|
||||
bishop_magic_info,
|
||||
rook_attacks,
|
||||
bishop_attacks,
|
||||
allocator,
|
||||
init.io,
|
||||
);
|
||||
std.debug.print("wrote generated magic tables to {s}\n", .{output_path});
|
||||
}
|
||||
}
|
||||
|
||||
fn printUsage() void {
|
||||
std.debug.print(
|
||||
\\Usage:
|
||||
\\ zig build run-magic-numbers -- [options]
|
||||
\\
|
||||
\\Options:
|
||||
\\ --piece rook|bishop Sliding piece to search for. Default: rook
|
||||
\\ --square 0..63 Single square to search. Default: all squares
|
||||
\\ --seed N Random seed. Supports decimal or 0x-prefixed hex
|
||||
\\ --attempts N Candidate magic numbers to try per square
|
||||
\\ --output PATH Write generated Zig tables to PATH
|
||||
\\ -h, --help Show this help
|
||||
\\
|
||||
,
|
||||
.{},
|
||||
);
|
||||
}
|
||||
|
||||
pub fn bit(square: Square) Bitboard {
|
||||
return @as(Bitboard, 1) << square;
|
||||
}
|
||||
|
||||
pub fn getRookAttackMask(square: Square) Bitboard {
|
||||
const rank = square / 8;
|
||||
const file = square % 8;
|
||||
|
||||
const rank_mask = rank_1_mask << (rank * 8);
|
||||
const file_mask = file_a_mask << file;
|
||||
|
||||
var rook_mask = (rank_mask | file_mask) & ~bit(square);
|
||||
if (rank_mask != rank_1_mask) {
|
||||
rook_mask = rook_mask & ~rank_1_mask;
|
||||
}
|
||||
if (rank_mask != (rank_1_mask << 56)) {
|
||||
rook_mask = rook_mask & ~(rank_1_mask << 56);
|
||||
}
|
||||
if (file_mask != file_a_mask) {
|
||||
rook_mask = rook_mask & ~file_a_mask;
|
||||
}
|
||||
if (file_mask != (file_a_mask << 7)) {
|
||||
rook_mask = rook_mask & ~(file_a_mask << 7);
|
||||
}
|
||||
return rook_mask;
|
||||
}
|
||||
|
||||
pub fn generateRookMagicNumber(square: Square, random: std.Random, rook_info: *[64]MagicInfo, rook_attacks: *[64][RookAttackTableSize]Bitboard, allocator: std.mem.Allocator, max_attempts: u64) !void {
|
||||
const mask = getRookAttackMask(square);
|
||||
const set_bits = try getMaskSetBits(mask, allocator);
|
||||
defer allocator.free(set_bits);
|
||||
|
||||
const relevant_bits = set_bits.len;
|
||||
const table_size = @as(usize, 1) << @intCast(relevant_bits);
|
||||
const shift: u7 = @intCast(64 - relevant_bits);
|
||||
|
||||
const occupancies = try getAllPotentialOccupiedSquares(set_bits, allocator);
|
||||
defer allocator.free(occupancies);
|
||||
|
||||
const expected_attacks = try allocator.alloc(Bitboard, table_size);
|
||||
defer allocator.free(expected_attacks);
|
||||
|
||||
for (occupancies, 0..) |occupancy, i| {
|
||||
expected_attacks[i] = getRookAttacks(square, occupancy);
|
||||
}
|
||||
|
||||
const temp_table = try allocator.alloc(Bitboard, table_size);
|
||||
defer allocator.free(temp_table);
|
||||
|
||||
const used = try allocator.alloc(bool, table_size);
|
||||
defer allocator.free(used);
|
||||
|
||||
var found_magic: Bitboard = 0;
|
||||
|
||||
var attempts: u64 = 0;
|
||||
while (attempts < max_attempts) : (attempts += 1) {
|
||||
const magic = magicRandom(random);
|
||||
|
||||
if (testMagicCandidate(magic, shift, occupancies, expected_attacks, temp_table, used)) {
|
||||
found_magic = magic;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (found_magic == 0) return error.MagicNotFound;
|
||||
|
||||
rook_info[square] = .{
|
||||
.magic = found_magic,
|
||||
.shift = shift,
|
||||
.mask = mask,
|
||||
};
|
||||
|
||||
@memset(&rook_attacks[square], 0);
|
||||
for (temp_table, 0..) |attack, i| {
|
||||
rook_attacks[square][i] = attack;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn generateBishopMagicNumber(square: Square, random: std.Random, bishop_info: *[64]MagicInfo, bishop_attacks: *[64][BishopAttackTableSize]Bitboard, allocator: std.mem.Allocator, max_attempts: u64) !void {
|
||||
const mask = getBishopAttackMask(square);
|
||||
const set_bits = try getMaskSetBits(mask, allocator);
|
||||
defer allocator.free(set_bits);
|
||||
|
||||
const relevant_bits = set_bits.len;
|
||||
const table_size = @as(usize, 1) << @intCast(relevant_bits);
|
||||
const shift: u7 = @intCast(64 - relevant_bits);
|
||||
|
||||
const occupancies = try getAllPotentialOccupiedSquares(set_bits, allocator);
|
||||
defer allocator.free(occupancies);
|
||||
|
||||
const expected_attacks = try allocator.alloc(Bitboard, table_size);
|
||||
defer allocator.free(expected_attacks);
|
||||
|
||||
for (occupancies, 0..) |occupancy, i| {
|
||||
expected_attacks[i] = getBishopAttacks(square, occupancy);
|
||||
}
|
||||
|
||||
const temp_table = try allocator.alloc(Bitboard, table_size);
|
||||
defer allocator.free(temp_table);
|
||||
|
||||
const used = try allocator.alloc(bool, table_size);
|
||||
defer allocator.free(used);
|
||||
|
||||
var found_magic: Bitboard = 0;
|
||||
|
||||
var attempts: u64 = 0;
|
||||
while (attempts < max_attempts) : (attempts += 1) {
|
||||
const magic = magicRandom(random);
|
||||
|
||||
if (testMagicCandidate(magic, shift, occupancies, expected_attacks, temp_table, used)) {
|
||||
found_magic = magic;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (found_magic == 0) return error.MagicNotFound;
|
||||
|
||||
bishop_info[square] = .{
|
||||
.mask = mask,
|
||||
.magic = found_magic,
|
||||
.shift = shift,
|
||||
};
|
||||
|
||||
@memset(&bishop_attacks[square], 0);
|
||||
for (temp_table, 0..) |attack, i| {
|
||||
bishop_attacks[square][i] = attack;
|
||||
}
|
||||
}
|
||||
|
||||
fn testMagicCandidate(
|
||||
magic: Bitboard,
|
||||
shift: u7,
|
||||
occupancies: []const Bitboard,
|
||||
expected_attacks: []const Bitboard,
|
||||
temp_table: []Bitboard,
|
||||
used: []bool,
|
||||
) bool {
|
||||
@memset(temp_table, 0);
|
||||
@memset(used, false);
|
||||
|
||||
for (occupancies, 0..) |occupancy, i| {
|
||||
const shift_amount: u6 = @intCast(shift);
|
||||
const index: usize = @intCast((occupancy *% magic) >> shift_amount);
|
||||
const expected = expected_attacks[i];
|
||||
|
||||
if (!used[index]) {
|
||||
used[index] = true;
|
||||
temp_table[index] = expected;
|
||||
} else if (temp_table[index] != expected) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
fn magicRandom(random: std.Random) u64 {
|
||||
return random.int(u64) & random.int(u64) & random.int(u64);
|
||||
}
|
||||
|
||||
fn writeGeneratedMagicTables(
|
||||
output_path: []const u8,
|
||||
rook_info: *const [64]MagicInfo,
|
||||
bishop_info: *const [64]MagicInfo,
|
||||
rook_attacks: *const [64][RookAttackTableSize]Bitboard,
|
||||
bishop_attacks: *const [64][BishopAttackTableSize]Bitboard,
|
||||
allocator: std.mem.Allocator,
|
||||
io: std.Io,
|
||||
) !void {
|
||||
var allocating_writer = std.Io.Writer.Allocating.init(allocator);
|
||||
defer allocating_writer.deinit();
|
||||
|
||||
try writeMagicTables(&allocating_writer.writer, rook_info, bishop_info, rook_attacks, bishop_attacks);
|
||||
|
||||
var contents = allocating_writer.toArrayList();
|
||||
defer contents.deinit(allocator);
|
||||
|
||||
try std.Io.Dir.cwd().writeFile(io, .{
|
||||
.sub_path = output_path,
|
||||
.data = contents.items,
|
||||
});
|
||||
}
|
||||
|
||||
fn writeMagicTables(
|
||||
writer: *std.Io.Writer,
|
||||
rook_info: *const [64]MagicInfo,
|
||||
bishop_info: *const [64]MagicInfo,
|
||||
rook_attacks: *const [64][RookAttackTableSize]Bitboard,
|
||||
bishop_attacks: *const [64][BishopAttackTableSize]Bitboard,
|
||||
) std.Io.Writer.Error!void {
|
||||
try writer.print(
|
||||
\\// Generated by tools/magic_numbers.zig. Do not edit by hand.
|
||||
\\const bitboard = @import("bitboard.zig");
|
||||
\\
|
||||
\\pub const Bitboard = bitboard.Bitboard;
|
||||
\\
|
||||
\\pub const MagicInfo = struct {{
|
||||
\\ mask: Bitboard,
|
||||
\\ magic: Bitboard,
|
||||
\\ shift: u7,
|
||||
\\}};
|
||||
\\
|
||||
\\pub const RookAttackTableSize = {};
|
||||
\\pub const BishopAttackTableSize = {};
|
||||
\\
|
||||
,
|
||||
.{ RookAttackTableSize, BishopAttackTableSize },
|
||||
);
|
||||
|
||||
try writeMagicInfoArray(writer, "rook_magic_info", rook_info);
|
||||
try writeMagicInfoArray(writer, "bishop_magic_info", bishop_info);
|
||||
try writeAttackTable(writer, "rook_attacks", RookAttackTableSize, rook_attacks);
|
||||
try writeAttackTable(writer, "bishop_attacks", BishopAttackTableSize, bishop_attacks);
|
||||
}
|
||||
|
||||
fn writeMagicInfoArray(writer: *std.Io.Writer, name: []const u8, infos: *const [64]MagicInfo) std.Io.Writer.Error!void {
|
||||
try writer.print("pub const {s}: [64]MagicInfo = .{{\n", .{name});
|
||||
for (infos) |info| {
|
||||
try writer.print(" .{{ .mask = 0x{x}, .magic = 0x{x}, .shift = {} }},\n", .{ info.mask, info.magic, info.shift });
|
||||
}
|
||||
try writer.print("}};\n\n", .{});
|
||||
}
|
||||
|
||||
fn writeAttackTable(
|
||||
writer: *std.Io.Writer,
|
||||
name: []const u8,
|
||||
comptime table_size: usize,
|
||||
attacks: *const [64][table_size]Bitboard,
|
||||
) std.Io.Writer.Error!void {
|
||||
try writer.print("pub const {s}: [64][{}]Bitboard = .{{\n", .{ name, table_size });
|
||||
for (attacks) |square_attacks| {
|
||||
try writer.print(" .{{\n", .{});
|
||||
for (square_attacks, 0..) |attack, i| {
|
||||
if (i % 4 == 0) try writer.print(" ", .{});
|
||||
try writer.print("0x{x}, ", .{attack});
|
||||
if (i % 4 == 3) try writer.print("\n", .{});
|
||||
}
|
||||
if (table_size % 4 != 0) try writer.print("\n", .{});
|
||||
try writer.print(" }},\n", .{});
|
||||
}
|
||||
try writer.print("}};\n\n", .{});
|
||||
}
|
||||
|
||||
pub fn getRookAttacks(square: Square, blockers: Bitboard) Bitboard {
|
||||
const rank: i32 = @intCast(square / 8);
|
||||
const file: i32 = @intCast(square % 8);
|
||||
|
||||
var attacks: Bitboard = 0;
|
||||
|
||||
var r = rank + 1;
|
||||
var f = file;
|
||||
while (r <= 7) : (r += 1) {
|
||||
const target: Square = @intCast(r * 8 + f);
|
||||
attacks |= @as(Bitboard, 1) << target;
|
||||
if ((blockers & (@as(Bitboard, 1) << target)) != 0) break;
|
||||
}
|
||||
r = rank - 1;
|
||||
f = file;
|
||||
while (r >= 0) : (r -= 1) {
|
||||
const target: Square = @intCast(r * 8 + f);
|
||||
attacks |= @as(Bitboard, 1) << target;
|
||||
if ((blockers & (@as(Bitboard, 1) << target)) != 0) break;
|
||||
}
|
||||
r = rank;
|
||||
f = file + 1;
|
||||
while (f <= 7) : (f += 1) {
|
||||
const target: Square = @intCast(r * 8 + f);
|
||||
attacks |= @as(Bitboard, 1) << target;
|
||||
if ((blockers & (@as(Bitboard, 1) << target)) != 0) break;
|
||||
}
|
||||
r = rank;
|
||||
f = file - 1;
|
||||
while (f >= 0) : (f -= 1) {
|
||||
const target: Square = @intCast(r * 8 + f);
|
||||
attacks |= @as(Bitboard, 1) << target;
|
||||
if ((blockers & (@as(Bitboard, 1) << target)) != 0) break;
|
||||
}
|
||||
return attacks;
|
||||
}
|
||||
|
||||
pub fn getBishopAttacks(square: Square, blockers: Bitboard) Bitboard {
|
||||
const rank: i32 = @intCast(square / 8);
|
||||
const file: i32 = @intCast(square % 8);
|
||||
|
||||
var attacks: Bitboard = 0;
|
||||
|
||||
var r = rank + 1;
|
||||
var f = file + 1;
|
||||
while (r <= 7 and f <= 7) {
|
||||
const target: Square = @intCast(r * 8 + f);
|
||||
attacks |= @as(Bitboard, 1) << target;
|
||||
if ((blockers & (@as(Bitboard, 1) << target)) != 0) break;
|
||||
r += 1;
|
||||
f += 1;
|
||||
}
|
||||
r = rank - 1;
|
||||
f = file + 1;
|
||||
while (r >= 0 and f <= 7) {
|
||||
const target: Square = @intCast(r * 8 + f);
|
||||
attacks |= @as(Bitboard, 1) << target;
|
||||
if ((blockers & (@as(Bitboard, 1) << target)) != 0) break;
|
||||
r -= 1;
|
||||
f += 1;
|
||||
}
|
||||
r = rank + 1;
|
||||
f = file - 1;
|
||||
while (r <= 7 and f >= 0) {
|
||||
const target: Square = @intCast(r * 8 + f);
|
||||
attacks |= @as(Bitboard, 1) << target;
|
||||
if ((blockers & (@as(Bitboard, 1) << target)) != 0) break;
|
||||
r += 1;
|
||||
f -= 1;
|
||||
}
|
||||
r = rank - 1;
|
||||
f = file - 1;
|
||||
while (r >= 0 and f >= 0) {
|
||||
const target: Square = @intCast(r * 8 + f);
|
||||
attacks |= @as(Bitboard, 1) << target;
|
||||
if ((blockers & (@as(Bitboard, 1) << target)) != 0) break;
|
||||
r -= 1;
|
||||
f -= 1;
|
||||
}
|
||||
return attacks;
|
||||
}
|
||||
|
||||
pub fn getQueenAttacks(square: Square, blockers: Bitboard) Bitboard {
|
||||
return getRookAttacks(square, blockers) | getBishopAttacks(square, blockers);
|
||||
}
|
||||
|
||||
pub fn getBishopAttackMask(square: Square) Bitboard {
|
||||
const rank = square / 8;
|
||||
const file = square % 8;
|
||||
|
||||
var bishop_mask: Bitboard = 0;
|
||||
|
||||
const rank_i: i32 = @intCast(rank);
|
||||
const file_i: i32 = @intCast(file);
|
||||
|
||||
var r: i32 = rank_i + 1;
|
||||
var f: i32 = file_i + 1;
|
||||
while (r < 7 and f < 7) {
|
||||
bishop_mask |= @as(Bitboard, 1) << @intCast(r * 8 + f);
|
||||
r += 1;
|
||||
f += 1;
|
||||
}
|
||||
r = rank_i + 1;
|
||||
f = file_i - 1;
|
||||
while (r < 7 and f > 0) {
|
||||
bishop_mask |= @as(Bitboard, 1) << @intCast(r * 8 + f);
|
||||
r += 1;
|
||||
f -= 1;
|
||||
}
|
||||
r = rank_i - 1;
|
||||
f = file_i + 1;
|
||||
while (r > 0 and f < 7) {
|
||||
bishop_mask |= @as(Bitboard, 1) << @intCast(r * 8 + f);
|
||||
r -= 1;
|
||||
f += 1;
|
||||
}
|
||||
r = rank_i - 1;
|
||||
f = file_i - 1;
|
||||
while (r > 0 and f > 0) {
|
||||
bishop_mask |= @as(Bitboard, 1) << @intCast(r * 8 + f);
|
||||
r -= 1;
|
||||
f -= 1;
|
||||
}
|
||||
|
||||
return bishop_mask;
|
||||
}
|
||||
|
||||
fn getMaskSetBits(bitboard: Bitboard, allocator: std.mem.Allocator) ![]u6 {
|
||||
const count: usize = @intCast(@popCount(bitboard));
|
||||
const squares = try allocator.alloc(u6, count);
|
||||
|
||||
var bb = bitboard;
|
||||
var i: usize = 0;
|
||||
|
||||
while (bb != 0) {
|
||||
const square: u6 = @intCast(@ctz(bb));
|
||||
squares[i] = square;
|
||||
i += 1;
|
||||
|
||||
bb &= bb - 1;
|
||||
}
|
||||
return squares;
|
||||
}
|
||||
|
||||
fn getAllPotentialOccupiedSquares(squares: []u6, allocator: std.mem.Allocator) ![]Bitboard {
|
||||
const count = @as(usize, 1) << @intCast(squares.len);
|
||||
const bitboards: []Bitboard = try allocator.alloc(Bitboard, count);
|
||||
|
||||
var combo: usize = 0;
|
||||
while (combo < count) : (combo += 1) {
|
||||
var occupancy: Bitboard = 0;
|
||||
|
||||
for (squares, 0..) |square, i| {
|
||||
const combo_bit = @as(usize, 1) << @intCast(i);
|
||||
|
||||
if ((combo & combo_bit) != 0) {
|
||||
occupancy |= @as(u64, 1) << square;
|
||||
}
|
||||
}
|
||||
bitboards[combo] = occupancy;
|
||||
}
|
||||
|
||||
return bitboards;
|
||||
}
|
||||
|
||||
pub fn getQueenAttackMask(square: Square) Bitboard {
|
||||
return getRookAttackMask(square) | getBishopAttackMask(square);
|
||||
}
|
||||
|
||||
fn printBitboard(bb: u64) void {
|
||||
var rank: i32 = 7;
|
||||
while (rank >= 0) : (rank -= 1) {
|
||||
var file: u6 = 0;
|
||||
while (file < 8) : (file += 1) {
|
||||
const square: u6 = @intCast((rank * 8) + file);
|
||||
const mask = @as(u64, 1) << square;
|
||||
|
||||
if ((bb & mask) != 0) {
|
||||
std.debug.print("1 ", .{});
|
||||
} else {
|
||||
std.debug.print(". ", .{});
|
||||
}
|
||||
}
|
||||
std.debug.print("\n", .{});
|
||||
}
|
||||
std.debug.print("\n", .{});
|
||||
}
|
||||
|
||||
test "bit returns a bitboard with one square set" {
|
||||
try std.testing.expectEqual(@as(Bitboard, 1), bit(0));
|
||||
try std.testing.expectEqual(@as(Bitboard, 1) << 63, bit(63));
|
||||
}
|
||||
|
||||
test "rook attack mask for A1 excludes self and board edges" {
|
||||
try std.testing.expectEqual(@as(Bitboard, 0x0001_0101_0101_017E), getRookAttackMask(0));
|
||||
}
|
||||
|
||||
test "rook attack mask for D4 excludes self and board edges" {
|
||||
try std.testing.expectEqual(@as(Bitboard, 0x0008_0808_7608_0800), getRookAttackMask(27));
|
||||
}
|
||||
|
||||
test "rook attack mask for H8 excludes self and board edges" {
|
||||
try std.testing.expectEqual(@as(Bitboard, 0x7E80_8080_8080_8000), getRookAttackMask(63));
|
||||
}
|
||||
|
||||
test "rook attack mask for A4 excludes self and board edges" {
|
||||
try std.testing.expectEqual(@as(Bitboard, 0x0001_0101_7E01_0100), getRookAttackMask(24));
|
||||
}
|
||||
|
||||
test "rook attack mask for D1 excludes self and board edges" {
|
||||
try std.testing.expectEqual(@as(Bitboard, 0x0008_0808_0808_0876), getRookAttackMask(3));
|
||||
}
|
||||
|
||||
test "bishop attack mask for A1 excludes self and board edges" {
|
||||
try std.testing.expectEqual(@as(Bitboard, 0x0040_2010_0804_0200), getBishopAttackMask(0));
|
||||
}
|
||||
|
||||
test "bishop attack mask for D4 excludes self and board edges" {
|
||||
try std.testing.expectEqual(@as(Bitboard, 0x0040_2214_0014_2200), getBishopAttackMask(27));
|
||||
}
|
||||
|
||||
test "bishop attack mask for H8 excludes self and board edges" {
|
||||
try std.testing.expectEqual(@as(Bitboard, 0x0040_2010_0804_0200), getBishopAttackMask(63));
|
||||
}
|
||||
|
||||
test "bishop attack mask for A4 excludes self and board edges" {
|
||||
try std.testing.expectEqual(@as(Bitboard, 0x0008_0402_0002_0400), getBishopAttackMask(24));
|
||||
}
|
||||
|
||||
test "bishop attack mask for D1 excludes self and board edges" {
|
||||
try std.testing.expectEqual(@as(Bitboard, 0x0000_0000_4022_1400), getBishopAttackMask(3));
|
||||
}
|
||||
|
||||
test "queen attack mask for A1 combines rook and bishop masks" {
|
||||
try std.testing.expectEqual(@as(Bitboard, 0x0041_2111_0905_037E), getQueenAttackMask(0));
|
||||
}
|
||||
|
||||
test "queen attack mask for D4 combines rook and bishop masks" {
|
||||
try std.testing.expectEqual(@as(Bitboard, 0x0048_2A1C_761C_2A00), getQueenAttackMask(27));
|
||||
}
|
||||
|
||||
test "queen attack mask for H8 combines rook and bishop masks" {
|
||||
try std.testing.expectEqual(@as(Bitboard, 0x7EC0_A090_8884_8200), getQueenAttackMask(63));
|
||||
}
|
||||
|
||||
test "queen attack mask for A4 combines rook and bishop masks" {
|
||||
try std.testing.expectEqual(@as(Bitboard, 0x0009_0503_7E03_0500), getQueenAttackMask(24));
|
||||
}
|
||||
|
||||
test "queen attack mask for D1 combines rook and bishop masks" {
|
||||
try std.testing.expectEqual(@as(Bitboard, 0x0008_0808_482A_1C76), getQueenAttackMask(3));
|
||||
}
|
||||
|
||||
test "rook attacks from D4 on empty board include board edges" {
|
||||
try std.testing.expectEqual(@as(Bitboard, 0x0808_0808_F708_0808), getRookAttacks(27, 0));
|
||||
}
|
||||
|
||||
test "rook attacks from D4 stop at nearest blockers and include blocker squares" {
|
||||
const blockers = bit(35) | bit(25) | bit(30) | bit(11) | bit(45) | bit(9);
|
||||
try std.testing.expectEqual(@as(Bitboard, 0x0000_0008_7608_0800), getRookAttacks(27, blockers));
|
||||
}
|
||||
|
||||
test "rook attacks from A1 on empty board include rank and file edges" {
|
||||
try std.testing.expectEqual(@as(Bitboard, 0x0101_0101_0101_01FE), getRookAttacks(0, 0));
|
||||
}
|
||||
|
||||
test "rook attacks from A1 stop at adjacent blockers" {
|
||||
const blockers = bit(8) | bit(1) | bit(9);
|
||||
try std.testing.expectEqual(@as(Bitboard, 0x0000_0000_0000_0102), getRookAttacks(0, blockers));
|
||||
}
|
||||
|
||||
test "bishop attacks from D4 on empty board include diagonal edges" {
|
||||
try std.testing.expectEqual(@as(Bitboard, 0x8041_2214_0014_2241), getBishopAttacks(27, 0));
|
||||
}
|
||||
|
||||
test "bishop attacks from D4 stop at nearest blockers and include blocker squares" {
|
||||
const blockers = bit(35) | bit(25) | bit(30) | bit(11) | bit(45) | bit(9);
|
||||
try std.testing.expectEqual(@as(Bitboard, 0x0001_2214_0014_2240), getBishopAttacks(27, blockers));
|
||||
}
|
||||
|
||||
test "bishop attacks from A1 on empty board include diagonal edge" {
|
||||
try std.testing.expectEqual(@as(Bitboard, 0x8040_2010_0804_0200), getBishopAttacks(0, 0));
|
||||
}
|
||||
|
||||
test "bishop attacks from A1 stop at adjacent diagonal blocker" {
|
||||
const blockers = bit(8) | bit(1) | bit(9);
|
||||
try std.testing.expectEqual(@as(Bitboard, 0x0000_0000_0000_0200), getBishopAttacks(0, blockers));
|
||||
}
|
||||
|
||||
test "queen attacks from D4 combine rook and bishop attacks on empty board" {
|
||||
try std.testing.expectEqual(@as(Bitboard, 0x8849_2A1C_F71C_2A49), getQueenAttacks(27, 0));
|
||||
}
|
||||
|
||||
test "queen attacks from D4 combine rook and bishop attacks with blockers" {
|
||||
const blockers = bit(35) | bit(25) | bit(30) | bit(11) | bit(45) | bit(9);
|
||||
try std.testing.expectEqual(@as(Bitboard, 0x0001_221C_761C_2A40), getQueenAttacks(27, blockers));
|
||||
}
|
||||
|
||||
test "queen attacks from A1 on empty board combine rook and bishop edges" {
|
||||
try std.testing.expectEqual(@as(Bitboard, 0x8141_2111_0905_03FE), getQueenAttacks(0, 0));
|
||||
}
|
||||
|
||||
test "queen attacks from A1 stop at adjacent blockers" {
|
||||
const blockers = bit(8) | bit(1) | bit(9);
|
||||
try std.testing.expectEqual(@as(Bitboard, 0x0000_0000_0000_0302), getQueenAttacks(0, blockers));
|
||||
}
|
||||
|
||||
test "getMaskSetBits returns an empty slice for an empty bitboard" {
|
||||
const squares = try getMaskSetBits(0, std.testing.allocator);
|
||||
defer std.testing.allocator.free(squares);
|
||||
|
||||
try std.testing.expectEqual(@as(usize, 0), squares.len);
|
||||
}
|
||||
|
||||
test "getMaskSetBits returns set square indexes from least to greatest" {
|
||||
const mask = bit(0) | bit(7) | bit(27) | bit(63);
|
||||
const squares = try getMaskSetBits(mask, std.testing.allocator);
|
||||
defer std.testing.allocator.free(squares);
|
||||
|
||||
try std.testing.expectEqualSlices(u6, &.{ 0, 7, 27, 63 }, squares);
|
||||
}
|
||||
|
||||
test "getMaskSetBits returns every set bit from a rook mask" {
|
||||
const squares = try getMaskSetBits(getRookAttackMask(0), std.testing.allocator);
|
||||
defer std.testing.allocator.free(squares);
|
||||
|
||||
try std.testing.expectEqualSlices(u6, &.{ 1, 2, 3, 4, 5, 6, 8, 16, 24, 32, 40, 48 }, squares);
|
||||
}
|
||||
|
||||
test "getAllPotentialOccupiedSquares returns only the empty occupancy for no squares" {
|
||||
var input = [_]u6{};
|
||||
const occupancies = try getAllPotentialOccupiedSquares(&input, std.testing.allocator);
|
||||
defer std.testing.allocator.free(occupancies);
|
||||
|
||||
try std.testing.expectEqual(@as(usize, 1), occupancies.len);
|
||||
try std.testing.expectEqual(@as(Bitboard, 0), occupancies[0]);
|
||||
}
|
||||
|
||||
test "getAllPotentialOccupiedSquares returns every subset for three squares" {
|
||||
var input = [_]u6{ 1, 3, 8 };
|
||||
const occupancies = try getAllPotentialOccupiedSquares(&input, std.testing.allocator);
|
||||
defer std.testing.allocator.free(occupancies);
|
||||
|
||||
try std.testing.expectEqual(@as(usize, 8), occupancies.len);
|
||||
try std.testing.expectEqualSlices(Bitboard, &.{
|
||||
0,
|
||||
bit(1),
|
||||
bit(3),
|
||||
bit(1) | bit(3),
|
||||
bit(8),
|
||||
bit(1) | bit(8),
|
||||
bit(3) | bit(8),
|
||||
bit(1) | bit(3) | bit(8),
|
||||
}, occupancies);
|
||||
}
|
||||
|
||||
test "getAllPotentialOccupiedSquares count is two to the number of squares" {
|
||||
var input = [_]u6{ 1, 2, 3, 4, 5 };
|
||||
const occupancies = try getAllPotentialOccupiedSquares(&input, std.testing.allocator);
|
||||
defer std.testing.allocator.free(occupancies);
|
||||
|
||||
try std.testing.expectEqual(@as(usize, 32), occupancies.len);
|
||||
}
|
||||
|
||||
test "testMagicCandidate accepts a magic that gives unique indexes" {
|
||||
const occupancies = [_]Bitboard{ 0, bit(0), bit(1), bit(0) | bit(1) };
|
||||
const expected_attacks = [_]Bitboard{ 0x11, 0x22, 0x44, 0x88 };
|
||||
var temp_table = [_]Bitboard{0} ** 4;
|
||||
var used = [_]bool{false} ** 4;
|
||||
|
||||
const magic = @as(Bitboard, 1) << 62;
|
||||
try std.testing.expect(testMagicCandidate(
|
||||
magic,
|
||||
62,
|
||||
&occupancies,
|
||||
&expected_attacks,
|
||||
&temp_table,
|
||||
&used,
|
||||
));
|
||||
|
||||
try std.testing.expectEqualSlices(Bitboard, &expected_attacks, &temp_table);
|
||||
try std.testing.expectEqualSlices(bool, &[_]bool{ true, true, true, true }, &used);
|
||||
}
|
||||
|
||||
test "testMagicCandidate rejects harmful collisions" {
|
||||
const occupancies = [_]Bitboard{ 0, bit(0) };
|
||||
const expected_attacks = [_]Bitboard{ 0x11, 0x22 };
|
||||
var temp_table = [_]Bitboard{0} ** 2;
|
||||
var used = [_]bool{false} ** 2;
|
||||
|
||||
try std.testing.expect(!testMagicCandidate(
|
||||
0,
|
||||
63,
|
||||
&occupancies,
|
||||
&expected_attacks,
|
||||
&temp_table,
|
||||
&used,
|
||||
));
|
||||
}
|
||||
|
||||
test "testMagicCandidate allows constructive collisions with identical attacks" {
|
||||
const occupancies = [_]Bitboard{ 0, bit(0), bit(1), bit(0) | bit(1) };
|
||||
const expected_attacks = [_]Bitboard{ 0x55, 0x55, 0x55, 0x55 };
|
||||
var temp_table = [_]Bitboard{0} ** 1;
|
||||
var used = [_]bool{false} ** 1;
|
||||
|
||||
try std.testing.expect(testMagicCandidate(
|
||||
0,
|
||||
63,
|
||||
&occupancies,
|
||||
&expected_attacks,
|
||||
&temp_table,
|
||||
&used,
|
||||
));
|
||||
|
||||
try std.testing.expect(used[0]);
|
||||
try std.testing.expectEqual(@as(Bitboard, 0x55), temp_table[0]);
|
||||
}
|
||||
|
||||
test "writeMagicInfoArray emits Zig constants" {
|
||||
var infos = [_]MagicInfo{.{ .mask = 0, .magic = 0, .shift = 0 }} ** 64;
|
||||
infos[0] = .{ .mask = 0x12, .magic = 0x34, .shift = 56 };
|
||||
|
||||
var allocating_writer = std.Io.Writer.Allocating.init(std.testing.allocator);
|
||||
defer allocating_writer.deinit();
|
||||
|
||||
try writeMagicInfoArray(&allocating_writer.writer, "test_infos", &infos);
|
||||
const output = allocating_writer.writer.buffer[0..allocating_writer.writer.end];
|
||||
|
||||
try std.testing.expect(std.mem.indexOf(u8, output, "pub const test_infos: [64]MagicInfo") != null);
|
||||
try std.testing.expect(std.mem.indexOf(u8, output, ".{ .mask = 0x12, .magic = 0x34, .shift = 56 },") != null);
|
||||
}
|
||||
|
||||
test "writeAttackTable emits nested Zig attack arrays" {
|
||||
var attacks = [_][2]Bitboard{.{ 0, 0 }} ** 64;
|
||||
attacks[0] = .{ 0x11, 0x22 };
|
||||
|
||||
var allocating_writer = std.Io.Writer.Allocating.init(std.testing.allocator);
|
||||
defer allocating_writer.deinit();
|
||||
|
||||
try writeAttackTable(&allocating_writer.writer, "test_attacks", 2, &attacks);
|
||||
const output = allocating_writer.writer.buffer[0..allocating_writer.writer.end];
|
||||
|
||||
try std.testing.expect(std.mem.indexOf(u8, output, "pub const test_attacks: [64][2]Bitboard") != null);
|
||||
try std.testing.expect(std.mem.indexOf(u8, output, "0x11, 0x22,") != null);
|
||||
}
|
||||