Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Changes to isDraw and isGameOver (to fix #112) and fixes for games that include put/remove #417

Open
wants to merge 15 commits into
base: master
Choose a base branch
from
Open
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
3 changes: 2 additions & 1 deletion .eslintrc.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -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'
},
}
99 changes: 93 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -424,17 +424,36 @@ chess.isCheckmate()
// -> true
```

### .isDraw()
### .canClaimDraw()

Returns true or false if the game is drawn (50-move rule or insufficient
material).
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).

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')
chess.isDraw()
// -> true
```

```ts
const chess = new Chess('8/8/8/2R5/8/7K/8/k2r4 w - - 149 129')
chess.isDraw()
// -> true

chess.isDraw({ strict: true })
// -> false

chess.move('Rd5')
chess.isDraw({ strict: true })
// -> true
```

### .isInsufficientMaterial()

Returns true if the game is drawn due to insufficient material (K vs. K, K vs.
Expand All @@ -446,10 +465,11 @@ chess.isInsufficientMaterial()
// -> true
```

### .isGameOver()
### .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.

Returns true if the game has ended via checkmate, stalemate, draw, threefold
repetition, or insufficient material. Otherwise, returns false.
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()
Expand Down Expand Up @@ -499,6 +519,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)

Expand Down
66 changes: 66 additions & 0 deletions __tests__/comments.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand All @@ -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()
})
})
58 changes: 58 additions & 0 deletions __tests__/is-draw.test.ts
Original file line number Diff line number Diff line change
@@ -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({strict: true})).toBe(false)
chess.move(move)
})

expect(chess.isDraw()).toBe(true)
expect(chess.isDraw({strict: true})).toBe(false)
chess.move('Nf3')
chess.move('Nf6')
chess.move('Ng1')
chess.move('Ng8')
expect(chess.isDraw({strict: 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({strict: true})).toBe(false)
chess.move(move)
})

strictMoves.forEach((move) => {
expect(chess.isDraw()).toBe(true)
expect(chess.isDraw({strict: true})).toBe(false)
chess.move(move)
})
expect(chess.isDraw()).toBe(true)
expect(chess.isDraw({strict: true})).toBe(true)
})
33 changes: 33 additions & 0 deletions __tests__/is-fifty-move-rule.test.ts
Original file line number Diff line number Diff line change
@@ -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)
})
13 changes: 13 additions & 0 deletions __tests__/is-fivefold-repetition.test.ts
Original file line number Diff line number Diff line change
@@ -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)
})
31 changes: 31 additions & 0 deletions __tests__/is-seventy-five-move-rule.test.ts
Original file line number Diff line number Diff line change
@@ -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)
})
4 changes: 4 additions & 0 deletions __tests__/is-threefold-repetition.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand All @@ -22,4 +24,6 @@ test('isThreefoldRepetition - 2', () => {
chess.move(move)
})
expect(chess.isThreefoldRepetition()).toBe(true)
chess.move('e4')
expect(chess.isThreefoldRepetition()).toBe(false)
})
Loading