UNPKG

kokopu

Version:

A JavaScript/TypeScript library implementing the chess game rules and providing tools to read/write the standard chess file formats.

800 lines (643 loc) 27.7 kB
/*! * -------------------------------------------------------------------------- * * * * Kokopu - A JavaScript/TypeScript chess library. * * <https://www.npmjs.com/package/kokopu> * * Copyright (C) 2018-2026 Yoann Le Montagner <yo35 -at- melix.net> * * * * Kokopu is free software: you can redistribute it and/or * * modify it under the terms of the GNU Lesser General Public License * * as published by the Free Software Foundation, either version 3 of * * the License, or (at your option) any later version. * * * * Kokopu is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU Lesser General Public License for more details. * * * * You should have received a copy of the GNU Lesser General * * Public License along with this program. If not, see * * <http://www.gnu.org/licenses/>. * * * * -------------------------------------------------------------------------- */ const { exception, DateValue, Game, Position, Variation } = require('../dist/lib/index'); const assert = require('node:assert/strict'); describe('General game header', () => { it('Initial state', () => { const game = new Game(); assert.deepEqual(game.event(), undefined); }); it('Set & get', () => { const game = new Game(); game.event('The event'); assert.deepEqual(game.event(), 'The event'); }); it('Set empty string', () => { const game = new Game(); game.event(''); assert.deepEqual(game.event(), ''); }); it('Set blank string', () => { const game = new Game(); game.event(' '); assert.deepEqual(game.event(), ' '); }); it('Set non-string (number)', () => { const game = new Game(); game.event(42); assert.deepEqual(game.event(), '42'); }); it('Set non-string (boolean)', () => { const game = new Game(); game.event(false); assert.deepEqual(game.event(), 'false'); }); it('Erase with undefined', () => { const game = new Game(); game.event('The event'); game.event(undefined); assert.deepEqual(game.event(), undefined); }); it('Erase with null', () => { const game = new Game(); game.event('The event'); game.event(null); assert.deepEqual(game.event(), undefined); }); }); describe('Result header', () => { it('Default value', () => { const game = new Game(); assert.deepEqual(game.result(), '*'); }); it('Set & get', () => { const game = new Game(); game.result('1-0'); assert.deepEqual(game.result(), '1-0'); }); function itInvalidValue(label, value) { it(label, () => { const game = new Game(); assert.throws(() => game.result(value), exception.IllegalArgument); }); } itInvalidValue('Set dummy value', 'dummy'); itInvalidValue('Set empty string', ''); itInvalidValue('Set undefined', undefined); itInvalidValue('Set null', null); }); describe('ECO header', () => { function itInvalidValue(label, value) { it(label, () => { const game = new Game(); assert.throws(() => game.eco(value), exception.IllegalArgument); }); } itInvalidValue('Set dummy value', 'dummy'); itInvalidValue('Set empty string', ''); itInvalidValue('Set out-of-range code', 'F00'); itInvalidValue('Set untrimmed code 1', ' A18'); itInvalidValue('Set untrimmed code 2', 'A18 '); }); describe('Color-dependant header', () => { function itInvalidColor(label, action) { it(label, () => { const game = new Game(); assert.throws(() => action(game), exception.IllegalArgument); }); } itInvalidColor('Dummy color 1', game => game.playerName('dummy', 'TheName')); itInvalidColor('Dummy color 2', game => game.playerTitle('ww', 'GM')); itInvalidColor('Dummy color 3', game => game.playerTitle('B', 'IM')); itInvalidColor('Empty color', game => game.playerElo('', '1234')); }); describe('Elo header', () => { it('Set number 1', () => { const game = new Game(); game.playerElo('w', 899); assert.deepEqual(game.playerElo('w'), 899); }); it('Set number 2', () => { const game = new Game(); game.playerElo('w', 0); assert.deepEqual(game.playerElo('w'), 0); }); it('Set number as string', () => { const game = new Game(); game.playerElo('b', '2000'); assert.deepEqual(game.playerElo('b'), 2000); }); function itInvalidElo(label, action) { it(label, () => { const game = new Game(); assert.throws(() => action(game), exception.IllegalArgument); }); } itInvalidElo('Non-convertible string', game => game.playerElo('w', 'two thousand')); itInvalidElo('Invalid elo value', game => game.playerElo('b', -42)); }); describe('Round / sub-round / sub-sub-round headers', () => { it('Set number 1', () => { const game = new Game(); game.round(3); assert.deepEqual(game.round(), 3); }); it('Set number 2', () => { const game = new Game(); game.subRound(0); assert.deepEqual(game.subRound(), 0); }); it('Set number 3', () => { const game = new Game(); game.subSubRound(9999); assert.deepEqual(game.subSubRound(), 9999); }); it('Set number as string', () => { const game = new Game(); game.round('2000'); assert.deepEqual(game.round(), 2000); }); function itInvalidRound(label, action) { it(label, () => { const game = new Game(); assert.throws(() => action(game), exception.IllegalArgument); }); } itInvalidRound('Non-convertible string', game => game.round('two')); itInvalidRound('Negative value', game => game.subRound(-42)); itInvalidRound('Non-integer value', game => game.subSubRound(1.2)); }); describe('Date header', () => { function testDateIsUndefined(game) { assert.deepEqual(game.date(), undefined); assert.deepEqual(game.dateAsDate(), undefined); } function testDateIs(game, value, dateValue) { assert(game.date() instanceof DateValue); assert.deepEqual(game.date().toString(), value); assert.deepEqual(game.dateAsDate(), dateValue); } it('Initial state', () => { const game = new Game(); testDateIsUndefined(game); }); it('Set JS Date & get', () => { const game = new Game(); game.date(new Date(2021, 8, 12)); testDateIs(game, '2021-09-12', new Date(2021, 8, 12)); }); it('Set full date & get', () => { const game = new Game(); game.date(2021, 9, 12); testDateIs(game, '2021-09-12', new Date(2021, 8, 12)); }); it('Set month+year date 1 & get', () => { const game = new Game(); game.date(2021, 12); testDateIs(game, '2021-12-**', new Date(2021, 11, 1)); }); it('Set month+year date 2 & get', () => { const game = new Game(); game.date(2021, 2, undefined); testDateIs(game, '2021-02-**', new Date(2021, 1, 1)); }); it('Set month+year date 3 & get', () => { const game = new Game(); game.date(2021, 2, null); testDateIs(game, '2021-02-**', new Date(2021, 1, 1)); }); it('Set year-only date 1 & get', () => { const game = new Game(); game.date(2021); testDateIs(game, '2021-**-**', new Date(2021, 0, 1)); }); it('Set year-only date 2 & get', () => { const game = new Game(); game.date(1, undefined); testDateIs(game, '0001-**-**', new Date(1, 0, 1)); }); it('Set year-only date 3 & get', () => { const game = new Game(); game.date(99, null); testDateIs(game, '0099-**-**', new Date(99, 0, 1)); }); it('Set year-only date 4 & get', () => { const game = new Game(); game.date(1921, undefined, undefined); testDateIs(game, '1921-**-**', new Date(1921, 0, 1)); }); it('Erase with undefined', () => { const game = new Game(); game.date(2021, 9, 12); game.date(undefined); testDateIsUndefined(game); }); it('Erase with null', () => { const game = new Game(); game.date(2021, 9, 12); game.date(null); testDateIsUndefined(game); }); it('Get as string (full date)', () => { const game = new Game(); game.date(2021, 9, 12); assert.deepEqual(game.dateAsString('en-us'), 'September 12, 2021'); assert.deepEqual(game.dateAsString('fr'), '12 septembre 2021'); }); it('Get as string (month+year)', () => { const game = new Game(); game.date(2021, 12); assert.deepEqual(game.dateAsString('en-us'), 'December 2021'); assert.deepEqual(game.dateAsString('fr'), 'décembre 2021'); }); it('Get as string (year only)', () => { const game = new Game(); game.date(2021); assert.deepEqual(game.dateAsString('en-us'), '2021'); assert.deepEqual(game.dateAsString('fr'), '2021'); }); function itInvalidValue(label, value) { it(label, () => { const game = new Game(); assert.throws(() => game.date(value), exception.IllegalArgument); }); } itInvalidValue('Set string', 'dummy'); itInvalidValue('Set boolean', false); itInvalidValue('Set empty object', {}); itInvalidValue('Set invalid year 1', -5); itInvalidValue('Set invalid year 2', 1989.3); function itInvalidYMD(label, year, month, day) { it(label, () => { const game = new Game(); assert.throws(() => game.date(year, month, day), exception.IllegalArgument); }); } itInvalidYMD('Set invalid month 1', 2021, 13, undefined); itInvalidYMD('Set invalid month 2', 2021, 0, undefined); itInvalidYMD('Set invalid month 3', 2021, 5.7, undefined); itInvalidYMD('Set invalid day 1', 2021, 8, 0); itInvalidYMD('Set invalid day 2', 2021, 6, 31); itInvalidYMD('Set invalid day 3', 2021, 3, 3.1); itInvalidYMD('Set day without month', 2021, undefined, 4); itInvalidYMD('Set month without year', undefined, 8, undefined); itInvalidYMD('Set all undefined', undefined, undefined, undefined); }); describe('NAGs', () => { function itOnNodeAndVariation(label, action) { it(label + ' (node)', () => { const game = new Game(); game.mainVariation().play('e4'); action(() => game.mainVariation().first()); }); it(label + ' (variation)', () => { const game = new Game(); action(() => game.mainVariation()); }); } itOnNodeAndVariation('Set & test', nodeGetter => { nodeGetter().addNag(34); assert.deepEqual(nodeGetter().nags(), [ 34 ]); assert.deepEqual(nodeGetter().hasNag(34), true); assert.deepEqual(nodeGetter().hasNag(42), false); }); itOnNodeAndVariation('Erase', nodeGetter => { nodeGetter().addNag(18); nodeGetter().addNag(21); nodeGetter().addNag(5); assert.deepEqual(nodeGetter().nags(), [ 5, 18, 21 ]); nodeGetter().removeNag(18); assert.deepEqual(nodeGetter().nags(), [ 5, 21 ]); assert.deepEqual(nodeGetter().hasNag(18), false); assert.deepEqual(nodeGetter().hasNag(21), true); nodeGetter().removeNag(5); assert.deepEqual(nodeGetter().nags(), [ 21 ]); nodeGetter().removeNag(16); assert.deepEqual(nodeGetter().nags(), [ 21 ]); nodeGetter().removeNag(21); assert.deepEqual(nodeGetter().nags(), []); }); itOnNodeAndVariation('Sorted NAGs', nodeGetter => { nodeGetter().addNag(18); nodeGetter().addNag(11); nodeGetter().addNag(34); nodeGetter().addNag(1234); nodeGetter().addNag(2); nodeGetter().addNag(1); assert.deepEqual(nodeGetter().nags(), [ 1, 2, 11, 18, 34, 1234 ]); }); itOnNodeAndVariation('Clear NAGs', nodeGetter => { nodeGetter().addNag(52); nodeGetter().addNag(3); nodeGetter().addNag(14); assert.deepEqual(nodeGetter().nags(), [ 3, 14, 52 ]); nodeGetter().clearNags(); assert.deepEqual(nodeGetter().nags(), []); }); itOnNodeAndVariation('Filter NAGs', nodeGetter => { nodeGetter().addNag(18); nodeGetter().addNag(1); nodeGetter().addNag(14); nodeGetter().addNag(24); nodeGetter().addNag(31); assert.deepEqual(nodeGetter().nags(), [ 1, 14, 18, 24, 31 ]); nodeGetter().filterNags(nag => nag % 2 === 1); assert.deepEqual(nodeGetter().nags(), [ 1, 31 ]); }); function itInvalidNag(label, value) { function doIt(nodeFactory) { assert.throws(() => nodeFactory().addNag(value), exception.IllegalArgument); assert.throws(() => nodeFactory().removeNag(value), exception.IllegalArgument); assert.throws(() => nodeFactory().hasNag(value), exception.IllegalArgument); } it(label + ' (node)', () => { doIt(() => { const game = new Game(); return game.mainVariation().play('e4'); }); }); it(label + ' (variation)', () => { doIt(() => { const game = new Game(); return game.mainVariation(); }); }); } itInvalidNag('Non-numeric NAG 1', 'dummy'); itInvalidNag('Non-numeric NAG 2', '42'); itInvalidNag('Negative NAG', -1); itInvalidNag('NaN NAG', NaN); }); describe('Tags', () => { function itOnNodeAndVariation(label, action) { it(label + ' (node)', () => { const game = new Game(); game.mainVariation().play('e4'); action(() => game.mainVariation().first()); }); it(label + ' (variation)', () => { const game = new Game(); action(() => game.mainVariation()); }); } itOnNodeAndVariation('Set & get', nodeGetter => { nodeGetter().tag('TheKey', 'TheValue'); assert.deepEqual(nodeGetter().tags(), [ 'TheKey' ]); assert.deepEqual(nodeGetter().tag('TheKey'), 'TheValue'); assert.deepEqual(nodeGetter().tag('AnotherKey'), undefined); }); itOnNodeAndVariation('Set empty string', nodeGetter => { nodeGetter().tag('TheKey1', ''); assert.deepEqual(nodeGetter().tags(), [ 'TheKey1' ]); assert.deepEqual(nodeGetter().tag('TheKey1'), ''); }); itOnNodeAndVariation('Set blank string', nodeGetter => { nodeGetter().tag('__TheKey__', ' '); assert.deepEqual(nodeGetter().tags(), [ '__TheKey__' ]); assert.deepEqual(nodeGetter().tag('__TheKey__'), ' '); }); itOnNodeAndVariation('Set non-string (number)', nodeGetter => { nodeGetter().tag('_', 42); assert.deepEqual(nodeGetter().tags(), [ '_' ]); assert.deepEqual(nodeGetter().tag('_'), '42'); }); itOnNodeAndVariation('Set non-string (boolean)', nodeGetter => { nodeGetter().tag('123', false); assert.deepEqual(nodeGetter().tags(), [ '123' ]); assert.deepEqual(nodeGetter().tag('123'), 'false'); }); itOnNodeAndVariation('Erase with undefined', nodeGetter => { nodeGetter().tag('TheKey', 'TheValue'); nodeGetter().tag('TheKey', undefined); assert.deepEqual(nodeGetter().tags(), []); assert.deepEqual(nodeGetter().tag('TheKey'), undefined); }); itOnNodeAndVariation('Erase with null', nodeGetter => { nodeGetter().tag('TheKey', 'TheValue'); nodeGetter().tag('TheKey', null); assert.deepEqual(nodeGetter().tags(), []); assert.deepEqual(nodeGetter().tag('TheKey'), undefined); }); itOnNodeAndVariation('Sorted keys', nodeGetter => { nodeGetter().tag('TheKey', 'TheValue'); nodeGetter().tag('ABCD', 'Another value'); nodeGetter().tag('1234', 'Some number'); nodeGetter().tag('_a', ''); nodeGetter().tag('32', 'blah'); nodeGetter().tag('xyz', 0); nodeGetter().tag('Blah', 33); assert.deepEqual(nodeGetter().tags(), [ '1234', '32', 'ABCD', 'Blah', 'TheKey', '_a', 'xyz' ]); }); itOnNodeAndVariation('Clear tags', nodeGetter => { nodeGetter().tag('TheKey1', 'TheValue'); nodeGetter().tag('TheKey2', 'TheOtherValue'); assert.deepEqual(nodeGetter().tags(), [ 'TheKey1', 'TheKey2' ]); nodeGetter().clearTags(); assert.deepEqual(nodeGetter().tags(), []); }); itOnNodeAndVariation('Filter tags', nodeGetter => { nodeGetter().tag('ab', 'a'); nodeGetter().tag('cd', 'b'); nodeGetter().tag('ef', 'c'); nodeGetter().tag('gh', 'd'); assert.deepEqual(nodeGetter().tags(), [ 'ab', 'cd', 'ef', 'gh' ]); nodeGetter().filterTags((tagKey, tagValue) => tagKey.includes('b') || tagValue.includes('d')); assert.deepEqual(nodeGetter().tags(), [ 'ab', 'gh' ]); }); function itInvalidKey(label, action) { it(label + ' (node)', () => { const game = new Game(); game.mainVariation().play('e4'); assert.throws(() => action(game.mainVariation().first()), exception.IllegalArgument); }); it(label + ' (variation)', () => { const game = new Game(); assert.throws(() => action(game.mainVariation()), exception.IllegalArgument); }); } itInvalidKey('Dummy key 1', node => node.tag('.', 'TheValue')); itInvalidKey('Dummy key 2', node => node.tag('-', undefined)); itInvalidKey('Empty key', node => node.tag('', 'Whatever')); itInvalidKey('Blank key', node => node.tag(' ', 'The value')); itInvalidKey('Undefined key', node => node.tag(undefined, 'Another value')); }); describe('ToString', () => { it('Node', () => { const game = new Game(); const node = game.mainVariation().play('e4').play('e5').play('Nf3').play('Nc6'); assert.deepEqual(node.toString(), '2b[Nc6]'); }); it('Main variation', () => { const game = new Game(); const variation = game.mainVariation(); assert.deepEqual(variation.toString(), 'start'); }); it('Sub-variation', () => { const game = new Game(); const variation = game.mainVariation().play('e4').addVariation(); assert.deepEqual(variation.toString(), '1w-v0-start'); }); }); describe('Invalid findById', () => { function itInvalidId(label, id) { it(label, () => { const game = new Game(); const current = game.mainVariation().play('e4').play('e5'); const alternative1 = current.addVariation(); alternative1.play('c5').play('Nf3'); const alternative2 = current.addVariation(); alternative2.play('e6').play('d4'); current.play('Bc4').play('Nc6').play('Qh5').play('Nf6').play('Qxf7#'); game.result('1-0'); assert.deepEqual(game.findById(id), undefined); }); } itInvalidId('Empty', ''); itInvalidId('Missing start', '1b-v0'); itInvalidId('Invalid variation index', '1b-vOne-start'); itInvalidId('Out of bound variation index', '1b-v2-start'); itInvalidId('Out of bound node 1', '5w'); itInvalidId('Out of bound node 2', '1b-v0-2b'); itInvalidId('Out of bound node 3', '5w-v0-start'); }); describe('FindById with aliases', () => { function buildGame() { const game = new Game(); const current = game.mainVariation().play('e4').play('e5'); current.addVariation(); const alternative2 = current.addVariation(); alternative2.play('e6').play('d4'); current.play('Bc4').play('Nc6').play('Qh5'); return game; } function itFindIdAlias(label, gameBuilder, idAlias, expectedId, expectedFEN) { it(label, () => { const game = gameBuilder(); const result = game.findById(idAlias); assert.deepEqual(result.id(), expectedId); assert.deepEqual(result instanceof Variation ? result.initialPosition().fen() : result.position().fen(), expectedFEN); const result2 = game.findById(idAlias, true); assert.deepEqual(result2.id(), expectedId); assert.deepEqual(result2 instanceof Variation ? result2.initialPosition().fen() : result2.position().fen(), expectedFEN); assert.deepEqual(game.findById(idAlias, false), undefined); }); } itFindIdAlias('End of main line', buildGame, 'end', '3w', 'r1bqkbnr/pppp1ppp/2n5/4p2Q/2B1P3/8/PPPP1PPP/RNB1K1NR b KQkq - 0 1'); itFindIdAlias('End of sub-variation', buildGame, '1b-v1-end', '1b-v1-2w', 'rnbqkbnr/pppp1ppp/4p3/8/3PP3/8/PPP2PPP/RNBQKBNR b KQkq - 0 1'); itFindIdAlias('End of empty sub-variation', buildGame, '1b-v0-end', '1b-v0-start', 'rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq - 0 1'); itFindIdAlias('End of empty main line', () => new Game(), 'end', 'start', 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1'); }); describe('Following ID', () => { function checkVariation(variation) { // Check the method on the variation itself. assert.deepEqual(variation.followingId(0), variation.id()); // Check the method on each node of the variation. const previousNodesOrVariation = [ variation ]; let currentNode = variation.first(); while (currentNode) { // Check current node. let index = 0; for (const previousNodeOrVariation of previousNodesOrVariation) { assert.deepEqual(previousNodeOrVariation.followingId(previousNodesOrVariation.length - index), currentNode.id()); ++index; } assert.deepEqual(currentNode.followingId(0), currentNode.id()); // Check the variations starting at the current node, if any. for (const subVariation of currentNode.variations()) { checkVariation(subVariation); } // Go to the next node. previousNodesOrVariation.push(currentNode); currentNode = currentNode.next(); } } function itFollowingID(label, gameBuilder) { it(label, () => { const game = gameBuilder(); checkVariation(game.mainVariation()); }); } itFollowingID('Linear game', () => { const game = new Game(); game.mainVariation().play('e4').play('e5').play('Nf3').play('Nc6').play('Bc4').play('Nf6'); return game; }); itFollowingID('Game with sub-variations', () => { const game = new Game(); const current = game.mainVariation().play('e4').play('e5'); const alternative1 = current.addVariation(); alternative1.play('c5').play('Nf3').play('d6'); const alternative2 = current.addVariation(); alternative2.play('e6').play('d4'); current.play('Bc4').play('Nc6').play('Qh5').play('Nf6').play('Qxf7#'); return game; }); }); describe('Invalid followingId', () => { function itInvalidFollowingId(label, distance) { it(label, () => { const game = new Game(); const mainVariation = game.mainVariation(); const firstNode = mainVariation.play('e4'); const secondNode = firstNode.play('e5'); assert.throws(() => mainVariation.followingId(distance), exception.IllegalArgument); assert.throws(() => firstNode.followingId(distance), exception.IllegalArgument); assert.throws(() => secondNode.followingId(distance), exception.IllegalArgument); }); } itInvalidFollowingId('String', '1'); itInvalidFollowingId('Negative integer', -1); itInvalidFollowingId('Non-integer', 1.2); itInvalidFollowingId('Infinite value', Number.POSITIVE_INFINITY); itInvalidFollowingId('NaN', Number.NaN); }); describe('Invalid initial position', () => { function itInvalidInitialPosition(label, action) { it(label, () => { const game = new Game(); assert.throws(() => action(game), exception.IllegalArgument); }); } itInvalidInitialPosition('Not a position 1', game => game.initialPosition(42)); itInvalidInitialPosition('Not a position 2', game => game.initialPosition('whatever')); itInvalidInitialPosition('Invalid full-move number', game => game.initialPosition(new Position(), 'not-a-number')); }); describe('Invalid variation index', () => { function itInvalidVariationIndex(label, action) { it(label, () => { const game = new Game(); const node = game.mainVariation().play('e4').play('e5'); node.addVariation().play('c5').play('Nf3'); node.addVariation().play('d5').play('exd5').play('Qxd5'); node.addVariation().play('c6').play('d4'); node.addVariation().play('e6').play('d4').play('d5'); node.play('Nf3').play('Nc6').play('Bc4'); assert.throws(() => action(node), exception.IllegalArgument); }); } itInvalidVariationIndex('Not a number (remove)', node => node.removeVariation('1')); itInvalidVariationIndex('Not a number (promote)', node => node.promoteVariation('2')); itInvalidVariationIndex('Not a number (swap 1)', node => node.swapVariations('0', 1)); itInvalidVariationIndex('Not a number (swap 2)', node => node.swapVariations(2, '3')); itInvalidVariationIndex('Out of range (remove)', node => node.removeVariation(4)); itInvalidVariationIndex('Out of range (promote)', node => node.promoteVariation(4)); itInvalidVariationIndex('Out of range (swap 1)', node => node.swapVariations(4, 1)); itInvalidVariationIndex('Out of range (swap 2)', node => node.swapVariations(2, 4)); }); describe('Figurine notation', () => { it('White piece', () => { const game = new Game(); const node = game.mainVariation().play('Nf3'); assert.deepEqual(node.figurineNotation(), '\u2658f3'); }); it('Black piece', () => { const game = new Game(); const node = game.mainVariation().play('e4').play('e5').play('Nc3').play('Bc5'); assert.deepEqual(node.figurineNotation(), '\u265dc5'); }); it('Null move', () => { const game = new Game(); const node = game.mainVariation().play('e4').play('--'); assert.deepEqual(node.figurineNotation(), '--'); }); });