Compare commits

...

3 Commits

72 changed files with 83179 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 owns the chess implementation. Do **not** modify files under `src/chess/` unless the owner explicitly asks for changes there. For UI/graphics work, adapt the caller-side code instead of changing chess logic or chess tests. If a requested graphics/UI change appears to require a chess implementation change, stop and explain the blocker instead of modifying `src/chess/`.
## Required research standard
For factual claims about algorithms, chess rules/standards, numerical methods, graphics, Zig behavior, compiler behavior, or performance:

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");
frag_cmd.addFileArg(b.path("shaders/square.frag"));
const piece_vert_cmd = b.addSystemCommand(&.{
"glslc",
"--target-env=vulkan1.2",
"-o",
});
const piece_vert_spv = piece_vert_cmd.addOutputFileArg("piece.vert.spv");
piece_vert_cmd.addFileArg(b.path("shaders/piece.vert"));
const piece_frag_cmd = b.addSystemCommand(&.{
"glslc",
"--target-env=vulkan1.2",
"-o",
});
const piece_frag_spv = piece_frag_cmd.addOutputFileArg("piece.frag.spv");
piece_frag_cmd.addFileArg(b.path("shaders/piece.frag"));
exe.root_module.addAnonymousImport("square_vertex_shader", .{
.root_source_file = vert_spv,
});
@ -61,11 +77,65 @@ pub fn build(b: *std.Build) void {
.root_source_file = frag_spv,
});
exe.root_module.addAnonymousImport("piece_vertex_shader", .{
.root_source_file = piece_vert_spv,
});
exe.root_module.addAnonymousImport("piece_fragment_shader", .{
.root_source_file = piece_frag_spv,
});
const piece_assets = [_]struct { name: []const u8, path: []const u8 }{
.{ .name = "white_pawn_rgba", .path = "assets/pieces/raw/Chess_plt45.rgba" },
.{ .name = "white_knight_rgba", .path = "assets/pieces/raw/Chess_nlt45.rgba" },
.{ .name = "white_bishop_rgba", .path = "assets/pieces/raw/Chess_blt45.rgba" },
.{ .name = "white_rook_rgba", .path = "assets/pieces/raw/Chess_rlt45.rgba" },
.{ .name = "white_queen_rgba", .path = "assets/pieces/raw/Chess_qlt45.rgba" },
.{ .name = "white_king_rgba", .path = "assets/pieces/raw/Chess_klt45.rgba" },
.{ .name = "black_pawn_rgba", .path = "assets/pieces/raw/Chess_pdt45.rgba" },
.{ .name = "black_knight_rgba", .path = "assets/pieces/raw/Chess_ndt45.rgba" },
.{ .name = "black_bishop_rgba", .path = "assets/pieces/raw/Chess_bdt45.rgba" },
.{ .name = "black_rook_rgba", .path = "assets/pieces/raw/Chess_rdt45.rgba" },
.{ .name = "black_queen_rgba", .path = "assets/pieces/raw/Chess_qdt45.rgba" },
.{ .name = "black_king_rgba", .path = "assets/pieces/raw/Chess_kdt45.rgba" },
};
for (piece_assets) |asset| {
exe.root_module.addAnonymousImport(asset.name, .{
.root_source_file = b.path(asset.path),
});
}
b.installArtifact(exe);
const run_cmd = b.addRunArtifact(exe);
run_cmd.step.dependOn(b.getInstallStep());
if (b.args) |args| {
run_cmd.addArgs(args);
}
const run_step = b.step("run", "Run zig-chess");
run_step.dependOn(&run_cmd.step);
const magic_numbers_exe = b.addExecutable(.{
.name = "magic-numbers",
.root_module = b.createModule(.{
.root_source_file = b.path("tools/magic_numbers.zig"),
.target = target,
.optimize = optimize,
}),
});
const install_magic_numbers = b.addInstallArtifact(magic_numbers_exe, .{});
const magic_numbers_step = b.step("magic-numbers", "Build the magic bitboard number utility");
magic_numbers_step.dependOn(&install_magic_numbers.step);
const run_magic_numbers_cmd = b.addRunArtifact(magic_numbers_exe);
if (b.args) |args| {
run_magic_numbers_cmd.addArgs(args);
}
const run_magic_numbers_step = b.step("run-magic-numbers", "Run the magic bitboard number utility");
run_magic_numbers_step.dependOn(&run_magic_numbers_cmd.step);
}

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;
```

View File

@ -0,0 +1,448 @@
# UCI Engine Architecture Plan
## Goal
Support two families of executables:
1. **GUI/UI executable**
- Renders and tracks games.
- Lets humans play/edit/replay games.
- Can let people play against bots.
- Can spawn bot/engine executables as subprocesses on demand.
- Speaks UCI to engine subprocesses.
- Can optionally use a user-selected external UCI engine, such as Stockfish, for playback analysis.
- Eventually stores games in a database.
2. **Bot/engine executables**
- No GUI.
- Implement chess engine logic.
- Speak UCI over stdin/stdout.
- Can be launched by the GUI.
- Can also run as long-lived processes connected to external services such as lichess.org through a service adapter.
## Important terminology
In UCI terminology, the **engine** is the side that implements the UCI command loop over stdin/stdout.
The GUI/app is the **UCI controller/client**:
```text
GUI/controller
├── starts engine process
├── writes UCI commands to engine stdin
├── reads UCI responses from engine stdout
└── applies bestmove to the current game
```
The engine executable is the **UCI engine/server-like process**:
```text
Engine
├── reads commands from stdin
├── maintains/searches position
├── writes info/bestmove responses to stdout
└── exits on quit
```
Avoid naming the GUI-side wrapper a "server" in code. Prefer names like:
```text
UciEngineClient
UciController
EngineProcess
```
## Desired long-term process architecture
```text
┌─────────────────────┐
│ zig-chess-gui │
│ │
│ - Vulkan UI │
│ - game tracking │
│ - human input │
│ - playback │
│ - database later │
│ - UCI client │
└──────────┬──────────┘
│ UCI stdin/stdout
┌─────────────────────┐
│ zig-chess-engine-* │
│ │
│ - evaluation │
│ - search │
│ - move selection │
│ - UCI command loop │
└─────────────────────┘
```
Future external-service architecture:
```text
┌─────────────────────┐
│ lichess-adapter │
│ │
│ - lichess API │
│ - account/game I/O │
│ - UCI client │
└──────────┬──────────┘
│ UCI stdin/stdout or socket/process bridge
┌─────────────────────┐
│ zig-chess-engine-* │
└─────────────────────┘
```
Key design principle:
```text
GUI ───────────────┐
├── UCI ── engine executable
Lichess adapter ───┘
```
The engine should not know whether it is being used by the GUI, a test harness, or a lichess adapter.
## Suggested source layout
Possible future layout:
```text
src/
├── chess/ // board/game/FEN/PGN/legal moves
├── main.zig // current GUI entry point, or later src/gui/main.zig
├── uci/
│ ├── protocol.zig // shared UCI parse/format types
│ ├── client.zig // GUI/service side: controls engine process
│ └── server.zig // engine side: stdin/stdout command loop
├── engine/
│ ├── engine.zig // common engine interface
│ ├── eval.zig // static evaluation
│ ├── search.zig // search algorithms
│ └── perft.zig // move-generator validation tooling
└── bots/
├── random.zig // random/legal-move bot executable
├── material.zig // material-eval bot executable
└── search.zig // stronger search bot executable
```
This layout can evolve. The important boundary is that engine executables communicate with outside controllers through UCI.
## Build targets
Eventually `build.zig` should produce multiple executables, for example:
```text
zig-chess-gui
zig-chess-random-bot
zig-chess-material-bot
zig-chess-search-bot
```
The GUI target links rendering/UI code.
The engine targets should avoid linking Vulkan/windowing code.
## Analysis engine policy
The GUI should support an optional external UCI engine for real-time analysis during game playback.
Primary intended use case:
```text
Playback mode
├── user steps through game
├── GUI sends current position to analysis engine
├── analysis engine returns eval/PV/best line
└── GUI displays analysis to the user
```
This analysis engine is separate from project bot engines:
- It is for user-facing analysis only.
- It must not be used by project bot engines as their search/evaluation implementation.
- Project bots should have their own evaluation/search logic.
- The GUI should treat the analysis engine as just another external UCI process.
### Stockfish licensing policy
Stockfish is GPL-3.0 licensed. Because this project is intended to be MIT licensed, do **not** bundle Stockfish directly into the executable or source distribution by default.
Preferred approaches:
1. **User-provided engine path**
- User installs Stockfish or another UCI engine separately.
- GUI stores/configures the path.
- GUI launches it as an external process.
2. **Optional download flow**
- GUI can offer to download Stockfish or guide the user through downloading it.
- The download should be explicit and optional.
- The UI should clearly identify the engine and its license before download/use.
- Keep downloaded engine files outside the MIT-licensed source tree/release bundle unless licensing obligations are intentionally handled.
3. **Generic UCI analysis engine support**
- Do not hard-code Stockfish as the only option.
- Any compatible UCI engine path should work.
### Analysis engine UI/config needs
Eventually the GUI should provide:
- analysis engine executable path
- enable/disable analysis
- analysis depth or movetime
- optional thread/hash settings via UCI options
- current eval display
- principal variation display
- best move display
- start/stop analysis when stepping through playback
Suggested app-side abstraction:
```text
AnalysisEngine
├── engine_process / UciEngineClient
├── executable_path
├── enabled
├── depth or movetime limit
├── latest_score
├── latest_pv
└── latest_bestmove
```
## UCI protocol responsibilities
### GUI/client side
The GUI should be able to:
- Spawn an engine executable.
- Send UCI commands.
- Read engine output asynchronously or incrementally.
- Track engine readiness.
- Track available engine options.
- Send current game position.
- Request a move/search.
- Stop a search.
- Shut down the engine process cleanly.
Common commands sent by GUI:
```text
uci
isready
ucinewgame
position startpos
position startpos moves e2e4 e7e5
position fen <fen fields> moves ...
go depth 5
go movetime 1000
stop
quit
```
Common responses parsed by GUI:
```text
id name <name>
id author <author>
option name <name> type <type> ...
uciok
readyok
info depth 3 score cp 20 nodes 1234 time 10 pv e2e4 e7e5
bestmove e2e4
bestmove e2e4 ponder e7e5
```
### Engine/server side
Each engine executable should:
- Read one line at a time from stdin.
- Parse UCI commands.
- Maintain an internal position.
- Search when given `go`.
- Print `bestmove ...` when done.
- Print `info ...` optionally during search.
- Respond to `stop` promptly.
- Exit on `quit`.
## First milestone: minimal UCI engine executable
Implement an engine executable that supports only:
Input:
```text
uci
isready
quit
```
Output:
```text
id name ZigChess Random Bot
id author WayfinderAK
uciok
readyok
```
No search or move generation required yet.
Purpose:
- Prove executable separation.
- Prove stdin/stdout loop.
- Prove GUI/test harness can launch and communicate with engine.
## Second milestone: GUI-side engine process wrapper
Implement a UI/application-side wrapper that can:
- Start an engine executable by path.
- Send `uci`.
- Wait for `uciok`.
- Send `isready`.
- Wait for `readyok`.
- Send `quit` on shutdown.
Suggested name:
```zig
UciEngineClient
```
or:
```zig
EngineProcess
```
This code belongs on the GUI/controller side, not inside the engine bot.
## Third milestone: position and bestmove
Add UCI support for:
```text
position startpos
position startpos moves e2e4 e7e5
position fen <fen> moves ...
go depth 1
bestmove <move>
```
For the first engine, `go depth 1` can simply pick the first legal move or a random legal move.
Move format is UCI long algebraic coordinate format:
```text
e2e4
e7e8q
e1g1
```
This is different from SAN/PGN notation.
## Fourth milestone: reusable protocol module
Create shared parse/format helpers for UCI text:
```text
src/uci/protocol.zig
```
Potential responsibilities:
- Parse GUI-to-engine commands.
- Parse engine-to-GUI responses.
- Format moves as UCI coordinate moves.
- Parse UCI coordinate moves into from/to/promotion.
- Format `position ...` commands from a `Game` move list.
Keep protocol parsing separate from subprocess management and separate from search/evaluation.
## Fifth milestone: stronger engines
Once the protocol boundary works:
- random legal move engine
- material evaluation engine
- shallow search engine
- alpha-beta search engine
- improved move ordering
- transposition table
- time management
Each stronger engine can be its own executable or selected by options/config.
## Future lichess adapter
Do not put lichess-specific code inside the engine.
Instead, later create a separate adapter/service that:
- talks to lichess.org APIs
- manages authentication/account events
- receives games/moves from lichess
- launches or connects to an engine executable
- sends positions/search commands via UCI
- sends engine moves back to lichess
This keeps engines reusable by:
- GUI
- lichess adapter
- local match runner
- tournament harness
- tests/benchmarks
## Future playback analysis milestone
After the generic UCI client exists, add analysis support to playback mode:
1. Let the user configure an external UCI engine path.
2. Start that engine as an analysis process.
3. Send `uci` / `isready` handshake.
4. When playback cursor changes, send:
```text
position fen <current playback FEN>
go depth <n>
```
or:
```text
position fen <current playback FEN>
go movetime <ms>
```
5. Parse `info` lines for score and PV.
6. Display the latest analysis in the UI.
7. Send `stop` when the user moves to another ply or disables analysis.
8. Send `quit` when closing the app or changing engine path.
Keep this separate from bot-vs-human play. The analysis engine is an advisor for the user, not the implementation of project bots.
## References
Primary UCI reference:
- Stefan Meyer-Kahlen, **Universal Chess Interface (UCI)** protocol, April 2006. Commonly distributed as `uci.txt` and mirrored by chess-programming resources. Covers `uci`, `isready`, `position`, `go`, `stop`, `quit`, `info`, `bestmove`, and engine options.
Stockfish licensing/reference:
- Official Stockfish repository: https://github.com/official-stockfish/Stockfish
- Stockfish `Copying.txt`: GNU General Public License v3.0, https://github.com/official-stockfish/Stockfish/blob/master/Copying.txt
- Because the project goal is MIT licensing, Stockfish should be user-provided or optionally downloaded rather than bundled by default.
Useful related notation distinction:
- UCI moves are coordinate moves such as `e2e4` or `e7e8q`.
- PGN/SAN moves are display/game-record notation such as `e4`, `Nf3`, `O-O`, `Qxf7#`.
- The GUI move list should display PGN/SAN.
- Engine communication should use UCI coordinate moves.

View File

@ -0,0 +1,382 @@
# UI Move List and Playback Mode Plan
## Goal
Add UI support for:
- A fixed-size move list panel on the left side of the board.
- Two move columns: White move and Black move, with turn number on the left.
- Standard PGN/SAN notation display.
- Scroll support when the move list is longer than the visible panel.
- A move-list button that copies the current game as PGN to the clipboard.
- A playback-only vertical analysis bar between the board and move list.
- A board-orientation control that can render from White's perspective or Black's perspective.
- A new playback mode where pieces cannot be moved, and the user can step through a loaded game.
- In edit mode, a UI control to choose whose move is next.
- Mode-specific paste behavior:
- Edit mode: `Ctrl+V` pastes FEN.
- Playback mode: `Ctrl+V` pastes PGN.
- Play mode: no FEN/PGN paste.
## Collaboration rule
The assistant should manage only UI/application-side changes for this feature.
Do **not** edit files under `src/chess/` unless explicitly requested.
When UI work needs chess-layer functionality, stop and describe the required chess API/data shape so the project owner can implement it.
## Current state
- `BoardState` owns the current chess position.
- `src/chess/game.zig` has a basic `Game` framework with:
- `initial_state`
- `state`
- `moves`
- `time_control`
- `clocks`
- `status`
- UI currently uses `current_state` directly in `src/main.zig`.
- Existing modes are:
- `play`
- `edit`
- Existing paste behavior parses clipboard as FEN regardless of mode.
- FEN copy is available via `Ctrl+C`.
- Desired copy behavior: `Ctrl+C` should continue to copy FEN in every mode. PGN copy should use an explicit move-list button, not replace `Ctrl+C`.
## Desired architecture
Eventually, UI should treat `Game` as the game/session model:
```text
UI/App
├── current mode: play | edit | playback
├── active Game
├── playback cursor / ply index
├── move list scroll offset
├── board orientation: white perspective | black perspective
├── analysis display state for playback mode
└── rendering state
```
The chess layer should provide enough data for the UI to display moves and reconstruct positions, but the UI should not implement chess notation rules itself.
## Stage 0: Board orientation and edit-side-to-move controls
### UI work
- Add app/UI state for board orientation:
```text
white perspective: current rendering
black perspective: visually flipped board, with White pieces/ranks at the top and Black pieces/ranks at the bottom
```
- This must be a purely visual transform.
- Keep the same chess square numbering and logical coordinates:
- `a1` is still square `0`
- White still starts on ranks 1 and 2 logically
- Black still starts on ranks 7 and 8 logically
- Update all board visual mappings consistently:
- square rendering
- piece rendering
- coordinate labels
- mouse hit-testing
- hover/selection/check/checkmate highlights
- valid/legal move dots
- dragging source/target display
- promotion popup placement
- Add a UI control to toggle orientation, probably near the mode buttons or move list panel.
- In edit mode, add a control to choose whose move is next:
- White to move
- Black to move
- Changing side-to-move in edit mode should update only the current board state's active color/turn field and then rebuild rendering.
- In non-edit modes, the side-to-move selector should either be hidden or disabled.
### Chess-layer needs
No chess-rule changes are required for board orientation. It is visual only.
For edit-side-to-move, the UI needs mutable access to the current position's active color. If `BoardState.turn` remains public, no new chess API is required. If you later make it private, add a small setter such as:
```zig
pub fn setTurn(self: *BoardState, color: piece.Color) void
```
## Stage 1: Add playback mode UI shell
### UI work
- Add `playback` to the app/input mode enum.
- Add a third mode button, likely labeled `VIEW` or `PGN`/`REPLAY`.
- In playback mode:
- ignore board clicks/drags for moving pieces
- no palette editing
- no legal-move highlight generation
- Keep reset behavior available or decide whether reset resets the active game/playback.
### Chess-layer needs
None.
## Stage 2: Make paste mode-specific
### UI work
Change clipboard paste behavior:
- If mode is `.edit`:
- parse clipboard as FEN
- replace current board state/game position
- If mode is `.playback`:
- send clipboard text to a future PGN import function
- for now, log that PGN paste is not yet implemented if chess API is missing
- If mode is `.play`:
- ignore paste or log debug message
### Chess-layer needs
Eventually needed:
```zig
pub fn loadPgn(allocator: std.mem.Allocator, pgn_text: []const u8) !Game
```
or equivalent parse API.
For stage 2, this can be stubbed from the UI side with no chess changes.
## Stage 3: Move list panel with placeholder/static data
### UI work
- Define a fixed rectangle left of the board.
- Render panel background and border.
- Render column headers, for example:
```text
# White Black
```
- Add a visible PGN copy button inside or attached to the move list panel.
- Clicking it should copy the current game PGN to the clipboard once chess-layer PGN export exists.
- Until PGN export exists, the button can be visually present but log that export is not implemented.
- Render rows using temporary/static strings or currently available move placeholders.
- Establish layout constants:
- panel left/right/top/bottom
- row height
- turn-number column x
- white column x
- black column x
- max visible rows
### Chess-layer needs
None for placeholder rendering.
## Stage 4: Record/display move notation from played games
### UI work
- Replace direct `current_state.move(...)` calls with `game.makeMove(...)` once `Game` is wired into `main.zig`.
- Read `game.moves` and draw move rows.
- Update the move list after every played move.
### Chess-layer needs
The `Game` move history should expose display notation for each ply.
Suggested shape:
```zig
pub const MoveRecord = struct {
from: bitboard.Square,
to: bitboard.Square,
promotion: ?piece.PieceType,
san: []const u8, // or fixed buffer / owned slice
time_remaining: u32,
};
```
If avoiding heap allocation in each move record, possible alternatives:
```zig
san: [16]u8,
san_len: u8,
```
The chess layer should generate SAN before mutating the board, because SAN depends on the pre-move position.
Needed API could be:
```zig
pub fn makeMove(self: *Game, allocator: std.mem.Allocator, from: Square, to: Square, promotion: ?PieceType) !void
```
or:
```zig
pub fn formatSanMove(allocator: std.mem.Allocator, state_before: BoardState, from: Square, to: Square, promotion: ?PieceType) ![]u8
```
## Stage 5: Playback analysis bar
### UI work
- Add a vertical analysis bar between the board and the move list panel.
- The analysis bar should be visible only in playback mode.
- The bar is divided into a White section and a Black section.
- When analysis is equal, the White/Black divide should be centered.
- When White is winning, the White portion grows and the divide moves toward Black's side.
- When Black is winning, the Black portion grows and the divide moves toward White's side.
- The bar is purely display; it should not affect board state or move legality.
- Initial placeholder behavior can render the bar at 50/50 until analysis data exists.
- Later, map engine evaluation scores to a display fraction.
Suggested visual mapping:
```text
score = 0.00 pawns -> 50% White / 50% Black
score > 0, White better -> White section larger
score < 0, Black better -> Black section larger
```
Use a bounded/nonlinear mapping so very large evaluations do not immediately consume the whole bar. For example, later UI code could map centipawns to a normalized value with a clamp or sigmoid-like curve. Exact mapping can be tuned visually.
### Chess/engine-layer needs
No chess-layer changes are needed for placeholder rendering.
Eventually, playback analysis needs external UCI analysis data:
```text
score cp <centipawns>
score mate <moves>
pv <principal variation moves>
```
The UI only needs a normalized analysis value or raw score from the UCI analysis client.
## Stage 6: Scrolling move list
### UI work
- Track `move_list_scroll_row` or similar state in `main.zig`.
- Add mouse wheel handling when cursor is over the move panel.
- Clamp scroll offset between `0` and `max(0, total_rows - visible_rows)`.
- Optional later: visual scrollbar.
### Chess-layer needs
None.
## Stage 7: Playback cursor and stepping
### UI work
- Add `playback_ply_index` app state.
- In playback mode:
- Right arrow: advance one ply
- Left arrow: go back one ply
- Home: first position
- End: final position
- Render the board for the current playback ply, not necessarily the final game state.
- Highlight or mark the current row/ply in the move list.
### Chess-layer needs
Need a way to reconstruct board state at an arbitrary ply.
Possible API:
```zig
pub fn stateAtPly(self: *const Game, ply_index: usize) board.BoardState
```
or, if allocation/errors are involved:
```zig
pub fn replayToPly(self: *const Game, ply_index: usize) !board.BoardState
```
This can replay from `initial_state` through the first `ply_index` moves.
## Stage 8: PGN paste/import and PGN copy/export
### UI work
- In playback mode, `Ctrl+V` gets clipboard text and passes it to chess PGN parser.
- Keep `Ctrl+C` as FEN copy in every mode.
- Add click handling for the move-list PGN copy button.
- When the PGN copy button is clicked, copy exported PGN to the clipboard.
- On success:
- replace active game with parsed game
- set mode to playback or remain playback
- set playback cursor to final ply or start; choose behavior intentionally
- reset move-list scroll
- On failure:
- log a warning
- keep current game unchanged
### Chess-layer needs
PGN parser that returns a `Game` with move history and final state, plus PGN export for the current game.
Suggested API:
```zig
pub fn initFromPgn(allocator: std.mem.Allocator, pgn_text: []const u8, time_control: TimeControl) !Game
```
or:
```zig
pub fn parsePgn(allocator: std.mem.Allocator, pgn_text: []const u8) !Game
```
Suggested export API:
```zig
pub fn formatPgn(allocator: std.mem.Allocator, game: Game) ![]u8
```
or as a method:
```zig
pub fn formatPgn(self: *const Game, allocator: std.mem.Allocator) ![]u8
```
Parser should understand at least:
- move numbers: `1.`, `1...`
- SAN moves
- castling: `O-O`, `O-O-O`
- captures: `x`
- promotion: `=Q`, `=R`, `=B`, `=N`
- check/checkmate suffixes: `+`, `#`
- game termination markers: `1-0`, `0-1`, `1/2-1/2`, `*`
- comments and tags can be added later if desired
Reference: Steven J. Edwards, *Portable Game Notation Specification and Implementation Guide*, especially movetext/SAN sections.
## Stage 9: Optional polish
- Auto-scroll move list to latest move in play mode.
- Click a move row in playback mode to jump to that ply.
- Show current move highlight.
- Add scrollbar.
- Show numeric eval next to the analysis bar.
- Show best line/PV next to or below the move list.
- Add richer PGN copy/export options, such as including/excluding tag pairs.
- Display game result in the move panel.
- Show time remaining per move later if clocks are implemented.
## Immediate next action
Start with Stage 0, then Stage 1 and Stage 2 in UI-only files:
- `src/board_input.zig`
- `src/text_render.zig`
- `src/main.zig`
Do not modify `src/chess/` for these stages.

11
shaders/piece.frag Normal file
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
layout(location = 0) in vec4 frag_color;
layout(location = 0) out vec4 out_color;
void main() {
out_color = vec4(0.1, 0.8, 1.0, 1.0);
out_color = frag_color;
}

View File

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

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

516
src/board_input.zig Normal file
View File

@ -0,0 +1,516 @@
const std = @import("std");
const chess_board = @import("chess/board.zig");
const bitboard = @import("chess/bitboard.zig");
const geometry = @import("geometry.zig");
const piece = @import("chess/piece.zig");
const piece_render = @import("piece_render.zig");
pub const SquareCoord = struct {
file: u3,
rank: u3,
};
pub const MoveRequest = struct {
from: bitboard.Square,
to: bitboard.Square,
};
pub const ClickResult = union(enum) {
none,
selected: SquareCoord,
cleared,
move_requested: MoveRequest,
};
pub const InputMode = enum {
play,
edit,
};
pub const ModeButton = enum {
play,
edit,
reset,
};
pub const PendingDragState = struct {
source: SquareCoord,
encoded: u4,
start_ndc: [2]f32,
previous_selection: ?SquareCoord,
};
pub const DragState = struct {
source: SquareCoord,
encoded: u4,
cursor_ndc: [2]f32,
previous_selection: ?SquareCoord,
};
pub const InteractionState = struct {
selection: SelectionState = .{},
selected_palette_piece: ?u4 = null,
pending_drag: ?PendingDragState = null,
current_drag: ?DragState = null,
};
pub const PressResult = union(enum) {
none,
mode_changed: InputMode,
reset_requested,
palette_changed,
edit_place: struct { square: SquareCoord, encoded: u4 },
edit_clear: SquareCoord,
play_selected: bitboard.Square,
play_cleared,
play_move_requested: MoveRequest,
};
pub const DragUpdateResult = union(enum) {
none,
drag_started: bitboard.Square,
drag_moved,
};
pub const ReleaseResult = union(enum) {
none,
click_without_drag,
drag_cancelled: ?bitboard.Square,
drag_selected: bitboard.Square,
drag_cleared,
drag_move_requested: MoveRequest,
};
fn squareCoordEql(a: SquareCoord, b: SquareCoord) bool {
return a.file == b.file and a.rank == b.rank;
}
pub fn coordToSquare(coord: SquareCoord) bitboard.Square {
return @intCast((@as(u6, coord.rank) * 8) + @as(u6, coord.file));
}
pub fn squareToCoord(square: bitboard.Square) SquareCoord {
return .{
.file = @intCast(square % 8),
.rank = @intCast(square / 8),
};
}
pub const SelectionState = struct {
selected: ?SquareCoord = null,
pub fn handleClick(
self: *SelectionState,
state: chess_board.BoardState,
maybe_square: ?SquareCoord,
) ClickResult {
const square = maybe_square orelse {
self.selected = null;
return .cleared;
};
const clicked_piece = state.getSquare(coordToSquare(square));
const clicked_color = piece.colorOf(clicked_piece);
if (self.selected) |from| {
if (squareCoordEql(from, square)) {
self.selected = null;
return .cleared;
}
if (clicked_color == state.turn) {
self.selected = square;
return .{ .selected = square };
}
self.selected = null;
return .{ .move_requested = .{
.from = coordToSquare(from),
.to = coordToSquare(square),
} };
}
if (clicked_color != state.turn) {
return .none;
}
self.selected = square;
return .{ .selected = square };
}
};
fn ndcNearlyEqual(a: [2]f32, b: [2]f32) bool {
const epsilon: f32 = 0.002;
return @abs(a[0] - b[0]) < epsilon and @abs(a[1] - b[1]) < epsilon;
}
fn hasDraggedFarEnough(start: [2]f32, current: [2]f32) bool {
const threshold: f32 = 0.025;
const dx = current[0] - start[0];
const dy = current[1] - start[1];
return (dx * dx + dy * dy) >= threshold * threshold;
}
pub fn hoveredSquareForState(_: InteractionState, _: InputMode, cursor_square: ?SquareCoord) ?SquareCoord {
return cursor_square;
}
pub fn handleDragUpdate(state: *InteractionState, cursor_ndc: ?[2]f32) DragUpdateResult {
const ndc = cursor_ndc orelse return .none;
if (state.pending_drag) |pending| {
if (hasDraggedFarEnough(pending.start_ndc, ndc)) {
state.current_drag = .{
.source = pending.source,
.encoded = pending.encoded,
.cursor_ndc = ndc,
.previous_selection = pending.previous_selection,
};
state.pending_drag = null;
state.selection.selected = null;
return .{ .drag_started = coordToSquare(state.current_drag.?.source) };
}
}
if (state.current_drag) |*drag| {
if (!ndcNearlyEqual(drag.cursor_ndc, ndc)) {
drag.cursor_ndc = ndc;
return .drag_moved;
}
}
return .none;
}
pub fn handleMousePress(
input: *InteractionState,
board_state: chess_board.BoardState,
mode: InputMode,
mode_button: ?ModeButton,
palette_piece: ?u4,
cursor_square: ?SquareCoord,
cursor_ndc: ?[2]f32,
) PressResult {
if (mode_button) |button| {
if (button == .reset) return .reset_requested;
const new_mode: InputMode = switch (button) {
.play => .play,
.edit => .edit,
.reset => unreachable,
};
if (new_mode != mode) {
input.selected_palette_piece = null;
input.selection.selected = null;
input.pending_drag = null;
input.current_drag = null;
return .{ .mode_changed = new_mode };
}
return .none;
}
if (mode == .edit) {
if (palette_piece) |encoded_piece| {
input.selected_palette_piece = if (input.selected_palette_piece != null and input.selected_palette_piece.? == encoded_piece) null else encoded_piece;
input.selection.selected = null;
input.pending_drag = null;
input.current_drag = null;
return .palette_changed;
}
const square = cursor_square orelse return .none;
if (input.selected_palette_piece) |encoded_piece| {
return .{ .edit_place = .{ .square = square, .encoded = encoded_piece } };
}
return .{ .edit_clear = square };
}
const square = cursor_square orelse return .none;
const ndc = cursor_ndc orelse return .none;
const encoded = board_state.getSquare(coordToSquare(square));
const previous_selection = input.selection.selected;
if (piece.colorOf(encoded) == board_state.turn) {
input.pending_drag = .{
.source = square,
.encoded = encoded,
.start_ndc = ndc,
.previous_selection = previous_selection,
};
}
return switch (input.selection.handleClick(board_state, cursor_square)) {
.none => .none,
.cleared => .play_cleared,
.selected => |selected| .{ .play_selected = coordToSquare(selected) },
.move_requested => |move| .{ .play_move_requested = move },
};
}
pub fn handleMouseRelease(input: *InteractionState, cursor_square: ?SquareCoord) ReleaseResult {
if (input.pending_drag != null) {
input.pending_drag = null;
return .click_without_drag;
}
const drag = input.current_drag orelse return .none;
input.current_drag = null;
if (cursor_square) |target| {
if (squareCoordEql(drag.source, target)) {
if (drag.previous_selection) |previous| {
if (squareCoordEql(previous, drag.source)) {
input.selection.selected = null;
return .drag_cleared;
}
}
input.selection.selected = drag.source;
return .{ .drag_selected = coordToSquare(drag.source) };
}
input.selection.selected = null;
return .{ .drag_move_requested = .{ .from = coordToSquare(drag.source), .to = coordToSquare(target) } };
}
input.selection.selected = drag.previous_selection;
return .{ .drag_cancelled = if (drag.previous_selection) |selected| coordToSquare(selected) else null };
}
pub fn screenToNdc(
mouse_x: f64,
mouse_y: f64,
window_width: u32,
window_height: u32,
) ?[2]f32 {
if (window_width == 0 or window_height == 0) return null;
const w: f64 = @floatFromInt(window_width);
const h: f64 = @floatFromInt(window_height);
return .{
@floatCast((mouse_x / w) * 2.0 - 1.0),
@floatCast(1.0 - (mouse_y / h) * 2.0),
};
}
pub fn screenToBoardSquare(
mouse_x: f64,
mouse_y: f64,
window_width: u32,
window_height: u32,
board_rect: geometry.BoardRect,
) ?SquareCoord {
const ndc = screenToNdc(mouse_x, mouse_y, window_width, window_height) orelse return null;
const ndc_x = ndc[0];
const ndc_y = ndc[1];
const right = board_rect.left + board_rect.width;
const top = board_rect.bottom + board_rect.height;
if (ndc_x < board_rect.left or ndc_x >= right) return null;
if (ndc_y < board_rect.bottom or ndc_y >= top) return null;
const local_x = (ndc_x - board_rect.left) / board_rect.width;
const local_y = (ndc_y - board_rect.bottom) / board_rect.height;
const file_float = local_x * 8.0;
const rank_float = local_y * 8.0;
const file_int: u32 = @intFromFloat(@floor(file_float));
const rank_int: u32 = @intFromFloat(@floor(rank_float));
if (file_int >= 8 or rank_int >= 8) return null;
return .{
.file = @intCast(file_int),
.rank = @intCast(rank_int),
};
}
pub fn screenToPromotionPiece(
mouse_x: f64,
mouse_y: f64,
window_width: u32,
window_height: u32,
board_rect: geometry.BoardRect,
square: bitboard.Square,
) ?piece.PieceType {
const ndc = screenToNdc(mouse_x, mouse_y, window_width, window_height) orelse return null;
const popup_rect = piece_render.promotionPopupRectForSquare(board_rect, square);
const right = popup_rect.left + popup_rect.width;
const top = popup_rect.bottom + popup_rect.height;
if (ndc[0] < popup_rect.left or ndc[0] >= right) return null;
if (ndc[1] < popup_rect.bottom or ndc[1] >= top) return null;
const local_x = (ndc[0] - popup_rect.left) / popup_rect.width;
const index_float = local_x * @as(f32, @floatFromInt(piece_render.promotion_piece_types.len));
const index: usize = @intFromFloat(@floor(index_float));
if (index >= piece_render.promotion_piece_types.len) return null;
return piece_render.promotion_piece_types[index];
}
pub fn screenToModeButton(
mouse_x: f64,
mouse_y: f64,
window_width: u32,
window_height: u32,
) ?ModeButton {
const ndc = screenToNdc(mouse_x, mouse_y, window_width, window_height) orelse return null;
const x = ndc[0];
const y = ndc[1];
const button_w: f32 = 0.16;
const button_h: f32 = 0.08;
const reset_button_w: f32 = 0.22;
const gap: f32 = 0.025;
const top: f32 = 0.96;
const bottom = top - button_h;
const play_left: f32 = -0.95;
const edit_left = play_left + button_w + gap;
const reset_left = edit_left + button_w + gap;
if (y < bottom or y >= top) return null;
if (x >= play_left and x < play_left + button_w) return .play;
if (x >= edit_left and x < edit_left + button_w) return .edit;
if (x >= reset_left and x < reset_left + reset_button_w) return .reset;
return null;
}
pub fn screenToPalettePiece(
mouse_x: f64,
mouse_y: f64,
window_width: u32,
window_height: u32,
board_rect: geometry.BoardRect,
) ?u4 {
const ndc = screenToNdc(mouse_x, mouse_y, window_width, window_height) orelse return null;
const ndc_x = ndc[0];
const ndc_y = ndc[1];
const palette_rect = piece_render.paletteRectForBoard(board_rect);
const right = palette_rect.left + palette_rect.width;
const top = palette_rect.bottom + palette_rect.height;
if (ndc_x < palette_rect.left or ndc_x >= right) return null;
if (ndc_y < palette_rect.bottom or ndc_y >= top) return null;
const local_y = (top - ndc_y) / palette_rect.height;
const index_float = local_y * @as(f32, @floatFromInt(piece_render.palette_entries.len));
const index: usize = @intFromFloat(@floor(index_float));
if (index >= piece_render.palette_entries.len) return null;
return piece_render.palette_entries[index].encoded;
}
fn screenPointForBoardCoord(
board_rect: geometry.BoardRect,
window_width: u32,
window_height: u32,
board_x: f32,
board_y: f32,
) [2]f64 {
const ndc = geometry.boardToNdc(board_rect, board_x, board_y);
const w: f64 = @floatFromInt(window_width);
const h: f64 = @floatFromInt(window_height);
return .{
((@as(f64, ndc[0]) + 1.0) / 2.0) * w,
((1.0 - @as(f64, ndc[1])) / 2.0) * h,
};
}
test "screenToBoardSquare maps board corners and center squares" {
const board_rect = geometry.boardRectForExtent(800, 600);
const a1 = screenPointForBoardCoord(board_rect, 800, 600, 0.5, 0.5);
try std.testing.expectEqual(SquareCoord{ .file = 0, .rank = 0 }, screenToBoardSquare(a1[0], a1[1], 800, 600, board_rect).?);
const h8 = screenPointForBoardCoord(board_rect, 800, 600, 7.5, 7.5);
try std.testing.expectEqual(SquareCoord{ .file = 7, .rank = 7 }, screenToBoardSquare(h8[0], h8[1], 800, 600, board_rect).?);
const e4 = screenPointForBoardCoord(board_rect, 800, 600, 4.5, 3.5);
try std.testing.expectEqual(SquareCoord{ .file = 4, .rank = 3 }, screenToBoardSquare(e4[0], e4[1], 800, 600, board_rect).?);
}
test "screenToBoardSquare returns null outside board" {
const board_rect = geometry.boardRectForExtent(800, 600);
try std.testing.expectEqual(null, screenToBoardSquare(0, 0, 800, 600, board_rect));
try std.testing.expectEqual(null, screenToBoardSquare(799, 599, 800, 600, board_rect));
try std.testing.expectEqual(null, screenToBoardSquare(400, 300, 0, 600, board_rect));
}
test "screenToPalettePiece maps palette cells to encoded pieces" {
const board_rect = geometry.boardRectForExtent(800, 600);
const palette_rect = piece_render.paletteRectForBoard(board_rect);
const cell_height = palette_rect.height / @as(f32, @floatFromInt(piece_render.palette_entries.len));
const top_cell_ndc_x = palette_rect.left + palette_rect.width / 2.0;
const top_cell_ndc_y = palette_rect.bottom + palette_rect.height - cell_height / 2.0;
const window_x = ((@as(f64, top_cell_ndc_x) + 1.0) / 2.0) * 800.0;
const window_y = ((1.0 - @as(f64, top_cell_ndc_y)) / 2.0) * 600.0;
try std.testing.expectEqual(piece.encode(.white, .king), screenToPalettePiece(window_x, window_y, 800, 600, board_rect).?);
}
test "SelectionState selects side-to-move piece and requests move on second click" {
var state = chess_board.BoardState.empty();
state.turn = .white;
state.setSquare(12, piece.encode(.white, .pawn));
var selection = SelectionState{};
try std.testing.expectEqual(ClickResult.none, selection.handleClick(state, .{ .file = 0, .rank = 0 }));
try std.testing.expectEqual(null, selection.selected);
try std.testing.expectEqual(ClickResult{ .selected = .{ .file = 4, .rank = 1 } }, selection.handleClick(state, .{ .file = 4, .rank = 1 }));
try std.testing.expectEqual(SquareCoord{ .file = 4, .rank = 1 }, selection.selected.?);
try std.testing.expectEqual(
ClickResult{ .move_requested = .{ .from = 12, .to = 28 } },
selection.handleClick(state, .{ .file = 4, .rank = 3 }),
);
try std.testing.expectEqual(null, selection.selected);
}
test "SelectionState ignores pieces that are not side to move" {
var state = chess_board.BoardState.empty();
state.turn = .white;
state.setSquare(52, piece.encode(.black, .pawn));
var selection = SelectionState{};
try std.testing.expectEqual(ClickResult.none, selection.handleClick(state, .{ .file = 4, .rank = 6 }));
try std.testing.expectEqual(null, selection.selected);
}
test "SelectionState deselects when selected square is clicked again" {
var state = chess_board.BoardState.empty();
state.turn = .white;
state.setSquare(12, piece.encode(.white, .pawn));
var selection = SelectionState{};
_ = selection.handleClick(state, .{ .file = 4, .rank = 1 });
try std.testing.expectEqual(SquareCoord{ .file = 4, .rank = 1 }, selection.selected.?);
try std.testing.expectEqual(ClickResult.cleared, selection.handleClick(state, .{ .file = 4, .rank = 1 }));
try std.testing.expectEqual(null, selection.selected);
}
test "SelectionState switches selection when same-color piece is clicked" {
var state = chess_board.BoardState.empty();
state.turn = .white;
state.setSquare(12, piece.encode(.white, .pawn));
state.setSquare(6, piece.encode(.white, .knight));
var selection = SelectionState{};
_ = selection.handleClick(state, .{ .file = 4, .rank = 1 });
try std.testing.expectEqual(SquareCoord{ .file = 4, .rank = 1 }, selection.selected.?);
try std.testing.expectEqual(ClickResult{ .selected = .{ .file = 6, .rank = 0 } }, selection.handleClick(state, .{ .file = 6, .rank = 0 }));
try std.testing.expectEqual(SquareCoord{ .file = 6, .rank = 0 }, selection.selected.?);
}

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

1943
src/chess/board.zig Normal file

File diff suppressed because it is too large Load Diff

628
src/chess/fen.zig Normal file
View File

@ -0,0 +1,628 @@
const std = @import("std");
const piece = @import("piece.zig");
const board = @import("board.zig");
pub fn parseFen(fen: []const u8) !board.BoardState {
var state = board.BoardState.empty();
var it = std.mem.splitScalar(u8, fen, ' ');
var field_index: usize = 0;
while (it.next()) |field| : (field_index += 1) {
if (field_index >= 6) return error.InvalidFenFieldCount;
switch (field_index) {
0 => try parseBoardPlacement(&state, field),
1 => try parseTurn(&state, field),
2 => try parseCastleRights(&state, field),
3 => try parseEnPassant(&state, field),
4 => try parseHalfmove(&state, field),
5 => try parseFullmove(&state, field),
else => unreachable,
}
}
if (field_index != 6) return error.InvalidFenFieldCount;
return state;
}
pub fn parseBoardPlacement(state: *board.BoardState, placement: []const u8) !void {
var it = std.mem.splitScalar(u8, placement, '/');
var rank_count: u4 = 0;
while (it.next()) |part| {
if (rank_count >= 8) return error.InvalidRankCount;
const rank: u3 = @intCast(7 - rank_count);
var file: u4 = 0;
for (part) |c| {
switch (c) {
'1'...'9' => {
file += @intCast(c - '0');
if (file > 8) return error.InvalidRankWidth;
},
'P',
'N',
'B',
'R',
'Q',
'K',
'p',
'n',
'b',
'r',
'q',
'k',
=> {
if (file >= 8) return error.InvalidRankWidth;
const p = try piece.fromFENChar(c);
state.setSquare((@as(u6, rank) * 8) + file, p);
file += 1;
},
else => return error.InvalidFenCharacter,
}
}
if (file != 8) return error.InvalidRankWidth;
rank_count += 1;
}
if (rank_count != 8) return error.InvalidRankCount;
}
pub fn parseTurn(state: *board.BoardState, turn_string: []const u8) !void {
if (turn_string.len != 1) return error.InvalidTurnChar;
const turn_char = turn_string[0];
if (turn_char == 'w') {
state.turn = piece.Color.white;
} else if (turn_char == 'b') {
state.turn = piece.Color.black;
} else {
return error.InvalidTurnChar;
}
}
pub fn parseHalfmove(state: *board.BoardState, halfmove_string: []const u8) !void {
state.halfmove = try std.fmt.parseInt(u8, halfmove_string, 10);
}
pub fn parseFullmove(state: *board.BoardState, fullmove_string: []const u8) !void {
const fullmove = try std.fmt.parseInt(u32, fullmove_string, 10);
if (fullmove == 0) return error.InvalidFullmoveNumber;
state.fullmove = fullmove;
}
pub fn parseCastleRights(state: *board.BoardState, castle_string: []const u8) !void {
if (std.mem.eql(u8, castle_string, "")) return error.InvalidCastlingRights;
if (std.mem.eql(u8, castle_string, "-")) {
state.castle_rights = 0;
return;
}
var rights: u4 = 0;
var last_order: u3 = 0;
for (castle_string) |c| {
const bit: u4 = switch (c) {
'K' => 0b1000,
'Q' => 0b0100,
'k' => 0b0010,
'q' => 0b0001,
else => return error.InvalidCastlingRights,
};
const order: u3 = switch (c) {
'K' => 1,
'Q' => 2,
'k' => 3,
'q' => 4,
else => unreachable,
};
if (order <= last_order) {
return error.InvalidCastlingRights;
}
if ((rights & bit) != 0) {
return error.InvalidCastlingRights;
}
rights |= bit;
last_order = order;
}
state.castle_rights = rights;
}
pub fn parseEnPassant(state: *board.BoardState, ep_string: []const u8) !void {
if (std.mem.eql(u8, ep_string, "-")) {
state.en_passant = 0;
return;
}
if (ep_string.len != 2) return error.InvalidSquare;
const ep_square = try board.parseSquareFromAlgebraic(ep_string);
if (ep_string[1] != '3' and ep_string[1] != '6') return error.InvalidEnPassantSquare;
state.en_passant = (1 << 6) | @as(u7, ep_square);
}
fn pieceToFenChar(encoded: u4) !u8 {
const color = piece.colorOf(encoded) orelse return error.InvalidPiece;
return switch (piece.typeOf(encoded)) {
.pawn => if (color == .white) 'P' else 'p',
.knight => if (color == .white) 'N' else 'n',
.bishop => if (color == .white) 'B' else 'b',
.rook => if (color == .white) 'R' else 'r',
.queen => if (color == .white) 'Q' else 'q',
.king => if (color == .white) 'K' else 'k',
.none => error.InvalidPiece,
};
}
fn appendBoardPlacement(allocator: std.mem.Allocator, out: *std.ArrayList(u8), state: board.BoardState) !void {
var rank: u4 = 8;
while (rank > 0) {
rank -= 1;
var empty_count: u4 = 0;
var file: u4 = 0;
while (file < 8) : (file += 1) {
const square_index: u6 = @intCast((@as(u6, @intCast(rank)) * 8) + @as(u6, @intCast(file)));
const square = state.getSquare(square_index);
if (square == 0) {
empty_count += 1;
continue;
}
if (empty_count > 0) {
try out.append(allocator, '0' + @as(u8, @intCast(empty_count)));
empty_count = 0;
}
try out.append(allocator, try pieceToFenChar(square));
}
if (empty_count > 0) {
try out.append(allocator, '0' + @as(u8, @intCast(empty_count)));
}
if (rank > 0) try out.append(allocator, '/');
}
}
fn appendCastleRights(allocator: std.mem.Allocator, out: *std.ArrayList(u8), rights: u4) !void {
if (rights == 0) {
try out.append(allocator, '-');
return;
}
if ((rights & 0b1000) != 0) try out.append(allocator, 'K');
if ((rights & 0b0100) != 0) try out.append(allocator, 'Q');
if ((rights & 0b0010) != 0) try out.append(allocator, 'k');
if ((rights & 0b0001) != 0) try out.append(allocator, 'q');
}
fn appendEnPassant(allocator: std.mem.Allocator, out: *std.ArrayList(u8), en_passant: u7) !void {
const valid = (en_passant & (@as(u7, 1) << 6)) != 0;
if (!valid) {
try out.append(allocator, '-');
return;
}
const square = en_passant & 0b111111;
const file: u8 = @intCast(square % 8);
const rank: u8 = @intCast(square / 8);
try out.append(allocator, 'a' + file);
try out.append(allocator, '1' + rank);
}
pub fn formatFen(allocator: std.mem.Allocator, state: board.BoardState) ![]u8 {
var out: std.ArrayList(u8) = .empty;
errdefer out.deinit(allocator);
try appendBoardPlacement(allocator, &out, state);
try out.append(allocator, ' ');
try out.append(allocator, if (state.turn == .white) 'w' else 'b');
try out.append(allocator, ' ');
try appendCastleRights(allocator, &out, state.castle_rights);
try out.append(allocator, ' ');
try appendEnPassant(allocator, &out, state.en_passant);
try out.append(allocator, ' ');
var halfmove_buf: [3]u8 = undefined;
const halfmove = try std.fmt.bufPrint(&halfmove_buf, "{}", .{state.halfmove});
try out.appendSlice(allocator, halfmove);
try out.append(allocator, ' ');
var fullmove_buf: [10]u8 = undefined;
const fullmove = try std.fmt.bufPrint(&fullmove_buf, "{}", .{state.fullmove});
try out.appendSlice(allocator, fullmove);
return try out.toOwnedSlice(allocator);
}
fn expectStartingBoardPieces(state: board.BoardState) !void {
try std.testing.expectEqual(piece.encode(.black, .rook), state.getSquare(56));
try std.testing.expectEqual(piece.encode(.black, .knight), state.getSquare(57));
try std.testing.expectEqual(piece.encode(.black, .bishop), state.getSquare(58));
try std.testing.expectEqual(piece.encode(.black, .queen), state.getSquare(59));
try std.testing.expectEqual(piece.encode(.black, .king), state.getSquare(60));
try std.testing.expectEqual(piece.encode(.black, .bishop), state.getSquare(61));
try std.testing.expectEqual(piece.encode(.black, .knight), state.getSquare(62));
try std.testing.expectEqual(piece.encode(.black, .rook), state.getSquare(63));
for (8..16) |square| {
try std.testing.expectEqual(piece.encode(.white, .pawn), state.getSquare(@intCast(square)));
}
for (16..48) |square| {
try std.testing.expectEqual(@as(u4, 0), state.getSquare(@intCast(square)));
}
for (48..56) |square| {
try std.testing.expectEqual(piece.encode(.black, .pawn), state.getSquare(@intCast(square)));
}
try std.testing.expectEqual(piece.encode(.white, .rook), state.getSquare(0));
try std.testing.expectEqual(piece.encode(.white, .knight), state.getSquare(1));
try std.testing.expectEqual(piece.encode(.white, .bishop), state.getSquare(2));
try std.testing.expectEqual(piece.encode(.white, .queen), state.getSquare(3));
try std.testing.expectEqual(piece.encode(.white, .king), state.getSquare(4));
try std.testing.expectEqual(piece.encode(.white, .bishop), state.getSquare(5));
try std.testing.expectEqual(piece.encode(.white, .knight), state.getSquare(6));
try std.testing.expectEqual(piece.encode(.white, .rook), state.getSquare(7));
}
test "parseBoardPlacement parses starting position" {
var state = board.BoardState.empty();
try parseBoardPlacement(
&state,
"rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR",
);
try expectStartingBoardPieces(state);
}
test "parseBoardPlacement rejects invalid rank counts" {
var state = board.BoardState.empty();
try std.testing.expectError(
error.InvalidRankCount,
parseBoardPlacement(&state, "8/8/8/8/8/8/8"),
);
try std.testing.expectError(
error.InvalidRankCount,
parseBoardPlacement(&state, "8/8/8/8/8/8/8/8/8"),
);
}
test "parseBoardPlacement rejects ranks that are not exactly eight squares" {
var state = board.BoardState.empty();
try std.testing.expectError(
error.InvalidRankWidth,
parseBoardPlacement(&state, "7/8/8/8/8/8/8/8"),
);
try std.testing.expectError(
error.InvalidRankWidth,
parseBoardPlacement(&state, "9/8/8/8/8/8/8/8"),
);
try std.testing.expectError(
error.InvalidRankWidth,
parseBoardPlacement(&state, "8P/8/8/8/8/8/8/8"),
);
}
test "parseTurn accepts white and black active colors" {
var state = board.BoardState.empty();
try parseTurn(&state, "b");
try std.testing.expectEqual(piece.Color.black, state.turn);
try parseTurn(&state, "w");
try std.testing.expectEqual(piece.Color.white, state.turn);
}
test "parseTurn rejects invalid active color" {
var state = board.BoardState.empty();
try std.testing.expectError(error.InvalidTurnChar, parseTurn(&state, "x"));
try std.testing.expectError(error.InvalidTurnChar, parseTurn(&state, "white"));
}
test "parseHalfmove parses decimal halfmove clock" {
var state = board.BoardState.empty();
try parseHalfmove(&state, "42");
try std.testing.expectEqual(@as(u8, 42), state.halfmove);
}
test "parseHalfmove rejects invalid or overflowing values" {
var state = board.BoardState.empty();
try std.testing.expectError(error.InvalidCharacter, parseHalfmove(&state, "abc"));
try std.testing.expectError(error.Overflow, parseHalfmove(&state, "256"));
}
test "parseFullmove parses decimal fullmove number" {
var state = board.BoardState.empty();
try parseFullmove(&state, "1");
try std.testing.expectEqual(@as(u32, 1), state.fullmove);
try parseFullmove(&state, "300");
try std.testing.expectEqual(@as(u32, 300), state.fullmove);
}
test "parseFullmove rejects zero and invalid values" {
var state = board.BoardState.empty();
try std.testing.expectError(error.InvalidFullmoveNumber, parseFullmove(&state, "0"));
try std.testing.expectError(error.InvalidCharacter, parseFullmove(&state, "abc"));
}
test "parseCastleRights parses no castling rights" {
var state = board.BoardState.empty();
try parseCastleRights(&state, "-");
try std.testing.expectEqual(@as(u4, 0b0000), state.castle_rights);
}
test "parseCastleRights parses individual and combined rights" {
var state = board.BoardState.empty();
try parseCastleRights(&state, "K");
try std.testing.expectEqual(@as(u4, 0b1000), state.castle_rights);
try parseCastleRights(&state, "Q");
try std.testing.expectEqual(@as(u4, 0b0100), state.castle_rights);
try parseCastleRights(&state, "k");
try std.testing.expectEqual(@as(u4, 0b0010), state.castle_rights);
try parseCastleRights(&state, "q");
try std.testing.expectEqual(@as(u4, 0b0001), state.castle_rights);
try parseCastleRights(&state, "KQkq");
try std.testing.expectEqual(@as(u4, 0b1111), state.castle_rights);
try parseCastleRights(&state, "Kq");
try std.testing.expectEqual(@as(u4, 0b1001), state.castle_rights);
try parseCastleRights(&state, "kq");
try std.testing.expectEqual(@as(u4, 0b0011), state.castle_rights);
}
test "parseCastleRights rejects invalid castling rights" {
var state = board.BoardState.empty();
try std.testing.expectError(error.InvalidCastlingRights, parseCastleRights(&state, ""));
try std.testing.expectError(error.InvalidCastlingRights, parseCastleRights(&state, "K-"));
try std.testing.expectError(error.InvalidCastlingRights, parseCastleRights(&state, "A"));
try std.testing.expectError(error.InvalidCastlingRights, parseCastleRights(&state, "KK"));
try std.testing.expectError(error.InvalidCastlingRights, parseCastleRights(&state, "qK"));
}
test "parseEnPassant parses no en passant target" {
var state = board.BoardState.empty();
try parseEnPassant(&state, "-");
try std.testing.expectEqual(@as(u7, 0), state.en_passant);
}
test "parseEnPassant stores syntactically valid target even when not capturable" {
var state = board.BoardState.empty();
state.turn = .black;
try parseEnPassant(&state, "e3");
try std.testing.expectEqual(@as(u7, 84), state.en_passant);
state.turn = .white;
try parseEnPassant(&state, "e6");
try std.testing.expectEqual(@as(u7, 108), state.en_passant);
}
test "parseEnPassant stores rank 6 target capturable by white pawn" {
var state = board.BoardState.empty();
state.turn = .white;
state.setSquare((@as(u6, @intCast(4)) * 8) + @as(u6, @intCast(3)), piece.encode(.white, .pawn)); // d5 can capture e6
try parseEnPassant(&state, "e6");
try std.testing.expectEqual(@as(u7, 108), state.en_passant);
}
test "parseEnPassant stores rank 3 target capturable by black pawn" {
var state = board.BoardState.empty();
state.turn = .black;
state.setSquare((@as(u6, @intCast(3)) * 8) + @as(u6, @intCast(5)), piece.encode(.black, .pawn)); // f4 can capture e3
try parseEnPassant(&state, "e3");
try std.testing.expectEqual(@as(u7, 84), state.en_passant);
}
test "parseEnPassant handles capturable edge-file targets" {
var white_state = board.BoardState.empty();
white_state.turn = .white;
white_state.setSquare((@as(u6, @intCast(4)) * 8) + @as(u6, @intCast(1)), piece.encode(.white, .pawn)); // b5 can capture a6
try parseEnPassant(&white_state, "a6");
try std.testing.expectEqual(@as(u7, 104), white_state.en_passant);
var black_state = board.BoardState.empty();
black_state.turn = .black;
black_state.setSquare((@as(u6, @intCast(3)) * 8) + @as(u6, @intCast(6)), piece.encode(.black, .pawn)); // g4 can capture h3
try parseEnPassant(&black_state, "h3");
try std.testing.expectEqual(@as(u7, 87), black_state.en_passant);
}
test "parseEnPassant stores target even if adjacent pawn is wrong color" {
var state = board.BoardState.empty();
state.turn = .white;
state.setSquare((@as(u6, @intCast(4)) * 8) + @as(u6, @intCast(3)), piece.encode(.black, .pawn));
try parseEnPassant(&state, "e6");
try std.testing.expectEqual(@as(u7, 108), state.en_passant);
}
test "parseEnPassant rejects invalid target squares" {
var state = board.BoardState.empty();
try std.testing.expectError(error.InvalidSquare, parseEnPassant(&state, ""));
try std.testing.expectError(error.InvalidSquare, parseEnPassant(&state, "e"));
try std.testing.expectError(error.InvalidSquare, parseEnPassant(&state, "i3"));
try std.testing.expectError(error.InvalidSquare, parseEnPassant(&state, "e9"));
}
test "parseEnPassant rejects target squares outside ranks 3 and 6" {
var state = board.BoardState.empty();
try std.testing.expectError(error.InvalidEnPassantSquare, parseEnPassant(&state, "e2"));
try std.testing.expectError(error.InvalidEnPassantSquare, parseEnPassant(&state, "e4"));
try std.testing.expectError(error.InvalidEnPassantSquare, parseEnPassant(&state, "e5"));
try std.testing.expectError(error.InvalidEnPassantSquare, parseEnPassant(&state, "e7"));
}
test "parseFen parses starting position" {
const state = try parseFen("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1");
try expectStartingBoardPieces(state);
try std.testing.expectEqual(piece.Color.white, state.turn);
try std.testing.expectEqual(@as(u4, 0b1111), state.castle_rights);
try std.testing.expectEqual(@as(u7, 0), state.en_passant);
try std.testing.expectEqual(@as(u8, 0), state.halfmove);
try std.testing.expectEqual(@as(u32, 1), state.fullmove);
}
test "parseFen parses capturable en passant position" {
const state = try parseFen("8/8/8/3Pp3/8/8/8/8 w - e6 0 1");
try std.testing.expectEqual(piece.Color.white, state.turn);
try std.testing.expectEqual(@as(u4, 0), state.castle_rights);
try std.testing.expectEqual(@as(u7, 108), state.en_passant);
try std.testing.expectEqual(piece.encode(.white, .pawn), state.getSquare(35));
try std.testing.expectEqual(piece.encode(.black, .pawn), state.getSquare(36));
}
test "parseFen preserves non-capturable en passant target" {
const state = try parseFen("r1bqkbnr/pppp1ppp/2n5/4p3/3P4/5N2/PPP2PPP/RNBQKB1R w KQkq e3 4 5");
try std.testing.expectEqual(piece.Color.white, state.turn);
try std.testing.expectEqual(@as(u4, 0b1111), state.castle_rights);
try std.testing.expectEqual(@as(u7, 84), state.en_passant);
try std.testing.expectEqual(@as(u8, 4), state.halfmove);
try std.testing.expectEqual(@as(u32, 5), state.fullmove);
try std.testing.expectEqual(piece.encode(.white, .pawn), state.getSquare(27));
try std.testing.expectEqual(piece.encode(.black, .pawn), state.getSquare(36));
}
test "parseFen rejects invalid field counts" {
try std.testing.expectError(
error.InvalidFenFieldCount,
parseFen("8/8/8/8/8/8/8/8 w - - 0"),
);
try std.testing.expectError(
error.InvalidFenFieldCount,
parseFen("8/8/8/8/8/8/8/8 w - - 0 1 extra"),
);
}
test "formatFen formats starting position" {
const expected = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1";
const state = try parseFen(expected);
const actual = try formatFen(std.testing.allocator, state);
defer std.testing.allocator.free(actual);
try std.testing.expectEqualStrings(expected, actual);
}
test "formatFen formats capturable en passant target" {
const expected = "8/8/8/3Pp3/8/8/8/8 w - e6 0 1";
const state = try parseFen(expected);
const actual = try formatFen(std.testing.allocator, state);
defer std.testing.allocator.free(actual);
try std.testing.expectEqualStrings(expected, actual);
}
test "formatFen preserves non-capturable en passant target" {
const input = "r1bqkbnr/pppp1ppp/2n5/4p3/3P4/5N2/PPP2PPP/RNBQKB1R w KQkq e3 4 5";
const expected = "r1bqkbnr/pppp1ppp/2n5/4p3/3P4/5N2/PPP2PPP/RNBQKB1R w KQkq e3 4 5";
const state = try parseFen(input);
const actual = try formatFen(std.testing.allocator, state);
defer std.testing.allocator.free(actual);
try std.testing.expectEqualStrings(expected, actual);
}
test "formatFen includes promoted white pieces" {
const cases = [_]struct {
promotion_type: piece.PieceType,
expected: []const u8,
}{
.{ .promotion_type = .queen, .expected = "4Q3/8/8/8/8/8/8/8 b - - 0 1" },
.{ .promotion_type = .rook, .expected = "4R3/8/8/8/8/8/8/8 b - - 0 1" },
.{ .promotion_type = .bishop, .expected = "4B3/8/8/8/8/8/8/8 b - - 0 1" },
.{ .promotion_type = .knight, .expected = "4N3/8/8/8/8/8/8/8 b - - 0 1" },
};
for (cases) |case| {
var state = board.BoardState.empty();
state.fullmove = 1;
state.setSquare(52, piece.encode(.white, .pawn));
state.move(52, 60, case.promotion_type);
const actual = try formatFen(std.testing.allocator, state);
defer std.testing.allocator.free(actual);
try std.testing.expectEqualStrings(case.expected, actual);
}
}
test "formatFen includes promoted black pieces" {
const cases = [_]struct {
promotion_type: piece.PieceType,
expected: []const u8,
}{
.{ .promotion_type = .queen, .expected = "8/8/8/8/8/8/8/3q4 w - - 0 8" },
.{ .promotion_type = .rook, .expected = "8/8/8/8/8/8/8/3r4 w - - 0 8" },
.{ .promotion_type = .bishop, .expected = "8/8/8/8/8/8/8/3b4 w - - 0 8" },
.{ .promotion_type = .knight, .expected = "8/8/8/8/8/8/8/3n4 w - - 0 8" },
};
for (cases) |case| {
var state = board.BoardState.empty();
state.turn = .black;
state.fullmove = 7;
state.setSquare(11, piece.encode(.black, .pawn));
state.move(11, 3, case.promotion_type);
const actual = try formatFen(std.testing.allocator, state);
defer std.testing.allocator.free(actual);
try std.testing.expectEqualStrings(case.expected, actual);
}
}
test "formatFen formats black turn no castling and larger counters" {
var state = board.BoardState.empty();
state.turn = .black;
state.castle_rights = 0;
state.halfmove = 42;
state.fullmove = 300;
state.setSquare((@as(u6, @intCast(0)) * 8) + @as(u6, @intCast(4)), piece.encode(.white, .king));
state.setSquare((@as(u6, @intCast(7)) * 8) + @as(u6, @intCast(4)), piece.encode(.black, .king));
state.setSquare((@as(u6, @intCast(0)) * 8) + @as(u6, @intCast(0)), piece.encode(.white, .rook));
state.setSquare((@as(u6, @intCast(7)) * 8) + @as(u6, @intCast(7)), piece.encode(.black, .rook));
const actual = try formatFen(std.testing.allocator, state);
defer std.testing.allocator.free(actual);
try std.testing.expectEqualStrings("4k2r/8/8/8/8/8/8/R3K3 b - - 42 300", actual);
}

134
src/chess/game.zig Normal file
View File

@ -0,0 +1,134 @@
const std = @import("std");
const board = @import("board.zig");
const piece = @import("piece.zig");
const bitboard = @import("bitboard.zig");
const fen = @import("fen.zig");
const Game = struct {
initial_state: board.BoardState,
state: board.BoardState,
moves: std.ArrayList(MoveRecord),
time_control: TimeControl,
clocks: [2]u32,
status: GameStatus,
pub fn deinit(self: *Game, allocator: std.mem.Allocator) void {
self.moves.deinit(allocator);
}
pub fn makeMove(self: *Game, from: bitboard.Square, to: bitboard.Square, promotion: ?piece.PieceType) void {
self.state.move(from, to, promotion);
}
pub fn legalMoves(self: *Game, square: bitboard.Square) bitboard.Bitboard {
if (!self.state.isCheckmate(self.state.turn) and !self.state.isStalemate(self.state.turn)) {
return self.state.getLegalMoves(square);
}
return 0;
}
};
const MoveRecord = struct {
from: u6,
to: u6,
promotion: u4,
time_remaining: u32,
captured_piece: u4,
previous_castling_rights: u4,
previous_en_passant: u7,
previous_halfmove: u8,
previous_fullmove: u32,
};
const TimeControl = enum {
tentwo,
fiveone,
};
const GameStatus = enum {
not_started,
playing,
check,
stalemate,
checkmate_black,
checkmate_white,
};
pub fn initStartingPosition(time_control: TimeControl) Game {
const state = board.BoardState.initStartingPosition();
return .{
.initial_state = state,
.state = state,
.moves = .empty,
.time_control = time_control,
.status = .not_started,
.clocks = [_]u32{0} ** 2,
};
}
pub fn initFromFen(fen_string: []const u8, time_control: TimeControl) !Game {
const state = try fen.parseFen(fen_string);
return .{
.initial_state = state,
.state = state,
.moves = .empty,
.time_control = time_control,
.status = .not_started,
.clocks = [_]u32{0} ** 2,
};
}
test "initStartingPosition creates independent current and initial states" {
var game = initStartingPosition(.tentwo);
defer game.deinit(std.testing.allocator);
const expected = board.BoardState.initStartingPosition();
try std.testing.expectEqualDeep(expected, game.initial_state);
try std.testing.expectEqualDeep(expected, game.state);
try std.testing.expectEqual(@as(usize, 0), game.moves.items.len);
try std.testing.expectEqual(TimeControl.tentwo, game.time_control);
try std.testing.expectEqual(GameStatus.not_started, game.status);
try std.testing.expectEqual(@as(u32, 0), game.clocks[0]);
try std.testing.expectEqual(@as(u32, 0), game.clocks[1]);
game.makeMove(12, 28, null); // e2-e4
try std.testing.expectEqualDeep(expected, game.initial_state);
try std.testing.expectEqual(piece.encode(.white, .pawn), game.state.getSquare(28));
try std.testing.expectEqual(@as(u4, 0), game.state.getSquare(12));
}
test "initFromFen stores parsed position as initial and current state" {
const fen_string = "7k/5Q2/6K1/8/8/8/8/8 b - - 0 1";
var game = try initFromFen(fen_string, .fiveone);
defer game.deinit(std.testing.allocator);
const expected = try fen.parseFen(fen_string);
try std.testing.expectEqualDeep(expected, game.initial_state);
try std.testing.expectEqualDeep(expected, game.state);
try std.testing.expectEqual(@as(usize, 0), game.moves.items.len);
try std.testing.expectEqual(TimeControl.fiveone, game.time_control);
try std.testing.expectEqual(GameStatus.not_started, game.status);
}
test "legalMoves returns legal destinations while game is active" {
var game = initStartingPosition(.tentwo);
defer game.deinit(std.testing.allocator);
const e2: bitboard.Square = 12;
const e3: bitboard.Square = 20;
const e4: bitboard.Square = 28;
try std.testing.expectEqual(bitboard.bit(e3) | bitboard.bit(e4), game.legalMoves(e2));
}
test "legalMoves returns no destinations after checkmate or stalemate" {
var mate_game = try initFromFen("rnb1kbnr/pppp1ppp/8/4p3/6Pq/5P2/PPPPP2P/RNBQKBNR w KQkq - 1 3", .tentwo);
defer mate_game.deinit(std.testing.allocator);
try std.testing.expectEqual(@as(bitboard.Bitboard, 0), mate_game.legalMoves(4)); // white king e1
var stalemate_game = try initFromFen("7k/5Q2/6K1/8/8/8/8/8 b - - 0 1", .tentwo);
defer stalemate_game.deinit(std.testing.allocator);
try std.testing.expectEqual(@as(bitboard.Bitboard, 0), stalemate_game.legalMoves(63)); // black king h8
}

74137
src/chess/generated_magics.zig Normal file

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 {
position: [2]f32,
color: [4]f32,
uv: [2]f32,
};
const square_vertices = [_]Vertex{
.{ .position = .{ -0.5, -0.5 } },
.{ .position = .{ 0.5, -0.5 } },
.{ .position = .{ 0.5, 0.5 } },
.{ .position = .{ -0.5, -0.5 } },
.{ .position = .{ 0.5, 0.5 } },
.{ .position = .{ -0.5, 0.5 } },
pub const BoardRect = struct {
left: f32,
bottom: f32,
width: f32,
height: f32,
};
pub const Light = [4]f32{ 0.85, 0.75, 0.60, 1.0 };
pub const Dark = [4]f32{ 0.35, 0.20, 0.10, 1.0 };
pub const White = [4]f32{ 1.0, 1.0, 1.0, 1.0 };
pub fn boardRectForExtent(width: u32, height: u32) BoardRect {
return boardRectForExtentWithPalette(width, height, true);
}
pub fn boardRectForExtentWithPalette(width: u32, height: u32, show_palette: bool) BoardRect {
const window_w: f32 = @floatFromInt(width);
const window_h: f32 = @floatFromInt(height);
const margin_px: f32 = 32.0;
const palette_gap_px: f32 = if (show_palette) 16.0 else 0.0;
const palette_width_ratio: f32 = if (show_palette) 1.0 / 12.0 else 0.0;
const menu_area_px: f32 = 80.0;
const fen_gap_ratio: f32 = 1.0 / 18.0;
const fen_glyph_height_ratio: f32 = 7.0 / 95.0;
const fen_area_ratio = fen_gap_ratio + fen_glyph_height_ratio;
const available_w = @max(1.0, window_w - (2.0 * margin_px) - palette_gap_px);
const available_h = @max(1.0, window_h - (2.0 * margin_px) - menu_area_px);
const board_from_width = available_w / (1.0 + palette_width_ratio);
const board_from_height = available_h / (1.0 + fen_area_ratio);
const board_size_px = @min(board_from_width, board_from_height);
const palette_width_px = board_size_px * palette_width_ratio;
const group_width_px = board_size_px + palette_gap_px + palette_width_px;
const board_left_px = (window_w - group_width_px) / 2.0;
const board_bottom_px = margin_px + (board_size_px * fen_area_ratio);
const max_board_top_px = window_h - margin_px - menu_area_px;
const board_top_px = @min(board_bottom_px + board_size_px, max_board_top_px);
const adjusted_board_size_px = @max(1.0, board_top_px - board_bottom_px);
return .{
.left = (board_left_px / window_w) * 2.0 - 1.0,
.bottom = (board_bottom_px / window_h) * 2.0 - 1.0,
.width = (adjusted_board_size_px / window_w) * 2.0,
.height = (adjusted_board_size_px / window_h) * 2.0,
};
}
pub fn appendQuad(
vertices: *std.ArrayList(Vertex),
allocator: std.mem.Allocator,
bottom_left: [2]f32,
bottom_right: [2]f32,
top_right: [2]f32,
top_left: [2]f32,
color: [4]f32,
) !void {
try appendTexturedQuad(
vertices,
allocator,
bottom_left,
bottom_right,
top_right,
top_left,
color,
.{ 0.0, 0.0 },
.{ 0.0, 0.0 },
.{ 0.0, 0.0 },
.{ 0.0, 0.0 },
);
}
pub fn appendTexturedQuad(
vertices: *std.ArrayList(Vertex),
allocator: std.mem.Allocator,
bottom_left: [2]f32,
bottom_right: [2]f32,
top_right: [2]f32,
top_left: [2]f32,
color: [4]f32,
bottom_left_uv: [2]f32,
bottom_right_uv: [2]f32,
top_right_uv: [2]f32,
top_left_uv: [2]f32,
) !void {
try vertices.append(allocator, .{
.position = bottom_left,
.color = color,
.uv = bottom_left_uv,
});
try vertices.append(allocator, .{
.position = bottom_right,
.color = color,
.uv = bottom_right_uv,
});
try vertices.append(allocator, .{
.position = top_right,
.color = color,
.uv = top_right_uv,
});
try vertices.append(allocator, .{
.position = bottom_left,
.color = color,
.uv = bottom_left_uv,
});
try vertices.append(allocator, .{
.position = top_right,
.color = color,
.uv = top_right_uv,
});
try vertices.append(allocator, .{
.position = top_left,
.color = color,
.uv = top_left_uv,
});
}
pub fn appendChessboard(
vertices: *std.ArrayList(Vertex),
board_rect: BoardRect,
allocator: std.mem.Allocator,
) !void {
var rank: usize = 0;
while (rank < 8) : (rank += 1) {
var file: usize = 0;
while (file < 8) : (file += 1) {
const color = if ((rank + file) % 2 == 0) Dark else Light;
const x0: f32 = @floatFromInt(file);
const x1: f32 = @floatFromInt(file + 1);
const y0: f32 = @floatFromInt(rank);
const y1: f32 = @floatFromInt(rank + 1);
try appendQuad(
vertices,
allocator,
boardToNdc(board_rect, x0, y0),
boardToNdc(board_rect, x1, y0),
boardToNdc(board_rect, x1, y1),
boardToNdc(board_rect, x0, y1),
color,
);
}
}
}
pub fn appendPieceQuad(
vertices: *std.ArrayList(Vertex),
board_rect: BoardRect,
allocator: std.mem.Allocator,
file: f32,
rank: f32,
) !void {
try appendPieceQuadWithColor(vertices, board_rect, allocator, file, rank, White);
}
pub fn appendPieceQuadWithColor(
vertices: *std.ArrayList(Vertex),
board_rect: BoardRect,
allocator: std.mem.Allocator,
file: f32,
rank: f32,
color: [4]f32,
) !void {
const inset: f32 = 0.08;
const x0 = file + inset;
const x1 = file + 1.0 - inset;
const y0 = rank + inset;
const y1 = rank + 1.0 - inset;
try appendTexturedQuad(
vertices,
allocator,
boardToNdc(board_rect, x0, y0),
boardToNdc(board_rect, x1, y0),
boardToNdc(board_rect, x1, y1),
boardToNdc(board_rect, x0, y1),
color,
.{ 0.0, 1.0 },
.{ 1.0, 1.0 },
.{ 1.0, 0.0 },
.{ 0.0, 0.0 },
);
}
pub fn appendPieceQuadCenteredAtNdc(
vertices: *std.ArrayList(Vertex),
allocator: std.mem.Allocator,
center: [2]f32,
size: [2]f32,
color: [4]f32,
) !void {
const half_w = size[0] / 2.0;
const half_h = size[1] / 2.0;
const x0 = center[0] - half_w;
const x1 = center[0] + half_w;
const y0 = center[1] - half_h;
const y1 = center[1] + half_h;
try appendTexturedQuad(
vertices,
allocator,
.{ x0, y0 },
.{ x1, y0 },
.{ x1, y1 },
.{ x0, y1 },
color,
.{ 0.0, 1.0 },
.{ 1.0, 1.0 },
.{ 1.0, 0.0 },
.{ 0.0, 0.0 },
);
}
pub fn appendPiecesFromBoard(
vertices: *std.ArrayList(Vertex),
board_rect: BoardRect,
allocator: std.mem.Allocator,
state: board.BoardState,
) !void {
for (0..8) |rank| {
for (0..8) |file| {
const p = state.getSquare(@intCast((rank * 8) + file));
if (piece.typeOf(p) != piece.PieceType.none) {
try appendPieceQuad(vertices, board_rect, allocator, @floatFromInt(file), @floatFromInt(rank));
}
}
}
}
pub fn boardToNdc(rect: BoardRect, x: f32, y: f32) [2]f32 {
return .{
rect.left + (x / 8.0) * rect.width,
rect.bottom + (y / 8.0) * rect.height,
};
}

File diff suppressed because it is too large Load Diff

477
src/piece_render.zig Normal file
View File

@ -0,0 +1,477 @@
const std = @import("std");
const board = @import("chess/board.zig");
const fen = @import("chess/fen.zig");
const bitboard = @import("chess/bitboard.zig");
const geometry = @import("geometry.zig");
const piece = @import("chess/piece.zig");
pub const PieceGroup = enum(u5) {
white_pawn,
white_knight,
white_bishop,
white_rook,
white_queen,
white_king,
black_pawn,
black_knight,
black_bishop,
black_rook,
black_queen,
black_king,
dragged_white_pawn,
dragged_white_knight,
dragged_white_bishop,
dragged_white_rook,
dragged_white_queen,
dragged_white_king,
dragged_black_pawn,
dragged_black_knight,
dragged_black_bishop,
dragged_black_rook,
dragged_black_queen,
dragged_black_king,
};
pub const PieceGroupCount = 24;
pub const PaletteEntry = struct {
group: PieceGroup,
encoded: u4,
};
pub const promotion_piece_types = [_]piece.PieceType{ .queen, .rook, .bishop, .knight };
pub const palette_entries = [_]PaletteEntry{
.{ .group = .white_king, .encoded = piece.encode(.white, .king) },
.{ .group = .white_queen, .encoded = piece.encode(.white, .queen) },
.{ .group = .white_rook, .encoded = piece.encode(.white, .rook) },
.{ .group = .white_bishop, .encoded = piece.encode(.white, .bishop) },
.{ .group = .white_knight, .encoded = piece.encode(.white, .knight) },
.{ .group = .white_pawn, .encoded = piece.encode(.white, .pawn) },
.{ .group = .black_king, .encoded = piece.encode(.black, .king) },
.{ .group = .black_queen, .encoded = piece.encode(.black, .queen) },
.{ .group = .black_rook, .encoded = piece.encode(.black, .rook) },
.{ .group = .black_bishop, .encoded = piece.encode(.black, .bishop) },
.{ .group = .black_knight, .encoded = piece.encode(.black, .knight) },
.{ .group = .black_pawn, .encoded = piece.encode(.black, .pawn) },
};
pub fn paletteRectForBoard(board_rect: geometry.BoardRect) geometry.BoardRect {
const gap = board_rect.width / 48.0;
return .{
.left = board_rect.left + board_rect.width + gap,
.bottom = board_rect.bottom,
.width = board_rect.width / @as(f32, @floatFromInt(palette_entries.len)),
.height = board_rect.height,
};
}
pub fn promotionPopupRectForSquare(board_rect: geometry.BoardRect, square: bitboard.Square) geometry.BoardRect {
const cell_width = (board_rect.width / 8.0) * 0.65;
const cell_height = (board_rect.height / 8.0) * 0.65;
const width = cell_width * @as(f32, @floatFromInt(promotion_piece_types.len));
const height = cell_height;
const center = geometry.boardToNdc(
board_rect,
@as(f32, @floatFromInt(square % 8)) + 0.5,
@as(f32, @floatFromInt(square / 8)) + 0.5,
);
const board_right = board_rect.left + board_rect.width;
const board_top = board_rect.bottom + board_rect.height;
const unclamped_left = center[0] - (width / 2.0);
const unclamped_bottom = center[1] - (height / 2.0);
return .{
.left = @max(board_rect.left, @min(unclamped_left, board_right - width)),
.bottom = @max(board_rect.bottom, @min(unclamped_bottom, board_top - height)),
.width = width,
.height = height,
};
}
pub fn promotionChoiceRectForIndex(board_rect: geometry.BoardRect, square: bitboard.Square, index: usize) geometry.BoardRect {
const popup_rect = promotionPopupRectForSquare(board_rect, square);
const cell_width = popup_rect.width / @as(f32, @floatFromInt(promotion_piece_types.len));
return .{
.left = popup_rect.left + (@as(f32, @floatFromInt(index)) * cell_width),
.bottom = popup_rect.bottom,
.width = cell_width,
.height = popup_rect.height,
};
}
pub const PieceVertexGroups = struct {
groups: [PieceGroupCount]std.ArrayList(geometry.Vertex),
pub fn init() PieceVertexGroups {
return .{
.groups = [_]std.ArrayList(geometry.Vertex){.empty} ** PieceGroupCount,
};
}
pub fn deinit(self: *PieceVertexGroups, allocator: std.mem.Allocator) void {
for (&self.groups) |*vertex_group| {
vertex_group.deinit(allocator);
}
}
pub fn group(self: *PieceVertexGroups, piece_group: PieceGroup) *std.ArrayList(geometry.Vertex) {
return &self.groups[@intFromEnum(piece_group)];
}
pub fn constGroup(self: *const PieceVertexGroups, piece_group: PieceGroup) *const std.ArrayList(geometry.Vertex) {
return &self.groups[@intFromEnum(piece_group)];
}
};
pub fn pieceGroupFromEncoded(encoded: u4) ?PieceGroup {
return pieceGroupFromEncodedWithOffset(encoded, false);
}
fn draggedPieceGroupFromEncoded(encoded: u4) ?PieceGroup {
return pieceGroupFromEncodedWithOffset(encoded, true);
}
fn pieceGroupFromEncodedWithOffset(encoded: u4, dragged: bool) ?PieceGroup {
if (encoded == 0) return null;
const color = piece.colorOf(encoded) orelse return null;
const piece_type = piece.typeOf(encoded);
return switch (color) {
.white => switch (piece_type) {
.pawn => if (dragged) .dragged_white_pawn else .white_pawn,
.knight => if (dragged) .dragged_white_knight else .white_knight,
.bishop => if (dragged) .dragged_white_bishop else .white_bishop,
.rook => if (dragged) .dragged_white_rook else .white_rook,
.queen => if (dragged) .dragged_white_queen else .white_queen,
.king => if (dragged) .dragged_white_king else .white_king,
.none => null,
},
.black => switch (piece_type) {
.pawn => if (dragged) .dragged_black_pawn else .black_pawn,
.knight => if (dragged) .dragged_black_knight else .black_knight,
.bishop => if (dragged) .dragged_black_bishop else .black_bishop,
.rook => if (dragged) .dragged_black_rook else .black_rook,
.queen => if (dragged) .dragged_black_queen else .black_queen,
.king => if (dragged) .dragged_black_king else .black_king,
.none => null,
},
};
}
fn appendSidewaysPieceQuad(
vertices: *std.ArrayList(geometry.Vertex),
board_rect: geometry.BoardRect,
allocator: std.mem.Allocator,
file: f32,
rank: f32,
) !void {
const inset: f32 = 0.08;
const x0 = file + inset;
const x1 = file + 1.0 - inset;
const y0 = rank + inset;
const y1 = rank + 1.0 - inset;
try geometry.appendTexturedQuad(
vertices,
allocator,
geometry.boardToNdc(board_rect, x1, y0),
geometry.boardToNdc(board_rect, x1, y1),
geometry.boardToNdc(board_rect, x0, y1),
geometry.boardToNdc(board_rect, x0, y0),
geometry.White,
.{ 0.0, 1.0 },
.{ 1.0, 1.0 },
.{ 1.0, 0.0 },
.{ 0.0, 0.0 },
);
}
fn appendPieceQuadInRect(
vertices: *std.ArrayList(geometry.Vertex),
allocator: std.mem.Allocator,
rect: geometry.BoardRect,
) !void {
const inset_x = rect.width * 0.08;
const inset_y = rect.height * 0.08;
const x0 = rect.left + inset_x;
const x1 = rect.left + rect.width - inset_x;
const y0 = rect.bottom + inset_y;
const y1 = rect.bottom + rect.height - inset_y;
try geometry.appendTexturedQuad(
vertices,
allocator,
.{ x0, y0 },
.{ x1, y0 },
.{ x1, y1 },
.{ x0, y1 },
geometry.White,
.{ 0.0, 1.0 },
.{ 1.0, 1.0 },
.{ 1.0, 0.0 },
.{ 0.0, 0.0 },
);
}
pub fn appendPiecesGroupedFromBoard(
groups: *PieceVertexGroups,
board_rect: geometry.BoardRect,
allocator: std.mem.Allocator,
state: board.BoardState,
) !void {
try appendPiecesGroupedFromBoardExcept(groups, board_rect, allocator, state, null);
}
pub fn appendPiecesGroupedFromBoardExcept(
groups: *PieceVertexGroups,
board_rect: geometry.BoardRect,
allocator: std.mem.Allocator,
state: board.BoardState,
except_square: ?@import("board_input.zig").SquareCoord,
) !void {
try appendPiecesGroupedFromBoardExceptWithGameOver(groups, board_rect, allocator, state, except_square, null);
}
pub fn appendPiecesGroupedFromBoardExceptWithGameOver(
groups: *PieceVertexGroups,
board_rect: geometry.BoardRect,
allocator: std.mem.Allocator,
state: board.BoardState,
except_square: ?@import("board_input.zig").SquareCoord,
losing_king_square: ?@import("board_input.zig").SquareCoord,
) !void {
for (0..8) |rank| {
for (0..8) |file| {
if (except_square) |except| {
if (except.file == file and except.rank == rank) continue;
}
const encoded = state.getSquare(@intCast((rank * 8) + file));
const piece_group = pieceGroupFromEncoded(encoded) orelse continue;
if (losing_king_square) |losing| {
if (losing.file == file and losing.rank == rank) {
try appendSidewaysPieceQuad(
groups.group(piece_group),
board_rect,
allocator,
@as(f32, @floatFromInt(file)),
@as(f32, @floatFromInt(rank)),
);
continue;
}
}
try geometry.appendPieceQuad(
groups.group(piece_group),
board_rect,
allocator,
@as(f32, @floatFromInt(file)),
@as(f32, @floatFromInt(rank)),
);
}
}
}
pub fn appendPieceGhostAtSquare(
groups: *PieceVertexGroups,
board_rect: geometry.BoardRect,
allocator: std.mem.Allocator,
encoded: u4,
square: @import("board_input.zig").SquareCoord,
) !void {
const piece_group = pieceGroupFromEncoded(encoded) orelse return;
try geometry.appendPieceQuadWithColor(
groups.group(piece_group),
board_rect,
allocator,
@floatFromInt(square.file),
@floatFromInt(square.rank),
.{ 1.0, 1.0, 1.0, 0.35 },
);
}
pub fn appendDraggedPiece(
groups: *PieceVertexGroups,
board_rect: geometry.BoardRect,
allocator: std.mem.Allocator,
encoded: u4,
center_ndc: [2]f32,
) !void {
const piece_group = draggedPieceGroupFromEncoded(encoded) orelse return;
try geometry.appendPieceQuadCenteredAtNdc(
groups.group(piece_group),
allocator,
center_ndc,
.{ board_rect.width / 8.0 * 0.84, board_rect.height / 8.0 * 0.84 },
geometry.White,
);
}
pub fn appendPalettePiecesGrouped(
groups: *PieceVertexGroups,
board_rect: geometry.BoardRect,
allocator: std.mem.Allocator,
) !void {
const palette_rect = paletteRectForBoard(board_rect);
const cell_height = palette_rect.height / @as(f32, @floatFromInt(palette_entries.len));
for (palette_entries, 0..) |entry, i| {
const y = palette_rect.bottom + (@as(f32, @floatFromInt(palette_entries.len - 1 - i)) * cell_height);
const cell_rect = geometry.BoardRect{
.left = palette_rect.left,
.bottom = y,
.width = palette_rect.width,
.height = cell_height,
};
try appendPieceQuadInRect(groups.group(entry.group), allocator, cell_rect);
}
}
pub fn appendPromotionPiecesGrouped(
groups: *PieceVertexGroups,
board_rect: geometry.BoardRect,
allocator: std.mem.Allocator,
color: piece.Color,
square: bitboard.Square,
) !void {
for (promotion_piece_types, 0..) |piece_type, i| {
const encoded = piece.encode(color, piece_type);
const piece_group = pieceGroupFromEncoded(encoded) orelse continue;
const cell_rect = promotionChoiceRectForIndex(board_rect, square, i);
const piece_rect = geometry.BoardRect{
.left = cell_rect.left + (cell_rect.width * 0.25),
.bottom = cell_rect.bottom + (cell_rect.height * 0.25),
.width = cell_rect.width * 0.5,
.height = cell_rect.height * 0.5,
};
try appendPieceQuadInRect(groups.group(piece_group), allocator, piece_rect);
}
}
pub fn paletteIndexForEncoded(encoded: u4) ?usize {
for (palette_entries, 0..) |entry, i| {
if (entry.encoded == encoded) return i;
}
return null;
}
fn expectGroupVertexCount(groups: *const PieceVertexGroups, piece_group: PieceGroup, expected: usize) !void {
try std.testing.expectEqual(expected, groups.constGroup(piece_group).items.len);
}
test "pieceGroupFromEncoded maps encoded pieces to render groups" {
try std.testing.expectEqual(PieceGroup.white_pawn, pieceGroupFromEncoded(piece.encode(.white, .pawn)).?);
try std.testing.expectEqual(PieceGroup.white_knight, pieceGroupFromEncoded(piece.encode(.white, .knight)).?);
try std.testing.expectEqual(PieceGroup.white_bishop, pieceGroupFromEncoded(piece.encode(.white, .bishop)).?);
try std.testing.expectEqual(PieceGroup.white_rook, pieceGroupFromEncoded(piece.encode(.white, .rook)).?);
try std.testing.expectEqual(PieceGroup.white_queen, pieceGroupFromEncoded(piece.encode(.white, .queen)).?);
try std.testing.expectEqual(PieceGroup.white_king, pieceGroupFromEncoded(piece.encode(.white, .king)).?);
try std.testing.expectEqual(PieceGroup.black_pawn, pieceGroupFromEncoded(piece.encode(.black, .pawn)).?);
try std.testing.expectEqual(PieceGroup.black_knight, pieceGroupFromEncoded(piece.encode(.black, .knight)).?);
try std.testing.expectEqual(PieceGroup.black_bishop, pieceGroupFromEncoded(piece.encode(.black, .bishop)).?);
try std.testing.expectEqual(PieceGroup.black_rook, pieceGroupFromEncoded(piece.encode(.black, .rook)).?);
try std.testing.expectEqual(PieceGroup.black_queen, pieceGroupFromEncoded(piece.encode(.black, .queen)).?);
try std.testing.expectEqual(PieceGroup.black_king, pieceGroupFromEncoded(piece.encode(.black, .king)).?);
try std.testing.expectEqual(null, pieceGroupFromEncoded(0));
}
test "appendPiecesGroupedFromBoard leaves all groups empty for empty board" {
const state = board.BoardState.empty();
const board_rect = geometry.boardRectForExtent(800, 600);
var groups = PieceVertexGroups.init();
defer groups.deinit(std.testing.allocator);
try appendPiecesGroupedFromBoard(&groups, board_rect, std.testing.allocator, state);
for (groups.groups) |vertex_group| {
try std.testing.expectEqual(@as(usize, 0), vertex_group.items.len);
}
}
test "appendPalettePiecesGrouped adds one piece to each piece group" {
const board_rect = geometry.boardRectForExtent(800, 600);
var groups = PieceVertexGroups.init();
defer groups.deinit(std.testing.allocator);
try appendPalettePiecesGrouped(&groups, board_rect, std.testing.allocator);
for (groups.groups, 0..) |vertex_group, i| {
const group: PieceGroup = @enumFromInt(@as(u5, @intCast(i)));
const is_dragged_group = @intFromEnum(group) >= @intFromEnum(PieceGroup.dragged_white_pawn);
const expected: usize = if (is_dragged_group) 0 else 6;
try std.testing.expectEqual(expected, vertex_group.items.len);
}
}
test "appendDraggedPiece uses dragged overlay group" {
const board_rect = geometry.boardRectForExtent(800, 600);
var groups = PieceVertexGroups.init();
defer groups.deinit(std.testing.allocator);
try appendDraggedPiece(&groups, board_rect, std.testing.allocator, piece.encode(.white, .queen), .{ 0.0, 0.0 });
try expectGroupVertexCount(&groups, .white_queen, 0);
try expectGroupVertexCount(&groups, .dragged_white_queen, 6);
}
test "appendPiecesGroupedFromBoardExceptWithGameOver renders sideways losing king" {
var state = board.BoardState.empty();
state.setSquare(63, piece.encode(.black, .king));
state.setSquare(45, piece.encode(.white, .king));
const board_rect = geometry.boardRectForExtent(800, 600);
var groups = PieceVertexGroups.init();
defer groups.deinit(std.testing.allocator);
try appendPiecesGroupedFromBoardExceptWithGameOver(
&groups,
board_rect,
std.testing.allocator,
state,
null,
.{ .file = 7, .rank = 7 },
);
try expectGroupVertexCount(&groups, .white_king, 6);
try expectGroupVertexCount(&groups, .black_king, 6);
}
test "appendPiecesGroupedFromBoard groups starting position vertices by piece" {
const state = try fen.parseFen("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1");
const board_rect = geometry.boardRectForExtent(800, 600);
var groups = PieceVertexGroups.init();
defer groups.deinit(std.testing.allocator);
try appendPiecesGroupedFromBoard(&groups, board_rect, std.testing.allocator, state);
try expectGroupVertexCount(&groups, .white_pawn, 8 * 6);
try expectGroupVertexCount(&groups, .white_knight, 2 * 6);
try expectGroupVertexCount(&groups, .white_bishop, 2 * 6);
try expectGroupVertexCount(&groups, .white_rook, 2 * 6);
try expectGroupVertexCount(&groups, .white_queen, 1 * 6);
try expectGroupVertexCount(&groups, .white_king, 1 * 6);
try expectGroupVertexCount(&groups, .black_pawn, 8 * 6);
try expectGroupVertexCount(&groups, .black_knight, 2 * 6);
try expectGroupVertexCount(&groups, .black_bishop, 2 * 6);
try expectGroupVertexCount(&groups, .black_rook, 2 * 6);
try expectGroupVertexCount(&groups, .black_queen, 1 * 6);
try expectGroupVertexCount(&groups, .black_king, 1 * 6);
var total_vertices: usize = 0;
for (groups.groups) |vertex_group| {
total_vertices += vertex_group.items.len;
}
try std.testing.expectEqual(@as(usize, 32 * 6), total_vertices);
}

585
src/text_render.zig Normal file
View File

@ -0,0 +1,585 @@
const std = @import("std");
const board_input = @import("board_input.zig");
const bitboard = @import("chess/bitboard.zig");
const chess_board = @import("chess/board.zig");
const geometry = @import("geometry.zig");
const piece_render = @import("piece_render.zig");
const Glyph = [7]u5;
pub const TextStyle = struct {
pixel_size: f32,
pixel_aspect: f32 = 1.0,
color: [4]f32,
};
fn glyphForChar(ch: u8) ?Glyph {
return switch (ch) {
'0' => .{ 0b01110, 0b10001, 0b10011, 0b10101, 0b11001, 0b10001, 0b01110 },
'1' => .{ 0b00100, 0b01100, 0b00100, 0b00100, 0b00100, 0b00100, 0b01110 },
'2' => .{ 0b01110, 0b10001, 0b00001, 0b00010, 0b00100, 0b01000, 0b11111 },
'3' => .{ 0b11110, 0b00001, 0b00001, 0b01110, 0b00001, 0b00001, 0b11110 },
'4' => .{ 0b00010, 0b00110, 0b01010, 0b10010, 0b11111, 0b00010, 0b00010 },
'5' => .{ 0b11111, 0b10000, 0b10000, 0b11110, 0b00001, 0b00001, 0b11110 },
'6' => .{ 0b01110, 0b10000, 0b10000, 0b11110, 0b10001, 0b10001, 0b01110 },
'7' => .{ 0b11111, 0b00001, 0b00010, 0b00100, 0b01000, 0b01000, 0b01000 },
'8' => .{ 0b01110, 0b10001, 0b10001, 0b01110, 0b10001, 0b10001, 0b01110 },
'9' => .{ 0b01110, 0b10001, 0b10001, 0b01111, 0b00001, 0b00001, 0b01110 },
'a' => .{ 0b00000, 0b00000, 0b01110, 0b00001, 0b01111, 0b10001, 0b01111 },
'b' => .{ 0b10000, 0b10000, 0b10110, 0b11001, 0b10001, 0b10001, 0b11110 },
'c' => .{ 0b00000, 0b00000, 0b01111, 0b10000, 0b10000, 0b10000, 0b01111 },
'd' => .{ 0b00001, 0b00001, 0b01101, 0b10011, 0b10001, 0b10001, 0b01111 },
'e' => .{ 0b00000, 0b00000, 0b01110, 0b10001, 0b11111, 0b10000, 0b01110 },
'f' => .{ 0b00110, 0b01000, 0b01000, 0b11100, 0b01000, 0b01000, 0b01000 },
'g' => .{ 0b00000, 0b01111, 0b10001, 0b10001, 0b01111, 0b00001, 0b01110 },
'i' => .{ 0b00100, 0b00000, 0b01100, 0b00100, 0b00100, 0b00100, 0b01110 },
'h' => .{ 0b10000, 0b10000, 0b10110, 0b11001, 0b10001, 0b10001, 0b10001 },
'k' => .{ 0b10000, 0b10010, 0b10100, 0b11000, 0b10100, 0b10010, 0b10001 },
'l' => .{ 0b01100, 0b00100, 0b00100, 0b00100, 0b00100, 0b00100, 0b01110 },
'n' => .{ 0b00000, 0b00000, 0b10110, 0b11001, 0b10001, 0b10001, 0b10001 },
'p' => .{ 0b00000, 0b00000, 0b11110, 0b10001, 0b11110, 0b10000, 0b10000 },
'q' => .{ 0b00000, 0b00000, 0b01101, 0b10011, 0b01111, 0b00001, 0b00001 },
'r' => .{ 0b00000, 0b00000, 0b10110, 0b11001, 0b10000, 0b10000, 0b10000 },
't' => .{ 0b01000, 0b01000, 0b11100, 0b01000, 0b01000, 0b01001, 0b00110 },
'w' => .{ 0b00000, 0b00000, 0b10001, 0b10001, 0b10101, 0b10101, 0b01010 },
'y' => .{ 0b00000, 0b10001, 0b10001, 0b01111, 0b00001, 0b00001, 0b01110 },
'A' => .{ 0b01110, 0b10001, 0b10001, 0b11111, 0b10001, 0b10001, 0b10001 },
'B' => .{ 0b11110, 0b10001, 0b10001, 0b11110, 0b10001, 0b10001, 0b11110 },
'D' => .{ 0b11110, 0b10001, 0b10001, 0b10001, 0b10001, 0b10001, 0b11110 },
'E' => .{ 0b11111, 0b10000, 0b10000, 0b11110, 0b10000, 0b10000, 0b11111 },
'I' => .{ 0b11111, 0b00100, 0b00100, 0b00100, 0b00100, 0b00100, 0b11111 },
'K' => .{ 0b10001, 0b10010, 0b10100, 0b11000, 0b10100, 0b10010, 0b10001 },
'L' => .{ 0b10000, 0b10000, 0b10000, 0b10000, 0b10000, 0b10000, 0b11111 },
'N' => .{ 0b10001, 0b11001, 0b10101, 0b10011, 0b10001, 0b10001, 0b10001 },
'P' => .{ 0b11110, 0b10001, 0b10001, 0b11110, 0b10000, 0b10000, 0b10000 },
'Q' => .{ 0b01110, 0b10001, 0b10001, 0b10001, 0b10101, 0b10010, 0b01101 },
'R' => .{ 0b11110, 0b10001, 0b10001, 0b11110, 0b10100, 0b10010, 0b10001 },
'S' => .{ 0b01111, 0b10000, 0b10000, 0b01110, 0b00001, 0b00001, 0b11110 },
'T' => .{ 0b11111, 0b00100, 0b00100, 0b00100, 0b00100, 0b00100, 0b00100 },
'Y' => .{ 0b10001, 0b10001, 0b01010, 0b00100, 0b00100, 0b00100, 0b00100 },
'#' => .{ 0b01010, 0b01010, 0b11111, 0b01010, 0b11111, 0b01010, 0b01010 },
'-' => .{ 0b00000, 0b00000, 0b00000, 0b11111, 0b00000, 0b00000, 0b00000 },
'/' => .{ 0b00001, 0b00010, 0b00010, 0b00100, 0b01000, 0b01000, 0b10000 },
else => null,
};
}
fn appendGlyph(
vertices: *std.ArrayList(geometry.Vertex),
allocator: std.mem.Allocator,
glyph: Glyph,
x: f32,
y: f32,
style: TextStyle,
) !void {
for (glyph, 0..) |row_bits, row| {
for (0..5) |col| {
const shift: u3 = @intCast(4 - col);
if (((row_bits >> shift) & 1) == 0) continue;
const pixel_width = style.pixel_size * style.pixel_aspect;
const x0 = x + (@as(f32, @floatFromInt(col)) * pixel_width);
const y0 = y + (@as(f32, @floatFromInt(6 - row)) * style.pixel_size);
const x1 = x0 + pixel_width;
const y1 = y0 + style.pixel_size;
try geometry.appendQuad(
vertices,
allocator,
.{ x0, y0 },
.{ x1, y0 },
.{ x1, y1 },
.{ x0, y1 },
style.color,
);
}
}
}
pub fn appendText(
vertices: *std.ArrayList(geometry.Vertex),
allocator: std.mem.Allocator,
text: []const u8,
x: f32,
y: f32,
style: TextStyle,
) !void {
var cursor_x = x;
const advance = style.pixel_size * style.pixel_aspect * 6.0;
for (text) |ch| {
if (ch == ' ') {
cursor_x += advance;
continue;
}
if (glyphForChar(ch)) |glyph| {
try appendGlyph(vertices, allocator, glyph, cursor_x, y, style);
}
cursor_x += advance;
}
}
const SelectedHighlightColor = [4]f32{ 0.12, 0.55, 0.12, 0.48 };
const HoverHighlightColor = [4]f32{ 0.45, 0.90, 0.45, 0.38 };
fn appendSquareOverlay(
vertices: *std.ArrayList(geometry.Vertex),
allocator: std.mem.Allocator,
board_rect: geometry.BoardRect,
square: board_input.SquareCoord,
color: [4]f32,
) !void {
const file: f32 = @floatFromInt(square.file);
const rank: f32 = @floatFromInt(square.rank);
try geometry.appendQuad(
vertices,
allocator,
geometry.boardToNdc(board_rect, file, rank),
geometry.boardToNdc(board_rect, file + 1.0, rank),
geometry.boardToNdc(board_rect, file + 1.0, rank + 1.0),
geometry.boardToNdc(board_rect, file, rank + 1.0),
color,
);
}
pub fn appendSelectedSquareHighlight(
vertices: *std.ArrayList(geometry.Vertex),
allocator: std.mem.Allocator,
board_rect: geometry.BoardRect,
selected: ?board_input.SquareCoord,
) !void {
const square = selected orelse return;
try appendSquareOverlay(vertices, allocator, board_rect, square, SelectedHighlightColor);
}
pub fn appendPalettePieceHighlight(
vertices: *std.ArrayList(geometry.Vertex),
allocator: std.mem.Allocator,
board_rect: geometry.BoardRect,
selected_piece: ?u4,
) !void {
const encoded = selected_piece orelse return;
const index = piece_render.paletteIndexForEncoded(encoded) orelse return;
const palette_rect = piece_render.paletteRectForBoard(board_rect);
const cell_height = palette_rect.height / @as(f32, @floatFromInt(piece_render.palette_entries.len));
const y = palette_rect.bottom + (@as(f32, @floatFromInt(piece_render.palette_entries.len - 1 - index)) * cell_height);
try geometry.appendQuad(
vertices,
allocator,
.{ palette_rect.left, y },
.{ palette_rect.left + palette_rect.width, y },
.{ palette_rect.left + palette_rect.width, y + cell_height },
.{ palette_rect.left, y + cell_height },
.{ 0.45, 0.90, 0.45, 0.38 },
);
}
pub fn appendHoveredSquareHighlight(
vertices: *std.ArrayList(geometry.Vertex),
allocator: std.mem.Allocator,
board_rect: geometry.BoardRect,
hovered: ?board_input.SquareCoord,
) !void {
const square = hovered orelse return;
try appendSquareOverlay(vertices, allocator, board_rect, square, HoverHighlightColor);
}
pub fn appendCheckBorder(
vertices: *std.ArrayList(geometry.Vertex),
allocator: std.mem.Allocator,
board_rect: geometry.BoardRect,
checked: ?board_input.SquareCoord,
) !void {
const square = checked orelse return;
const file: f32 = @floatFromInt(square.file);
const rank: f32 = @floatFromInt(square.rank);
const thickness: f32 = 0.08;
const color = [4]f32{ 1.0, 0.05, 0.05, 0.85 };
try geometry.appendQuad(vertices, allocator, geometry.boardToNdc(board_rect, file, rank), geometry.boardToNdc(board_rect, file + 1.0, rank), geometry.boardToNdc(board_rect, file + 1.0, rank + thickness), geometry.boardToNdc(board_rect, file, rank + thickness), color);
try geometry.appendQuad(vertices, allocator, geometry.boardToNdc(board_rect, file, rank + 1.0 - thickness), geometry.boardToNdc(board_rect, file + 1.0, rank + 1.0 - thickness), geometry.boardToNdc(board_rect, file + 1.0, rank + 1.0), geometry.boardToNdc(board_rect, file, rank + 1.0), color);
try geometry.appendQuad(vertices, allocator, geometry.boardToNdc(board_rect, file, rank), geometry.boardToNdc(board_rect, file + thickness, rank), geometry.boardToNdc(board_rect, file + thickness, rank + 1.0), geometry.boardToNdc(board_rect, file, rank + 1.0), color);
try geometry.appendQuad(vertices, allocator, geometry.boardToNdc(board_rect, file + 1.0 - thickness, rank), geometry.boardToNdc(board_rect, file + 1.0, rank), geometry.boardToNdc(board_rect, file + 1.0, rank + 1.0), geometry.boardToNdc(board_rect, file + 1.0 - thickness, rank + 1.0), color);
}
pub fn appendCheckmateMarker(
vertices: *std.ArrayList(geometry.Vertex),
allocator: std.mem.Allocator,
board_rect: geometry.BoardRect,
winning_king: ?board_input.SquareCoord,
) !void {
const square = winning_king orelse return;
const square_w = board_rect.width / 8.0;
const square_h = board_rect.height / 8.0;
const pixel_size = @min(square_w, square_h) * 0.045;
const glyph_w = pixel_size * 5.0;
const glyph_h = pixel_size * 7.0;
const padding_x = square_w * 0.10;
const padding_y = square_h * 0.10;
const top_right = geometry.boardToNdc(
board_rect,
@as(f32, @floatFromInt(square.file)) + 1.0,
@as(f32, @floatFromInt(square.rank)) + 1.0,
);
try appendText(
vertices,
allocator,
"#",
top_right[0] - padding_x - glyph_w,
top_right[1] - padding_y - glyph_h,
.{ .pixel_size = pixel_size, .color = .{ 1.0, 0.92, 0.18, 1.0 } },
);
}
pub fn appendValidMoveDots(
vertices: *std.ArrayList(geometry.Vertex),
allocator: std.mem.Allocator,
board_rect: geometry.BoardRect,
state: chess_board.BoardState,
valid_moves: bitboard.Bitboard,
) !void {
const square_w = board_rect.width / 8.0;
const square_h = board_rect.height / 8.0;
const radius_x = square_w * 0.11;
const radius_y = square_h * 0.11;
const color = SelectedHighlightColor;
const segments = 48;
var moves = valid_moves;
while (moves != 0) {
const move_square: bitboard.Square = @intCast(@ctz(moves));
moves &= moves - 1;
const file: u3 = @intCast(move_square % 8);
const rank: u3 = @intCast(move_square / 8);
if (state.getSquare(@intCast((@as(u6, rank) * 8) + @as(u6, file))) != 0) {
const x0 = @as(f32, @floatFromInt(file));
const x1 = x0 + 1.0;
const y0 = @as(f32, @floatFromInt(rank));
const y1 = y0 + 1.0;
const thickness: f32 = 0.07;
try geometry.appendQuad(vertices, allocator, geometry.boardToNdc(board_rect, x0, y0), geometry.boardToNdc(board_rect, x1, y0), geometry.boardToNdc(board_rect, x1, y0 + thickness), geometry.boardToNdc(board_rect, x0, y0 + thickness), color);
try geometry.appendQuad(vertices, allocator, geometry.boardToNdc(board_rect, x0, y1 - thickness), geometry.boardToNdc(board_rect, x1, y1 - thickness), geometry.boardToNdc(board_rect, x1, y1), geometry.boardToNdc(board_rect, x0, y1), color);
try geometry.appendQuad(vertices, allocator, geometry.boardToNdc(board_rect, x0, y0), geometry.boardToNdc(board_rect, x0 + thickness, y0), geometry.boardToNdc(board_rect, x0 + thickness, y1), geometry.boardToNdc(board_rect, x0, y1), color);
try geometry.appendQuad(vertices, allocator, geometry.boardToNdc(board_rect, x1 - thickness, y0), geometry.boardToNdc(board_rect, x1, y0), geometry.boardToNdc(board_rect, x1, y1), geometry.boardToNdc(board_rect, x1 - thickness, y1), color);
} else {
const center = geometry.boardToNdc(
board_rect,
@as(f32, @floatFromInt(file)) + 0.5,
@as(f32, @floatFromInt(rank)) + 0.5,
);
var i: usize = 0;
while (i < segments) : (i += 1) {
const angle0 = (@as(f32, @floatFromInt(i)) / @as(f32, @floatFromInt(segments))) * std.math.tau;
const angle1 = (@as(f32, @floatFromInt(i + 1)) / @as(f32, @floatFromInt(segments))) * std.math.tau;
const p0 = [2]f32{ center[0] + (@cos(angle0) * radius_x), center[1] + (@sin(angle0) * radius_y) };
const p1 = [2]f32{ center[0] + (@cos(angle1) * radius_x), center[1] + (@sin(angle1) * radius_y) };
try vertices.append(allocator, .{ .position = center, .color = color, .uv = .{ 0.0, 0.0 } });
try vertices.append(allocator, .{ .position = p0, .color = color, .uv = .{ 0.0, 0.0 } });
try vertices.append(allocator, .{ .position = p1, .color = color, .uv = .{ 0.0, 0.0 } });
}
}
}
}
fn appendRectBorder(
vertices: *std.ArrayList(geometry.Vertex),
allocator: std.mem.Allocator,
rect: geometry.BoardRect,
thickness: f32,
color: [4]f32,
) !void {
const x0 = rect.left;
const x1 = rect.left + rect.width;
const y0 = rect.bottom;
const y1 = rect.bottom + rect.height;
try geometry.appendQuad(vertices, allocator, .{ x0, y0 }, .{ x1, y0 }, .{ x1, y0 + thickness }, .{ x0, y0 + thickness }, color);
try geometry.appendQuad(vertices, allocator, .{ x0, y1 - thickness }, .{ x1, y1 - thickness }, .{ x1, y1 }, .{ x0, y1 }, color);
try geometry.appendQuad(vertices, allocator, .{ x0, y0 }, .{ x0 + thickness, y0 }, .{ x0 + thickness, y1 }, .{ x0, y1 }, color);
try geometry.appendQuad(vertices, allocator, .{ x1 - thickness, y0 }, .{ x1, y0 }, .{ x1, y1 }, .{ x1 - thickness, y1 }, color);
}
pub fn appendPromotionPopup(
vertices: *std.ArrayList(geometry.Vertex),
allocator: std.mem.Allocator,
board_rect: geometry.BoardRect,
square: bitboard.Square,
) !void {
const popup_rect = piece_render.promotionPopupRectForSquare(board_rect, square);
try geometry.appendQuad(
vertices,
allocator,
.{ popup_rect.left, popup_rect.bottom },
.{ popup_rect.left + popup_rect.width, popup_rect.bottom },
.{ popup_rect.left + popup_rect.width, popup_rect.bottom + popup_rect.height },
.{ popup_rect.left, popup_rect.bottom + popup_rect.height },
.{ 0.04, 0.04, 0.04, 0.98 },
);
for (piece_render.promotion_piece_types, 0..) |_, i| {
const cell = piece_render.promotionChoiceRectForIndex(board_rect, square, i);
const border_color = geometry.White;
const thickness = board_rect.width / 220.0;
try appendRectBorder(vertices, allocator, cell, thickness, border_color);
}
}
pub fn appendModeMenu(
vertices: *std.ArrayList(geometry.Vertex),
allocator: std.mem.Allocator,
edit_mode: bool,
) !void {
const button_w: f32 = 0.16;
const button_h: f32 = 0.08;
const reset_button_w: f32 = 0.22;
const gap: f32 = 0.025;
const top: f32 = 0.96;
const bottom = top - button_h;
const play_left: f32 = -0.95;
const edit_left = play_left + button_w + gap;
const reset_left = edit_left + button_w + gap;
const inactive = [4]f32{ 0.18, 0.18, 0.18, 1.0 };
const active = [4]f32{ 0.25, 0.45, 0.25, 1.0 };
const reset_color = [4]f32{ 0.25, 0.25, 0.35, 1.0 };
try geometry.appendQuad(
vertices,
allocator,
.{ play_left, bottom },
.{ play_left + button_w, bottom },
.{ play_left + button_w, top },
.{ play_left, top },
if (edit_mode) inactive else active,
);
try geometry.appendQuad(
vertices,
allocator,
.{ edit_left, bottom },
.{ edit_left + button_w, bottom },
.{ edit_left + button_w, top },
.{ edit_left, top },
if (edit_mode) active else inactive,
);
try geometry.appendQuad(
vertices,
allocator,
.{ reset_left, bottom },
.{ reset_left + reset_button_w, bottom },
.{ reset_left + reset_button_w, top },
.{ reset_left, top },
reset_color,
);
const pixel_size: f32 = button_h / 13.0;
const y = bottom + (button_h - pixel_size * 7.0) / 2.0;
try appendText(vertices, allocator, "PLAY", play_left + 0.018, y, .{ .pixel_size = pixel_size, .color = geometry.White });
try appendText(vertices, allocator, "EDIT", edit_left + 0.018, y, .{ .pixel_size = pixel_size, .color = geometry.White });
try appendText(vertices, allocator, "RESET", reset_left + 0.018, y, .{ .pixel_size = pixel_size, .color = geometry.White });
}
pub fn appendBoardCoordinateLabels(
vertices: *std.ArrayList(geometry.Vertex),
allocator: std.mem.Allocator,
board_rect: geometry.BoardRect,
) !void {
const square_w = board_rect.width / 8.0;
const square_h = board_rect.height / 8.0;
const square_size = if (square_w < square_h) square_w else square_h;
const pixel_size = square_size / 36.0;
const padding = square_size / 24.0;
const glyph_w = pixel_size * 5.0;
const glyph_h = pixel_size * 7.0;
const files = "abcdefgh";
for (files, 0..) |file_ch, file| {
const is_dark = (file % 2) == 0;
const color = if (is_dark) geometry.Light else geometry.Dark;
const x = board_rect.left + (@as(f32, @floatFromInt(file + 1)) * square_w) - padding - glyph_w;
const y = board_rect.bottom + padding;
try appendText(vertices, allocator, files[file .. file + 1], x, y, .{
.pixel_size = pixel_size,
.color = color,
});
_ = file_ch;
}
const ranks = "12345678";
for (ranks, 0..) |rank_ch, rank| {
const is_dark = (rank % 2) == 0;
const color = if (is_dark) geometry.Light else geometry.Dark;
const x = board_rect.left + padding;
const y = board_rect.bottom + (@as(f32, @floatFromInt(rank + 1)) * square_h) - padding - glyph_h;
try appendText(vertices, allocator, ranks[rank .. rank + 1], x, y, .{
.pixel_size = pixel_size,
.color = color,
});
_ = rank_ch;
}
}
pub fn appendFenText(
vertices: *std.ArrayList(geometry.Vertex),
allocator: std.mem.Allocator,
board_rect: geometry.BoardRect,
fen_text: []const u8,
) !void {
const max_text_width = board_rect.width;
const pixel_aspect = board_rect.width / board_rect.height;
const natural_pixel_size = board_rect.height / 95.0;
const fit_pixel_size = max_text_width / (@as(f32, @floatFromInt(fen_text.len)) * 6.0 * pixel_aspect);
const pixel_size = if (fit_pixel_size < natural_pixel_size) fit_pixel_size else natural_pixel_size;
const gap = board_rect.height / 18.0;
const y = board_rect.bottom - gap - (pixel_size * 7.0);
try appendText(vertices, allocator, fen_text, board_rect.left, y, .{
.pixel_size = pixel_size,
.pixel_aspect = pixel_aspect,
.color = geometry.White,
});
}
test "appendText appends vertices for supported glyph pixels" {
var vertices: std.ArrayList(geometry.Vertex) = .empty;
defer vertices.deinit(std.testing.allocator);
try appendText(&vertices, std.testing.allocator, "1", 0.0, 0.0, .{
.pixel_size = 0.01,
.color = geometry.White,
});
try std.testing.expect(vertices.items.len > 0);
try std.testing.expectEqual(@as(usize, 0), vertices.items.len % 6);
}
test "appendSelectedSquareHighlight appends one quad when selected" {
var vertices: std.ArrayList(geometry.Vertex) = .empty;
defer vertices.deinit(std.testing.allocator);
try appendSelectedSquareHighlight(
&vertices,
std.testing.allocator,
geometry.boardRectForExtent(800, 600),
.{ .file = 4, .rank = 1 },
);
try std.testing.expectEqual(@as(usize, 6), vertices.items.len);
}
test "appendSelectedSquareHighlight appends nothing without selection" {
var vertices: std.ArrayList(geometry.Vertex) = .empty;
defer vertices.deinit(std.testing.allocator);
try appendSelectedSquareHighlight(
&vertices,
std.testing.allocator,
geometry.boardRectForExtent(800, 600),
null,
);
try std.testing.expectEqual(@as(usize, 0), vertices.items.len);
}
test "appendHoveredSquareHighlight appends one quad when hovered" {
var vertices: std.ArrayList(geometry.Vertex) = .empty;
defer vertices.deinit(std.testing.allocator);
try appendHoveredSquareHighlight(
&vertices,
std.testing.allocator,
geometry.boardRectForExtent(800, 600),
.{ .file = 2, .rank = 2 },
);
try std.testing.expectEqual(@as(usize, 6), vertices.items.len);
}
test "appendCheckBorder appends four border quads when checked" {
var vertices: std.ArrayList(geometry.Vertex) = .empty;
defer vertices.deinit(std.testing.allocator);
try appendCheckBorder(
&vertices,
std.testing.allocator,
geometry.boardRectForExtent(800, 600),
.{ .file = 4, .rank = 0 },
);
try std.testing.expectEqual(@as(usize, 4 * 6), vertices.items.len);
}
test "appendCheckBorder appends nothing without checked square" {
var vertices: std.ArrayList(geometry.Vertex) = .empty;
defer vertices.deinit(std.testing.allocator);
try appendCheckBorder(
&vertices,
std.testing.allocator,
geometry.boardRectForExtent(800, 600),
null,
);
try std.testing.expectEqual(@as(usize, 0), vertices.items.len);
}
test "appendCheckmateMarker appends hash glyph when winner is present" {
var vertices: std.ArrayList(geometry.Vertex) = .empty;
defer vertices.deinit(std.testing.allocator);
try appendCheckmateMarker(
&vertices,
std.testing.allocator,
geometry.boardRectForExtent(800, 600),
.{ .file = 5, .rank = 5 },
);
try std.testing.expect(vertices.items.len > 0);
}
test "appendCheckmateMarker appends nothing without winner" {
var vertices: std.ArrayList(geometry.Vertex) = .empty;
defer vertices.deinit(std.testing.allocator);
try appendCheckmateMarker(
&vertices,
std.testing.allocator,
geometry.boardRectForExtent(800, 600),
null,
);
try std.testing.expectEqual(@as(usize, 0), vertices.items.len);
}
test "appendValidMoveDots appends one circular triangle fan per move" {
var vertices: std.ArrayList(geometry.Vertex) = .empty;
defer vertices.deinit(std.testing.allocator);
const state = chess_board.BoardState.empty();
const moves = bitboard.bit(20) | bitboard.bit(28);
try appendValidMoveDots(
&vertices,
std.testing.allocator,
geometry.boardRectForExtent(800, 600),
state,
moves,
);
try std.testing.expectEqual(@as(usize, 2 * 48 * 3), vertices.items.len);
}

View File

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

View File

@ -1,4 +1,5 @@
const std = @import("std");
const log = std.log.scoped(.graphics);
const vk = @import("vulkan");
const device = @import("device.zig");
@ -7,6 +8,8 @@ const render_pass = @import("render_pass.zig");
const swapchain = @import("swapchain.zig");
const pipeline = @import("pipeline.zig");
const buffer = @import("buffer.zig");
const descriptors = @import("descriptors.zig");
const piece_render = @import("../piece_render.zig");
pub const CommandContext = struct {
command_pool: vk.CommandPool,
@ -19,13 +22,8 @@ pub const CommandContext = struct {
}
};
pub fn initCommandBuffers(
pub fn initCommandPoolOnly(
ldc: device.LogicalDeviceContext,
render_pass_context: render_pass.RenderPassContext,
framebuffer_context: framebuffers.FramebufferContext,
pipeline_context: pipeline.PipelineContext,
swapchain_context: swapchain.SwapchainContext,
vertex_buffer_context: buffer.VertexBufferContext,
allocator: std.mem.Allocator,
) !CommandContext {
const command_pool_create_info = vk.CommandPoolCreateInfo{
@ -41,32 +39,56 @@ pub fn initCommandBuffers(
null,
);
errdefer ldc.vkd.destroyCommandPool(ldc.device, command_pool, null);
std.debug.print("created command pool\n", .{});
const command_buffers = try allocator.alloc(vk.CommandBuffer, framebuffer_context.framebuffers.len);
const command_buffers = try allocator.alloc(vk.CommandBuffer, 0);
errdefer allocator.free(command_buffers);
const command_buffer_allocate_info = vk.CommandBufferAllocateInfo{
return .{
.command_pool = command_pool,
.command_buffers = command_buffers,
.allocator = allocator,
};
}
pub fn initCommandBuffers(
ldc: device.LogicalDeviceContext,
render_pass_context: render_pass.RenderPassContext,
framebuffer_context: framebuffers.FramebufferContext,
board_pipeline_context: pipeline.PipelineContext,
piece_pipeline_context: pipeline.PipelineContext,
descriptor_contexts: [piece_render.PieceGroupCount]?descriptors.DescriptorContext,
swapchain_context: swapchain.SwapchainContext,
board_vertex_buffer_context: buffer.VertexBufferContext,
piece_vertex_buffers: [piece_render.PieceGroupCount]?buffer.VertexBufferContext,
allocator: std.mem.Allocator,
) !CommandContext {
var command_context = try initCommandPoolOnly(ldc, allocator);
errdefer command_context.destroy(&ldc);
log.debug("created command pool", .{});
allocator.free(command_context.command_buffers);
command_context.command_buffers = try allocator.alloc(vk.CommandBuffer, framebuffer_context.framebuffers.len);
errdefer allocator.free(command_context.command_buffers);
const command_buffer_allocate_info = vk.CommandBufferAllocateInfo{
.command_pool = command_context.command_pool,
.level = .primary,
.command_buffer_count = @intCast(command_buffers.len),
.command_buffer_count = @intCast(command_context.command_buffers.len),
};
try ldc.vkd.allocateCommandBuffers(
ldc.device,
&command_buffer_allocate_info,
command_buffers.ptr,
command_context.command_buffers.ptr,
);
std.debug.print("allocated command buffers: {}\n", .{command_buffers.len});
log.debug("allocated command buffers: {}", .{command_context.command_buffers.len});
for (command_buffers, 0..) |command_buffer, i| {
for (command_context.command_buffers, 0..) |command_buffer, i| {
const begin_info = vk.CommandBufferBeginInfo{};
try ldc.vkd.beginCommandBuffer(command_buffer, &begin_info);
// This is the only "drawing" currently happening: begin a render pass
// and clear the swapchain image. The embedded shaders are not used yet.
const clear_color = vk.ClearValue{
.color = .{
.float_32 = .{ 0.02, 0.02, 0.08, 1.0 },
@ -93,36 +115,107 @@ pub fn initCommandBuffers(
ldc.vkd.cmdBindPipeline(
command_buffer,
.graphics,
pipeline_context.graphics_pipeline,
board_pipeline_context.graphics_pipeline,
);
const vertex_buffers = [_]vk.Buffer{
vertex_buffer_context.buffer,
};
const offsets = [_]vk.DeviceSize{
0,
};
const board_vertex_buffers = [_]vk.Buffer{board_vertex_buffer_context.buffer};
const board_offsets = [_]vk.DeviceSize{0};
ldc.vkd.cmdBindVertexBuffers(
command_buffer,
0,
&vertex_buffers,
&offsets,
&board_vertex_buffers,
&board_offsets,
);
ldc.vkd.cmdDraw(command_buffer, vertex_buffer_context.vertex_count, 1, 0, 0);
ldc.vkd.cmdDraw(command_buffer, board_vertex_buffer_context.vertex_count, 1, 0, 0);
ldc.vkd.cmdBindPipeline(
command_buffer,
.graphics,
piece_pipeline_context.graphics_pipeline,
);
for (piece_vertex_buffers, descriptor_contexts) |maybe_piece_buffer, maybe_descriptor_context| {
const piece_buffer = maybe_piece_buffer orelse continue;
const piece_descriptor_context = maybe_descriptor_context orelse continue;
const descriptor_sets = [_]vk.DescriptorSet{piece_descriptor_context.descriptor_set};
ldc.vkd.cmdBindDescriptorSets(
command_buffer,
.graphics,
piece_pipeline_context.pipeline_layout,
0,
&descriptor_sets,
null,
);
const piece_buffers = [_]vk.Buffer{piece_buffer.buffer};
const piece_offsets = [_]vk.DeviceSize{0};
ldc.vkd.cmdBindVertexBuffers(
command_buffer,
0,
&piece_buffers,
&piece_offsets,
);
ldc.vkd.cmdDraw(command_buffer, piece_buffer.vertex_count, 1, 0, 0);
}
ldc.vkd.cmdEndRenderPass(command_buffer);
try ldc.vkd.endCommandBuffer(command_buffer);
}
std.debug.print("recorded command buffers\n", .{});
log.debug("recorded command buffers", .{});
return .{
.command_pool = command_pool,
.command_buffers = command_buffers,
.allocator = allocator,
};
return command_context;
}
pub fn beginSingleTimeCommands(
ldc: device.LogicalDeviceContext,
command_context: CommandContext,
) !vk.CommandBuffer {
const command_buffer_allocate_info = vk.CommandBufferAllocateInfo{
.command_pool = command_context.command_pool,
.level = .primary,
.command_buffer_count = 1,
};
var command_buffer: vk.CommandBuffer = undefined;
try ldc.vkd.allocateCommandBuffers(
ldc.device,
&command_buffer_allocate_info,
@ptrCast(&command_buffer),
);
const begin_info = vk.CommandBufferBeginInfo{
.flags = .{
.one_time_submit_bit = true,
},
};
try ldc.vkd.beginCommandBuffer(command_buffer, &begin_info);
return command_buffer;
}
pub fn endSingleTimeCommands(
ldc: device.LogicalDeviceContext,
command_context: CommandContext,
command_buffer: vk.CommandBuffer,
) !void {
try ldc.vkd.endCommandBuffer(command_buffer);
const command_buffers = [_]vk.CommandBuffer{command_buffer};
const submit_info = vk.SubmitInfo{
.command_buffer_count = command_buffers.len,
.p_command_buffers = &command_buffers,
};
try ldc.vkd.queueSubmit(ldc.graphics_queue, &[_]vk.SubmitInfo{submit_info}, .null_handle);
try ldc.vkd.queueWaitIdle(ldc.graphics_queue);
ldc.vkd.freeCommandBuffers(ldc.device, command_context.command_pool, &command_buffers);
}

View File

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

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

View File

@ -1,4 +1,5 @@
const std = @import("std");
const log = std.log.scoped(.graphics);
const vk = @import("vulkan");
const commands = @import("commands.zig");
@ -33,7 +34,7 @@ pub fn drawFrame(
const image_index = image_result.image_index;
const should_recreate_after_present = image_result.result == .suboptimal_khr;
std.debug.print("Acquired swapchain image: {}\n", .{image_index});
log.debug("Acquired swapchain image: {}", .{image_index});
try ldc.vkd.resetFences(ldc.device, &wait_fences);
@ -76,7 +77,7 @@ pub fn drawFrame(
error.OutOfDateKHR => return .Recreate,
else => return err,
};
std.debug.print("Presented one frame\n", .{});
log.debug("Presented one frame", .{});
if (should_recreate_after_present or present_result == .suboptimal_khr) {
return FrameResult.Recreate;

View File

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

View File

@ -7,10 +7,14 @@ const geometry = @import("../geometry.zig");
pub const PipelineContext = struct {
graphics_pipeline: vk.Pipeline,
pipeline_layout: vk.PipelineLayout,
descriptor_set_layout: vk.DescriptorSetLayout = .null_handle,
pub fn destroy(self: *const PipelineContext, ldc: *const device.LogicalDeviceContext) void {
ldc.vkd.destroyPipeline(ldc.device, self.graphics_pipeline, null);
ldc.vkd.destroyPipelineLayout(ldc.device, self.pipeline_layout, null);
if (self.descriptor_set_layout != .null_handle) {
ldc.vkd.destroyDescriptorSetLayout(ldc.device, self.descriptor_set_layout, null);
}
}
};
@ -31,8 +35,112 @@ pub fn createShaderModule(ldc: device.LogicalDeviceContext, allocator: std.mem.A
return try ldc.vkd.createShaderModule(ldc.device, &create_info, null);
}
pub fn initPipelineContext(ldc: device.LogicalDeviceContext, extent: vk.Extent2D, render_pass: vk.RenderPass, vert_spv: []const u8, frag_spv: []const u8, allocator: std.mem.Allocator) !PipelineContext {
const pipeline_layout_create_info = vk.PipelineLayoutCreateInfo{};
pub fn initBoardPipelineContext(ldc: device.LogicalDeviceContext, extent: vk.Extent2D, render_pass: vk.RenderPass, vert_spv: []const u8, frag_spv: []const u8, allocator: std.mem.Allocator) !PipelineContext {
const attribute_descriptions = [_]vk.VertexInputAttributeDescription{
.{
.location = 0,
.binding = 0,
.format = .r32g32_sfloat,
.offset = @offsetOf(geometry.Vertex, "position"),
},
.{
.location = 1,
.binding = 0,
.format = .r32g32b32a32_sfloat,
.offset = @offsetOf(geometry.Vertex, "color"),
},
};
return try initPipelineContext(
ldc,
extent,
render_pass,
vert_spv,
frag_spv,
allocator,
null,
&attribute_descriptions,
);
}
pub fn initPiecePipelineContext(ldc: device.LogicalDeviceContext, extent: vk.Extent2D, render_pass: vk.RenderPass, vert_spv: []const u8, frag_spv: []const u8, allocator: std.mem.Allocator) !PipelineContext {
const sampler_layout_binding = vk.DescriptorSetLayoutBinding{
.binding = 0,
.descriptor_type = .combined_image_sampler,
.descriptor_count = 1,
.stage_flags = .{
.fragment_bit = true,
},
.p_immutable_samplers = null,
};
const descriptor_set_layout_bindings = [_]vk.DescriptorSetLayoutBinding{
sampler_layout_binding,
};
const descriptor_set_layout_create_info = vk.DescriptorSetLayoutCreateInfo{
.binding_count = descriptor_set_layout_bindings.len,
.p_bindings = &descriptor_set_layout_bindings,
};
const descriptor_set_layout = try ldc.vkd.createDescriptorSetLayout(
ldc.device,
&descriptor_set_layout_create_info,
null,
);
errdefer ldc.vkd.destroyDescriptorSetLayout(ldc.device, descriptor_set_layout, null);
const descriptor_set_layouts = [_]vk.DescriptorSetLayout{
descriptor_set_layout,
};
const attribute_descriptions = [_]vk.VertexInputAttributeDescription{
.{
.location = 0,
.binding = 0,
.format = .r32g32_sfloat,
.offset = @offsetOf(geometry.Vertex, "position"),
},
.{
.location = 1,
.binding = 0,
.format = .r32g32b32a32_sfloat,
.offset = @offsetOf(geometry.Vertex, "color"),
},
.{
.location = 2,
.binding = 0,
.format = .r32g32_sfloat,
.offset = @offsetOf(geometry.Vertex, "uv"),
},
};
return try initPipelineContext(
ldc,
extent,
render_pass,
vert_spv,
frag_spv,
allocator,
&descriptor_set_layouts,
&attribute_descriptions,
);
}
fn initPipelineContext(
ldc: device.LogicalDeviceContext,
extent: vk.Extent2D,
render_pass: vk.RenderPass,
vert_spv: []const u8,
frag_spv: []const u8,
allocator: std.mem.Allocator,
descriptor_set_layouts: ?[]const vk.DescriptorSetLayout,
attribute_descriptions: []const vk.VertexInputAttributeDescription,
) !PipelineContext {
const pipeline_layout_create_info = vk.PipelineLayoutCreateInfo{
.set_layout_count = if (descriptor_set_layouts) |layouts| @intCast(layouts.len) else 0,
.p_set_layouts = if (descriptor_set_layouts) |layouts| layouts.ptr else null,
};
const pipeline_layout = try ldc.vkd.createPipelineLayout(
ldc.device,
@ -70,18 +178,11 @@ pub fn initPipelineContext(ldc: device.LogicalDeviceContext, extent: vk.Extent2D
.input_rate = .vertex,
};
const attribute_description = vk.VertexInputAttributeDescription{
.location = 0,
.binding = 0,
.format = .r32g32_sfloat,
.offset = @offsetOf(geometry.Vertex, "position"),
};
const vertex_input_info = vk.PipelineVertexInputStateCreateInfo{
.vertex_binding_description_count = 1,
.p_vertex_binding_descriptions = @ptrCast(&binding_description),
.vertex_attribute_description_count = 1,
.p_vertex_attribute_descriptions = @ptrCast(&attribute_description),
.vertex_attribute_description_count = @intCast(attribute_descriptions.len),
.p_vertex_attribute_descriptions = attribute_descriptions.ptr,
};
const input_assembly = vk.PipelineInputAssemblyStateCreateInfo{
@ -91,9 +192,9 @@ pub fn initPipelineContext(ldc: device.LogicalDeviceContext, extent: vk.Extent2D
const viewport = vk.Viewport{
.x = 0.0,
.y = 0.0,
.y = @floatFromInt(extent.height),
.width = @floatFromInt(extent.width),
.height = @floatFromInt(extent.height),
.height = -@as(f32, @floatFromInt(extent.height)),
.min_depth = 0.0,
.max_depth = 1.0,
};
@ -115,9 +216,7 @@ pub fn initPipelineContext(ldc: device.LogicalDeviceContext, extent: vk.Extent2D
.rasterizer_discard_enable = .false,
.polygon_mode = .fill,
.line_width = 1.0,
.cull_mode = .{
.back_bit = true,
},
.cull_mode = .{},
.front_face = .clockwise,
.depth_bias_enable = .false,
.depth_bias_constant_factor = 0.0,
@ -141,9 +240,9 @@ pub fn initPipelineContext(ldc: device.LogicalDeviceContext, extent: vk.Extent2D
.b_bit = true,
.a_bit = true,
},
.blend_enable = .false,
.src_color_blend_factor = .one,
.dst_color_blend_factor = .zero,
.blend_enable = .true,
.src_color_blend_factor = .src_alpha,
.dst_color_blend_factor = .one_minus_src_alpha,
.color_blend_op = .add,
.src_alpha_blend_factor = .one,
.dst_alpha_blend_factor = .zero,
@ -176,10 +275,7 @@ pub fn initPipelineContext(ldc: device.LogicalDeviceContext, extent: vk.Extent2D
.base_pipeline_index = -1,
};
const pipeline_infos = [_]vk.GraphicsPipelineCreateInfo{
pipeline_info,
};
const pipeline_infos = [_]vk.GraphicsPipelineCreateInfo{pipeline_info};
var pipelines: [1]vk.Pipeline = undefined;
_ = try ldc.vkd.createGraphicsPipelines(
@ -193,5 +289,6 @@ pub fn initPipelineContext(ldc: device.LogicalDeviceContext, extent: vk.Extent2D
return .{
.graphics_pipeline = pipelines[0],
.pipeline_layout = pipeline_layout,
.descriptor_set_layout = if (descriptor_set_layouts) |layouts| layouts[0] else .null_handle,
};
}

View File

@ -1,4 +1,5 @@
const std = @import("std");
const log = std.log.scoped(.graphics);
const vk = @import("vulkan");
const device = @import("device.zig");
@ -59,7 +60,7 @@ pub fn initRenderPass(ldc: device.LogicalDeviceContext, format: vk.Format) !Rend
};
const render_pass = try ldc.vkd.createRenderPass(ldc.device, &render_pass_create_info, null);
std.debug.print("created render pass\n", .{});
log.debug("created render pass", .{});
return .{ .render_pass = render_pass };
}

View File

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

View File

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

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

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