From d938c50b8fec36003316b8bd459f5e09e6135406 Mon Sep 17 00:00:00 2001 From: Gavin Date: Wed, 14 Jun 2023 18:18:39 +0100 Subject: [PATCH 01/15] Fix issue #144 & optional adherence to FIDE rules --- src/chess.ts | 66 ++++++++++++++++++++++++++++------------------------ 1 file changed, 35 insertions(+), 31 deletions(-) diff --git a/src/chess.ts b/src/chess.ts index b01c6bd..1c4a058 100644 --- a/src/chess.ts +++ b/src/chess.ts @@ -533,6 +533,7 @@ export class Chess { private _header: Record = {} private _kings: Record = { w: EMPTY, b: EMPTY } private _epSquare = -1 + // TODO: Perhaps rename `_halfMoves` to something more descriptive, eg. `_halfMoveClock`? private _halfMoves = 0 private _moveNumber = 0 private _history: History[] = [] @@ -989,53 +990,56 @@ export class Chess { return false } - isThreefoldRepetition() { - const moves = [] - const positions: Record = {} - let repetition = false + private _getRepetitionCount() { + // remove the last two fields in the FEN string, they're not needed when checking for repetition + const trimFen = (fen: string) => fen.split(' ').slice(0, 4).join(' ') + const finalFen = trimFen(this.fen()) + const moves = [] while (true) { const move = this._undoMove() if (!move) break moves.push(move) } + let repetitionCount = 0 while (true) { - /* - * remove the last two fields in the FEN string, they're not needed when - * checking for draw by rep - */ - const fen = this.fen().split(' ').slice(0, 4).join(' ') - - // has the position occurred three or move times - positions[fen] = fen in positions ? positions[fen] + 1 : 1 - if (positions[fen] >= 3) { - repetition = true - } + const currentFen = trimFen(this.fen()) + if (currentFen === finalFen) repetitionCount++ const move = moves.pop() - - if (!move) { - break - } else { - this._makeMove(move) - } + if (move) this._makeMove(move) + else break } + return repetitionCount + } - return repetition + isThreefoldRepetition() { + return this._getRepetitionCount() >= 3 } - isDraw() { - return ( - this._halfMoves >= 100 || // 50 moves per side = 100 half moves - this.isStalemate() || - this.isInsufficientMaterial() || - this.isThreefoldRepetition() - ) + isFivefoldRepetition() { + return this._getRepetitionCount() >= 5 + } + + isFiftyMoveRule() { + // 50 moves per side = 100 half moves + return this._halfMoves >= 100 + } + + isSeventyFiveMoveRule() { + // 75 moves per side = 150 half moves + return this._halfMoves >= 150 + } + + isDraw(strict = false) { + return this.isStalemate() || this.isInsufficientMaterial() || strict + ? this.isFivefoldRepetition() || this.isSeventyFiveMoveRule() + : this.isThreefoldRepetition() || this.isFiftyMoveRule() } - isGameOver() { - return this.isCheckmate() || this.isStalemate() || this.isDraw() + isGameOver(strict = false) { + return this.isCheckmate() || this.isDraw(strict) } moves(): string[] From 1f29bdba587ae516e24b606f2a5ad5ef2f757406 Mon Sep 17 00:00:00 2001 From: Gavin Date: Wed, 14 Jun 2023 18:19:11 +0100 Subject: [PATCH 02/15] added tests for new strict FIDE adherence features --- __tests__/is-draw.test.ts | 58 +++++++++++++++++++++ __tests__/is-fifty-move-rule.test.ts | 33 ++++++++++++ __tests__/is-fivefold-repetition.test.ts | 13 +++++ __tests__/is-seventy-five-move-rule.test.ts | 31 +++++++++++ __tests__/is-threefold-repetition.test.ts | 4 ++ 5 files changed, 139 insertions(+) create mode 100644 __tests__/is-draw.test.ts create mode 100644 __tests__/is-fifty-move-rule.test.ts create mode 100644 __tests__/is-fivefold-repetition.test.ts create mode 100644 __tests__/is-seventy-five-move-rule.test.ts diff --git a/__tests__/is-draw.test.ts b/__tests__/is-draw.test.ts new file mode 100644 index 0000000..db2d357 --- /dev/null +++ b/__tests__/is-draw.test.ts @@ -0,0 +1,58 @@ +import { Chess } from '../src/chess' + +test('isDraw - strict mode, fivefold repetition', () => { + const moves = 'Nf3 Nf6 Ng1 Ng8 Nf3 Nf6 Ng1 Ng8 Nf3 Nf6 Ng1 Ng8'.split(/\s+/) + const chess = new Chess() + + moves.forEach((move) => { + expect(chess.isDraw(true)).toBe(false) + chess.move(move) + }) + + expect(chess.isDraw()).toBe(true) + expect(chess.isDraw(true)).toBe(false) + chess.move('Nf3') + chess.move('Nf6') + chess.move('Ng1') + chess.move('Ng8') + expect(chess.isDraw(true)).toBe(true) + +}) + +test('isDraw - strict mode, seventy-five-move rule', () => { + const pgn = `1.e4 e5 2.Nf3 Nc6 3.Bb5 a6 4.Ba4 Nf6 5.O-O Nxe4 6.d4 b5 7.Bb3 d5 8.dxe5 Be6 + 9.c3 Be7 10.Re1 O-O 11.Nbd2 Nc5 12.Bc2 d4 13.Nf1 d3 14.Bb3 Nxb3 15.axb3 Qd7 16.Ng5 Bf5 + 17.Ng3 Bg6 18.h4 Rfd8 19.Bd2 h6 20.h5 Bf5 21.Nxf5 Qxf5 22.Qf3 Qxf3 23.Nxf3 a5 24.Re4 Rd5 + 25.Rae1 Rad8 26.g4 Ra8 27.Kg2 Kf8 28.Nd4 Nxd4 29.Rxd4 Rxd4 30.cxd4 Rd8 31.Be3 Rd5 32.Rd1 + c5 33.dxc5 Bxc5 34.Bxc5+ Rxc5 35.Rxd3 Rxe5 36.Rd8+ Ke7 37.Ra8 b4 38.Kf3 Rc5 39.Ra7+ Ke6 + 40.Ke4 Rc2 41.Ra6+ Ke7 42.Rxa5 Rxb2 43.f4 Rxb3 44.Rb5 Rb1 45.g5 b3 46.gxh6 gxh6 47.Kf5 b2 + 48.Rb7+ Ke8 49.Kf6 Rh1 50.Rxb2 Rxh5 51.Rb8+ Kd7 52.Kxf7 Rf5+ 53.Kg6 Rxf4 54.Kxh6 Ke6 + 55.Rb6+ Kf7 56.Rb7+ Kf6 57.Rb6+ Kf5 58.Rb5+ Kg4 59.Rg5+ Kf3 60.Rb5 Ra4 61.Kg5 Ra1 62.Rb3+ + Ke4 63.Rb4+ Kd5 64.Rb5+ Kc6 65.Rb2 Kc5 66.Kf4 Kd4 67.Rd2+ Kc3 68.Rd7 Kc2 69.Ke3 Ra3+ + 70.Ke4 Ra4+ 71.Ke5 Ra8 72.Ke4 Kc3 73.Kf5 Ra5+ 74.Kg4 Kc4 75.Kh4 Ra4 76.Kh3 Kc5 77.Kh2 + Kc6 78.Rd2 Kc5 79.Kh3 Ra3+ 80.Kh4 Kc6 81.Kh5 Kc7 82.Kh6 Kc8 83.Kh7 Rh3+ 84.Kg6 Kc7 85.Kf7 + Kc6 86.Ke6 Kc5 87.Kf5 Kc4 88.Kg4 Rh8 89.Rd7 Rg8+ 90.Kf3 Kc5 91.Kf4 Rf8+ 92.Ke5 Kc6 93.Rd2 + Re8+ 94.Kf4 Rf8+ 95.Ke5 Kc5 96.Ke4 Kc4 97.Rc2+ Kb3 98.Rc7 Re8+ 99.Kd5 Rd8+ 100.Ke6 Kb4 + 101.Kf6 Rd1 102.Ke6 Kb5 103.Kf6 Re1 104.Kf5` + const moves = pgn.split(/\s*\d+\.\s*|\s+/).slice(1) + const strictPgn = `104.Rd1 105.Rc8 Ka5 106.Kf4 Re1 107.Kg4 Ka4 108.Rb8 + Rf1 109.Kg5 Ka3 110.Rc8 Ka2 111.Kh5 Re1 112.Kg5 Ka1 113.Rc7 Rf1 114.Rd7 Kb2 115.Kg6 Re1 116. + Rc7 Kb1 117.Kg5 Re2 118.Rc4 Re6 119.Kg4 Kb2 120.Kg3 Ka3 121.Rc2 Re8 122.Kg2 Ka4 123.Rc4+ Ka3 + 124.Kg1 Re2 125.Rc5 Ka2 126.Kh1 Re1+ 127.Kh2 Ka1 128.Kh3 Rd1 129.Rd5` + const strictMoves = strictPgn.split(/\s*\d+\.\s*|\s+/).slice(1) + + const chess = new Chess() + moves.forEach((move) => { + expect(chess.isDraw()).toBe(false) + expect(chess.isDraw(true)).toBe(false) + chess.move(move) + }) + + strictMoves.forEach((move) => { + expect(chess.isDraw()).toBe(true) + expect(chess.isDraw(true)).toBe(false) + chess.move(move) + }) + expect(chess.isDraw()).toBe(true) + expect(chess.isDraw(true)).toBe(true) +}) diff --git a/__tests__/is-fifty-move-rule.test.ts b/__tests__/is-fifty-move-rule.test.ts new file mode 100644 index 0000000..d2e3c02 --- /dev/null +++ b/__tests__/is-fifty-move-rule.test.ts @@ -0,0 +1,33 @@ +import { Chess } from '../src/chess' + +test('isFiftyMoveRule', () => { + const pgn = `1. e4 e5 2. Nf3 Nc6 3. Bb5 a6 4. Ba4 Nf6 5. O-O Nxe4 6. d4 b5 7. Bb3 d5 8. dxe5 + Be6 9. c3 Be7 10. Re1 O-O 11. Nbd2 Nc5 12. Bc2 d4 13. Nf1 d3 14. Bb3 Nxb3 15. + axb3 Qd7 16. Ng5 Bf5 17. Ng3 Bg6 18. h4 Rfd8 19. Bd2 h6 20. h5 Bf5 21. Nxf5 Qxf5 + 22. Qf3 Qxf3 23. Nxf3 a5 24. Re4 Rd5 25. Rae1 Rad8 26. g4 Ra8 27. Kg2 Kf8 28. + Nd4 Nxd4 29. Rxd4 Rxd4 30. cxd4 Rd8 31. Be3 Rd5 32. Rd1 c5 33. dxc5 Bxc5 34. + Bxc5+ Rxc5 35. Rxd3 Rxe5 36. Rd8+ Ke7 37. Ra8 b4 38. Kf3 Rc5 39. Ra7+ Ke6 40. + Ke4 Rc2 41. Ra6+ Ke7 42. Rxa5 Rxb2 43. f4 Rxb3 44. Rb5 Rb1 45. g5 b3 46. gxh6 + gxh6 47. Kf5 b2 48. Rb7+ Ke8 49. Kf6 Rh1 50. Rxb2 Rxh5 51. Rb8+ Kd7 52. Kxf7 + Rf5+ 53. Kg6 Rxf4 54. Kxh6 Ke6 55. Rb6+ Kf7 56. Rb7+ Kf6 57. Rb6+ Kf5 58. Rb5+ + Kg4 59. Rg5+ Kf3 60. Rb5 Ra4 61. Kg5 Ra1 62. Rb3+ Ke4 63. Rb4+ Kd5 64. Rb5+ Kc6 + 65. Rb2 Kc5 66. Kf4 Kd4 67. Rd2+ Kc3 68. Rd7 Kc2 69. Ke3 Ra3+ 70. Ke4 Ra4+ 71. + Ke5 Ra8 72. Ke4 Kc3 73. Kf5 Ra5+ 74. Kg4 Kc4 75. Kh4 Ra4 76. Kh3 Kc5 77. Kh2 Kc6 + 78. Rd2 Kc5 79. Kh3 Ra3+ 80. Kh4 Kc6 81. Kh5 Kc7 82. Kh6 Kc8 83. Kh7 Rh3+ 84. + Kg6 Kc7 85. Kf7 Kc6 86. Ke6 Kc5 87. Kf5 Kc4 88. Kg4 Rh8 89. Rd7 Rg8+ 90. Kf3 Kc5 + 91. Kf4 Rf8+ 92. Ke5 Kc6 93. Rd2 Re8+ 94. Kf4 Rf8+ 95. Ke5 Kc5 96. Ke4 Kc4 97. + Rc2+ Kb3 98. Rc7 Re8+ 99. Kd5 Rd8+ 100. Ke6 Kb4 101. Kf6 Rd1 102. Ke6 Kb5 103. + Kf6 Re1 104. Kf5` + const moves = pgn.split(/\s*\d+\.\s*|\s+/).slice(1) + + const chess = new Chess() + moves.forEach((move) => { + expect(chess.isFiftyMoveRule()).toBe(false) + chess.move(move) + }) + expect(chess.isFiftyMoveRule()).toBe(true) + chess.move('Re5') + expect(chess.isFiftyMoveRule()).toBe(true) + chess.move('Kxe5') + expect(chess.isFiftyMoveRule()).toBe(false) +}) diff --git a/__tests__/is-fivefold-repetition.test.ts b/__tests__/is-fivefold-repetition.test.ts new file mode 100644 index 0000000..a90cdf8 --- /dev/null +++ b/__tests__/is-fivefold-repetition.test.ts @@ -0,0 +1,13 @@ +import { Chess } from '../src/chess' + +test('isFivefoldRepetition', () => { + const moves = 'Nf3 Nf6 Ng1 Ng8 Nf3 Nf6 Ng1 Ng8 Nf3 Nf6 Ng1 Ng8 Nf3 Nf6 Ng1 Ng8'.split(/\s+/) + const chess = new Chess() + moves.forEach((move) => { + expect(chess.isFivefoldRepetition()).toBe(false) + chess.move(move) + }) + expect(chess.isFivefoldRepetition()).toBe(true) + chess.move('e4') + expect(chess.isFivefoldRepetition()).toBe(false) +}) diff --git a/__tests__/is-seventy-five-move-rule.test.ts b/__tests__/is-seventy-five-move-rule.test.ts new file mode 100644 index 0000000..75e4881 --- /dev/null +++ b/__tests__/is-seventy-five-move-rule.test.ts @@ -0,0 +1,31 @@ +import { Chess } from '../src/chess' + +test('isSeventyFiveMoveRule', () => { + const pgn = `1.e4 e5 2.Nf3 Nc6 3.Bb5 a6 4.Ba4 Nf6 5.O-O Nxe4 6.d4 b5 7.Bb3 d5 8.dxe5 Be6 + 9.c3 Be7 10.Re1 O-O 11.Nbd2 Nc5 12.Bc2 d4 13.Nf1 d3 14.Bb3 Nxb3 15.axb3 Qd7 16.Ng5 Bf5 + 17.Ng3 Bg6 18.h4 Rfd8 19.Bd2 h6 20.h5 Bf5 21.Nxf5 Qxf5 22.Qf3 Qxf3 23.Nxf3 a5 24.Re4 Rd5 + 25.Rae1 Rad8 26.g4 Ra8 27.Kg2 Kf8 28.Nd4 Nxd4 29.Rxd4 Rxd4 30.cxd4 Rd8 31.Be3 Rd5 32.Rd1 + c5 33.dxc5 Bxc5 34.Bxc5+ Rxc5 35.Rxd3 Rxe5 36.Rd8+ Ke7 37.Ra8 b4 38.Kf3 Rc5 39.Ra7+ Ke6 + 40.Ke4 Rc2 41.Ra6+ Ke7 42.Rxa5 Rxb2 43.f4 Rxb3 44.Rb5 Rb1 45.g5 b3 46.gxh6 gxh6 47.Kf5 b2 + 48.Rb7+ Ke8 49.Kf6 Rh1 50.Rxb2 Rxh5 51.Rb8+ Kd7 52.Kxf7 Rf5+ 53.Kg6 Rxf4 54.Kxh6 Ke6 + 55.Rb6+ Kf7 56.Rb7+ Kf6 57.Rb6+ Kf5 58.Rb5+ Kg4 59.Rg5+ Kf3 60.Rb5 Ra4 61.Kg5 Ra1 62.Rb3+ + Ke4 63.Rb4+ Kd5 64.Rb5+ Kc6 65.Rb2 Kc5 66.Kf4 Kd4 67.Rd2+ Kc3 68.Rd7 Kc2 69.Ke3 Ra3+ + 70.Ke4 Ra4+ 71.Ke5 Ra8 72.Ke4 Kc3 73.Kf5 Ra5+ 74.Kg4 Kc4 75.Kh4 Ra4 76.Kh3 Kc5 77.Kh2 + Kc6 78.Rd2 Kc5 79.Kh3 Ra3+ 80.Kh4 Kc6 81.Kh5 Kc7 82.Kh6 Kc8 83.Kh7 Rh3+ 84.Kg6 Kc7 85.Kf7 + Kc6 86.Ke6 Kc5 87.Kf5 Kc4 88.Kg4 Rh8 89.Rd7 Rg8+ 90.Kf3 Kc5 91.Kf4 Rf8+ 92.Ke5 Kc6 93.Rd2 + Re8+ 94.Kf4 Rf8+ 95.Ke5 Kc5 96.Ke4 Kc4 97.Rc2+ Kb3 98.Rc7 Re8+ 99.Kd5 Rd8+ 100.Ke6 Kb4 + 101.Kf6 Rd1 102.Ke6 Kb5 103.Kf6 Re1 104.Kf5 Rd1 105.Rc8 Ka5 106.Kf4 Re1 107.Kg4 Ka4 108.Rb8 + Rf1 109.Kg5 Ka3 110.Rc8 Ka2 111.Kh5 Re1 112.Kg5 Ka1 113.Rc7 Rf1 114.Rd7 Kb2 115.Kg6 Re1 116. + Rc7 Kb1 117.Kg5 Re2 118.Rc4 Re6 119.Kg4 Kb2 120.Kg3 Ka3 121.Rc2 Re8 122.Kg2 Ka4 123.Rc4+ Ka3 + 124.Kg1 Re2 125.Rc5 Ka2 126.Kh1 Re1+ 127.Kh2 Ka1 128.Kh3 Rd1 129.Rd5` + const moves = pgn.split(/\s*\d+\.\s*|\s+/).slice(1) + + const chess = new Chess() + moves.forEach((move) => { + expect(chess.isSeventyFiveMoveRule()).toBe(false) + chess.move(move) + }) + expect(chess.isSeventyFiveMoveRule()).toBe(true) + chess.move('Rxd5') + expect(chess.isSeventyFiveMoveRule()).toBe(false) +}) diff --git a/__tests__/is-threefold-repetition.test.ts b/__tests__/is-threefold-repetition.test.ts index 908a79a..c8ab41d 100644 --- a/__tests__/is-threefold-repetition.test.ts +++ b/__tests__/is-threefold-repetition.test.ts @@ -12,6 +12,8 @@ test('isThreefoldRepetition', () => { chess.move(move) }) expect(chess.isThreefoldRepetition()).toBe(true) + chess.move('a6') + expect(chess.isThreefoldRepetition()).toBe(false) }) test('isThreefoldRepetition - 2', () => { @@ -22,4 +24,6 @@ test('isThreefoldRepetition - 2', () => { chess.move(move) }) expect(chess.isThreefoldRepetition()).toBe(true) + chess.move('e4') + expect(chess.isThreefoldRepetition()).toBe(false) }) From 1e2ea77c66be751176c452f184d38fcb7ac1cc81 Mon Sep 17 00:00:00 2001 From: Gavin Date: Wed, 14 Jun 2023 18:19:36 +0100 Subject: [PATCH 03/15] updated docs for new strict FIDE adherence feature --- README.md | 94 +++++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 88 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 07c61f3..3664ce6 100644 --- a/README.md +++ b/README.md @@ -424,10 +424,11 @@ chess.isCheckmate() // -> true ``` -### .isDraw() +### .isDraw(strict = false) -Returns true or false if the game is drawn (50-move rule or insufficient -material). +Returns true or false if the game is drawn (by stalemate, insufficient material, threefold repetition or 50-move rule). + +When the optional `strict` parameter is given as `true`, returns true or false if the game is strictly drawn as per latest [FIDE Laws of Chess](https://handbook.fide.com/chapter/E012023) Article 9 (by stalemate, insufficient material, fivefold repetition or 75-move rule). ```ts const chess = new Chess('4k3/4P3/4K3/8/8/8/8/8 b - - 0 78') @@ -435,6 +436,19 @@ chess.isDraw() // -> true ``` +```ts +const chess = new Chess('8/8/8/2R5/8/7K/8/k2r4 w - - 149 129') +chess.isDraw() +// -> true + +chess.isDraw(true) +// -> false + +chess.move('Rd5') +chess.isDraw(true) +// -> true +``` + ### .isInsufficientMaterial() Returns true if the game is drawn due to insufficient material (K vs. K, K vs. @@ -446,10 +460,11 @@ chess.isInsufficientMaterial() // -> true ``` -### .isGameOver() +### .isGameOver(strict = false) + +Returns true if the game has ended via checkmate or draw (by stalemate, insufficient material, threefold repetition or 50-move rule). Otherwise, returns false. -Returns true if the game has ended via checkmate, stalemate, draw, threefold -repetition, or insufficient material. Otherwise, returns false. +If optional `strict` parameter is given, it determines whether the game is strictly drawn as per latest [FIDE Laws of Chess](https://handbook.fide.com/chapter/E012023) Article 9 (by stalemate, insufficient material, fivefold repetition or 75-move rule). ```ts const chess = new Chess() @@ -499,6 +514,73 @@ chess.move('Nf3') chess.move('Nf6') chess.move('Ng1') chess.move('Ng8') chess.isThreefoldRepetition() // -> true ``` +### .isFivefoldRepetition() + +Returns true or false if the current board position has occurred five or more +times. + +```ts +const chess = new Chess('rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1') +// -> true +// rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq occurs 1st time +chess.isFivefoldRepetition() +// -> false + +chess.move('Nf3') chess.move('Nf6') chess.move('Ng1') chess.move('Ng8') +// rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq occurs 2nd time +chess.isFivefoldRepetition() +// -> false + +chess.move('Nf3') chess.move('Nf6') chess.move('Ng1') chess.move('Ng8') +chess.move('Nf3') chess.move('Nf6') chess.move('Ng1') chess.move('Ng8') +chess.move('Nf3') chess.move('Nf6') chess.move('Ng1') chess.move('Ng8') +// rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq occurs 5th time +chess.isFivefoldRepetition() +// -> true +``` + +### .isFiftyMoveRule() + +Returns true or false if the [fifty-move rule](https://en.wikipedia.org/wiki/Fifty-move_rule) is in effect. + +```ts +const chess = new Chess('8/2R5/5K2/1k6/8/8/8/4r3 w - - 99 104') + +chess.isFiftyMoveRule() +// -> false + +chess.move('Kf5') +chess.isFiftyMoveRule() +// -> true + +chess.move('Re5') +chess.isFiftyMoveRule() +// -> true + +chess.move('Kxe5') +chess.isFiftyMoveRule() +// -> false +``` + +### .isSeventyFiveMoveRule() + +Returns true or false if the [seventy-five-move rule](https://en.wikipedia.org/wiki/Fifty-move_rule#Seventy-five-move_rule) is in effect. + + +```ts +const chess = new Chess('8/8/8/2R5/8/7K/8/k2r4 w - - 149 129') + +chess.isSeventyFiveMoveRule() +// -> false + +chess.move('Rd5') +chess.isSeventyFiveMoveRule() +// -> true + +chess.move('Rxd5') +chess.isSeventyFiveMoveRule() +// -> false +``` ### .load(fen) From 076ffcc54c75b5a444e5bb6ee7f3df390228ab2f Mon Sep 17 00:00:00 2001 From: Gavin Date: Wed, 14 Jun 2023 18:24:32 +0100 Subject: [PATCH 04/15] added '@types/node' dev dependency Needed to install this dependency for __tests__/utils.ts to import from --- package-lock.json | 13 +++++++------ package.json | 1 + 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/package-lock.json b/package-lock.json index 540a41c..bad827b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "license": "BSD-2-Clause", "devDependencies": { "@types/jest": "^27.4.1", + "@types/node": "^20.3.1", "@typescript-eslint/eslint-plugin": "^5.17.0", "@typescript-eslint/parser": "^5.17.0", "eslint": "^8.12.0", @@ -1307,9 +1308,9 @@ "dev": true }, "node_modules/@types/node": { - "version": "17.0.23", - "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.23.tgz", - "integrity": "sha512-UxDxWn7dl97rKVeVS61vErvw086aCYhDLyvRQZ5Rk65rZKepaFdm53GeqXaKBuOhED4e9uWq34IC3TdSdJJ2Gw==", + "version": "20.3.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.3.1.tgz", + "integrity": "sha512-EhcH/wvidPy1WeML3TtYFGR83UzjxeWRen9V402T8aUGYsCHOmfoisV3ZSg03gAFIbLq8TnWOJ0f4cALtnSEUg==", "dev": true }, "node_modules/@types/prettier": { @@ -6849,9 +6850,9 @@ "dev": true }, "@types/node": { - "version": "17.0.23", - "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.23.tgz", - "integrity": "sha512-UxDxWn7dl97rKVeVS61vErvw086aCYhDLyvRQZ5Rk65rZKepaFdm53GeqXaKBuOhED4e9uWq34IC3TdSdJJ2Gw==", + "version": "20.3.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.3.1.tgz", + "integrity": "sha512-EhcH/wvidPy1WeML3TtYFGR83UzjxeWRen9V402T8aUGYsCHOmfoisV3ZSg03gAFIbLq8TnWOJ0f4cALtnSEUg==", "dev": true }, "@types/prettier": { diff --git a/package.json b/package.json index d2c18e2..ed27344 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ }, "devDependencies": { "@types/jest": "^27.4.1", + "@types/node": "^20.3.1", "@typescript-eslint/eslint-plugin": "^5.17.0", "@typescript-eslint/parser": "^5.17.0", "eslint": "^8.12.0", From cc4ee5aad09ce61882878ff5c0c885ff8ea3f496 Mon Sep 17 00:00:00 2001 From: Gavin <94984768+gavin-lb@users.noreply.github.com> Date: Wed, 14 Jun 2023 18:46:33 +0100 Subject: [PATCH 05/15] adjusted formatting of isDraw --- src/chess.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/chess.ts b/src/chess.ts index 1c4a058..569a9aa 100644 --- a/src/chess.ts +++ b/src/chess.ts @@ -1033,9 +1033,13 @@ export class Chess { } isDraw(strict = false) { - return this.isStalemate() || this.isInsufficientMaterial() || strict - ? this.isFivefoldRepetition() || this.isSeventyFiveMoveRule() - : this.isThreefoldRepetition() || this.isFiftyMoveRule() + return ( + this.isStalemate() || + this.isInsufficientMaterial() || + (strict + ? this.isFivefoldRepetition() || this.isSeventyFiveMoveRule() + : this.isThreefoldRepetition() || this.isFiftyMoveRule()) + ) } isGameOver(strict = false) { From aeb53f307105108d5c0f947a2afe922096b200df Mon Sep 17 00:00:00 2001 From: Gavin Date: Wed, 14 Jun 2023 20:20:14 +0100 Subject: [PATCH 06/15] Changed parameters of isDraw and isGameOver Updated parameter to be an optional object of shape `{ strict?: boolean }` to better align with conventions already established elsewhere in the library. --- README.md | 12 ++++++------ __tests__/is-draw.test.ts | 12 ++++++------ src/chess.ts | 6 +++--- 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 3664ce6..9fd05f0 100644 --- a/README.md +++ b/README.md @@ -424,11 +424,11 @@ chess.isCheckmate() // -> true ``` -### .isDraw(strict = false) +### .isDraw({ strict = false }: { strict?: boolean } = {}) Returns true or false if the game is drawn (by stalemate, insufficient material, threefold repetition or 50-move rule). -When the optional `strict` parameter is given as `true`, returns true or false if the game is strictly drawn as per latest [FIDE Laws of Chess](https://handbook.fide.com/chapter/E012023) Article 9 (by stalemate, insufficient material, fivefold repetition or 75-move rule). +If optional `{ strict: true }` argument is given, returns true or false if the game is strictly drawn as per Article 9 of the [FIDE Laws of Chess](https://handbook.fide.com/chapter/E012023) (by stalemate, insufficient material, fivefold repetition or 75-move rule). ```ts const chess = new Chess('4k3/4P3/4K3/8/8/8/8/8 b - - 0 78') @@ -441,11 +441,11 @@ const chess = new Chess('8/8/8/2R5/8/7K/8/k2r4 w - - 149 129') chess.isDraw() // -> true -chess.isDraw(true) +chess.isDraw({ strict: true }) // -> false chess.move('Rd5') -chess.isDraw(true) +chess.isDraw({ strict: true }) // -> true ``` @@ -460,11 +460,11 @@ chess.isInsufficientMaterial() // -> true ``` -### .isGameOver(strict = false) +### .isGameOver({ strict = false }: { strict?: boolean } = {}) Returns true if the game has ended via checkmate or draw (by stalemate, insufficient material, threefold repetition or 50-move rule). Otherwise, returns false. -If optional `strict` parameter is given, it determines whether the game is strictly drawn as per latest [FIDE Laws of Chess](https://handbook.fide.com/chapter/E012023) Article 9 (by stalemate, insufficient material, fivefold repetition or 75-move rule). +If optional `{ strict: true }` argument is given, it determines whether the game is strictly drawn as per Article 9 of the [FIDE Laws of Chess](https://handbook.fide.com/chapter/E012023) (by stalemate, insufficient material, fivefold repetition or 75-move rule). ```ts const chess = new Chess() diff --git a/__tests__/is-draw.test.ts b/__tests__/is-draw.test.ts index db2d357..f0b3ced 100644 --- a/__tests__/is-draw.test.ts +++ b/__tests__/is-draw.test.ts @@ -5,17 +5,17 @@ test('isDraw - strict mode, fivefold repetition', () => { const chess = new Chess() moves.forEach((move) => { - expect(chess.isDraw(true)).toBe(false) + expect(chess.isDraw({strict: true})).toBe(false) chess.move(move) }) expect(chess.isDraw()).toBe(true) - expect(chess.isDraw(true)).toBe(false) + expect(chess.isDraw({strict: true})).toBe(false) chess.move('Nf3') chess.move('Nf6') chess.move('Ng1') chess.move('Ng8') - expect(chess.isDraw(true)).toBe(true) + expect(chess.isDraw({strict: true})).toBe(true) }) @@ -44,15 +44,15 @@ test('isDraw - strict mode, seventy-five-move rule', () => { const chess = new Chess() moves.forEach((move) => { expect(chess.isDraw()).toBe(false) - expect(chess.isDraw(true)).toBe(false) + expect(chess.isDraw({strict: true})).toBe(false) chess.move(move) }) strictMoves.forEach((move) => { expect(chess.isDraw()).toBe(true) - expect(chess.isDraw(true)).toBe(false) + expect(chess.isDraw({strict: true})).toBe(false) chess.move(move) }) expect(chess.isDraw()).toBe(true) - expect(chess.isDraw(true)).toBe(true) + expect(chess.isDraw({strict: true})).toBe(true) }) diff --git a/src/chess.ts b/src/chess.ts index 569a9aa..bb92d1b 100644 --- a/src/chess.ts +++ b/src/chess.ts @@ -1032,7 +1032,7 @@ export class Chess { return this._halfMoves >= 150 } - isDraw(strict = false) { + isDraw({ strict = false }: { strict?: boolean } = {}) { return ( this.isStalemate() || this.isInsufficientMaterial() || @@ -1042,8 +1042,8 @@ export class Chess { ) } - isGameOver(strict = false) { - return this.isCheckmate() || this.isDraw(strict) + isGameOver({ strict = false }: { strict?: boolean } = {}) { + return this.isCheckmate() || this.isDraw({strict}) } moves(): string[] From 43635dc46a8c0014700f64af13f79144f824b1ad Mon Sep 17 00:00:00 2001 From: Gavin Date: Thu, 15 Jun 2023 13:06:33 +0100 Subject: [PATCH 07/15] Added .canClaimDraw() API method --- README.md | 5 +++++ src/chess.ts | 28 ++++++++++++++-------------- 2 files changed, 19 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 9fd05f0..f06152d 100644 --- a/README.md +++ b/README.md @@ -424,6 +424,11 @@ chess.isCheckmate() // -> true ``` +### .canClaimDraw() + +Returns true or false if a draw can be claimed in the current position as per Article 9 of the [FIDE Laws of Chess](https://handbook.fide.com/chapter/E012023), ie. if `.isThreefoldRepetition()` or `.isFiftyMoveRule()`. + + ### .isDraw({ strict = false }: { strict?: boolean } = {}) Returns true or false if the game is drawn (by stalemate, insufficient material, threefold repetition or 50-move rule). diff --git a/src/chess.ts b/src/chess.ts index bb92d1b..74e6d03 100644 --- a/src/chess.ts +++ b/src/chess.ts @@ -1014,36 +1014,36 @@ export class Chess { return repetitionCount } - isThreefoldRepetition() { + isThreefoldRepetition(): boolean { return this._getRepetitionCount() >= 3 } - isFivefoldRepetition() { + isFivefoldRepetition(): boolean { return this._getRepetitionCount() >= 5 } - isFiftyMoveRule() { + isFiftyMoveRule(): boolean { // 50 moves per side = 100 half moves return this._halfMoves >= 100 } - isSeventyFiveMoveRule() { + isSeventyFiveMoveRule(): boolean { // 75 moves per side = 150 half moves return this._halfMoves >= 150 } - isDraw({ strict = false }: { strict?: boolean } = {}) { - return ( - this.isStalemate() || - this.isInsufficientMaterial() || - (strict - ? this.isFivefoldRepetition() || this.isSeventyFiveMoveRule() - : this.isThreefoldRepetition() || this.isFiftyMoveRule()) - ) + canClaimDraw(): boolean { + return this.isThreefoldRepetition() || this.isFiftyMoveRule() + } + + isDraw({ strict = false }: { strict?: boolean } = {}): boolean { + return this.isStalemate() || this.isInsufficientMaterial() || (strict + ? this.isFivefoldRepetition() || this.isSeventyFiveMoveRule() + : this.canClaimDraw()) } - isGameOver({ strict = false }: { strict?: boolean } = {}) { - return this.isCheckmate() || this.isDraw({strict}) + isGameOver({ strict = false }: { strict?: boolean } = {}): boolean { + return this.isCheckmate() || this.isDraw({ strict }) } moves(): string[] From f691e123b5711271fe2786720bcebfe8e8ae55f5 Mon Sep 17 00:00:00 2001 From: Gavin Date: Thu, 15 Jun 2023 15:56:52 +0100 Subject: [PATCH 08/15] Improve efficiency of _getRepetitionCounts Since this method is very likely going to be called after every single move (for end of game detection) I think it makes sense to optimise it a little bit. Instead of replaying the entire game every turn to determine whether the position has been repeated, we simply keep track of position counts as the game progresses. We just need to make sure to properly update the count when a move is undone or the position reset. --- src/chess.ts | 75 +++++++++++++++++++++++++++++++++++----------------- 1 file changed, 51 insertions(+), 24 deletions(-) diff --git a/src/chess.ts b/src/chess.ts index 74e6d03..3a342da 100644 --- a/src/chess.ts +++ b/src/chess.ts @@ -539,6 +539,7 @@ export class Chess { private _history: History[] = [] private _comments: Record = {} private _castling: Record = { w: 0, b: 0 } + private _positionCounts: Record = {} constructor(fen = DEFAULT_POSITION) { this.load(fen) @@ -556,6 +557,7 @@ export class Chess { this._comments = {} this._header = keepHeaders ? this._header : {} this._updateSetup(this.fen()) + this._positionCounts = {} } removeHeader(key: string) { @@ -594,6 +596,24 @@ export class Chess { square += parseInt(piece, 10) } else { const color = piece < 'a' ? WHITE : BLACK + /* + * TODO: `.load` calls `.put` for every piece in the fen, which in turns calls `._updateSetup`, which + * in turn calls `.fen`. Is it necessary to call `._updateSetup` for every piece? We are already + * calling it once at the end of this method. Perhaps the publicly exposed `.put` method should call + * a private `._put` method (which does not call `._updateSetup`) and after call .`_updateSetup`, eg. + * ``` + * private _put(...args) { + * ${ code from `.put` as it is currently except } + * ${ without the `this._updateSetup(this.fen())` at the end } + * } + * + * put(...args) { + * this._put(...args) + * this._updateSetup(this.fen()) + * } + * ``` + * This change would prevent many unnecessary calls to `._updateSetup` and `.fen`. + */ this.put( { type: piece.toLowerCase() as PieceSymbol, color }, algebraic(square) @@ -621,7 +641,29 @@ export class Chess { this._halfMoves = parseInt(tokens[4], 10) this._moveNumber = parseInt(tokens[5], 10) - this._updateSetup(this.fen()) + this._updateSetup(fen) + + /* + * Instantiate a proxy that keeps track of position occurrence counts for the purpose + * of repetition checking. The getter and setter methods automatically handle trimming + * irrelevent information from the fen, initialising new positions, and removing old + * positions from the record if their counts are reduced to 0. + */ + this._positionCounts = new Proxy({[this._trimFen(fen)]: 1}, { + get: (target, position: string) => target?.[this._trimFen(position)] || 0, + set: (target, position: string, count: number) => { + const trimmedFen = this._trimFen(position) + if (count === 0) delete target[trimmedFen] + else target[trimmedFen] = count + return true + }, + }) + } + + private _trimFen(fen: string): string { + // remove last two fields in FEN string as they're not needed when checking for repetition + return fen.split(' ').slice(0, 4).join(' ') + } fen() { @@ -991,27 +1033,7 @@ export class Chess { } private _getRepetitionCount() { - // remove the last two fields in the FEN string, they're not needed when checking for repetition - const trimFen = (fen: string) => fen.split(' ').slice(0, 4).join(' ') - const finalFen = trimFen(this.fen()) - - const moves = [] - while (true) { - const move = this._undoMove() - if (!move) break - moves.push(move) - } - - let repetitionCount = 0 - while (true) { - const currentFen = trimFen(this.fen()) - if (currentFen === finalFen) repetitionCount++ - - const move = moves.pop() - if (move) this._makeMove(move) - else break - } - return repetitionCount + return this._positionCounts[this.fen()] } isThreefoldRepetition(): boolean { @@ -1366,7 +1388,7 @@ export class Chess { const prettyMove = this._makePretty(moveObj) this._makeMove(moveObj) - + this._positionCounts[prettyMove.after]++ return prettyMove } @@ -1480,7 +1502,12 @@ export class Chess { undo() { const move = this._undoMove() - return move ? this._makePretty(move) : null + if (move) { + const prettyMove = this._makePretty(move) + this._positionCounts[prettyMove.before]-- + return prettyMove + } + else return null } private _undoMove() { From 3d037093aabec0c4ae9b60a9f1035dae992bd066 Mon Sep 17 00:00:00 2001 From: Gavin Date: Thu, 15 Jun 2023 16:17:50 +0100 Subject: [PATCH 09/15] Improve efficiency of load The `load` method used to unnecessarily call `_updateCastlingRights`, `_updateEnPassantSquare`, `_updateSetup` and `fen` methods for every piece it placed on the board (because `put` method called these methods). Optimised `load` by implementing a private `_put` method which doesn't call these methods, then changed publicly exposed API method `put` to be a wrapper around `_put` that calls these methods if `put` was successful. --- src/chess.ts | 34 +++++++++++----------------------- 1 file changed, 11 insertions(+), 23 deletions(-) diff --git a/src/chess.ts b/src/chess.ts index 3a342da..a2a01f3 100644 --- a/src/chess.ts +++ b/src/chess.ts @@ -596,25 +596,7 @@ export class Chess { square += parseInt(piece, 10) } else { const color = piece < 'a' ? WHITE : BLACK - /* - * TODO: `.load` calls `.put` for every piece in the fen, which in turns calls `._updateSetup`, which - * in turn calls `.fen`. Is it necessary to call `._updateSetup` for every piece? We are already - * calling it once at the end of this method. Perhaps the publicly exposed `.put` method should call - * a private `._put` method (which does not call `._updateSetup`) and after call .`_updateSetup`, eg. - * ``` - * private _put(...args) { - * ${ code from `.put` as it is currently except } - * ${ without the `this._updateSetup(this.fen())` at the end } - * } - * - * put(...args) { - * this._put(...args) - * this._updateSetup(this.fen()) - * } - * ``` - * This change would prevent many unnecessary calls to `._updateSetup` and `.fen`. - */ - this.put( + this._put( { type: piece.toLowerCase() as PieceSymbol, color }, algebraic(square) ) @@ -794,6 +776,16 @@ export class Chess { } put({ type, color }: { type: PieceSymbol; color: Color }, square: Square) { + const result = this._put({ type, color }, square) + if (result) { + this._updateCastlingRights() + this._updateEnPassantSquare() + this._updateSetup(this.fen()) + } + return result + } + + private _put({ type, color }: { type: PieceSymbol; color: Color }, square: Square) { // check for piece if (SYMBOLS.indexOf(type.toLowerCase()) === -1) { return false @@ -820,10 +812,6 @@ export class Chess { this._kings[color] = sq } - this._updateCastlingRights() - this._updateEnPassantSquare() - this._updateSetup(this.fen()) - return true } From ea3e41bdfe64c1a6be1f6a25d436f25b42cc3f73 Mon Sep 17 00:00:00 2001 From: Gavin Date: Thu, 15 Jun 2023 23:41:54 +0100 Subject: [PATCH 10/15] Fixed bug with _positionCounts when loading FEN --- src/chess.ts | 68 +++++++++++++++++++++++++++------------------------- 1 file changed, 36 insertions(+), 32 deletions(-) diff --git a/src/chess.ts b/src/chess.ts index a2a01f3..2bd585f 100644 --- a/src/chess.ts +++ b/src/chess.ts @@ -557,7 +557,24 @@ export class Chess { this._comments = {} this._header = keepHeaders ? this._header : {} this._updateSetup(this.fen()) - this._positionCounts = {} + /* + * Instantiate a proxy that keeps track of position occurrence counts for the purpose + * of repetition checking. The getter and setter methods automatically handle trimming + * irrelevent information from the fen, initialising new positions, and removing old + * positions from the record if their counts are reduced to 0. + */ + this._positionCounts = new Proxy({} as Record, { + get: (target, position: string) => + position === 'length' + ? Object.keys(target).length // length for unit testing + : target?.[this._trimFen(position)] || 0, + set: (target, position: string, count: number) => { + const trimmedFen = this._trimFen(position) + if (count === 0) delete target[trimmedFen] + else target[trimmedFen] = count + return true + }, + }) } removeHeader(key: string) { @@ -624,28 +641,12 @@ export class Chess { this._moveNumber = parseInt(tokens[5], 10) this._updateSetup(fen) - - /* - * Instantiate a proxy that keeps track of position occurrence counts for the purpose - * of repetition checking. The getter and setter methods automatically handle trimming - * irrelevent information from the fen, initialising new positions, and removing old - * positions from the record if their counts are reduced to 0. - */ - this._positionCounts = new Proxy({[this._trimFen(fen)]: 1}, { - get: (target, position: string) => target?.[this._trimFen(position)] || 0, - set: (target, position: string, count: number) => { - const trimmedFen = this._trimFen(position) - if (count === 0) delete target[trimmedFen] - else target[trimmedFen] = count - return true - }, - }) + this._positionCounts[fen]++ } private _trimFen(fen: string): string { // remove last two fields in FEN string as they're not needed when checking for repetition return fen.split(' ').slice(0, 4).join(' ') - } fen() { @@ -776,13 +777,13 @@ export class Chess { } put({ type, color }: { type: PieceSymbol; color: Color }, square: Square) { - const result = this._put({ type, color }, square) - if (result) { + if (this._put({ type, color }, square)) { this._updateCastlingRights() this._updateEnPassantSquare() this._updateSetup(this.fen()) + return true } - return result + return false } private _put({ type, color }: { type: PieceSymbol; color: Color }, square: Square) { @@ -818,13 +819,15 @@ export class Chess { remove(square: Square) { const piece = this.get(square) delete this._board[Ox88[square]] - if (piece && piece.type === KING) { - this._kings[piece.color] = EMPTY - } - this._updateCastlingRights() - this._updateEnPassantSquare() - this._updateSetup(this.fen()) + if (piece) { + this._updateCastlingRights() + this._updateEnPassantSquare() + this._updateSetup(this.fen()) + if (piece.type === KING) { + this._kings[piece.color] = EMPTY + } + } return piece } @@ -1048,8 +1051,8 @@ export class Chess { isDraw({ strict = false }: { strict?: boolean } = {}): boolean { return this.isStalemate() || this.isInsufficientMaterial() || (strict - ? this.isFivefoldRepetition() || this.isSeventyFiveMoveRule() - : this.canClaimDraw()) + ? this.isFivefoldRepetition() || this.isSeventyFiveMoveRule() + : this.canClaimDraw()) } isGameOver({ strict = false }: { strict?: boolean } = {}): boolean { @@ -1492,10 +1495,10 @@ export class Chess { const move = this._undoMove() if (move) { const prettyMove = this._makePretty(move) - this._positionCounts[prettyMove.before]-- + this._positionCounts[prettyMove.after]-- return prettyMove - } - else return null + } + return null } private _undoMove() { @@ -1919,6 +1922,7 @@ export class Chess { // reset the end of game marker if making a valid move result = '' this._makeMove(move) + this._positionCounts[this.fen()]++ } } From c190ffdcbb2bc363dd62273ba38cd452b2ab22ae Mon Sep 17 00:00:00 2001 From: Gavin Date: Thu, 15 Jun 2023 23:42:05 +0100 Subject: [PATCH 11/15] Added tests for _positionCounts --- __tests__/position-counts.test.ts | 97 +++++++++++++++++++++++++++++++ 1 file changed, 97 insertions(+) create mode 100644 __tests__/position-counts.test.ts diff --git a/__tests__/position-counts.test.ts b/__tests__/position-counts.test.ts new file mode 100644 index 0000000..96aff7e --- /dev/null +++ b/__tests__/position-counts.test.ts @@ -0,0 +1,97 @@ +import { Chess as ChessClass, DEFAULT_POSITION } from '../src/chess' + +// We need to use `Chess as any` to access private property +// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/naming-convention +const Chess = ChessClass as any +const e4Fen = 'rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq - 0 1' + +test('_positionCounts - counts repeated positions', () => { + const chess = new Chess() + expect(chess._positionCounts[DEFAULT_POSITION]).toBe(1) + + const fens: string[] = [DEFAULT_POSITION] + const moves: string[] = ['Nf3', 'Nf6', 'Ng1', 'Ng8'] + for (const move of moves) { + for (const fen of fens) { + expect(chess._positionCounts[fen]).toBe(1) + } + chess.move(move) + fens.push(chess.fen()) + } + expect(chess._positionCounts[DEFAULT_POSITION]).toBe(2) + expect(chess._positionCounts.length).toBe(4) +}) + +test('_positionCounts - removes when undo', () => { + const chess = new Chess() + expect(chess._positionCounts[DEFAULT_POSITION]).toBe(1) + expect(chess._positionCounts[e4Fen]).toBe(0) + chess.move('e4') + expect(chess._positionCounts[DEFAULT_POSITION]).toBe(1) + expect(chess.fen()).toBe(e4Fen) + expect(chess._positionCounts[e4Fen]).toBe(1) + + chess.undo() + expect(chess._positionCounts[DEFAULT_POSITION]).toBe(1) + expect(chess._positionCounts[e4Fen]).toBe(0) + expect(chess._positionCounts.length).toBe(1) +}) + +test('_positionCounts - resets when cleared', () => { + const chess = new Chess() + + chess.move('e4') + chess.clear() + expect(chess._positionCounts[DEFAULT_POSITION]).toBe(0) + expect(chess._positionCounts.length).toBe(0) +}) + +test('_positionCounts - resets when loading FEN', () => { + const chess = new Chess() + expect(chess._positionCounts[DEFAULT_POSITION]).toBe(1) + chess.move('e4') + expect(chess._positionCounts[DEFAULT_POSITION]).toBe(1) + expect(chess._positionCounts[e4Fen]).toBe(1) + + const newFen = 'rnbqkbnr/pp1ppppp/8/2p5/4P3/5N2/PPPP1PPP/RNBQKB1R b KQkq - 1 2' + chess.load(newFen) + expect(chess._positionCounts[DEFAULT_POSITION]).toBe(0) + expect(chess._positionCounts[e4Fen]).toBe(0) + expect(chess._positionCounts[newFen]).toBe(1) + expect(chess._positionCounts.length).toBe(1) +}) + +test('_positionCounts - resets when loading PGN', () => { + const chess = new Chess() + chess.move('e4') + + chess.loadPgn('1. d4 d5') + expect(chess._positionCounts[DEFAULT_POSITION]).toBe(1) + expect(chess._positionCounts[e4Fen]).toBe(0) + expect(chess._positionCounts['rnbqkbnr/pppppppp/8/8/3P4/8/PPP1PPPP/RNBQKBNR b KQkq - 0 1']).toBe(1) + expect(chess._positionCounts['rnbqkbnr/ppp1pppp/8/3p4/3P4/8/PPP1PPPP/RNBQKBNR w KQkq - 0 2']).toBe(1) + expect(chess._positionCounts.length).toBe(3) +}) + +test('_positionCounts - properly updates if put is called', () => { + const chess = new Chess() + expect(chess._positionCounts[DEFAULT_POSITION]).toBe(1) + chess.move('e4') + expect(chess._positionCounts[e4Fen]).toBe(1) + + chess.put({type: 'p', color: 'w'}, 'e2') + chess.move('e5') + expect(chess._positionCounts['rnbqkbnr/pppp1ppp/8/4p3/4P3/8/PPPPPPPP/RNBQKBNR w KQkq - 0 3']).toBe(1) +}) + +test('_positionCounts - properly updates if remove is called', () => { + const chess = new Chess() + expect(chess._positionCounts[DEFAULT_POSITION]).toBe(1) + chess.move('e4') + expect(chess._positionCounts[e4Fen]).toBe(1) + + chess.remove('e4') + chess.move('e5') + expect(chess._positionCounts['rnbqkbnr/pppp1ppp/8/4p3/8/8/PPPP1PPP/RNBQKBNR w KQkq - 0 3']).toBe(1) +}) + From 74cfcfd23c62c8bf4e6808de8e52bdd954bcd80a Mon Sep 17 00:00:00 2001 From: Gavin Date: Sun, 18 Jun 2023 19:58:35 +0100 Subject: [PATCH 12/15] Added put/remove calls to _history Required updating history, undo, pgn and loadPgn methods to work with the new _history structure: - undo method now accepts an optional argument of shape { untilMove?: boolean } (defaults to { untilMove: true }). When true (ie. default behaviour) it will automatically undo all put/removes until the previous move (inclusive). When false, it will only undo the last move/put/remove. - history method now accepts an onlyMoves property of its options (defaults to true). When true, history will return only the previous moves (ie. its previous behaviour). When false, history will return all previous moves, puts and removes (where each element is an object of shape { historyType: 'move' | 'put' | 'remove', move: Move | Put | Remove } to indicate which type of history entry it is) - pgn method now generates PGNs with noted put/removes as PGN comments. Although, technically these are still not valid PGNs as per the spec, they make more sense than the previous implementation and will be read properly by loadPgn which will perform the necessary put/removes as the game is loaded. --- src/chess.ts | 372 ++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 261 insertions(+), 111 deletions(-) diff --git a/src/chess.ts b/src/chess.ts index 2bd585f..b0304d5 100644 --- a/src/chess.ts +++ b/src/chess.ts @@ -67,8 +67,23 @@ type InternalMove = { flags: number } -interface History { - move: InternalMove +export interface Put { + piece: Piece, + square: Square + oldPiece?: Piece +} + +export interface Remove { + piece: Piece + square: Square +} + +interface HistoryType { + historyType: 'move' | 'put' | 'remove' + move: InternalMove | Put | Remove +} + +interface History extends HistoryType{ kings: Record turn: Color castling: Record @@ -77,6 +92,11 @@ interface History { moveNumber: number } +export type HistoryMove = { + historyType: 'move' | 'put' | 'remove' + move: Move | Put | Remove +} + export type Move = { color: Color from: Square @@ -615,7 +635,8 @@ export class Chess { const color = piece < 'a' ? WHITE : BLACK this._put( { type: piece.toLowerCase() as PieceSymbol, color }, - algebraic(square) + algebraic(square), + {update: false, push: false} ) square++ } @@ -776,17 +797,22 @@ export class Chess { return this._board[Ox88[square]] || false } - put({ type, color }: { type: PieceSymbol; color: Color }, square: Square) { - if (this._put({ type, color }, square)) { - this._updateCastlingRights() - this._updateEnPassantSquare() - this._updateSetup(this.fen()) - return true - } - return false + put({ type, color }: { type: PieceSymbol; color: Color }, square: Square): boolean { + return this._history.length === 0 + /* + * On the first turn (ie. before any moves have been made) we don't want to push the + * put to _history as we just consider it altering the starting FEN + */ + ? this._put({ type, color }, square, {update: true, push: false}) + : this._put({ type, color }, square) } - private _put({ type, color }: { type: PieceSymbol; color: Color }, square: Square) { + private _put( + { type, color }: { type: PieceSymbol; color: Color }, + square: Square, + options: { update?: boolean; push?: boolean } = {update: true, push: true} + ): boolean { + // check for piece if (SYMBOLS.indexOf(type.toLowerCase()) === -1) { return false @@ -807,23 +833,49 @@ export class Chess { return false } + if (options.push) + this._push({ piece: { type, color }, square, oldPiece: this._board[sq] }, 'put') + this._board[sq] = { type: type as PieceSymbol, color: color as Color } if (type === KING) { this._kings[color] = sq } + if (options.update) { + this._updateCastlingRights() + this._updateEnPassantSquare() + this._updateSetup(this.fen()) + } + return true } - remove(square: Square) { - const piece = this.get(square) - delete this._board[Ox88[square]] + remove(square: Square): Piece | false { + return this._history.length === 0 + /* + * On the first turn (ie. before any moves have been made) we don't want to push the + * remove to _history as we just consider it altering the starting FEN + */ + ? this._remove(square, {update: true, push: false}) + : this._remove(square) + } + + private _remove( + square: Square, + options: { update?: boolean; push?: boolean } = { update: true, push: true } + ): Piece | false { + const piece = this.get(square) + if (piece) { - this._updateCastlingRights() - this._updateEnPassantSquare() - this._updateSetup(this.fen()) + if (options.push) this._push({piece, square}, 'remove') + delete this._board[Ox88[square]] + if (options.update) { + this._updateCastlingRights() + this._updateEnPassantSquare() + this._updateSetup(this.fen()) + } if (piece.type === KING) { this._kings[piece.color] = EMPTY } @@ -1383,8 +1435,9 @@ export class Chess { return prettyMove } - _push(move: InternalMove) { + _push(move: History['move'], historyType: History['historyType'] = 'move'): void { this._history.push({ + historyType, move, kings: { b: this._kings.b, w: this._kings.w }, turn: this._turn, @@ -1487,28 +1540,44 @@ export class Chess { if (us === BLACK) { this._moveNumber++ } - + this._turn = them } + + undo(): Move | null + undo({ untilMove }: { untilMove: true }): Move | null + undo({ untilMove }: { untilMove: false }): HistoryMove | null + undo({ untilMove = true }: { untilMove?: boolean } = {}): + | HistoryMove + | Move + | null { + const old = this._undoMove() + if (old === null) { + return null + } - undo() { - const move = this._undoMove() - if (move) { - const prettyMove = this._makePretty(move) + const { historyType, move } = old + if (historyType === 'move') { + const prettyMove = this._makePretty(move as InternalMove) this._positionCounts[prettyMove.after]-- - return prettyMove - } - return null + return untilMove + ? { historyType, move: prettyMove } + : prettyMove + } else { + // old is a put/remove + return untilMove + ? this.undo() + : { historyType, move: move as Put | Remove } + } } - private _undoMove() { + private _undoMove(): History | null { const old = this._history.pop() + if (old === undefined) { return null } - - const move = old.move - + this._kings = old.kings this._turn = old.turn this._castling = old.castling @@ -1516,44 +1585,76 @@ export class Chess { this._halfMoves = old.halfMoves this._moveNumber = old.moveNumber - const us = this._turn - const them = swapColor(us) - - this._board[move.from] = this._board[move.to] - this._board[move.from].type = move.piece // to undo any promotions - delete this._board[move.to] - - if (move.captured) { - if (move.flags & BITS.EP_CAPTURE) { - // en passant capture - let index: number - if (us === BLACK) { - index = move.to - 16 + if (old.historyType === 'put') { + const putEvent = old.move as Put + if (putEvent.oldPiece) { + this._put(putEvent.oldPiece, putEvent.square, {update: true, push: false}) + } else { + this._remove(putEvent.square, {update: true, push: false}) + } + } else if (old.historyType === 'remove') { + const removeEvent = old.move as Remove + this._put(removeEvent.piece, removeEvent.square, {update: true, push: false}) + + } else if (old.historyType === 'move') { + const move = old.move as InternalMove + + const us = this._turn + const them = swapColor(us) + + this._board[move.from] = this._board[move.to] + this._board[move.from].type = move.piece // to undo any promotions + delete this._board[move.to] + + if (move.captured) { + if (move.flags & BITS.EP_CAPTURE) { + // en passant capture + let index: number + if (us === BLACK) { + index = move.to - 16 + } else { + index = move.to + 16 + } + this._board[index] = { type: PAWN, color: them } } else { - index = move.to + 16 + // regular capture + this._board[move.to] = { type: move.captured, color: them } } - this._board[index] = { type: PAWN, color: them } - } else { - // regular capture - this._board[move.to] = { type: move.captured, color: them } } - } - - if (move.flags & (BITS.KSIDE_CASTLE | BITS.QSIDE_CASTLE)) { - let castlingTo: number, castlingFrom: number - if (move.flags & BITS.KSIDE_CASTLE) { - castlingTo = move.to + 1 - castlingFrom = move.to - 1 - } else { - castlingTo = move.to - 2 - castlingFrom = move.to + 1 + + if (move.flags & (BITS.KSIDE_CASTLE | BITS.QSIDE_CASTLE)) { + let castlingTo: number, castlingFrom: number + if (move.flags & BITS.KSIDE_CASTLE) { + castlingTo = move.to + 1 + castlingFrom = move.to - 1 + } else { + castlingTo = move.to - 2 + castlingFrom = move.to + 1 + } + + this._board[castlingTo] = this._board[castlingFrom] + delete this._board[castlingFrom] } - - this._board[castlingTo] = this._board[castlingFrom] - delete this._board[castlingFrom] + } else { + throw Error(`Unexpected historyType of _history entry: ${old}`) } - return move + return old + } + + private _redoMove({historyType, move}): void { + // Used to redo moves undone with _undoMove (not with undo) + switch (historyType) { + case 'move': + this._makeMove(move) + break + case 'put': + this.put(move.piece, move.square) + break + case 'remove': + this.remove(move.square) + break + } } pgn({ @@ -1582,9 +1683,14 @@ export class Chess { result.push(newline) } - const appendComment = (moveString: string) => { - const comment = this._comments[this.fen()] - if (typeof comment !== 'undefined') { + const appendComment = ( + moveString: string, + comment: string | undefined = undefined + ): string => { + if (comment === undefined) { + comment = this._comments[this.fen()] + } + if (comment !== undefined) { const delimiter = moveString.length > 0 ? ' ' : '' moveString = `${moveString}${delimiter}{${comment}}` } @@ -1592,7 +1698,7 @@ export class Chess { } // pop all of history onto reversed_history - const reversedHistory = [] + const reversedHistory: History[] = [] while (this._history.length > 0) { reversedHistory.push(this._undoMove()) } @@ -1607,30 +1713,37 @@ export class Chess { // build the list of moves. a move_string looks like: "3. e3 e6" while (reversedHistory.length > 0) { - moveString = appendComment(moveString) - const move = reversedHistory.pop() + const { historyType, move } = reversedHistory.pop() + + if (historyType === 'move') { + const moveEvent = move as InternalMove + moveString = appendComment(moveString) + + // if the position started with black to move, start PGN with #. ... + if (!this._history.length && moveEvent.color === 'b') { + const prefix = `${this._moveNumber}. ...` + // is there a comment preceding the first move? + moveString = moveString ? `${moveString} ${prefix}` : prefix + } else if (moveEvent.color === 'w') { + // store the previous generated move_string if we have one + if (moveString.length) { + moves.push(moveString) + } + moveString = this._moveNumber + '.' + } + + moveString = + moveString + ' ' + this._moveToSan(moveEvent, this._moves({ legal: true })) - // make TypeScript stop complaining about move being undefined - if (!move) { - break - } + } else if (historyType === 'put') { + const putEvent = move as Put + moveString = appendComment(moveString, `Chess.js: ${putEvent.piece.color} ${putEvent.piece.type} put on ${putEvent.square}`) - // if the position started with black to move, start PGN with #. ... - if (!this._history.length && move.color === 'b') { - const prefix = `${this._moveNumber}. ...` - // is there a comment preceding the first move? - moveString = moveString ? `${moveString} ${prefix}` : prefix - } else if (move.color === 'w') { - // store the previous generated move_string if we have one - if (moveString.length) { - moves.push(moveString) - } - moveString = this._moveNumber + '.' + } else if (historyType === 'remove') { + const removeEvent = move as Remove + moveString = appendComment(moveString, `Chess.js: ${removeEvent.piece.color} ${removeEvent.piece.type} removed from ${removeEvent.square}`) } - - moveString = - moveString + ' ' + this._moveToSan(move, this._moves({ legal: true })) - this._makeMove(move) + this._redoMove({ historyType, move }) } // are there any other leftover moves? @@ -1855,7 +1968,7 @@ export class Chess { const encodeComment = function (s: string) { s = s.replace(new RegExp(mask(newlineChar), 'g'), ' ') - return `{${toHex(s.slice(1, s.length - 1))}}` + return ` {${toHex(s)}} ` } const decodeComment = function (s: string) { @@ -1869,12 +1982,8 @@ export class Chess { .replace(headerString, '') .replace( // encode comments so they don't get deleted below - new RegExp(`({[^}]*})+?|;([^${mask(newlineChar)}]*)`, 'g'), - function (_match, bracket, semicolon) { - return bracket !== undefined - ? encodeComment(bracket) - : ' ' + encodeComment(`{${semicolon.slice(1)}}`) - } + new RegExp(`\\s*{([^}]*)}\\s*|;[^\\S\\n\\r]*([^${mask(newlineChar)}]*)[${mask(newlineChar)}]?`, 'g'), + (_match, bracket, semicolon) => encodeComment(bracket !== undefined ? bracket : semicolon) ) .replace(new RegExp(mask(newlineChar), 'g'), ' ') @@ -1904,7 +2013,24 @@ export class Chess { for (let halfMove = 0; halfMove < moves.length; halfMove++) { const comment = decodeComment(moves[halfMove]) if (comment !== undefined) { - this._comments[this.fen()] = comment + const match = comment.match( + /Chess\.js: ([wb]) ([pbnrqk]) (put|removed) (?:on|from) ([a-h][1-8])/ + ) + if (match) { + const [color, piece, event, square] = match.slice(1) + switch (event) { + case 'put': + this.put({type: piece as PieceSymbol, color: color as Color}, square as Square) + break + case 'removed': + this.remove(square as Square) + break + } + } else { + const fen = this.fen() + if (this._comments[fen]) this._comments[fen] += ' ' + comment + else this._comments[fen] = comment + } continue } @@ -2250,26 +2376,49 @@ export class Chess { history({ verbose }: { verbose: true }): Move[] history({ verbose }: { verbose: false }): string[] history({ verbose }: { verbose: boolean }): string[] | Move[] - history({ verbose = false }: { verbose?: boolean } = {}) { - const reversedHistory = [] + history({ verbose, onlyMoves }: { verbose: false, onlyMoves: true }): string[] + history({ verbose, onlyMoves }: { verbose: true, onlyMoves: true }): Move[] + history({ verbose, onlyMoves }: { verbose: true, onlyMoves: false }): HistoryMove[] + history({ verbose, onlyMoves }: { verbose: false, onlyMoves: false }): never + history({ verbose, onlyMoves }: { verbose: boolean, onlyMoves: boolean }): string[] | Move[] | HistoryMove[] + history({ verbose = false, onlyMoves = true }: { verbose?: boolean, onlyMoves?: boolean } = {}) { + /* + * TODO: Why does history need to replay the entire game to get the history? We are already + * storing the game history in _history. It seems the only reason we are replaying the entire + * game is just to call _makePretty on the moves, ie. to get FENs and SANs. Is this necessary? + * Perhaps we should just store this information in _history? + */ + if (!verbose && !onlyMoves) { + throw Error( + 'Unsupported options for history: { verbose: false, onlyMoves: false }.\nPlease use { verbose: true, onlyMoves: false } instead.' + ) + } + + const reversedHistory: History[]= [] const moveHistory = [] while (this._history.length > 0) { reversedHistory.push(this._undoMove()) } - while (true) { - const move = reversedHistory.pop() - if (!move) { - break - } + while (reversedHistory.length > 0) { + const { historyType, move } = reversedHistory.pop() - if (verbose) { - moveHistory.push(this._makePretty(move)) - } else { - moveHistory.push(this._moveToSan(move, this._moves())) + if (onlyMoves && historyType === 'move') { + if (verbose) { + moveHistory.push(this._makePretty(move as InternalMove)) + } else { + moveHistory.push(this._moveToSan(move as InternalMove, this._moves())) + } + } else if (!onlyMoves) { + if (historyType === 'move') { + moveHistory.push({ historyType, move: this._makePretty(move as InternalMove) }) + } else { + moveHistory.push({ historyType, move }) + } } - this._makeMove(move) + + this._redoMove({ historyType, move }) } return moveHistory @@ -2292,11 +2441,12 @@ export class Chess { copyComment(this.fen()) while (true) { - const move = reversedHistory.pop() - if (!move) { + const old = reversedHistory.pop() + if (!old) { break } - this._makeMove(move) + const { historyType, move } = old + this._redoMove({historyType, move}) copyComment(this.fen()) } this._comments = currentComments From 1f9c92a7464afbea43a8c07f0c18778b32ccdfd5 Mon Sep 17 00:00:00 2001 From: Gavin Date: Sun, 18 Jun 2023 19:59:36 +0100 Subject: [PATCH 13/15] added tests for new _history functionality --- __tests__/comments.test.ts | 66 +++++++++++++++++++++++++++++++ __tests__/position-counts.test.ts | 20 ++++++++++ __tests__/put.test.ts | 10 +++++ __tests__/remove.test.ts | 10 +++++ 4 files changed, 106 insertions(+) diff --git a/__tests__/comments.test.ts b/__tests__/comments.test.ts index 066af59..191ef4a 100644 --- a/__tests__/comments.test.ts +++ b/__tests__/comments.test.ts @@ -244,6 +244,21 @@ describe('Load Comments', () => { {test comment} 16. ... Rfb8`, }, + { + name: 'bracket comments without spaces', + input: '1. e4{e4 is good}e5', + output: '1. e4 {e4 is good} e5', + }, + { + name: 'semicolon comments without spaces', + input: '1. e4;e4 is good\ne5', + output: '1. e4 {e4 is good} e5', + }, + { + name: 'multiple comments for the same move', + input: '1. e4 {e4 is good} {it is well studied} e5', + output: '1. e4 {e4 is good it is well studied} e5', + }, ] tests.forEach((test) => { @@ -254,3 +269,54 @@ describe('Load Comments', () => { }) }) }) + +describe('Put/remove comments', () => { + it('adds puts as comments', () => { + const chess = new Chess() + chess.move('e4') + chess.put({type: 'p', color: 'w'}, 'e2') + expect(chess.pgn()).toBe('1. e4 {Chess.js: w p put on e2}') + }) + it('adds removes as comments', () => { + const chess = new Chess() + chess.move('e4') + chess.remove('d2') + expect(chess.pgn()).toBe('1. e4 {Chess.js: w p removed from d2}') + }) + it('adds multiple puts/removes in the same turn', () => { + const chess = new Chess() + chess.move('e4') + chess.put({type: 'p', color: 'w'}, 'e2') + chess.remove('d2') + expect(chess.pgn()).toBe('1. e4 {Chess.js: w p put on e2} {Chess.js: w p removed from d2}') + }) + it('loads puts from comments', () => { + const chess = new Chess() + chess.loadPgn('1. e4 {Chess.js: w p put on e2}') + expect(chess.fen()).toBe('rnbqkbnr/pppppppp/8/8/4P3/8/PPPPPPPP/RNBQKBNR b KQkq - 0 1') + }) + it('loads removes from comments', () => { + const chess = new Chess() + chess.loadPgn('1. e4 {Chess.js: w p removed from d2}') + expect(chess.fen()).toBe('rnbqkbnr/pppppppp/8/8/4P3/8/PPP2PPP/RNBQKBNR b KQkq - 0 1') + }) + it('loads multiple puts/removes in the same turn', () => { + const chess = new Chess() + chess.loadPgn('1. e4 {Chess.js: w p put on e2} {Chess.js: w p removed from d2}') + expect(chess.fen()).toBe('rnbqkbnr/pppppppp/8/8/4P3/8/PPP1PPPP/RNBQKBNR b KQkq - 0 1') + }) + it('can .loadPgn with puts generated by .pgn', () => { + const chess = new Chess() + chess.loadPgn('1. e4 f6 2. Qh5+') + chess.put({type: 'p', color:'b'}, 'f7') + chess.move('Nc6') // illegal move without the put + expect(() => chess.loadPgn(chess.pgn())).not.toThrow() + }) + it('can .loadPgn with removes generated by .pgn', () => { + const chess = new Chess() + chess.loadPgn('1. g3 e6 2. Bg2 Ne7 3. Bc6') + chess.remove('b8') + chess.move('Nxc6') // ambiguous SAN without the remove + expect(() => chess.loadPgn(chess.pgn())).not.toThrow() + }) +}) \ No newline at end of file diff --git a/__tests__/position-counts.test.ts b/__tests__/position-counts.test.ts index 96aff7e..c0fbac7 100644 --- a/__tests__/position-counts.test.ts +++ b/__tests__/position-counts.test.ts @@ -95,3 +95,23 @@ test('_positionCounts - properly updates if remove is called', () => { expect(chess._positionCounts['rnbqkbnr/pppp1ppp/8/4p3/8/8/PPPP1PPP/RNBQKBNR w KQkq - 0 3']).toBe(1) }) +test('_positionCounts - properly updates if put/remove is called but undone', () => { + const chess = new Chess() + expect(chess._positionCounts[DEFAULT_POSITION]).toBe(1) + chess.move('e4') + expect(chess._positionCounts[e4Fen]).toBe(1) + + chess.move('e5') + chess.put({type: 'p', color: 'w'}, 'e2') + chess.remove('e4') + chess.move('Nc3') + expect(chess._positionCounts['rnbqkbnr/pppp1ppp/8/4p3/8/2N5/PPPPPPPP/R1BQKBNR b KQkq - 1 2']).toBe(1) + chess.undo() + chess.undo() + expect(chess._positionCounts['rnbqkbnr/pppp1ppp/8/4p3/8/2N5/PPPPPPPP/R1BQKBNR b KQkq - 1 2']).toBe(0) + chess.move('e5') + chess.move('Nc3') + expect(chess._positionCounts['rnbqkbnr/pppp1ppp/8/4p3/8/2N5/PPPPPPPP/R1BQKBNR b KQkq - 1 2']).toBe(0) + expect(chess._positionCounts['rnbqkbnr/pppp1ppp/8/4p3/4P3/2N5/PPPP1PPP/R1BQKBNR b KQkq - 1 2']).toBe(1) +}) + diff --git a/__tests__/put.test.ts b/__tests__/put.test.ts index dffb8be..f14ed35 100644 --- a/__tests__/put.test.ts +++ b/__tests__/put.test.ts @@ -182,3 +182,13 @@ test('put - replacing white pawn clears black en passant square 2', () => { chess.put({ type :BISHOP, color: WHITE}, 'b5') expect(chess.moves()).not.toContain('bxc6') }); + +test('put - can be undone', () => { + const chess = new Chess() + + chess.move('e4') + chess.put({ type: PAWN, color: WHITE}, 'e2') + expect(chess.fen()).toBe('rnbqkbnr/pppppppp/8/8/4P3/8/PPPPPPPP/RNBQKBNR b KQkq - 0 1') + chess.undo({untilMove: false}) + expect(chess.fen()).toBe('rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq - 0 1') +}); diff --git a/__tests__/remove.test.ts b/__tests__/remove.test.ts index 761260e..ea4060a 100644 --- a/__tests__/remove.test.ts +++ b/__tests__/remove.test.ts @@ -101,3 +101,13 @@ test('remove - removing white pawn clears black en passant square 2', () => { chess.remove('b5') expect(chess.moves()).not.toContain('bxc6') }); + +test('remove - can be undone', () => { + const chess = new Chess() + + chess.move('e4') + chess.remove('e4') + expect(chess.fen()).toBe('rnbqkbnr/pppppppp/8/8/8/8/PPPP1PPP/RNBQKBNR b KQkq - 0 1') + chess.undo({untilMove: false}) + expect(chess.fen()).toBe('rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq - 0 1') +}); From 96df22bc572d48a7941eacdbdfbece8fda966791 Mon Sep 17 00:00:00 2001 From: Gavin Date: Sun, 18 Jun 2023 20:39:13 +0100 Subject: [PATCH 14/15] Updated linting rule to allow non-null assertions There are lots of places where eslint annoying thinks something might be null when it definitely isn't (eg. the return from .pop() in a while loop where .length > 0), so it is very useful to be able to tell the linter that it isn't null. --- .eslintrc.cjs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.eslintrc.cjs b/.eslintrc.cjs index c5db25f..370e15e 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -27,6 +27,7 @@ module.exports = { format: ['PascalCase'], }, ], - 'multiline-comment-style': ['error', 'starred-block'] + 'multiline-comment-style': ['error', 'starred-block'], + "@typescript-eslint/no-non-null-assertion": 'allow' }, } From 31d17428910d60b44531f8350f0e7e7f018c2d5f Mon Sep 17 00:00:00 2001 From: Gavin Date: Sun, 18 Jun 2023 20:39:42 +0100 Subject: [PATCH 15/15] Fixed typing --- src/chess.ts | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/chess.ts b/src/chess.ts index b0304d5..38af876 100644 --- a/src/chess.ts +++ b/src/chess.ts @@ -1642,17 +1642,17 @@ export class Chess { return old } - private _redoMove({historyType, move}): void { + private _redoMove({historyType, move}: HistoryType): void { // Used to redo moves undone with _undoMove (not with undo) switch (historyType) { case 'move': - this._makeMove(move) + this._makeMove(move as InternalMove) break case 'put': - this.put(move.piece, move.square) + this.put(move.piece as Piece, (move as Put).square) break case 'remove': - this.remove(move.square) + this.remove((move as Remove).square) break } } @@ -1700,7 +1700,7 @@ export class Chess { // pop all of history onto reversed_history const reversedHistory: History[] = [] while (this._history.length > 0) { - reversedHistory.push(this._undoMove()) + reversedHistory.push(this._undoMove()!) } const moves = [] @@ -1713,7 +1713,7 @@ export class Chess { // build the list of moves. a move_string looks like: "3. e3 e6" while (reversedHistory.length > 0) { - const { historyType, move } = reversedHistory.pop() + const { historyType, move } = reversedHistory.pop()! if (historyType === 'move') { const moveEvent = move as InternalMove @@ -2381,7 +2381,7 @@ export class Chess { history({ verbose, onlyMoves }: { verbose: true, onlyMoves: false }): HistoryMove[] history({ verbose, onlyMoves }: { verbose: false, onlyMoves: false }): never history({ verbose, onlyMoves }: { verbose: boolean, onlyMoves: boolean }): string[] | Move[] | HistoryMove[] - history({ verbose = false, onlyMoves = true }: { verbose?: boolean, onlyMoves?: boolean } = {}) { + history({ verbose = false, onlyMoves = true }: { verbose?: boolean, onlyMoves?: boolean } = {}): (string | Move | HistoryMove)[] { /* * TODO: Why does history need to replay the entire game to get the history? We are already * storing the game history in _history. It seems the only reason we are replaying the entire @@ -2398,11 +2398,11 @@ export class Chess { const moveHistory = [] while (this._history.length > 0) { - reversedHistory.push(this._undoMove()) + reversedHistory.push(this._undoMove() as History) } while (reversedHistory.length > 0) { - const { historyType, move } = reversedHistory.pop() + const { historyType, move } = reversedHistory.pop() as History if (onlyMoves && historyType === 'move') { if (verbose) { @@ -2414,7 +2414,7 @@ export class Chess { if (historyType === 'move') { moveHistory.push({ historyType, move: this._makePretty(move as InternalMove) }) } else { - moveHistory.push({ historyType, move }) + moveHistory.push({ historyType, move: move as Put | Remove }) } }