From b9e0c5de4429e15dbff54935d7b1b1293286e593 Mon Sep 17 00:00:00 2001 From: Chander Ramesh Date: Sat, 29 Nov 2025 15:58:33 -0800 Subject: [PATCH 1/4] Modify moves encoding to handle resignation --- docs-src/examples/preset-moves.md | 2 + docs/examples/change-look/index.html | 12 +++--- docs/examples/custom-pieces/index.html | 16 ++++---- docs/examples/darkmode/index.html | 16 ++++---- docs/examples/hexfen/index.html | 16 ++++---- docs/examples/index.html | 16 ++++---- docs/examples/preset-moves/index.html | 17 +++++---- src/test/utils.test.ts | 51 +++++++++++++++++++++++++- src/utils.ts | 16 +++++++- 9 files changed, 113 insertions(+), 49 deletions(-) diff --git a/docs-src/examples/preset-moves.md b/docs-src/examples/preset-moves.md index 40f329a..7d34491 100644 --- a/docs-src/examples/preset-moves.md +++ b/docs-src/examples/preset-moves.md @@ -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
Example: Customizing colors
  • - Override the built-in set with slots + Dark and light modes are both supported.
  • - Dark and light modes are both supported. + Common keyboard operations
  • - Customizing the board layout with Hex-FEN + Set player roles
  • - Common keyboard operations + Analyzing a game that has a specific set of moves played.
  • - Set player roles + Override the built-in set with slots
  • - Analyzing a game that has a specific set of moves played. + Customizing the board layout with Hex-FEN
  • diff --git a/docs/examples/custom-pieces/index.html b/docs/examples/custom-pieces/index.html index 47ee634..8aed95c 100644 --- a/docs/examples/custom-pieces/index.html +++ b/docs/examples/custom-pieces/index.html @@ -38,18 +38,10 @@

    Example: Custom pieces

    Changing the look and feel of the board -
  • - Override the built-in set with slots -
  • -
  • Dark and light modes are both supported.
  • -
  • - Customizing the board layout with Hex-FEN -
  • -
  • Common keyboard operations
  • @@ -62,6 +54,14 @@

    Example: Custom pieces

    Analyzing a game that has a specific set of moves played. +
  • + Override the built-in set with slots +
  • + +
  • + Customizing the board layout with Hex-FEN +
  • +
    diff --git a/docs/examples/darkmode/index.html b/docs/examples/darkmode/index.html index 66d3d93..389c871 100644 --- a/docs/examples/darkmode/index.html +++ b/docs/examples/darkmode/index.html @@ -38,18 +38,10 @@

    Example: Dark Mode

    Changing the look and feel of the board -
  • - Override the built-in set with slots -
  • -
  • Dark and light modes are both supported.
  • -
  • - Customizing the board layout with Hex-FEN -
  • -
  • Common keyboard operations
  • @@ -62,6 +54,14 @@

    Example: Dark Mode

    Analyzing a game that has a specific set of moves played. +
  • + Override the built-in set with slots +
  • + +
  • + Customizing the board layout with Hex-FEN +
  • +
    diff --git a/docs/examples/hexfen/index.html b/docs/examples/hexfen/index.html index bf4f181..acf1825 100644 --- a/docs/examples/hexfen/index.html +++ b/docs/examples/hexfen/index.html @@ -38,18 +38,10 @@

    Example: Hex-FEN Notation

    Changing the look and feel of the board -
  • - Override the built-in set with slots -
  • -
  • Dark and light modes are both supported.
  • -
  • - Customizing the board layout with Hex-FEN -
  • -
  • Common keyboard operations
  • @@ -62,6 +54,14 @@

    Example: Hex-FEN Notation

    Analyzing a game that has a specific set of moves played. +
  • + Override the built-in set with slots +
  • + +
  • + Customizing the board layout with Hex-FEN +
  • +
    diff --git a/docs/examples/index.html b/docs/examples/index.html index 15351c5..c9f5d87 100644 --- a/docs/examples/index.html +++ b/docs/examples/index.html @@ -38,18 +38,10 @@

    Example: Keyboard input

    Changing the look and feel of the board -
  • - Override the built-in set with slots -
  • -
  • Dark and light modes are both supported.
  • -
  • - Customizing the board layout with Hex-FEN -
  • -
  • Common keyboard operations
  • @@ -62,6 +54,14 @@

    Example: Keyboard input

    Analyzing a game that has a specific set of moves played. +
  • + Override the built-in set with slots +
  • + +
  • + Customizing the board layout with Hex-FEN +
  • +
    diff --git a/docs/examples/preset-moves/index.html b/docs/examples/preset-moves/index.html index 0c524a1..22ede5f 100644 --- a/docs/examples/preset-moves/index.html +++ b/docs/examples/preset-moves/index.html @@ -38,18 +38,10 @@

    Example: Preset Moves

    Changing the look and feel of the board -
  • - Override the built-in set with slots -
  • -
  • Dark and light modes are both supported.
  • -
  • - Customizing the board layout with Hex-FEN -
  • -
  • Common keyboard operations
  • @@ -62,6 +54,14 @@

    Example: Preset Moves

    Analyzing a game that has a specific set of moves played. +
  • + Override the built-in set with slots +
  • + +
  • + Customizing the board layout with Hex-FEN +
  • +
    @@ -96,6 +96,7 @@

    Example: Preset Moves

    En passant captures are notated with an $ at the end. This is done to simplify the format of notations into a standard of a square, followed by x or -, followed by a square, and then either $ or =.

    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.

    <div style="width: 575px; height: 500px">
    <hexchess-board
    id="hexchess-board"
    board="start"
    orientation="white"
    moves="B1-B3,B7-B5,C2-C4,D7-D5,C4xD5p,F11xB3P,C1xC7p"
    >
    </hexchess-board>
    </div>
    { 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('Converts a CSV string to moves properly', () => { @@ -79,6 +81,53 @@ 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('Invalid move: R'); + }); + test('fenToBoard falls back to an empty board for invalid input', () => { expect(fenToBoard('invalid').numPieces()).toBe(0); expect(fenToBoard(null).numPieces()).toBe(0); diff --git a/src/utils.ts b/src/utils.ts index 4aa776b..d03c629 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -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', @@ -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 @@ -202,6 +204,9 @@ export const movesToString = (moves: Move[]): string => { } result.push(newString); } + if (resigned) { + result.push(RESIGNATION_MARKER); + } return result.join(','); }; @@ -214,7 +219,14 @@ 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(`Invalid move: ${move}`); + } + break; + } if (!moveRegex.test(move)) { throw new Error(`Invalid move: ${move}`); } From 31f3666a0caa748a28cdc17c1404ef36b38dffae Mon Sep 17 00:00:00 2001 From: Chander Ramesh Date: Sat, 13 Dec 2025 09:43:05 -0800 Subject: [PATCH 2/4] Use better error message --- src/test/utils.test.ts | 2 +- src/utils.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/test/utils.test.ts b/src/test/utils.test.ts index 10900dd..9b36922 100644 --- a/src/test/utils.test.ts +++ b/src/test/utils.test.ts @@ -125,7 +125,7 @@ describe('Utils', () => { expect(stringToMoves(`${base},R`)).toEqual(expected); expect(stringToMoves('R')).toEqual([]); - expect(() => stringToMoves('R,B1-B3')).toThrow('Invalid move: R'); + 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', () => { diff --git a/src/utils.ts b/src/utils.ts index d03c629..4ea9972 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -223,7 +223,7 @@ export const stringToMoves = (movesStr: string): Move[] => { const move = moves[i]; if (move === RESIGNATION_MARKER) { if (i !== moves.length - 1) { - throw new Error(`Invalid move: ${move}`); + throw new Error('Cannot process resignation when subsequent moves still exist.'); } break; } From 4a4aef19c2379eed432d0ef0d5a2990330bd411b Mon Sep 17 00:00:00 2001 From: Chander Ramesh Date: Sat, 13 Dec 2025 09:47:36 -0800 Subject: [PATCH 3/4] add round trip test --- src/test/utils.test.ts | 61 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/src/test/utils.test.ts b/src/test/utils.test.ts index 9b36922..b70684c 100644 --- a/src/test/utils.test.ts +++ b/src/test/utils.test.ts @@ -37,6 +37,67 @@ describe('Utils', () => { 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', () => { const moveStr = 'B1-B3,C7-C5,B3-B4,C5xB4P,D3-D5,E7-E5,D5xE6p$,B4-B3,E6xF7p,B3-B2,F7xG7p,B2-B1=q'; From d5b5137cb0f213e3d3038ae806851b1aef8a8b2d Mon Sep 17 00:00:00 2001 From: Chander Ramesh Date: Sat, 13 Dec 2025 09:50:15 -0800 Subject: [PATCH 4/4] format --- src/test/utils.test.ts | 4 +++- src/utils.ts | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/test/utils.test.ts b/src/test/utils.test.ts index b70684c..da19f35 100644 --- a/src/test/utils.test.ts +++ b/src/test/utils.test.ts @@ -186,7 +186,9 @@ describe('Utils', () => { expect(stringToMoves(`${base},R`)).toEqual(expected); expect(stringToMoves('R')).toEqual([]); - expect(() => stringToMoves('R,B1-B3')).toThrow('Cannot process resignation when subsequent moves still exist.'); + 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', () => { diff --git a/src/utils.ts b/src/utils.ts index 4ea9972..aec6c5a 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -223,7 +223,9 @@ export const stringToMoves = (movesStr: string): Move[] => { const move = moves[i]; if (move === RESIGNATION_MARKER) { if (i !== moves.length - 1) { - throw new Error('Cannot process resignation when subsequent moves still exist.'); + throw new Error( + 'Cannot process resignation when subsequent moves still exist.', + ); } break; }