Skip to content

Commit

Permalink
Refactor positionCount proxy
Browse files Browse the repository at this point in the history
The positionCount proxy was throwing errors when storing the Chess
object in a pinia store (a state management library for Vue.js). The
positionCount was refactored into a simple record with accessor methods.
  • Loading branch information
jhlywa committed Feb 24, 2024
1 parent f39e627 commit c1909fe
Show file tree
Hide file tree
Showing 2 changed files with 71 additions and 63 deletions.
68 changes: 34 additions & 34 deletions __tests__/position-counts.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,79 +5,79 @@ import { Chess as ChessClass, DEFAULT_POSITION } from '../src/chess'
const Chess = ChessClass as any
const e4Fen = 'rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq - 0 1'

test('_positionCounts - counts repeated positions', () => {
test('positionCount - counts repeated positions', () => {
const chess = new Chess()
expect(chess._positionCounts[DEFAULT_POSITION]).toBe(1)
expect(chess._getPositionCount(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)
expect(chess._getPositionCount(fen)).toBe(1)
}
chess.move(move)
fens.push(chess.fen())
}
expect(chess._positionCounts[DEFAULT_POSITION]).toBe(2)
expect(chess._positionCounts.length).toBe(4)
expect(chess._getPositionCount(DEFAULT_POSITION)).toBe(2)
expect(Object.keys(chess._positionCount).length).toBe(4)
})

test('_positionCounts - removes when undo', () => {
test('positionCount - removes when undo', () => {
const chess = new Chess()
expect(chess._positionCounts[DEFAULT_POSITION]).toBe(1)
expect(chess._positionCounts[e4Fen]).toBe(0)
expect(chess._getPositionCount(DEFAULT_POSITION)).toBe(1)
expect(chess._getPositionCount(e4Fen)).toBe(0)
chess.move('e4')
expect(chess._positionCounts[DEFAULT_POSITION]).toBe(1)
expect(chess._getPositionCount(DEFAULT_POSITION)).toBe(1)
expect(chess.fen()).toBe(e4Fen)
expect(chess._positionCounts[e4Fen]).toBe(1)
expect(chess._getPositionCount(e4Fen)).toBe(1)

chess.undo()
expect(chess._positionCounts[DEFAULT_POSITION]).toBe(1)
expect(chess._positionCounts[e4Fen]).toBe(0)
expect(chess._positionCounts.length).toBe(1)
expect(chess._getPositionCount(DEFAULT_POSITION)).toBe(1)
expect(chess._getPositionCount(e4Fen)).toBe(0)
expect(Object.keys(chess._positionCount).length).toBe(1)
})

test('_positionCounts - resets when cleared', () => {
test('positionCount - 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)
expect(chess._getPositionCount(DEFAULT_POSITION)).toBe(0)
expect(Object.keys(chess._positionCount).length).toBe(0)
})

test('_positionCounts - resets when loading FEN', () => {
test('positionCount - resets when loading FEN', () => {
const chess = new Chess()
expect(chess._positionCounts[DEFAULT_POSITION]).toBe(1)
expect(chess._getPositionCount(DEFAULT_POSITION)).toBe(1)
chess.move('e4')
expect(chess._positionCounts[DEFAULT_POSITION]).toBe(1)
expect(chess._positionCounts[e4Fen]).toBe(1)
expect(chess._getPositionCount(DEFAULT_POSITION)).toBe(1)
expect(chess._getPositionCount(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)
expect(chess._getPositionCount(DEFAULT_POSITION)).toBe(0)
expect(chess._getPositionCount(e4Fen)).toBe(0)
expect(chess._getPositionCount(newFen)).toBe(1)
expect(Object.keys(chess._positionCount).length).toBe(1)
})

test('_positionCounts - resets when loading PGN', () => {
test('positionCount - 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._getPositionCount(DEFAULT_POSITION)).toBe(1)
expect(chess._getPositionCount(e4Fen)).toBe(0)
expect(
chess._positionCounts[
'rnbqkbnr/pppppppp/8/8/3P4/8/PPP1PPPP/RNBQKBNR b KQkq - 0 1'
],
chess._getPositionCount(
'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'
],
chess._getPositionCount(
'rnbqkbnr/ppp1pppp/8/3p4/3P4/8/PPP1PPPP/RNBQKBNR w KQkq - 0 2',
),
).toBe(1)
expect(chess._positionCounts.length).toBe(3)
expect(Object.keys(chess._positionCount).length).toBe(3)
})
66 changes: 37 additions & 29 deletions src/chess.ts
Original file line number Diff line number Diff line change
Expand Up @@ -546,7 +546,9 @@ export class Chess {
private _history: History[] = []
private _comments: Record<string, string> = {}
private _castling: Record<Color, number> = { w: 0, b: 0 }
private _positionCounts: Record<string, number> = {}

// tracks number of times a position has been seen for repetition checking
private _positionCount: Record<string, number> = {}

constructor(fen = DEFAULT_POSITION) {
this.load(fen)
Expand All @@ -563,6 +565,7 @@ export class Chess {
this._history = []
this._comments = {}
this._header = preserveHeaders ? this._header : {}
this._positionCount = {}

/*
* Delete the SetUp and FEN headers (if preserved), the board is empty and
Expand All @@ -571,25 +574,6 @@ export class Chess {
*/
delete this._header['SetUp']
delete this._header['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({} as Record<string, number>, {
get: (target, position: string) =>
position === 'length'
? Object.keys(target).length // length for unit testing
: target?.[trimFen(position)] || 0,
set: (target, position: string, count: number) => {
const trimmedFen = trimFen(position)
if (count === 0) delete target[trimmedFen]
else target[trimmedFen] = count
return true
},
})
}

removeHeader(key: string) {
Expand Down Expand Up @@ -658,7 +642,7 @@ export class Chess {
this._moveNumber = parseInt(tokens[5], 10)

this._updateSetup(fen)
this._positionCounts[fen]++
this._incPositionCount(fen)
}

fen() {
Expand Down Expand Up @@ -1063,12 +1047,8 @@ export class Chess {
return false
}

private _getRepetitionCount() {
return this._positionCounts[this.fen()]
}

isThreefoldRepetition(): boolean {
return this._getRepetitionCount() >= 3
return this._getPositionCount(this.fen()) >= 3
}

isDraw() {
Expand Down Expand Up @@ -1404,7 +1384,7 @@ export class Chess {
const prettyMove = this._makePretty(moveObj)

this._makeMove(moveObj)
this._positionCounts[prettyMove.after]++
this._incPositionCount(prettyMove.after)
return prettyMove
}

Expand Down Expand Up @@ -1520,7 +1500,7 @@ export class Chess {
const move = this._undoMove()
if (move) {
const prettyMove = this._makePretty(move)
this._positionCounts[prettyMove.after]--
this._decPositionCount(prettyMove.after)
return prettyMove
}
return null
Expand Down Expand Up @@ -1947,7 +1927,7 @@ export class Chess {
// reset the end of game marker if making a valid move
result = ''
this._makeMove(move)
this._positionCounts[this.fen()]++
this._incPositionCount(this.fen())
}
}

Expand Down Expand Up @@ -2303,6 +2283,34 @@ export class Chess {
return moveHistory
}

/*
* Keeps track of position occurrence counts for the purpose of repetition
* checking. All three methods (`_inc`, `_dec`, and `_get`) trim the
* irrelevent information from the fen, initialising new positions, and
* removing old positions from the record if their counts are reduced to 0.
*/
private _getPositionCount(fen: string) {
const trimmedFen = trimFen(fen)
return this._positionCount[trimmedFen] || 0
}

private _incPositionCount(fen: string) {
const trimmedFen = trimFen(fen)
if (this._positionCount[trimmedFen] === undefined) {
this._positionCount[trimmedFen] = 0
}
this._positionCount[trimmedFen] += 1
}

private _decPositionCount(fen: string) {
const trimmedFen = trimFen(fen)
if (this._positionCount[trimmedFen] === 1) {
delete this._positionCount[trimmedFen]
} else {
this._positionCount[trimmedFen] -= 1
}
}

private _pruneComments() {
const reversedHistory = []
const currentComments: Record<string, string> = {}
Expand Down

0 comments on commit c1909fe

Please sign in to comment.