Add chess GUI and move generation
@ -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;
|
||||
```
|
||||
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 = 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");
|
||||
496
src/board_input.zig
Normal file
@ -0,0 +1,496 @@
|
||||
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: SquareCoord,
|
||||
to: SquareCoord,
|
||||
};
|
||||
|
||||
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(square.file, square.rank);
|
||||
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 = from,
|
||||
.to = 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(state: InteractionState, mode: InputMode, cursor_square: ?SquareCoord) ?SquareCoord {
|
||||
if (state.selection.selected != null or state.selected_palette_piece != null or state.current_drag != null or mode == .edit) {
|
||||
return cursor_square;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
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(square.file, square.rank);
|
||||
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 = drag.source, .to = 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 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(4, 1, 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 = .{ .file = 4, .rank = 1 }, .to = .{ .file = 4, .rank = 3 } } },
|
||||
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(4, 6, 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(4, 1, 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(4, 1, piece.encode(.white, .pawn));
|
||||
state.setSquare(6, 0, 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));
|
||||
}
|
||||
771
src/chess/board.zig
Normal file
@ -0,0 +1,771 @@
|
||||
const std = @import("std");
|
||||
const piece = @import("piece.zig");
|
||||
const bitboard = @import("bitboard.zig");
|
||||
const magic = @import("magic.zig");
|
||||
|
||||
const king_masks: [64]bitboard.Bitboard = generateKingMasks();
|
||||
const knight_masks: [64]bitboard.Bitboard = generateKnightMasks();
|
||||
const knight_offsets = [_][2]i32{
|
||||
.{ -2, -1 },
|
||||
.{ -2, 1 },
|
||||
.{ -1, -2 },
|
||||
.{ -1, 2 },
|
||||
.{ 1, -2 },
|
||||
.{ 1, 2 },
|
||||
.{ 2, -1 },
|
||||
.{ 2, 1 },
|
||||
};
|
||||
const white_pawn_masks: [64]bitboard.Bitboard = generatePawnMasks(piece.Color.white);
|
||||
const black_pawn_masks: [64]bitboard.Bitboard = generatePawnMasks(piece.Color.black);
|
||||
|
||||
pub const BoardState = struct {
|
||||
board: [8]u32,
|
||||
turn: piece.Color,
|
||||
castle_rights: u4,
|
||||
en_passant: u7,
|
||||
halfmove: u8,
|
||||
fullmove: u32,
|
||||
bitboards: [16]bitboard.Bitboard,
|
||||
|
||||
pub fn empty() BoardState {
|
||||
return .{
|
||||
.board = [_]u32{0} ** 8,
|
||||
.turn = piece.Color.white,
|
||||
.castle_rights = 0,
|
||||
.en_passant = 0,
|
||||
.halfmove = 0,
|
||||
.fullmove = 0,
|
||||
.bitboards = [_]bitboard.Bitboard{0} ** 16,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn getSquare(self: BoardState, file: u3, rank: u3) u4 {
|
||||
const shift: u5 = @as(u5, file) * 4;
|
||||
return @intCast((self.board[rank] >> shift) & 0xF);
|
||||
}
|
||||
|
||||
pub fn setSquare(self: *BoardState, file: u3, rank: u3, value: u4) void {
|
||||
const shift: u5 = @as(u5, file) * 4;
|
||||
const mask: u32 = @as(u32, 0xF) << shift;
|
||||
const existing: u4 = @intCast((self.board[rank] & mask) >> shift);
|
||||
self.board[rank] = (self.board[rank] & ~mask) | (@as(u32, value) << shift);
|
||||
if (value != 0) {
|
||||
self.bitboards[value] |= @as(bitboard.Bitboard, 1) << @intCast(@as(u6, rank) * 8 + @as(u6, file));
|
||||
self.bitboards[existing] &= ~(@as(bitboard.Bitboard, 1) << @intCast(@as(u6, rank) * 8 + @as(u6, file)));
|
||||
} else {
|
||||
self.bitboards[existing] &= ~(@as(bitboard.Bitboard, 1) << @intCast(@as(u6, rank) * 8 + @as(u6, file)));
|
||||
}
|
||||
self.bitboards[7] = self.bitboards[1] | self.bitboards[2] | self.bitboards[3] | self.bitboards[4] | self.bitboards[5] | self.bitboards[6];
|
||||
self.bitboards[15] = self.bitboards[9] | self.bitboards[10] | self.bitboards[11] | self.bitboards[12] | self.bitboards[13] | self.bitboards[14];
|
||||
self.bitboards[0] = self.bitboards[7] | self.bitboards[15];
|
||||
}
|
||||
|
||||
pub fn printBitboards(self: *BoardState) void {
|
||||
for (self.bitboards) |bb| {
|
||||
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(bitboard.Bitboard, 1) << square;
|
||||
|
||||
if ((bb & mask) != 0) {
|
||||
std.debug.print("1 ", .{});
|
||||
} else {
|
||||
std.debug.print(". ", .{});
|
||||
}
|
||||
}
|
||||
std.debug.print("\n", .{});
|
||||
}
|
||||
std.debug.print("\n", .{});
|
||||
}
|
||||
}
|
||||
|
||||
fn swapTurn(self: *BoardState) void {
|
||||
if (self.turn == piece.Color.white) {
|
||||
self.turn = piece.Color.black;
|
||||
} else {
|
||||
self.turn = piece.Color.white;
|
||||
self.fullmove += 1;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn move(self: *BoardState, from_file: u3, from_rank: u3, to_file: u3, to_rank: u3) !void {
|
||||
const p = self.getSquare(from_file, from_rank);
|
||||
self.setSquare(to_file, to_rank, p);
|
||||
self.setSquare(from_file, from_rank, 0);
|
||||
if (piece.typeOf(p) != piece.PieceType.pawn) {
|
||||
self.halfmove += 1;
|
||||
} else {
|
||||
self.halfmove = 0;
|
||||
}
|
||||
self.swapTurn();
|
||||
self.printBitboards();
|
||||
}
|
||||
|
||||
pub fn getValidMoves(self: *BoardState, square: bitboard.Square, valid_moves: *std.ArrayList(bitboard.Square), allocator: std.mem.Allocator) !void {
|
||||
const file: u3 = @intCast(square % 8);
|
||||
const rank: u3 = @intCast(square / 8);
|
||||
const p = self.getSquare(file, rank);
|
||||
switch (piece.typeOf(p)) {
|
||||
piece.PieceType.pawn => try self.getValidPawnMoves(p, square, valid_moves, allocator),
|
||||
piece.PieceType.knight => try self.getValidKnightMoves(p, square, valid_moves, allocator),
|
||||
piece.PieceType.bishop => try self.getValidBishopMoves(p, square, valid_moves, allocator),
|
||||
piece.PieceType.rook => try self.getValidRookMoves(p, square, valid_moves, allocator),
|
||||
piece.PieceType.queen => try self.getValidQueenMoves(p, square, valid_moves, allocator),
|
||||
piece.PieceType.king => try self.getValidKingMoves(p, square, valid_moves, allocator),
|
||||
else => return,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn isSquareOccupied(self: *BoardState, square: bitboard.Square) bool {
|
||||
return (@as(bitboard.Bitboard, 1) << square) & self.bitboards[0] != 0;
|
||||
}
|
||||
|
||||
pub fn isOccupantBlack(self: *BoardState, square: bitboard.Square) bool {
|
||||
return (@as(bitboard.Bitboard, 1) << square) & self.bitboards[7] != 0;
|
||||
}
|
||||
|
||||
pub fn isOccupantWhite(self: *BoardState, square: bitboard.Square) bool {
|
||||
return (@as(bitboard.Bitboard, 1) << square) & self.bitboards[15] != 0;
|
||||
}
|
||||
|
||||
pub fn isOccupantOppositeColor(self: *BoardState, square: bitboard.Square, color: piece.Color) bool {
|
||||
switch (color) {
|
||||
.white => {
|
||||
return (@as(bitboard.Bitboard, 1) << square) & self.bitboards[7] != 0;
|
||||
},
|
||||
.black => {
|
||||
return (@as(bitboard.Bitboard, 1) << square) & self.bitboards[15] != 0;
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn getValidPawnMoves(self: *BoardState, p: u4, square: bitboard.Square, valid_moves: *std.ArrayList(bitboard.Square), allocator: std.mem.Allocator) !void {
|
||||
const file: u3 = @intCast(square % 8);
|
||||
valid_moves.clearRetainingCapacity();
|
||||
const color = piece.colorOf(p) orelse return;
|
||||
switch (color) {
|
||||
.white => {
|
||||
if (square >= 56) return;
|
||||
|
||||
if (!self.isSquareOccupied(square + 8)) {
|
||||
try valid_moves.append(allocator, square + 8);
|
||||
|
||||
if (square >= 8 and square < 16 and !self.isSquareOccupied(square + 16)) {
|
||||
try valid_moves.append(allocator, square + 16);
|
||||
}
|
||||
}
|
||||
|
||||
if (file > 0 and self.isOccupantBlack(square + 7)) {
|
||||
try valid_moves.append(allocator, square + 7);
|
||||
}
|
||||
|
||||
if (file < 7 and self.isOccupantBlack(square + 9)) {
|
||||
try valid_moves.append(allocator, square + 9);
|
||||
}
|
||||
},
|
||||
.black => {
|
||||
if (square <= 7) return;
|
||||
|
||||
if (!self.isSquareOccupied(square - 8)) {
|
||||
try valid_moves.append(allocator, square - 8);
|
||||
|
||||
if (square <= 55 and square >= 48 and !self.isSquareOccupied(square - 16)) {
|
||||
try valid_moves.append(allocator, square - 16);
|
||||
}
|
||||
}
|
||||
|
||||
if (file > 0 and self.isOccupantWhite(square - 9)) {
|
||||
try valid_moves.append(allocator, square - 9);
|
||||
}
|
||||
|
||||
if (file < 7 and self.isOccupantWhite(square - 7)) {
|
||||
try valid_moves.append(allocator, square - 7);
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
pub fn getValidKnightMoves(self: *BoardState, p: u4, square: bitboard.Square, valid_moves: *std.ArrayList(bitboard.Square), allocator: std.mem.Allocator) !void {
|
||||
const file: u3 = @intCast(square % 8);
|
||||
const rank: u3 = @intCast(square / 8);
|
||||
valid_moves.clearRetainingCapacity();
|
||||
const color = piece.colorOf(p) orelse return;
|
||||
if (rank >= 1 and file >= 2 and (!self.isSquareOccupied(square - 10) or self.isOccupantOppositeColor(square - 10, color))) {
|
||||
try valid_moves.append(allocator, square - 10);
|
||||
}
|
||||
if (rank >= 1 and file <= 5 and (!self.isSquareOccupied(square - 6) or self.isOccupantOppositeColor(square - 6, color))) {
|
||||
try valid_moves.append(allocator, square - 6);
|
||||
}
|
||||
if (rank >= 2 and file >= 1 and (!self.isSquareOccupied(square - 17) or self.isOccupantOppositeColor(square - 17, color))) {
|
||||
try valid_moves.append(allocator, square - 17);
|
||||
}
|
||||
if (rank >= 2 and file <= 6 and (!self.isSquareOccupied(square - 15) or self.isOccupantOppositeColor(square - 15, color))) {
|
||||
try valid_moves.append(allocator, square - 15);
|
||||
}
|
||||
if (rank <= 6 and file >= 2 and (!self.isSquareOccupied(square + 6) or self.isOccupantOppositeColor(square + 6, color))) {
|
||||
try valid_moves.append(allocator, square + 6);
|
||||
}
|
||||
if (rank <= 6 and file <= 5 and (!self.isSquareOccupied(square + 10) or self.isOccupantOppositeColor(square + 10, color))) {
|
||||
try valid_moves.append(allocator, square + 10);
|
||||
}
|
||||
if (rank <= 5 and file >= 1 and (!self.isSquareOccupied(square + 15) or self.isOccupantOppositeColor(square + 15, color))) {
|
||||
try valid_moves.append(allocator, square + 15);
|
||||
}
|
||||
if (rank <= 5 and file <= 6 and (!self.isSquareOccupied(square + 17) or self.isOccupantOppositeColor(square + 17, color))) {
|
||||
try valid_moves.append(allocator, square + 17);
|
||||
}
|
||||
}
|
||||
pub fn getValidBishopMoves(self: *BoardState, p: u4, square: bitboard.Square, valid_moves: *std.ArrayList(bitboard.Square), allocator: std.mem.Allocator) !void {
|
||||
valid_moves.clearRetainingCapacity();
|
||||
const color = piece.colorOf(p) orelse return;
|
||||
const friendlies = switch (color) {
|
||||
.white => self.bitboards[15],
|
||||
.black => self.bitboards[7],
|
||||
};
|
||||
const all_occ = self.bitboards[0];
|
||||
|
||||
const attacks = magic.bishopAttacks(square, all_occ);
|
||||
const legal = attacks & ~friendlies;
|
||||
|
||||
try appendMovesFromBitboard(legal, valid_moves, allocator);
|
||||
}
|
||||
pub fn getValidRookMoves(self: *BoardState, p: u4, square: bitboard.Square, valid_moves: *std.ArrayList(bitboard.Square), allocator: std.mem.Allocator) !void {
|
||||
valid_moves.clearRetainingCapacity();
|
||||
const color = piece.colorOf(p) orelse return;
|
||||
const friendlies = switch (color) {
|
||||
.white => self.bitboards[15],
|
||||
.black => self.bitboards[7],
|
||||
};
|
||||
const all_occ = self.bitboards[0];
|
||||
|
||||
const attacks = magic.rookAttacks(square, all_occ);
|
||||
const legal = attacks & ~friendlies;
|
||||
|
||||
try appendMovesFromBitboard(legal, valid_moves, allocator);
|
||||
}
|
||||
pub fn getValidQueenMoves(self: *BoardState, p: u4, square: bitboard.Square, valid_moves: *std.ArrayList(bitboard.Square), allocator: std.mem.Allocator) !void {
|
||||
valid_moves.clearRetainingCapacity();
|
||||
const color = piece.colorOf(p) orelse return;
|
||||
const friendlies = switch (color) {
|
||||
.white => self.bitboards[15],
|
||||
.black => self.bitboards[7],
|
||||
};
|
||||
const all_occ = self.bitboards[0];
|
||||
|
||||
const attacks = magic.queenAttacks(square, all_occ);
|
||||
const legal = attacks & ~friendlies;
|
||||
|
||||
try appendMovesFromBitboard(legal, valid_moves, allocator);
|
||||
}
|
||||
pub fn getValidKingMoves(self: *BoardState, p: u4, square: bitboard.Square, valid_moves: *std.ArrayList(bitboard.Square), allocator: std.mem.Allocator) !void {
|
||||
valid_moves.clearRetainingCapacity();
|
||||
const friendlies = if (piece.colorOf(p) == .white)
|
||||
self.bitboards[15]
|
||||
else
|
||||
self.bitboards[7];
|
||||
|
||||
try appendMovesFromBitboard(king_masks[square] & ~friendlies, valid_moves, allocator);
|
||||
}
|
||||
};
|
||||
|
||||
fn appendMovesFromBitboard(moves: bitboard.Bitboard, valid_moves: *std.ArrayList(bitboard.Square), allocator: std.mem.Allocator) !void {
|
||||
var bb = moves;
|
||||
while (bb != 0) {
|
||||
const to: bitboard.Square = @intCast(@ctz(bb));
|
||||
try valid_moves.append(allocator, to);
|
||||
bb &= bb - 1;
|
||||
}
|
||||
}
|
||||
|
||||
fn squareIndex(file: u8, rank: u8) !u6 {
|
||||
if (file > 7) return error.InvalidSquare;
|
||||
if (rank < 1 or rank > 8) return error.InvalidSquare;
|
||||
return @intCast(((rank - 1) * 8) + file);
|
||||
}
|
||||
|
||||
pub fn parseSquareFromAlgebraic(square: []const u8) !u6 {
|
||||
if (square.len != 2) return error.InvalidSquare;
|
||||
const file_ch = square[0];
|
||||
const rank_ch = square[1];
|
||||
|
||||
if (file_ch < 'a' or file_ch > 'h') return error.InvalidSquare;
|
||||
if (rank_ch < '1' or rank_ch > '8') return error.InvalidSquare;
|
||||
|
||||
return squareIndex(file_ch - 'a', rank_ch - '0');
|
||||
}
|
||||
|
||||
fn generateKingMasks() [64]bitboard.Bitboard {
|
||||
@setEvalBranchQuota(10_000);
|
||||
var masks: [64]bitboard.Bitboard = [_]bitboard.Bitboard{0} ** 64;
|
||||
|
||||
var square_index: usize = 0;
|
||||
while (square_index < 64) : (square_index += 1) {
|
||||
const square: bitboard.Square = @intCast(square_index);
|
||||
masks[square_index] = kingMaskForSquare(square);
|
||||
}
|
||||
|
||||
return masks;
|
||||
}
|
||||
|
||||
fn kingMaskForSquare(square: bitboard.Square) bitboard.Bitboard {
|
||||
const file: i32 = @intCast(square % 8);
|
||||
const rank: i32 = @intCast(square / 8);
|
||||
|
||||
var mask: bitboard.Bitboard = 0;
|
||||
|
||||
var dr: i32 = -1;
|
||||
while (dr <= 1) : (dr += 1) {
|
||||
var df: i32 = -1;
|
||||
while (df <= 1) : (df += 1) {
|
||||
if (dr == 0 and df == 0) continue;
|
||||
|
||||
const target_rank = rank + dr;
|
||||
const target_file = file + df;
|
||||
|
||||
if (target_rank < 0 or target_rank > 7) continue;
|
||||
if (target_file < 0 or target_file > 7) continue;
|
||||
|
||||
mask |= bitboard.bit(@intCast((target_rank * 8) + target_file));
|
||||
}
|
||||
}
|
||||
return mask;
|
||||
}
|
||||
|
||||
fn generateKnightMasks() [64]bitboard.Bitboard {
|
||||
@setEvalBranchQuota(10_000);
|
||||
var masks: [64]bitboard.Bitboard = [_]bitboard.Bitboard{0} ** 64;
|
||||
|
||||
var square_index: usize = 0;
|
||||
while (square_index < 64) : (square_index += 1) {
|
||||
const square: bitboard.Square = @intCast(square_index);
|
||||
masks[square_index] = knightMaskForSquare(square);
|
||||
}
|
||||
|
||||
return masks;
|
||||
}
|
||||
|
||||
fn knightMaskForSquare(square: bitboard.Square) bitboard.Bitboard {
|
||||
const file: i32 = @intCast(square % 8);
|
||||
const rank: i32 = @intCast(square / 8);
|
||||
|
||||
var mask: bitboard.Bitboard = 0;
|
||||
|
||||
for (knight_offsets) |offset| {
|
||||
const dr: i32 = offset[0];
|
||||
const df: i32 = offset[1];
|
||||
|
||||
const target_rank = rank + dr;
|
||||
const target_file = file + df;
|
||||
|
||||
if (target_rank < 0 or target_rank > 7) continue;
|
||||
if (target_file < 0 or target_file > 7) continue;
|
||||
|
||||
mask |= bitboard.bit(@intCast((target_rank * 8) + target_file));
|
||||
}
|
||||
|
||||
return mask;
|
||||
}
|
||||
|
||||
fn generatePawnMasks(color: piece.Color) [64]bitboard.Bitboard {
|
||||
@setEvalBranchQuota(10_000);
|
||||
var masks: [64]bitboard.Bitboard = [_]bitboard.Bitboard{0} ** 64;
|
||||
|
||||
var square_index: usize = 0;
|
||||
while (square_index < 64) : (square_index += 1) {
|
||||
const square: bitboard.Square = @intCast(square_index);
|
||||
masks[square_index] = switch (color) {
|
||||
.white => whitePawnMaskForSquare(square),
|
||||
.black => blackPawnMaskForSquare(square),
|
||||
};
|
||||
}
|
||||
|
||||
return masks;
|
||||
}
|
||||
|
||||
fn whitePawnMaskForSquare(square: bitboard.Square) bitboard.Bitboard {
|
||||
const file: i32 = @intCast(square % 8);
|
||||
const rank: i32 = @intCast(square / 8);
|
||||
|
||||
var mask: bitboard.Bitboard = 0;
|
||||
|
||||
const target_rank = rank + 1;
|
||||
if (target_rank > 7) return 0;
|
||||
var target_file = file + 1;
|
||||
if (target_file < 7) mask |= bitboard.bit(@intCast((target_rank * 8) + target_file));
|
||||
target_file = file - 1;
|
||||
if (target_file > 0) mask |= bitboard.bit(@intCast((target_rank * 8) + target_file));
|
||||
|
||||
return mask;
|
||||
}
|
||||
|
||||
fn blackPawnMaskForSquare(square: bitboard.Square) bitboard.Bitboard {
|
||||
const file: i32 = @intCast(square % 8);
|
||||
const rank: i32 = @intCast(square / 8);
|
||||
|
||||
var mask: bitboard.Bitboard = 0;
|
||||
|
||||
const target_rank = rank - 1;
|
||||
if (target_rank < 0) return 0;
|
||||
var target_file = file + 1;
|
||||
if (target_file < 7) mask |= bitboard.bit(@intCast((target_rank * 8) + target_file));
|
||||
target_file = file - 1;
|
||||
if (target_file > 0) mask |= bitboard.bit(@intCast((target_rank * 8) + target_file));
|
||||
|
||||
return mask;
|
||||
}
|
||||
|
||||
test "setSquare and getSquare store one 4-bit piece per square" {
|
||||
var state = BoardState.empty();
|
||||
|
||||
state.setSquare(0, 0, piece.encode(.white, .rook));
|
||||
state.setSquare(0, 4, piece.encode(.white, .king));
|
||||
state.setSquare(7, 4, piece.encode(.black, .king));
|
||||
state.setSquare(6, 0, piece.encode(.black, .pawn));
|
||||
|
||||
try std.testing.expectEqual(piece.encode(.white, .rook), state.getSquare(0, 0));
|
||||
try std.testing.expectEqual(piece.encode(.white, .king), state.getSquare(0, 4));
|
||||
try std.testing.expectEqual(piece.encode(.black, .king), state.getSquare(7, 4));
|
||||
try std.testing.expectEqual(piece.encode(.black, .pawn), state.getSquare(6, 0));
|
||||
|
||||
try std.testing.expectEqual(@as(u4, 0), state.getSquare(3, 3));
|
||||
}
|
||||
|
||||
test "empty board has empty piece bitboards" {
|
||||
const state = BoardState.empty();
|
||||
|
||||
for (state.bitboards) |bb| {
|
||||
try std.testing.expectEqual(@as(bitboard.Bitboard, 0), bb);
|
||||
}
|
||||
}
|
||||
|
||||
test "setSquare sets matching piece bitboard bit" {
|
||||
var state = BoardState.empty();
|
||||
const white_rook = piece.encode(.white, .rook);
|
||||
const black_pawn = piece.encode(.black, .pawn);
|
||||
|
||||
state.setSquare(0, 0, white_rook);
|
||||
state.setSquare(6, 1, black_pawn);
|
||||
|
||||
try std.testing.expectEqual(bitboard.bit(0), state.bitboards[white_rook]);
|
||||
try std.testing.expectEqual(bitboard.bit(14), state.bitboards[black_pawn]);
|
||||
}
|
||||
|
||||
test "setSquare replacing a piece clears old piece bitboard bit" {
|
||||
var state = BoardState.empty();
|
||||
const white_rook = piece.encode(.white, .rook);
|
||||
const white_queen = piece.encode(.white, .queen);
|
||||
|
||||
state.setSquare(0, 0, white_rook);
|
||||
state.setSquare(0, 0, white_queen);
|
||||
|
||||
try std.testing.expectEqual(@as(bitboard.Bitboard, 0), state.bitboards[white_rook]);
|
||||
try std.testing.expectEqual(bitboard.bit(0), state.bitboards[white_queen]);
|
||||
}
|
||||
|
||||
test "setSquare clearing a square clears piece bitboard bit" {
|
||||
var state = BoardState.empty();
|
||||
const black_king = piece.encode(.black, .king);
|
||||
|
||||
state.setSquare(4, 7, black_king);
|
||||
state.setSquare(4, 7, 0);
|
||||
|
||||
try std.testing.expectEqual(@as(u4, 0), state.getSquare(4, 7));
|
||||
try std.testing.expectEqual(@as(bitboard.Bitboard, 0), state.bitboards[black_king]);
|
||||
}
|
||||
|
||||
fn expectPawnMoves(state: *BoardState, from: bitboard.Square, expected: []const bitboard.Square) !void {
|
||||
var moves: std.ArrayList(bitboard.Square) = .empty;
|
||||
defer moves.deinit(std.testing.allocator);
|
||||
|
||||
try state.getValidMoves(from, &moves, std.testing.allocator);
|
||||
try std.testing.expectEqualSlices(bitboard.Square, expected, moves.items);
|
||||
}
|
||||
|
||||
test "white pawn moves one or two squares from starting rank when clear" {
|
||||
var state = BoardState.empty();
|
||||
state.setSquare(4, 1, piece.encode(.white, .pawn));
|
||||
|
||||
try expectPawnMoves(&state, 12, &.{ 20, 28 });
|
||||
}
|
||||
|
||||
test "white pawn cannot move forward into occupied square but can capture diagonally" {
|
||||
var state = BoardState.empty();
|
||||
state.setSquare(4, 1, piece.encode(.white, .pawn));
|
||||
state.setSquare(4, 2, piece.encode(.black, .knight));
|
||||
state.setSquare(3, 2, piece.encode(.black, .bishop));
|
||||
state.setSquare(5, 2, piece.encode(.black, .rook));
|
||||
|
||||
try expectPawnMoves(&state, 12, &.{ 19, 21 });
|
||||
}
|
||||
|
||||
test "white pawn captures do not wrap around board edges" {
|
||||
var state = BoardState.empty();
|
||||
state.setSquare(0, 1, piece.encode(.white, .pawn));
|
||||
state.setSquare(7, 1, piece.encode(.black, .rook));
|
||||
state.setSquare(1, 2, piece.encode(.black, .knight));
|
||||
|
||||
try expectPawnMoves(&state, 8, &.{ 16, 24, 17 });
|
||||
}
|
||||
|
||||
test "black pawn moves one or two squares from starting rank when clear" {
|
||||
var state = BoardState.empty();
|
||||
state.setSquare(4, 6, piece.encode(.black, .pawn));
|
||||
|
||||
try expectPawnMoves(&state, 52, &.{ 44, 36 });
|
||||
}
|
||||
|
||||
test "black pawn cannot move forward into occupied square but can capture diagonally" {
|
||||
var state = BoardState.empty();
|
||||
state.setSquare(4, 6, piece.encode(.black, .pawn));
|
||||
state.setSquare(4, 5, piece.encode(.white, .knight));
|
||||
state.setSquare(3, 5, piece.encode(.white, .bishop));
|
||||
state.setSquare(5, 5, piece.encode(.white, .rook));
|
||||
|
||||
try expectPawnMoves(&state, 52, &.{ 43, 45 });
|
||||
}
|
||||
|
||||
test "black pawn captures do not wrap around board edges" {
|
||||
var state = BoardState.empty();
|
||||
state.setSquare(7, 6, piece.encode(.black, .pawn));
|
||||
state.setSquare(0, 5, piece.encode(.white, .rook));
|
||||
state.setSquare(6, 5, piece.encode(.white, .knight));
|
||||
|
||||
try expectPawnMoves(&state, 55, &.{ 47, 39, 46 });
|
||||
}
|
||||
|
||||
test "white knight in center can move to all eight target squares" {
|
||||
var state = BoardState.empty();
|
||||
state.setSquare(3, 3, piece.encode(.white, .knight));
|
||||
|
||||
try expectPawnMoves(&state, 27, &.{ 17, 21, 10, 12, 33, 37, 42, 44 });
|
||||
}
|
||||
|
||||
test "white knight in corner only has two target squares" {
|
||||
var state = BoardState.empty();
|
||||
state.setSquare(0, 0, piece.encode(.white, .knight));
|
||||
|
||||
try expectPawnMoves(&state, 0, &.{ 10, 17 });
|
||||
}
|
||||
|
||||
test "white knight can capture enemy pieces but not move onto friendly pieces" {
|
||||
var state = BoardState.empty();
|
||||
state.setSquare(3, 3, piece.encode(.white, .knight));
|
||||
state.setSquare(2, 1, piece.encode(.white, .pawn));
|
||||
state.setSquare(4, 1, piece.encode(.white, .bishop));
|
||||
state.setSquare(1, 2, piece.encode(.black, .pawn));
|
||||
state.setSquare(5, 2, piece.encode(.black, .rook));
|
||||
|
||||
try expectPawnMoves(&state, 27, &.{ 17, 21, 33, 37, 42, 44 });
|
||||
}
|
||||
|
||||
test "black knight can capture white pieces but not move onto black pieces" {
|
||||
var state = BoardState.empty();
|
||||
state.setSquare(3, 3, piece.encode(.black, .knight));
|
||||
state.setSquare(2, 1, piece.encode(.black, .pawn));
|
||||
state.setSquare(4, 1, piece.encode(.black, .bishop));
|
||||
state.setSquare(1, 2, piece.encode(.white, .pawn));
|
||||
state.setSquare(5, 2, piece.encode(.white, .rook));
|
||||
|
||||
try expectPawnMoves(&state, 27, &.{ 17, 21, 33, 37, 42, 44 });
|
||||
}
|
||||
|
||||
test "black knight near edge does not wrap around board" {
|
||||
var state = BoardState.empty();
|
||||
state.setSquare(7, 7, piece.encode(.black, .knight));
|
||||
|
||||
try expectPawnMoves(&state, 63, &.{ 53, 46 });
|
||||
}
|
||||
|
||||
test "king mask for A1 includes three neighboring squares" {
|
||||
try std.testing.expectEqual(bitboard.bit(1) | bitboard.bit(8) | bitboard.bit(9), king_masks[0]);
|
||||
}
|
||||
|
||||
test "king mask for D4 includes all eight neighboring squares" {
|
||||
const expected = bitboard.bit(18) |
|
||||
bitboard.bit(19) |
|
||||
bitboard.bit(20) |
|
||||
bitboard.bit(26) |
|
||||
bitboard.bit(28) |
|
||||
bitboard.bit(34) |
|
||||
bitboard.bit(35) |
|
||||
bitboard.bit(36);
|
||||
|
||||
try std.testing.expectEqual(expected, king_masks[27]);
|
||||
}
|
||||
|
||||
test "king mask for H8 includes three neighboring squares" {
|
||||
try std.testing.expectEqual(bitboard.bit(54) | bitboard.bit(55) | bitboard.bit(62), king_masks[63]);
|
||||
}
|
||||
|
||||
test "knight mask for A1 includes two target squares" {
|
||||
try std.testing.expectEqual(bitboard.bit(10) | bitboard.bit(17), knight_masks[0]);
|
||||
}
|
||||
|
||||
test "knight mask for D4 includes all eight target squares" {
|
||||
const expected = bitboard.bit(10) |
|
||||
bitboard.bit(12) |
|
||||
bitboard.bit(17) |
|
||||
bitboard.bit(21) |
|
||||
bitboard.bit(33) |
|
||||
bitboard.bit(37) |
|
||||
bitboard.bit(42) |
|
||||
bitboard.bit(44);
|
||||
|
||||
try std.testing.expectEqual(expected, knight_masks[27]);
|
||||
}
|
||||
|
||||
test "knight mask for H8 includes two target squares" {
|
||||
try std.testing.expectEqual(bitboard.bit(46) | bitboard.bit(53), knight_masks[63]);
|
||||
}
|
||||
|
||||
test "white pawn attack mask for A2 includes only B3" {
|
||||
try std.testing.expectEqual(bitboard.bit(17), white_pawn_masks[8]);
|
||||
}
|
||||
|
||||
test "white pawn attack mask for D4 includes C5 and E5" {
|
||||
try std.testing.expectEqual(bitboard.bit(34) | bitboard.bit(36), white_pawn_masks[27]);
|
||||
}
|
||||
|
||||
test "white pawn attack mask for H7 includes only G8" {
|
||||
try std.testing.expectEqual(bitboard.bit(62), white_pawn_masks[55]);
|
||||
}
|
||||
|
||||
test "black pawn attack mask for H7 includes only G6" {
|
||||
try std.testing.expectEqual(bitboard.bit(46), black_pawn_masks[55]);
|
||||
}
|
||||
|
||||
test "black pawn attack mask for D5 includes C4 and E4" {
|
||||
try std.testing.expectEqual(bitboard.bit(26) | bitboard.bit(28), black_pawn_masks[35]);
|
||||
}
|
||||
|
||||
test "black pawn attack mask for A2 includes only B1" {
|
||||
try std.testing.expectEqual(bitboard.bit(1), black_pawn_masks[8]);
|
||||
}
|
||||
|
||||
test "white king in center can move to all eight target squares" {
|
||||
var state = BoardState.empty();
|
||||
state.setSquare(3, 3, piece.encode(.white, .king));
|
||||
|
||||
try expectPawnMoves(&state, 27, &.{ 18, 19, 20, 26, 28, 34, 35, 36 });
|
||||
}
|
||||
|
||||
test "white king in corner only has three target squares" {
|
||||
var state = BoardState.empty();
|
||||
state.setSquare(0, 0, piece.encode(.white, .king));
|
||||
|
||||
try expectPawnMoves(&state, 0, &.{ 1, 8, 9 });
|
||||
}
|
||||
|
||||
test "white king can capture enemy pieces but not move onto friendly pieces" {
|
||||
var state = BoardState.empty();
|
||||
state.setSquare(3, 3, piece.encode(.white, .king));
|
||||
state.setSquare(2, 2, piece.encode(.white, .pawn));
|
||||
state.setSquare(3, 2, piece.encode(.white, .bishop));
|
||||
state.setSquare(4, 2, piece.encode(.black, .pawn));
|
||||
state.setSquare(2, 3, piece.encode(.black, .rook));
|
||||
|
||||
try expectPawnMoves(&state, 27, &.{ 20, 26, 28, 34, 35, 36 });
|
||||
}
|
||||
|
||||
test "black king can capture white pieces but not move onto black pieces" {
|
||||
var state = BoardState.empty();
|
||||
state.setSquare(3, 3, piece.encode(.black, .king));
|
||||
state.setSquare(2, 2, piece.encode(.black, .pawn));
|
||||
state.setSquare(3, 2, piece.encode(.black, .bishop));
|
||||
state.setSquare(4, 2, piece.encode(.white, .pawn));
|
||||
state.setSquare(2, 3, piece.encode(.white, .rook));
|
||||
|
||||
try expectPawnMoves(&state, 27, &.{ 20, 26, 28, 34, 35, 36 });
|
||||
}
|
||||
|
||||
test "white rook in center can move along open rank and file" {
|
||||
var state = BoardState.empty();
|
||||
state.setSquare(3, 3, piece.encode(.white, .rook));
|
||||
|
||||
try expectPawnMoves(&state, 27, &.{ 3, 11, 19, 24, 25, 26, 28, 29, 30, 31, 35, 43, 51, 59 });
|
||||
}
|
||||
|
||||
test "white rook stops at blockers captures enemies and excludes friendly squares" {
|
||||
var state = BoardState.empty();
|
||||
state.setSquare(3, 3, piece.encode(.white, .rook));
|
||||
state.setSquare(3, 4, piece.encode(.black, .pawn));
|
||||
state.setSquare(3, 1, piece.encode(.black, .knight));
|
||||
state.setSquare(6, 3, piece.encode(.black, .bishop));
|
||||
state.setSquare(1, 3, piece.encode(.white, .pawn));
|
||||
|
||||
try expectPawnMoves(&state, 27, &.{ 11, 19, 26, 28, 29, 30, 35 });
|
||||
}
|
||||
|
||||
test "black bishop in center can move along open diagonals" {
|
||||
var state = BoardState.empty();
|
||||
state.setSquare(3, 3, piece.encode(.black, .bishop));
|
||||
|
||||
try expectPawnMoves(&state, 27, &.{ 0, 6, 9, 13, 18, 20, 34, 36, 41, 45, 48, 54, 63 });
|
||||
}
|
||||
|
||||
test "black bishop stops at blockers captures enemies and excludes friendly squares" {
|
||||
var state = BoardState.empty();
|
||||
state.setSquare(3, 3, piece.encode(.black, .bishop));
|
||||
state.setSquare(5, 5, piece.encode(.white, .pawn));
|
||||
state.setSquare(1, 1, piece.encode(.white, .knight));
|
||||
state.setSquare(1, 5, piece.encode(.black, .rook));
|
||||
state.setSquare(5, 1, piece.encode(.black, .pawn));
|
||||
|
||||
try expectPawnMoves(&state, 27, &.{ 9, 18, 20, 34, 36, 45 });
|
||||
}
|
||||
|
||||
test "white queen in center combines rook and bishop moves on open board" {
|
||||
var state = BoardState.empty();
|
||||
state.setSquare(3, 3, piece.encode(.white, .queen));
|
||||
|
||||
try expectPawnMoves(&state, 27, &.{ 0, 3, 6, 9, 11, 13, 18, 19, 20, 24, 25, 26, 28, 29, 30, 31, 34, 35, 36, 41, 43, 45, 48, 51, 54, 59, 63 });
|
||||
}
|
||||
|
||||
test "white queen stops at blockers captures enemies and excludes friendly squares" {
|
||||
var state = BoardState.empty();
|
||||
state.setSquare(3, 3, piece.encode(.white, .queen));
|
||||
state.setSquare(3, 4, piece.encode(.black, .pawn));
|
||||
state.setSquare(3, 1, piece.encode(.black, .knight));
|
||||
state.setSquare(6, 3, piece.encode(.black, .bishop));
|
||||
state.setSquare(1, 3, piece.encode(.white, .pawn));
|
||||
state.setSquare(5, 5, piece.encode(.black, .pawn));
|
||||
state.setSquare(1, 1, piece.encode(.black, .knight));
|
||||
state.setSquare(1, 5, piece.encode(.white, .rook));
|
||||
state.setSquare(5, 1, piece.encode(.white, .pawn));
|
||||
|
||||
try expectPawnMoves(&state, 27, &.{ 9, 11, 18, 19, 20, 26, 28, 29, 30, 34, 35, 36, 45 });
|
||||
}
|
||||
|
||||
test "rank packing matches reference layout" {
|
||||
var state = BoardState.empty();
|
||||
|
||||
state.setSquare(0, 0, piece.encode(.white, .rook));
|
||||
state.setSquare(1, 0, piece.encode(.white, .knight));
|
||||
state.setSquare(2, 0, piece.encode(.white, .bishop));
|
||||
state.setSquare(3, 0, piece.encode(.white, .queen));
|
||||
state.setSquare(4, 0, piece.encode(.white, .king));
|
||||
state.setSquare(5, 0, piece.encode(.white, .bishop));
|
||||
state.setSquare(6, 0, piece.encode(.white, .knight));
|
||||
state.setSquare(7, 0, piece.encode(.white, .rook));
|
||||
|
||||
try std.testing.expectEqual(@as(u32, 0xCABEDBAC), state.board[0]);
|
||||
}
|
||||
|
||||
test "parseSquareFromAlgebraic parses board coordinates" {
|
||||
try std.testing.expectEqual(@as(u6, 0), try parseSquareFromAlgebraic("a1"));
|
||||
try std.testing.expectEqual(@as(u6, 7), try parseSquareFromAlgebraic("h1"));
|
||||
try std.testing.expectEqual(@as(u6, 56), try parseSquareFromAlgebraic("a8"));
|
||||
try std.testing.expectEqual(@as(u6, 63), try parseSquareFromAlgebraic("h8"));
|
||||
try std.testing.expectEqual(@as(u6, 20), try parseSquareFromAlgebraic("e3"));
|
||||
try std.testing.expectEqual(@as(u6, 44), try parseSquareFromAlgebraic("e6"));
|
||||
}
|
||||
|
||||
test "parseSquareFromAlgebraic rejects invalid coordinates" {
|
||||
try std.testing.expectError(error.InvalidSquare, parseSquareFromAlgebraic(""));
|
||||
try std.testing.expectError(error.InvalidSquare, parseSquareFromAlgebraic("e"));
|
||||
try std.testing.expectError(error.InvalidSquare, parseSquareFromAlgebraic("e33"));
|
||||
try std.testing.expectError(error.InvalidSquare, parseSquareFromAlgebraic("i3"));
|
||||
try std.testing.expectError(error.InvalidSquare, parseSquareFromAlgebraic("e9"));
|
||||
try std.testing.expectError(error.InvalidSquare, parseSquareFromAlgebraic("E3"));
|
||||
}
|
||||
600
src/chess/fen.zig
Normal file
@ -0,0 +1,600 @@
|
||||
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(@intCast(file), rank, 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 isEnPassantCapturable(state: *board.BoardState, ep_square: u6) bool {
|
||||
const file: u3 = @intCast(ep_square % 8);
|
||||
const rank: u3 = @intCast((ep_square / 8) + 1);
|
||||
|
||||
var pawn_rank: u3 = 0;
|
||||
if (state.turn == piece.Color.white) {
|
||||
if (rank != 6) return false;
|
||||
pawn_rank = 4;
|
||||
} else {
|
||||
if (rank != 3) return false;
|
||||
pawn_rank = 3;
|
||||
}
|
||||
var p: u4 = 0;
|
||||
if (file == 0) {
|
||||
p = state.getSquare(file + 1, pawn_rank);
|
||||
if (piece.typeOf(p) == piece.PieceType.pawn and piece.colorOf(p) == state.turn) {
|
||||
return true;
|
||||
}
|
||||
} else if (file == 7) {
|
||||
p = state.getSquare(file - 1, pawn_rank);
|
||||
if (piece.typeOf(p) == piece.PieceType.pawn and piece.colorOf(p) == state.turn) {
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
p = state.getSquare(file - 1, pawn_rank);
|
||||
if (piece.typeOf(p) == piece.PieceType.pawn and piece.colorOf(p) == state.turn) {
|
||||
return true;
|
||||
}
|
||||
p = state.getSquare(file + 1, pawn_rank);
|
||||
if (piece.typeOf(p) == piece.PieceType.pawn and piece.colorOf(p) == state.turn) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
if (isEnPassantCapturable(state, ep_square)) {
|
||||
state.en_passant = (1 << 6) | @as(u7, ep_square);
|
||||
} else {
|
||||
state.en_passant = 0;
|
||||
}
|
||||
}
|
||||
|
||||
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 = state.getSquare(@intCast(file), @intCast(rank));
|
||||
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);
|
||||
}
|
||||
|
||||
test "parseBoardPlacement parses starting position" {
|
||||
var state = board.BoardState.empty();
|
||||
try parseBoardPlacement(
|
||||
&state,
|
||||
"rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR",
|
||||
);
|
||||
|
||||
try std.testing.expectEqual(@as(u32, 0xCABEDBAC), state.board[0]);
|
||||
try std.testing.expectEqual(@as(u32, 0x99999999), state.board[1]);
|
||||
try std.testing.expectEqual(@as(u32, 0x00000000), state.board[2]);
|
||||
try std.testing.expectEqual(@as(u32, 0x00000000), state.board[3]);
|
||||
try std.testing.expectEqual(@as(u32, 0x00000000), state.board[4]);
|
||||
try std.testing.expectEqual(@as(u32, 0x00000000), state.board[5]);
|
||||
try std.testing.expectEqual(@as(u32, 0x11111111), state.board[6]);
|
||||
try std.testing.expectEqual(@as(u32, 0x42365324), state.board[7]);
|
||||
}
|
||||
|
||||
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 zero when target is not capturable" {
|
||||
var state = board.BoardState.empty();
|
||||
|
||||
state.turn = .black;
|
||||
try parseEnPassant(&state, "e3");
|
||||
try std.testing.expectEqual(@as(u7, 0), state.en_passant);
|
||||
|
||||
state.turn = .white;
|
||||
try parseEnPassant(&state, "e6");
|
||||
try std.testing.expectEqual(@as(u7, 0), state.en_passant);
|
||||
}
|
||||
|
||||
test "parseEnPassant stores rank 6 target capturable by white pawn" {
|
||||
var state = board.BoardState.empty();
|
||||
state.turn = .white;
|
||||
state.setSquare(3, 4, 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(5, 3, 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(1, 4, 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(6, 3, 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 ignores adjacent pawn of wrong color" {
|
||||
var state = board.BoardState.empty();
|
||||
state.turn = .white;
|
||||
state.setSquare(3, 4, piece.encode(.black, .pawn));
|
||||
|
||||
try parseEnPassant(&state, "e6");
|
||||
try std.testing.expectEqual(@as(u7, 0), 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 std.testing.expectEqual(@as(u32, 0xCABEDBAC), state.board[0]);
|
||||
try std.testing.expectEqual(@as(u32, 0x99999999), state.board[1]);
|
||||
try std.testing.expectEqual(@as(u32, 0x00000000), state.board[2]);
|
||||
try std.testing.expectEqual(@as(u32, 0x00000000), state.board[3]);
|
||||
try std.testing.expectEqual(@as(u32, 0x00000000), state.board[4]);
|
||||
try std.testing.expectEqual(@as(u32, 0x00000000), state.board[5]);
|
||||
try std.testing.expectEqual(@as(u32, 0x11111111), state.board[6]);
|
||||
try std.testing.expectEqual(@as(u32, 0x42365324), state.board[7]);
|
||||
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(3, 4));
|
||||
try std.testing.expectEqual(piece.encode(.black, .pawn), state.getSquare(4, 4));
|
||||
}
|
||||
|
||||
test "parseFen stores zero for 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, 0), 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(3, 3));
|
||||
try std.testing.expectEqual(piece.encode(.black, .pawn), state.getSquare(4, 4));
|
||||
}
|
||||
|
||||
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 formats non-capturable en passant as dash" {
|
||||
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 - 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 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(4, 0, piece.encode(.white, .king));
|
||||
state.setSquare(4, 7, piece.encode(.black, .king));
|
||||
state.setSquare(0, 0, piece.encode(.white, .rook));
|
||||
state.setSquare(7, 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);
|
||||
}
|
||||
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(file), @intCast(rank));
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
||||
1128
src/main.zig
309
src/piece_render.zig
Normal file
@ -0,0 +1,309 @@
|
||||
const std = @import("std");
|
||||
|
||||
const board = @import("chess/board.zig");
|
||||
const fen = @import("chess/fen.zig");
|
||||
const geometry = @import("geometry.zig");
|
||||
const piece = @import("chess/piece.zig");
|
||||
|
||||
pub const PieceGroup = enum(u4) {
|
||||
white_pawn,
|
||||
white_knight,
|
||||
white_bishop,
|
||||
white_rook,
|
||||
white_queen,
|
||||
white_king,
|
||||
black_pawn,
|
||||
black_knight,
|
||||
black_bishop,
|
||||
black_rook,
|
||||
black_queen,
|
||||
black_king,
|
||||
};
|
||||
|
||||
pub const PieceGroupCount = 12;
|
||||
|
||||
pub const PaletteEntry = struct {
|
||||
group: PieceGroup,
|
||||
encoded: u4,
|
||||
};
|
||||
|
||||
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 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 {
|
||||
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 => .white_pawn,
|
||||
.knight => .white_knight,
|
||||
.bishop => .white_bishop,
|
||||
.rook => .white_rook,
|
||||
.queen => .white_queen,
|
||||
.king => .white_king,
|
||||
.none => null,
|
||||
},
|
||||
.black => switch (piece_type) {
|
||||
.pawn => .black_pawn,
|
||||
.knight => .black_knight,
|
||||
.bishop => .black_bishop,
|
||||
.rook => .black_rook,
|
||||
.queen => .black_queen,
|
||||
.king => .black_king,
|
||||
.none => null,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
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 {
|
||||
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(file), @intCast(rank));
|
||||
const piece_group = pieceGroupFromEncoded(encoded) orelse 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 = pieceGroupFromEncoded(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 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) |vertex_group| {
|
||||
try std.testing.expectEqual(@as(usize, 6), vertex_group.items.len);
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
430
src/text_render.zig
Normal file
@ -0,0 +1,430 @@
|
||||
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 },
|
||||
|
||||
'-' => .{ 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;
|
||||
}
|
||||
}
|
||||
|
||||
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, .{ 0.45, 0.90, 0.45, 0.38 });
|
||||
}
|
||||
|
||||
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, .{ 0.68, 1.0, 0.68, 0.28 });
|
||||
}
|
||||
|
||||
pub fn appendValidMoveDots(
|
||||
vertices: *std.ArrayList(geometry.Vertex),
|
||||
allocator: std.mem.Allocator,
|
||||
board_rect: geometry.BoardRect,
|
||||
state: chess_board.BoardState,
|
||||
valid_moves: []const bitboard.Square,
|
||||
) !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 = [4]f32{ 0.55, 0.55, 0.55, 0.55 };
|
||||
const segments = 48;
|
||||
|
||||
for (valid_moves) |move_square| {
|
||||
const file: u3 = @intCast(move_square % 8);
|
||||
const rank: u3 = @intCast(move_square / 8);
|
||||
if (state.getSquare(file, rank) != 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 } });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 "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.Square{ 20, 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 = .{
|
||||
const usage: vk.BufferUsageFlags = .{
|
||||
.vertex_buffer_bit = true,
|
||||
},
|
||||
.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,
|
||||
.{
|
||||
const memory_properties: vk.MemoryPropertyFlags = .{
|
||||
.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);
|
||||
}
|
||||