Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs-src/examples/preset-moves.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ Since hexagonal chess has no castling, we do not need to deal with that notation

Lastly, promotion is handled with the `=` sign. So a white pawn that moved from `A5` to `A6` and became a Rook would be written as `A5-A6=R`.

If a player resigns instead of being checkmated, append `,R` to the end of the moves list. The alternating turn order already encodes which player resigned, so the marker only needs to appear once at the end.

```html
<div style="width: 575px; height: 500px">
<hexchess-board
Expand Down
12 changes: 6 additions & 6 deletions docs/examples/change-look/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -39,27 +39,27 @@ <h1>Example: Customizing colors</h1>
</li>

<li class=>
<a href="../custom-pieces/">Override the built-in set with slots</a>
<a href="../darkmode/">Dark and light modes are both supported.</a>
</li>

<li class=>
<a href="../darkmode/">Dark and light modes are both supported.</a>
<a href="../">Common keyboard operations</a>
</li>

<li class=>
<a href="../hexfen/">Customizing the board layout with Hex-FEN</a>
<a href="../player-roles/">Set player roles</a>
</li>

<li class=>
<a href="../">Common keyboard operations</a>
<a href="../preset-moves/">Analyzing a game that has a specific set of moves played.</a>
</li>

<li class=>
<a href="../player-roles/">Set player roles</a>
<a href="../custom-pieces/">Override the built-in set with slots</a>
</li>

<li class=>
<a href="../preset-moves/">Analyzing a game that has a specific set of moves played.</a>
<a href="../hexfen/">Customizing the board layout with Hex-FEN</a>
</li>

</ul>
Expand Down
16 changes: 8 additions & 8 deletions docs/examples/custom-pieces/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -38,18 +38,10 @@ <h1>Example: Custom pieces</h1>
<a href="../change-look/">Changing the look and feel of the board</a>
</li>

<li class=selected>
<a href="">Override the built-in set with slots</a>
</li>

<li class=>
<a href="../darkmode/">Dark and light modes are both supported.</a>
</li>

<li class=>
<a href="../hexfen/">Customizing the board layout with Hex-FEN</a>
</li>

<li class=>
<a href="../">Common keyboard operations</a>
</li>
Expand All @@ -62,6 +54,14 @@ <h1>Example: Custom pieces</h1>
<a href="../preset-moves/">Analyzing a game that has a specific set of moves played.</a>
</li>

<li class=selected>
<a href="">Override the built-in set with slots</a>
</li>

<li class=>
<a href="../hexfen/">Customizing the board layout with Hex-FEN</a>
</li>

</ul>
</nav>
<div>
Expand Down
16 changes: 8 additions & 8 deletions docs/examples/darkmode/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -38,18 +38,10 @@ <h1>Example: Dark Mode</h1>
<a href="../change-look/">Changing the look and feel of the board</a>
</li>

<li class=>
<a href="../custom-pieces/">Override the built-in set with slots</a>
</li>

<li class=selected>
<a href="">Dark and light modes are both supported.</a>
</li>

<li class=>
<a href="../hexfen/">Customizing the board layout with Hex-FEN</a>
</li>

<li class=>
<a href="../">Common keyboard operations</a>
</li>
Expand All @@ -62,6 +54,14 @@ <h1>Example: Dark Mode</h1>
<a href="../preset-moves/">Analyzing a game that has a specific set of moves played.</a>
</li>

<li class=>
<a href="../custom-pieces/">Override the built-in set with slots</a>
</li>

<li class=>
<a href="../hexfen/">Customizing the board layout with Hex-FEN</a>
</li>

</ul>
</nav>
<div>
Expand Down
16 changes: 8 additions & 8 deletions docs/examples/hexfen/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -38,18 +38,10 @@ <h1>Example: Hex-FEN Notation</h1>
<a href="../change-look/">Changing the look and feel of the board</a>
</li>

<li class=>
<a href="../custom-pieces/">Override the built-in set with slots</a>
</li>

<li class=>
<a href="../darkmode/">Dark and light modes are both supported.</a>
</li>

<li class=selected>
<a href="">Customizing the board layout with Hex-FEN</a>
</li>

<li class=>
<a href="../">Common keyboard operations</a>
</li>
Expand All @@ -62,6 +54,14 @@ <h1>Example: Hex-FEN Notation</h1>
<a href="../preset-moves/">Analyzing a game that has a specific set of moves played.</a>
</li>

<li class=>
<a href="../custom-pieces/">Override the built-in set with slots</a>
</li>

<li class=selected>
<a href="">Customizing the board layout with Hex-FEN</a>
</li>

</ul>
</nav>
<div>
Expand Down
16 changes: 8 additions & 8 deletions docs/examples/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -38,18 +38,10 @@ <h1>Example: Keyboard input</h1>
<a href="change-look/">Changing the look and feel of the board</a>
</li>

<li class=>
<a href="custom-pieces/">Override the built-in set with slots</a>
</li>

<li class=>
<a href="darkmode/">Dark and light modes are both supported.</a>
</li>

<li class=>
<a href="hexfen/">Customizing the board layout with Hex-FEN</a>
</li>

<li class=selected>
<a href="">Common keyboard operations</a>
</li>
Expand All @@ -62,6 +54,14 @@ <h1>Example: Keyboard input</h1>
<a href="preset-moves/">Analyzing a game that has a specific set of moves played.</a>
</li>

<li class=>
<a href="custom-pieces/">Override the built-in set with slots</a>
</li>

<li class=>
<a href="hexfen/">Customizing the board layout with Hex-FEN</a>
</li>

</ul>
</nav>
<div>
Expand Down
17 changes: 9 additions & 8 deletions docs/examples/preset-moves/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -38,18 +38,10 @@ <h1>Example: Preset Moves</h1>
<a href="../change-look/">Changing the look and feel of the board</a>
</li>

<li class=>
<a href="../custom-pieces/">Override the built-in set with slots</a>
</li>

<li class=>
<a href="../darkmode/">Dark and light modes are both supported.</a>
</li>

<li class=>
<a href="../hexfen/">Customizing the board layout with Hex-FEN</a>
</li>

<li class=>
<a href="../">Common keyboard operations</a>
</li>
Expand All @@ -62,6 +54,14 @@ <h1>Example: Preset Moves</h1>
<a href="">Analyzing a game that has a specific set of moves played.</a>
</li>

<li class=>
<a href="../custom-pieces/">Override the built-in set with slots</a>
</li>

<li class=>
<a href="../hexfen/">Customizing the board layout with Hex-FEN</a>
</li>

</ul>
</nav>
<div>
Expand Down Expand Up @@ -96,6 +96,7 @@ <h1>Example: Preset Moves</h1>
<p>En passant captures are notated with an <code>$</code> at the end. This is done to simplify the format of notations into a standard of a square, followed by <code>x</code> or <code>-</code>, followed by a square, and then either <code>$</code> or <code>=</code>.</p>
<p>Since hexagonal chess has no castling, we do not need to deal with that notation.</p>
<p>Lastly, promotion is handled with the <code>=</code> sign. So a white pawn that moved from <code>A5</code> to <code>A6</code> and became a Rook would be written as <code>A5-A6=R</code>.</p>
<p>If a player resigns instead of being checkmated, append <code>,R</code> to the end of the moves list. The alternating turn order already encodes which player resigned, so the marker only needs to appear once at the end.</p>
<pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>div</span> <span class="token special-attr"><span class="token attr-name">style</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span><span class="token value css language-css"><span class="token property">width</span><span class="token punctuation">:</span> 575px<span class="token punctuation">;</span> <span class="token property">height</span><span class="token punctuation">:</span> 500px</span><span class="token punctuation">"</span></span></span><span class="token punctuation">></span></span><br> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>hexchess-board</span><br> <span class="token attr-name">id</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>hexchess-board<span class="token punctuation">"</span></span><br> <span class="token attr-name">board</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>start<span class="token punctuation">"</span></span><br> <span class="token attr-name">orientation</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>white<span class="token punctuation">"</span></span><br> <span class="token attr-name">moves</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>B1-B3,B7-B5,C2-C4,D7-D5,C4xD5p,F11xB3P,C1xC7p<span class="token punctuation">"</span></span><br> <span class="token punctuation">></span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>hexchess-board</span><span class="token punctuation">></span></span><br><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>div</span><span class="token punctuation">></span></span></code></pre>
<div style="width: 575px; height: 500px">
<hexchess-board
Expand Down
114 changes: 113 additions & 1 deletion src/test/utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,70 @@ describe('Utils', () => {
promotion: 'N',
},
];
expect(movesToString(moves)).toBe('A1-A2,B2-B1=q,C2-C4,D4xC3P$,B6xA6r=N');
const encoded = 'A1-A2,B2-B1=q,C2-C4,D4xC3P$,B6xA6r=N';
expect(movesToString(moves)).toBe(encoded);
expect(movesToString(moves, true)).toBe(`${encoded},R`);
});

test('Move strings round-trip without losing information', () => {
const moves: Move[] = [
{ from: 'B1', to: 'B3', enPassant: false, promotion: null },
{ from: 'C7', to: 'C5', enPassant: false, promotion: null },
{ from: 'B3', to: 'B4', enPassant: false, promotion: null },
{
from: 'C5',
to: 'B4',
capturedPiece: 'P',
enPassant: false,
promotion: null,
},
{ from: 'D3', to: 'D5', enPassant: false, promotion: null },
{ from: 'E7', to: 'E5', enPassant: false, promotion: null },
{
from: 'D5',
to: 'E6',
capturedPiece: 'p',
enPassant: true,
promotion: null,
},
{ from: 'B4', to: 'B3', enPassant: false, promotion: null },
{
from: 'E6',
to: 'F7',
capturedPiece: 'p',
enPassant: false,
promotion: null,
},
{ from: 'B3', to: 'B2', enPassant: false, promotion: null },
{
from: 'F7',
to: 'G7',
capturedPiece: 'p',
enPassant: false,
promotion: null,
},
{ from: 'B2', to: 'B1', enPassant: false, promotion: 'q' },
];

const encoded = movesToString(moves);
expect(stringToMoves(encoded)).toEqual(moves);
});

test('Move strings round-trip when resignation is included', () => {
const moves: Move[] = [
{ from: 'A1', to: 'A2', enPassant: false, promotion: null },
{
from: 'B2',
to: 'A3',
capturedPiece: 'n',
enPassant: false,
promotion: null,
},
];

const encoded = movesToString(moves, true);
expect(encoded.endsWith(',R')).toBe(true);
expect(stringToMoves(encoded)).toEqual(moves);
});

test('Converts a CSV string to moves properly', () => {
Expand Down Expand Up @@ -79,6 +142,55 @@ describe('Utils', () => {
expect(stringToMoves(moveStr)).toEqual(moves);
});

test('stringToMoves handles a resignation marker', () => {
const base =
'B1-B3,C7-C5,B3-B4,C5xB4P,D3-D5,E7-E5,D5xE6p$,B4-B3,E6xF7p,B3-B2,F7xG7p,B2-B1=q';
const expected: Move[] = [
{ from: 'B1', to: 'B3', enPassant: false, promotion: null },
{ from: 'C7', to: 'C5', enPassant: false, promotion: null },
{ from: 'B3', to: 'B4', enPassant: false, promotion: null },
{
from: 'C5',
to: 'B4',
capturedPiece: 'P',
enPassant: false,
promotion: null,
},
{ from: 'D3', to: 'D5', enPassant: false, promotion: null },
{ from: 'E7', to: 'E5', enPassant: false, promotion: null },
{
from: 'D5',
to: 'E6',
capturedPiece: 'p',
enPassant: true,
promotion: null,
},
{ from: 'B4', to: 'B3', enPassant: false, promotion: null },
{
from: 'E6',
to: 'F7',
capturedPiece: 'p',
enPassant: false,
promotion: null,
},
{ from: 'B3', to: 'B2', enPassant: false, promotion: null },
{
from: 'F7',
to: 'G7',
capturedPiece: 'p',
enPassant: false,
promotion: null,
},
{ from: 'B2', to: 'B1', enPassant: false, promotion: 'q' },
];

expect(stringToMoves(`${base},R`)).toEqual(expected);
expect(stringToMoves('R')).toEqual([]);
expect(() => stringToMoves('R,B1-B3')).toThrow(
'Cannot process resignation when subsequent moves still exist.',
);
});

test('fenToBoard falls back to an empty board for invalid input', () => {
expect(fenToBoard('invalid').numPieces()).toBe(0);
expect(fenToBoard(null).numPieces()).toBe(0);
Expand Down
18 changes: 16 additions & 2 deletions src/utils.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { Board } from './board';
import type { Color, Move, Piece } from './types';

export const RESIGNATION_MARKER = 'R';

export const COLUMN_ARRAY = [
'A',
'B',
Expand Down Expand Up @@ -188,7 +190,7 @@ export const fenToBoard = (position: string | null): Board => {
return converted;
};

export const movesToString = (moves: Move[]): string => {
export const movesToString = (moves: Move[], resigned = false): string => {
const result: string[] = [];
for (const move of moves) {
let newString = move.capturedPiece
Expand All @@ -202,6 +204,9 @@ export const movesToString = (moves: Move[]): string => {
}
result.push(newString);
}
if (resigned) {
result.push(RESIGNATION_MARKER);
}
return result.join(',');
};

Expand All @@ -214,7 +219,16 @@ export const stringToMoves = (movesStr: string): Move[] => {

const moves = movesStr.split(',');
const result: Move[] = [];
for (const move of moves) {
for (let i = 0; i < moves.length; i++) {
const move = moves[i];
if (move === RESIGNATION_MARKER) {
if (i !== moves.length - 1) {
throw new Error(
'Cannot process resignation when subsequent moves still exist.',
);
}
break;
}
if (!moveRegex.test(move)) {
throw new Error(`Invalid move: ${move}`);
}
Expand Down