Add chess GUI and move generation

This commit is contained in:
WayfinderAK 2026-05-19 16:05:53 -08:00
parent b36f5b0b84
commit 4131c93f99
No known key found for this signature in database
69 changed files with 80469 additions and 219 deletions

View File

@ -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 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 ## Required research standard
For factual claims about algorithms, chess rules/standards, numerical methods, graphics, Zig behavior, compiler behavior, or performance: For factual claims about algorithms, chess rules/standards, numerical methods, graphics, Zig behavior, compiler behavior, or performance:

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View File

@ -53,6 +53,22 @@ pub fn build(b: *std.Build) void {
const frag_spv = frag_cmd.addOutputFileArg("square.frag.spv"); const frag_spv = frag_cmd.addOutputFileArg("square.frag.spv");
frag_cmd.addFileArg(b.path("shaders/square.frag")); 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", .{ exe.root_module.addAnonymousImport("square_vertex_shader", .{
.root_source_file = vert_spv, .root_source_file = vert_spv,
}); });
@ -61,11 +77,65 @@ pub fn build(b: *std.Build) void {
.root_source_file = frag_spv, .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); b.installArtifact(exe);
const run_cmd = b.addRunArtifact(exe); const run_cmd = b.addRunArtifact(exe);
run_cmd.step.dependOn(b.getInstallStep()); run_cmd.step.dependOn(b.getInstallStep());
if (b.args) |args| {
run_cmd.addArgs(args);
}
const run_step = b.step("run", "Run zig-chess"); const run_step = b.step("run", "Run zig-chess");
run_step.dependOn(&run_cmd.step); 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);
} }

View 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
View 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
View 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;
}

View File

@ -1,7 +1,8 @@
#version 450 #version 450
layout(location = 0) in vec4 frag_color;
layout(location = 0) out vec4 out_color; layout(location = 0) out vec4 out_color;
void main() { void main() {
out_color = vec4(0.1, 0.8, 1.0, 1.0); out_color = frag_color;
} }

View File

@ -1,7 +1,11 @@
#version 450 #version 450
layout(location=0) in vec2 in_position; layout(location = 0) in vec2 in_position;
layout(location = 1) in vec4 in_color;
layout(location = 0) out vec4 frag_color;
void main() { void main() {
gl_Position = vec4(in_position, 0.0, 1.0); gl_Position = vec4(in_position, 0.0, 1.0);
frag_color = in_color;
} }

18
src/assets.zig Normal file
View 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
View 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
View 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
View 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
View 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

File diff suppressed because it is too large Load Diff

25
src/chess/magic.zig Normal file
View 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
View 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));
}

View File

@ -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 { pub const Vertex = extern struct {
position: [2]f32, position: [2]f32,
color: [4]f32,
uv: [2]f32,
}; };
const square_vertices = [_]Vertex{ pub const BoardRect = struct {
.{ .position = .{ -0.5, -0.5 } }, left: f32,
.{ .position = .{ 0.5, -0.5 } }, bottom: f32,
.{ .position = .{ 0.5, 0.5 } }, width: f32,
.{ .position = .{ -0.5, -0.5 } }, height: f32,
.{ .position = .{ 0.5, 0.5 } },
.{ .position = .{ -0.5, 0.5 } },
}; };
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,
};
}

File diff suppressed because it is too large Load Diff

309
src/piece_render.zig Normal file
View 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
View 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);
}

View File

@ -15,6 +15,15 @@ pub const VertexBufferContext = struct {
ldc.vkd.freeMemory(ldc.device, self.memory, null); 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( pub fn initVertexBuffer(
vc: context.VulkanContext, vc: context.VulkanContext,
@ -22,50 +31,24 @@ pub fn initVertexBuffer(
vertices: []const geometry.Vertex, vertices: []const geometry.Vertex,
) !VertexBufferContext { ) !VertexBufferContext {
const buffer_size: vk.DeviceSize = @intCast(@sizeOf(geometry.Vertex) * vertices.len); const buffer_size: vk.DeviceSize = @intCast(@sizeOf(geometry.Vertex) * vertices.len);
const usage: vk.BufferUsageFlags = .{
const buffer_create_info = vk.BufferCreateInfo{ .vertex_buffer_bit = true,
.size = buffer_size, };
.usage = .{ const memory_properties: vk.MemoryPropertyFlags = .{
.vertex_buffer_bit = true, .host_visible_bit = true,
}, .host_coherent_bit = true,
.sharing_mode = .exclusive,
}; };
const buffer = try ldc.vkd.createBuffer(ldc.device, &buffer_create_info, null); const buffer_context = try createBuffer(vc, ldc, buffer_size, usage, memory_properties);
errdefer ldc.vkd.destroyBuffer(ldc.device, buffer, null);
const memory_requirements = ldc.vkd.getBufferMemoryRequirements(
ldc.device,
buffer,
);
const memory_index = try findMemoryType(
vc,
ldc,
memory_requirements.memory_type_bits,
.{
.host_visible_bit = true,
.host_coherent_bit = true,
},
);
const alloc_info = vk.MemoryAllocateInfo{
.allocation_size = memory_requirements.size,
.memory_type_index = memory_index,
};
const memory = try ldc.vkd.allocateMemory(ldc.device, &alloc_info, null);
errdefer ldc.vkd.freeMemory(ldc.device, memory, null);
try ldc.vkd.bindBufferMemory(ldc.device, buffer, memory, 0);
const mapped = try ldc.vkd.mapMemory( const mapped = try ldc.vkd.mapMemory(
ldc.device, ldc.device,
memory, buffer_context.memory,
0, 0,
buffer_size, 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: [*]u8 = @ptrCast(mapped);
const dst_slice = dst[0..buffer_size]; const dst_slice = dst[0..buffer_size];
@ -74,13 +57,13 @@ pub fn initVertexBuffer(
std.mem.copyForwards(u8, dst_slice, src_bytes); std.mem.copyForwards(u8, dst_slice, src_bytes);
return .{ return .{
.buffer = buffer, .buffer = buffer_context.buffer,
.memory = memory, .memory = buffer_context.memory,
.vertex_count = @intCast(vertices.len), .vertex_count = @intCast(vertices.len),
}; };
} }
fn findMemoryType( pub fn findMemoryType(
vc: context.VulkanContext, vc: context.VulkanContext,
ldc: device.LogicalDeviceContext, ldc: device.LogicalDeviceContext,
type_filter: u32, type_filter: u32,
@ -107,3 +90,83 @@ fn findMemoryType(
return error.NoSuitableMemoryType; 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;
}

View File

@ -1,4 +1,5 @@
const std = @import("std"); const std = @import("std");
const log = std.log.scoped(.graphics);
const vk = @import("vulkan"); const vk = @import("vulkan");
const device = @import("device.zig"); const device = @import("device.zig");
@ -7,6 +8,8 @@ const render_pass = @import("render_pass.zig");
const swapchain = @import("swapchain.zig"); const swapchain = @import("swapchain.zig");
const pipeline = @import("pipeline.zig"); const pipeline = @import("pipeline.zig");
const buffer = @import("buffer.zig"); const buffer = @import("buffer.zig");
const descriptors = @import("descriptors.zig");
const piece_render = @import("../piece_render.zig");
pub const CommandContext = struct { pub const CommandContext = struct {
command_pool: vk.CommandPool, command_pool: vk.CommandPool,
@ -19,13 +22,8 @@ pub const CommandContext = struct {
} }
}; };
pub fn initCommandBuffers( pub fn initCommandPoolOnly(
ldc: device.LogicalDeviceContext, 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, allocator: std.mem.Allocator,
) !CommandContext { ) !CommandContext {
const command_pool_create_info = vk.CommandPoolCreateInfo{ const command_pool_create_info = vk.CommandPoolCreateInfo{
@ -41,32 +39,56 @@ pub fn initCommandBuffers(
null, null,
); );
errdefer ldc.vkd.destroyCommandPool(ldc.device, command_pool, 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); errdefer allocator.free(command_buffers);
const command_buffer_allocate_info = vk.CommandBufferAllocateInfo{ return .{
.command_pool = command_pool, .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, .level = .primary,
.command_buffer_count = @intCast(command_buffers.len), .command_buffer_count = @intCast(command_context.command_buffers.len),
}; };
try ldc.vkd.allocateCommandBuffers( try ldc.vkd.allocateCommandBuffers(
ldc.device, ldc.device,
&command_buffer_allocate_info, &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{}; const begin_info = vk.CommandBufferBeginInfo{};
try ldc.vkd.beginCommandBuffer(command_buffer, &begin_info); 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{ const clear_color = vk.ClearValue{
.color = .{ .color = .{
.float_32 = .{ 0.02, 0.02, 0.08, 1.0 }, .float_32 = .{ 0.02, 0.02, 0.08, 1.0 },
@ -93,36 +115,107 @@ pub fn initCommandBuffers(
ldc.vkd.cmdBindPipeline( ldc.vkd.cmdBindPipeline(
command_buffer, command_buffer,
.graphics, .graphics,
pipeline_context.graphics_pipeline, board_pipeline_context.graphics_pipeline,
); );
const vertex_buffers = [_]vk.Buffer{ const board_vertex_buffers = [_]vk.Buffer{board_vertex_buffer_context.buffer};
vertex_buffer_context.buffer, const board_offsets = [_]vk.DeviceSize{0};
};
const offsets = [_]vk.DeviceSize{
0,
};
ldc.vkd.cmdBindVertexBuffers( ldc.vkd.cmdBindVertexBuffers(
command_buffer, command_buffer,
0, 0,
&vertex_buffers, &board_vertex_buffers,
&offsets, &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); ldc.vkd.cmdEndRenderPass(command_buffer);
try ldc.vkd.endCommandBuffer(command_buffer); try ldc.vkd.endCommandBuffer(command_buffer);
} }
std.debug.print("recorded command buffers\n", .{}); log.debug("recorded command buffers", .{});
return .{ return command_context;
.command_pool = command_pool, }
.command_buffers = command_buffers,
.allocator = allocator, 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);
} }

View File

@ -1,4 +1,5 @@
const std = @import("std"); const std = @import("std");
const log = std.log.scoped(.graphics);
const glfw = @import("zglfw"); const glfw = @import("zglfw");
const vk = @import("vulkan"); const vk = @import("vulkan");
@ -16,9 +17,9 @@ pub fn initInstance(name: [:0]const u8) !VulkanContext {
const base = vk.BaseWrapper.load(glfw.getInstanceProcAddress); const base = vk.BaseWrapper.load(glfw.getInstanceProcAddress);
const required_extensions = try glfw.getRequiredInstanceExtensions(); const required_extensions = try glfw.getRequiredInstanceExtensions();
std.debug.print("Required instance extensions:\n", .{}); log.debug("Required instance extensions:", .{});
for (required_extensions) |extension| { for (required_extensions) |extension| {
std.debug.print(" {s}\n", .{extension}); log.debug(" {s}", .{extension});
} }
const app_info = vk.ApplicationInfo{ const app_info = vk.ApplicationInfo{
.p_application_name = name, .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); 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.?); const vki = vk.InstanceWrapper.load(instance, base.dispatch.vkGetInstanceProcAddr.?);

View 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,
};
}

View File

@ -1,4 +1,5 @@
const std = @import("std"); const std = @import("std");
const log = std.log.scoped(.graphics);
const vk = @import("vulkan"); const vk = @import("vulkan");
const context = @import("context.zig"); const context = @import("context.zig");
@ -41,11 +42,11 @@ pub fn initLogicalDevice(
const device = try vc.vki.createDevice(physical_device, &device_create_info, null); const device = try vc.vki.createDevice(physical_device, &device_create_info, null);
errdefer vc.vki.destroyDevice(device, 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 vkd = vk.DeviceWrapper.load(device, vc.vki.dispatch.vkGetDeviceProcAddr.?);
const graphics_queue = vkd.getDeviceQueue(device, graphics_queue_family_index, 0); const graphics_queue = vkd.getDeviceQueue(device, graphics_queue_family_index, 0);
std.debug.print("retrieved graphics queue\n", .{}); log.debug("retrieved graphics queue", .{});
return .{ return .{
.physical_device = physical_device, .physical_device = physical_device,
@ -61,11 +62,11 @@ pub fn debugPhysicalGPUs(
physical_devices: []vk.PhysicalDevice, physical_devices: []vk.PhysicalDevice,
surface: vk.SurfaceKHR, surface: vk.SurfaceKHR,
) !void { ) !void {
std.debug.print("physical devices: {}\n", .{physical_devices.len}); log.debug("physical devices: {}", .{physical_devices.len});
for (physical_devices, 0..) |physical_device, i| { for (physical_devices, 0..) |physical_device, i| {
const props = vc.vki.getPhysicalDeviceProperties(physical_device); 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( const queue_families = try vc.vki.getPhysicalDeviceQueueFamilyPropertiesAlloc(
physical_device, physical_device,
std.heap.page_allocator, std.heap.page_allocator,
@ -83,8 +84,8 @@ pub fn debugPhysicalGPUs(
surface, surface,
); );
std.debug.print( log.debug(
" queue {}: count={}, graphics={}, compute={}, transfer={}, present={}\n", " queue {}: count={}, graphics={}, compute={}, transfer={}, present={}",
.{ .{
queue_index, queue_index,
queue_family.queue_count, queue_family.queue_count,

View File

@ -1,4 +1,5 @@
const std = @import("std"); const std = @import("std");
const log = std.log.scoped(.graphics);
const vk = @import("vulkan"); const vk = @import("vulkan");
const commands = @import("commands.zig"); const commands = @import("commands.zig");
@ -33,7 +34,7 @@ pub fn drawFrame(
const image_index = image_result.image_index; const image_index = image_result.image_index;
const should_recreate_after_present = image_result.result == .suboptimal_khr; 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); try ldc.vkd.resetFences(ldc.device, &wait_fences);
@ -76,7 +77,7 @@ pub fn drawFrame(
error.OutOfDateKHR => return .Recreate, error.OutOfDateKHR => return .Recreate,
else => return err, 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) { if (should_recreate_after_present or present_result == .suboptimal_khr) {
return FrameResult.Recreate; return FrameResult.Recreate;

View File

@ -1,4 +1,5 @@
const std = @import("std"); const std = @import("std");
const log = std.log.scoped(.graphics);
const vk = @import("vulkan"); const vk = @import("vulkan");
const device = @import("device.zig"); const device = @import("device.zig");
@ -53,7 +54,7 @@ pub fn initFramebuffers(
created_framebuffer_count += 1; created_framebuffer_count += 1;
} }
std.debug.print("created framebuffers: {}\n", .{framebuffers.len}); log.debug("created framebuffers: {}", .{framebuffers.len});
return .{ return .{
.framebuffers = framebuffers, .framebuffers = framebuffers,

View File

@ -7,10 +7,14 @@ const geometry = @import("../geometry.zig");
pub const PipelineContext = struct { pub const PipelineContext = struct {
graphics_pipeline: vk.Pipeline, graphics_pipeline: vk.Pipeline,
pipeline_layout: vk.PipelineLayout, pipeline_layout: vk.PipelineLayout,
descriptor_set_layout: vk.DescriptorSetLayout = .null_handle,
pub fn destroy(self: *const PipelineContext, ldc: *const device.LogicalDeviceContext) void { pub fn destroy(self: *const PipelineContext, ldc: *const device.LogicalDeviceContext) void {
ldc.vkd.destroyPipeline(ldc.device, self.graphics_pipeline, null); ldc.vkd.destroyPipeline(ldc.device, self.graphics_pipeline, null);
ldc.vkd.destroyPipelineLayout(ldc.device, self.pipeline_layout, 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); 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 { 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 pipeline_layout_create_info = vk.PipelineLayoutCreateInfo{}; 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( const pipeline_layout = try ldc.vkd.createPipelineLayout(
ldc.device, ldc.device,
@ -70,18 +178,11 @@ pub fn initPipelineContext(ldc: device.LogicalDeviceContext, extent: vk.Extent2D
.input_rate = .vertex, .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{ const vertex_input_info = vk.PipelineVertexInputStateCreateInfo{
.vertex_binding_description_count = 1, .vertex_binding_description_count = 1,
.p_vertex_binding_descriptions = @ptrCast(&binding_description), .p_vertex_binding_descriptions = @ptrCast(&binding_description),
.vertex_attribute_description_count = 1, .vertex_attribute_description_count = @intCast(attribute_descriptions.len),
.p_vertex_attribute_descriptions = @ptrCast(&attribute_description), .p_vertex_attribute_descriptions = attribute_descriptions.ptr,
}; };
const input_assembly = vk.PipelineInputAssemblyStateCreateInfo{ const input_assembly = vk.PipelineInputAssemblyStateCreateInfo{
@ -91,9 +192,9 @@ pub fn initPipelineContext(ldc: device.LogicalDeviceContext, extent: vk.Extent2D
const viewport = vk.Viewport{ const viewport = vk.Viewport{
.x = 0.0, .x = 0.0,
.y = 0.0, .y = @floatFromInt(extent.height),
.width = @floatFromInt(extent.width), .width = @floatFromInt(extent.width),
.height = @floatFromInt(extent.height), .height = -@as(f32, @floatFromInt(extent.height)),
.min_depth = 0.0, .min_depth = 0.0,
.max_depth = 1.0, .max_depth = 1.0,
}; };
@ -115,9 +216,7 @@ pub fn initPipelineContext(ldc: device.LogicalDeviceContext, extent: vk.Extent2D
.rasterizer_discard_enable = .false, .rasterizer_discard_enable = .false,
.polygon_mode = .fill, .polygon_mode = .fill,
.line_width = 1.0, .line_width = 1.0,
.cull_mode = .{ .cull_mode = .{},
.back_bit = true,
},
.front_face = .clockwise, .front_face = .clockwise,
.depth_bias_enable = .false, .depth_bias_enable = .false,
.depth_bias_constant_factor = 0.0, .depth_bias_constant_factor = 0.0,
@ -141,9 +240,9 @@ pub fn initPipelineContext(ldc: device.LogicalDeviceContext, extent: vk.Extent2D
.b_bit = true, .b_bit = true,
.a_bit = true, .a_bit = true,
}, },
.blend_enable = .false, .blend_enable = .true,
.src_color_blend_factor = .one, .src_color_blend_factor = .src_alpha,
.dst_color_blend_factor = .zero, .dst_color_blend_factor = .one_minus_src_alpha,
.color_blend_op = .add, .color_blend_op = .add,
.src_alpha_blend_factor = .one, .src_alpha_blend_factor = .one,
.dst_alpha_blend_factor = .zero, .dst_alpha_blend_factor = .zero,
@ -176,10 +275,7 @@ pub fn initPipelineContext(ldc: device.LogicalDeviceContext, extent: vk.Extent2D
.base_pipeline_index = -1, .base_pipeline_index = -1,
}; };
const pipeline_infos = [_]vk.GraphicsPipelineCreateInfo{ const pipeline_infos = [_]vk.GraphicsPipelineCreateInfo{pipeline_info};
pipeline_info,
};
var pipelines: [1]vk.Pipeline = undefined; var pipelines: [1]vk.Pipeline = undefined;
_ = try ldc.vkd.createGraphicsPipelines( _ = try ldc.vkd.createGraphicsPipelines(
@ -193,5 +289,6 @@ pub fn initPipelineContext(ldc: device.LogicalDeviceContext, extent: vk.Extent2D
return .{ return .{
.graphics_pipeline = pipelines[0], .graphics_pipeline = pipelines[0],
.pipeline_layout = pipeline_layout, .pipeline_layout = pipeline_layout,
.descriptor_set_layout = if (descriptor_set_layouts) |layouts| layouts[0] else .null_handle,
}; };
} }

View File

@ -1,4 +1,5 @@
const std = @import("std"); const std = @import("std");
const log = std.log.scoped(.graphics);
const vk = @import("vulkan"); const vk = @import("vulkan");
const device = @import("device.zig"); 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); 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 }; return .{ .render_pass = render_pass };
} }

View File

@ -1,4 +1,5 @@
const std = @import("std"); const std = @import("std");
const log = std.log.scoped(.graphics);
const glfw = @import("zglfw"); const glfw = @import("zglfw");
const vk = @import("vulkan"); const vk = @import("vulkan");
@ -30,6 +31,7 @@ pub fn initSwapchain(
ldc: device.LogicalDeviceContext, ldc: device.LogicalDeviceContext,
surface: vk.SurfaceKHR, surface: vk.SurfaceKHR,
window: *glfw.Window, window: *glfw.Window,
old_swapchain: vk.SwapchainKHR,
allocator: std.mem.Allocator, allocator: std.mem.Allocator,
) !SwapchainContext { ) !SwapchainContext {
const surface_caps = try vc.vki.getPhysicalDeviceSurfaceCapabilitiesKHR( const surface_caps = try vc.vki.getPhysicalDeviceSurfaceCapabilitiesKHR(
@ -37,16 +39,16 @@ pub fn initSwapchain(
surface, surface,
); );
std.debug.print( log.debug(
"surface current extent: {}x{}\n", "surface current extent: {}x{}",
.{ .{
surface_caps.current_extent.width, surface_caps.current_extent.width,
surface_caps.current_extent.height, surface_caps.current_extent.height,
}, },
); );
std.debug.print( log.debug(
"surface min/max image count: {}/{}\n", "surface min/max image count: {}/{}",
.{ .{
surface_caps.min_image_count, surface_caps.min_image_count,
surface_caps.max_image_count, surface_caps.max_image_count,
@ -100,16 +102,16 @@ pub fn initSwapchain(
chosen_image_count = surface_caps.max_image_count; chosen_image_count = surface_caps.max_image_count;
} }
std.debug.print( log.debug(
"chosen swapchain format={any}, color_space={any}\n", "chosen swapchain format={any}, color_space={any}",
.{ chosen_surface_format.format, chosen_surface_format.color_space }, .{ chosen_surface_format.format, chosen_surface_format.color_space },
); );
std.debug.print("chosen present mode={any}\n", .{chosen_present_mode}); log.debug("chosen present mode={any}", .{chosen_present_mode});
std.debug.print( log.debug(
"chosen extent={}x{}\n", "chosen extent={}x{}",
.{ chosen_extent.width, chosen_extent.height }, .{ 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{ const swapchain_create_info = vk.SwapchainCreateInfoKHR{
.surface = surface, .surface = surface,
@ -128,11 +130,12 @@ pub fn initSwapchain(
}, },
.present_mode = chosen_present_mode, .present_mode = chosen_present_mode,
.clipped = .true, .clipped = .true,
.old_swapchain = old_swapchain,
}; };
const swapchain = try ldc.vkd.createSwapchainKHR(ldc.device, &swapchain_create_info, null); const swapchain = try ldc.vkd.createSwapchainKHR(ldc.device, &swapchain_create_info, null);
errdefer ldc.vkd.destroySwapchainKHR(ldc.device, swapchain, 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( const swapchain_images = try ldc.vkd.getSwapchainImagesAllocKHR(
ldc.device, ldc.device,
@ -140,7 +143,7 @@ pub fn initSwapchain(
allocator, allocator,
); );
errdefer allocator.free(swapchain_images); 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); const swapchain_image_views = try allocator.alloc(vk.ImageView, swapchain_images.len);
errdefer allocator.free(swapchain_image_views); errdefer allocator.free(swapchain_image_views);
@ -182,7 +185,7 @@ pub fn initSwapchain(
created_image_view_count += 1; 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 .{ return .{
.swapchain = swapchain, .swapchain = swapchain,

View File

@ -1,4 +1,5 @@
const std = @import("std"); const std = @import("std");
const log = std.log.scoped(.graphics);
const vk = @import("vulkan"); const vk = @import("vulkan");
const device = @import("device.zig"); const device = @import("device.zig");
@ -44,7 +45,7 @@ pub fn initSyncObjects(ldc: device.LogicalDeviceContext) !SyncContext {
null, null,
); );
std.debug.print("created synchronization objects\n", .{}); log.debug("created synchronization objects", .{});
return .{ return .{
.image_available_semaphore = image_available_semaphore, .image_available_semaphore = image_available_semaphore,

293
src/vulkan/texture.zig Normal file
View 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,
&regions,
);
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;
}

View File

@ -1,4 +1,5 @@
const std = @import("std"); const std = @import("std");
const log = std.log.scoped(.graphics);
const glfw = @import("zglfw"); const glfw = @import("zglfw");
pub fn initWindow(x: c_int, y: c_int, name: [:0]const u8) !*glfw.Window { 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, null,
); );
std.debug.print("GLFW platform: {any}\n", .{glfw.getPlatform()}); log.debug("GLFW platform: {any}", .{glfw.getPlatform()});
std.debug.print("Vulkan supported by GLFW: {}\n", .{glfw.isVulkanSupported()}); log.debug("Vulkan supported by GLFW: {}", .{glfw.isVulkanSupported()});
const size = window.getSize(); const size = window.getSize();
const fb_size = window.getFramebufferSize(); const fb_size = window.getFramebufferSize();
std.debug.print("Window size: {}x{}\n", .{ size[0], size[1] }); log.debug("Window size: {}x{}", .{ size[0], size[1] });
std.debug.print("Framebuffer size: {}x{}\n", .{ fb_size[0], fb_size[1] }); log.debug("Framebuffer size: {}x{}", .{ fb_size[0], fb_size[1] });
std.debug.print("Window visible: {}\n", .{window.getAttribute(.visible)}); log.debug("Window visible: {}", .{window.getAttribute(.visible)});
return window; return window;
} }

835
tools/magic_numbers.zig Normal file
View 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);
}