UNPKG

kokopu

Version:

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

726 lines (589 loc) 25 kB
/*! * -------------------------------------------------------------------------- * * * * Kokopu - A JavaScript/TypeScript chess library. * * <https://www.npmjs.com/package/kokopu> * * Copyright (C) 2018-2025 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, Game, Position, Variation } = require('../dist/lib/index'); const test = require('unit.js'); describe('General game header', () => { it('Initial state', () => { const game = new Game(); test.value(game.event()).is(undefined); }); it('Set & get', () => { const game = new Game(); game.event('The event'); test.value(game.event()).is('The event'); }); it('Set empty string', () => { const game = new Game(); game.event(''); test.value(game.event()).is(''); }); it('Set blank string', () => { const game = new Game(); game.event(' '); test.value(game.event()).is(' '); }); it('Set non-string (number)', () => { const game = new Game(); game.event(42); test.value(game.event()).is('42'); }); it('Set non-string (boolean)', () => { const game = new Game(); game.event(false); test.value(game.event()).is('false'); }); it('Erase with undefined', () => { const game = new Game(); game.event('The event'); game.event(undefined); test.value(game.event()).is(undefined); }); it('Erase with null', () => { const game = new Game(); game.event('The event'); game.event(null); test.value(game.event()).is(undefined); }); }); describe('Result header', () => { it('Default value', () => { const game = new Game(); test.value(game.result()).is('*'); }); it('Set & get', () => { const game = new Game(); game.result('1-0'); test.value(game.result()).is('1-0'); }); function itInvalidValue(label, value) { it(label, () => { const game = new Game(); test.exception(() => game.result(value)).isInstanceOf(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(); test.exception(() => game.eco(value)).isInstanceOf(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(); test.exception(() => action(game)).isInstanceOf(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); test.value(game.playerElo('w')).is(899); }); it('Set number 2', () => { const game = new Game(); game.playerElo('w', 0); test.value(game.playerElo('w')).is(0); }); it('Set number as string', () => { const game = new Game(); game.playerElo('b', '2000'); test.value(game.playerElo('b')).is(2000); }); function itInvalidElo(label, action) { it(label, () => { const game = new Game(); test.exception(() => action(game)).isInstanceOf(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); test.value(game.round()).is(3); }); it('Set number 2', () => { const game = new Game(); game.subRound(0); test.value(game.subRound()).is(0); }); it('Set number 3', () => { const game = new Game(); game.subSubRound(9999); test.value(game.subSubRound()).is(9999); }); it('Set number as string', () => { const game = new Game(); game.round('2000'); test.value(game.round()).is(2000); }); function itInvalidRound(label, action) { it(label, () => { const game = new Game(); test.exception(() => action(game)).isInstanceOf(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) { test.value(game.date()).is(undefined); test.value(game.dateAsDate()).is(undefined); } function testDateIs(game, value, dateValue) { test.value(game.date()).isNotFalse(); test.value(game.date().toString()).is(value); test.value(game.dateAsDate()).is(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); test.value(game.dateAsString('en-us')).is('September 12, 2021'); test.value(game.dateAsString('fr')).is('12 septembre 2021'); }); it('Get as string (month+year)', () => { const game = new Game(); game.date(2021, 12); test.value(game.dateAsString('en-us')).is('December 2021'); test.value(game.dateAsString('fr')).is('décembre 2021'); }); it('Get as string (year only)', () => { const game = new Game(); game.date(2021); test.value(game.dateAsString('en-us')).is('2021'); test.value(game.dateAsString('fr')).is('2021'); }); function itInvalidValue(label, value) { it(label, () => { const game = new Game(); test.exception(() => game.date(value)).isInstanceOf(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(); test.exception(() => game.date(year, month, day)).isInstanceOf(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); test.value(nodeGetter().nags()).is([ 34 ]); test.value(nodeGetter().hasNag(34)).is(true); test.value(nodeGetter().hasNag(42)).is(false); }); itOnNodeAndVariation('Erase', nodeGetter => { nodeGetter().addNag(18); nodeGetter().addNag(21); nodeGetter().addNag(5); test.value(nodeGetter().nags()).is([ 5, 18, 21 ]); nodeGetter().removeNag(18); test.value(nodeGetter().nags()).is([ 5, 21 ]); test.value(nodeGetter().hasNag(18)).is(false); test.value(nodeGetter().hasNag(21)).is(true); nodeGetter().removeNag(5); test.value(nodeGetter().nags()).is([ 21 ]); nodeGetter().removeNag(16); test.value(nodeGetter().nags()).is([ 21 ]); nodeGetter().removeNag(21); test.value(nodeGetter().nags()).is([]); }); itOnNodeAndVariation('Sorted NAGs', nodeGetter => { nodeGetter().addNag(18); nodeGetter().addNag(11); nodeGetter().addNag(34); nodeGetter().addNag(1234); nodeGetter().addNag(2); nodeGetter().addNag(1); test.value(nodeGetter().nags()).is([ 1, 2, 11, 18, 34, 1234 ]); }); itOnNodeAndVariation('Clear NAGs', nodeGetter => { nodeGetter().addNag(52); nodeGetter().addNag(3); nodeGetter().addNag(14); test.value(nodeGetter().nags()).is([ 3, 14, 52 ]); nodeGetter().clearNags(); test.value(nodeGetter().nags()).is([]); }); itOnNodeAndVariation('Filter NAGs', nodeGetter => { nodeGetter().addNag(18); nodeGetter().addNag(1); nodeGetter().addNag(14); nodeGetter().addNag(24); nodeGetter().addNag(31); test.value(nodeGetter().nags()).is([ 1, 14, 18, 24, 31 ]); nodeGetter().filterNags(nag => nag % 2 === 1); test.value(nodeGetter().nags()).is([ 1, 31 ]); }); function itInvalidNag(label, value) { function doIt(nodeFactory) { test.exception(() => nodeFactory().addNag(value)).isInstanceOf(exception.IllegalArgument); test.exception(() => nodeFactory().removeNag(value)).isInstanceOf(exception.IllegalArgument); test.exception(() => nodeFactory().hasNag(value)).isInstanceOf(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'); test.value(nodeGetter().tags()).is([ 'TheKey' ]); test.value(nodeGetter().tag('TheKey')).is('TheValue'); test.value(nodeGetter().tag('AnotherKey')).is(undefined); }); itOnNodeAndVariation('Set empty string', nodeGetter => { nodeGetter().tag('TheKey1', ''); test.value(nodeGetter().tags()).is([ 'TheKey1' ]); test.value(nodeGetter().tag('TheKey1')).is(''); }); itOnNodeAndVariation('Set blank string', nodeGetter => { nodeGetter().tag('__TheKey__', ' '); test.value(nodeGetter().tags()).is([ '__TheKey__' ]); test.value(nodeGetter().tag('__TheKey__')).is(' '); }); itOnNodeAndVariation('Set non-string (number)', nodeGetter => { nodeGetter().tag('_', 42); test.value(nodeGetter().tags()).is([ '_' ]); test.value(nodeGetter().tag('_')).is('42'); }); itOnNodeAndVariation('Set non-string (boolean)', nodeGetter => { nodeGetter().tag('123', false); test.value(nodeGetter().tags()).is([ '123' ]); test.value(nodeGetter().tag('123')).is('false'); }); itOnNodeAndVariation('Erase with undefined', nodeGetter => { nodeGetter().tag('TheKey', 'TheValue'); nodeGetter().tag('TheKey', undefined); test.value(nodeGetter().tags()).is([]); test.value(nodeGetter().tag('TheKey')).is(undefined); }); itOnNodeAndVariation('Erase with null', nodeGetter => { nodeGetter().tag('TheKey', 'TheValue'); nodeGetter().tag('TheKey', null); test.value(nodeGetter().tags()).is([]); test.value(nodeGetter().tag('TheKey')).is(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); test.value(nodeGetter().tags()).is([ '1234', '32', 'ABCD', 'Blah', 'TheKey', '_a', 'xyz' ]); }); itOnNodeAndVariation('Clear tags', nodeGetter => { nodeGetter().tag('TheKey1', 'TheValue'); nodeGetter().tag('TheKey2', 'TheOtherValue'); test.value(nodeGetter().tags()).is([ 'TheKey1', 'TheKey2' ]); nodeGetter().clearTags(); test.value(nodeGetter().tags()).is([]); }); itOnNodeAndVariation('Filter tags', nodeGetter => { nodeGetter().tag('ab', 'a'); nodeGetter().tag('cd', 'b'); nodeGetter().tag('ef', 'c'); nodeGetter().tag('gh', 'd'); test.value(nodeGetter().tags()).is([ 'ab', 'cd', 'ef', 'gh' ]); nodeGetter().filterTags((tagKey, tagValue) => tagKey.includes('b') || tagValue.includes('d')); test.value(nodeGetter().tags()).is([ 'ab', 'gh' ]); }); function itInvalidKey(label, action) { it(label + ' (node)', () => { const game = new Game(); game.mainVariation().play('e4'); test.exception(() => action(game.mainVariation().first())).isInstanceOf(exception.IllegalArgument); }); it(label + ' (variation)', () => { const game = new Game(); test.exception(() => action(game.mainVariation())).isInstanceOf(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'); test.value(node.toString()).is('2b[Nc6]'); }); it('Main variation', () => { const game = new Game(); const variation = game.mainVariation(); test.value(variation.toString()).is('start'); }); it('Sub-variation', () => { const game = new Game(); const variation = game.mainVariation().play('e4').addVariation(); test.value(variation.toString()).is('1w-v0-start'); }); }); describe('Invalid findById', () => { function itInvalidId(label, id) { it(label, () => { const game = new Game(); let current = game.mainVariation(); current = current.play('e4'); current = current.play('e5'); const alternative1 = current.addVariation(); alternative1.play('c5').play('Nf3'); const alternative2 = current.addVariation(); alternative2.play('e6').play('d4'); current = current.play('Bc4'); current = current.play('Nc6'); current = current.play('Qh5'); current = current.play('Nf6'); current = current.play('Qxf7#'); game.result('1-0'); test.value(game.findById(id)).is(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(); let current = game.mainVariation(); current = current.play('e4'); current = current.play('e5'); current.addVariation(); const alternative2 = current.addVariation(); alternative2.play('e6').play('d4'); current = current.play('Bc4'); current = current.play('Nc6'); current = current.play('Qh5'); return game; } function itFindIdAlias(label, gameBuilder, idAlias, expectedId, expectedFEN) { it(label, () => { const game = gameBuilder(); const result = game.findById(idAlias); test.value(result.id()).is(expectedId); test.value(result instanceof Variation ? result.initialPosition().fen() : result.position().fen()).is(expectedFEN); const result2 = game.findById(idAlias, true); test.value(result2.id()).is(expectedId); test.value(result2 instanceof Variation ? result2.initialPosition().fen() : result2.position().fen()).is(expectedFEN); test.value(game.findById(idAlias, false)).is(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('Invalid initial position', () => { function itInvalidInitialPosition(label, action) { it(label, () => { const game = new Game(); test.exception(() => action(game)).isInstanceOf(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'); test.exception(() => action(node)).isInstanceOf(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'); test.value(node.figurineNotation()).is('\u2658f3'); }); it('Black piece', () => { const game = new Game(); const node = game.mainVariation().play('e4').play('e5').play('Nc3').play('Bc5'); test.value(node.figurineNotation()).is('\u265dc5'); }); it('Null move', () => { const game = new Game(); const node = game.mainVariation().play('e4').play('--'); test.value(node.figurineNotation()).is('--'); }); });