Skip to content

Commit

Permalink
test: add pgn util specs
Browse files Browse the repository at this point in the history
  • Loading branch information
mwiraszka committed Dec 29, 2024
1 parent 4c8ed86 commit bd4ed5f
Show file tree
Hide file tree
Showing 17 changed files with 316 additions and 35 deletions.
78 changes: 78 additions & 0 deletions src/app/mocks/pgns.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
export const mockPgns = [
`[Event "Mock Event 1 (White wins)"]
[Site "?"]
[Date "2000.01.01"]
[Round "1"]
[White "Whiteman, J."]
[Black "Blackley, G."]
[Result "1-0"]
[ECO "A01"]
[EventDate "2000.01.01"]
[PlyCount "57"]
1. d4 e6 2. c4 f5 3. g3 Nf6 4. Bg2 d5 5. Nf3 c6 6. O-O Bd6 7. b3 Qe7 8. Bb2
b6 9. Nbd2 O-O 10. Ne5 Bb7 11. Rc1 c5 12. dxc5 bxc5 13. cxd5 exd5 14. Ndc4
dxc4 15. Bxb7 Bxe5 16. Bd5+ Nxd5 17. Qxd5+ Qf7 18. Qxa8 Bxb2 19. Rxc4 Nd7
20. Qxa7 Qd5 21. Qa5 Bd4 22. Rc2 f4 23. Qd2 f3 24. e3 Qf5 25. Kh1 Qh3 26.
Rg1 Ne5 27. Rxc5 Ng4 28. Rh5 Qxh5 29. h4 1-0`,

`[Event "Mock Event 2 (Draw)"]
[Site "?"]
[Date "2010.01.01"]
[Round "5"]
[White "Blanc, Matt"]
[Black "Noir, Necromanie"]
[Result "1/2-1/2"]
[ECO "C95"]
[EventDate "2010.01.01"]
[PlyCount "67"]
1. e4 e5 2. Nf3 Nc6 3. Bb5 a6 4. Ba4 Nf6 5. O-O Be7 6. Re1 b5 7. Bb3 d6 8.
c3 O-O 9. h3 Nb8 10. d4 Nbd7 11. Nbd2 Bb7 12. Bc2 Re8 13. Nf1 Bf8 14. Ng3
g6 15. b3 Bg7 16. d5 Nb6 17. Be3 Nfd7 18. Qd2 Nc5 19. Rad1 Qe7 20. h4 Rad8
21. h5 c6 22. c4 bxc4 23. Qa5 cxb3 24. axb3 Na8 25. b4 Nd7 26. dxc6 Bxc6
27. Qxa6 Nb8 28. Qa2 Nc7 29. Bb3 Nba6 30. Ng5 Rf8 31. hxg6 hxg6 32. Qe2 Bb5
33. Qg4 Bf6 34. Qh4 1/2-1/2`,

`["Mock Event 3 (Black wins)"]
[Site "?"]
[Date "2020.01.01"]
[Round "72"]
[White "W. W."]
[Black "B. B."]
[Result "0-1"]
[ECO "A01"]
[EventDate "2020.01.01"]
[PlyCount "108"]
1. e4 e5 2. Nf3 Nc6 3. Bb5 f5 4. Nc3 Nd4 5. Bc4 d6 6. d3 Nf6 7. Ng5 { # }
7... fxe4 8. Nf7 Bg4 9. Qd2 Qd7 10. Nxh8 d5 11. Nxd5 Nxd5 12. dxe4 Nf6 13.
c3 Be6 14. Bxe6 Nxe6 15. Qxd7+ Nxd7 16. Be3 Bc5 17. Ke2 Bxe3 18. Kxe3 Ke7
19. g3 g5 20. h4 g4 21. Ng6+ hxg6 22. h5 g5 23. f3 Nf6 24. Raf1 Rh8 25.
fxg4 Nxg4+ 26. Kf3 Nh6 27. g4 Nc5 28. Rhg1 Nd7 29. Rd1 Nf6 { Time: White
56', Black 20'. } 30. Rg2 Rf8 31. Ke3 Nfxg4+ 32. Ke2 Rf4 33. Rd5 Rxe4+ 34.
Kd3 Rf4 35. Ke2 Rf8 36. Ra5 a6 37. Ra4 Rf4 38. Ra5 Ke6 39. b4 Kf5 40. Rc5
c6 41. a4 Nf6 42. b5 axb5 43. axb5 cxb5 44. Rxb5 Nd5 45. Rxb7 Nxc3+ 46. Kd3
Rf3+ 47. Kc4 Ne4 48. Rh7 Nd6+ 49. Kd5 Rd3+ 50. Kc5 Ndf7 51. Kc4 Rd4+ 52.
Kc3 Rh4 53. Rf2+ Rf4 54. Rg2 Kf6 { The game went 62 moves. } 0-1`,

`[Event "Mock Event 4 (Inconclusive game)"]
[Site "?"]
[Date "2025.01.01"]
[Round "72"]
[White "W. W."]
[Black "B. B."]
[Result "*"]
[ECO "C63"]
[EventDate "2020.01.01"]
[PlyCount "67"]
1. e4 e5 2. Nf3 Nc6 3. Bb5 a6 4. Ba4 Nf6 5. O-O Be7 6. Re1 b5 7. Bb3 d6 8.
c3 O-O 9. h3 Nb8 10. d4 Nbd7 11. Nbd2 Bb7 12. Bc2 Re8 13. Nf1 Bf8 14. Ng3
g6 15. b3 Bg7 16. d5 Nb6 17. Be3 Nfd7 18. Qd2 Nc5 19. Rad1 Qe7 20. h4 Rad8
21. h5 c6 22. c4 bxc4 23. Qa5 cxb3 24. axb3 Na8 25. b4 Nd7 26. dxc6 Bxc6
27. Qxa6 Nb8 28. Qa2 Nc7 29. Bb3 Nba6 30. Ng5 Rf8 31. hxg6 hxg6 32. Qe2 Bb5
33. Qg4 Bf6 34. Qh4 *`,

`["Mock Event 5 (Empty PGN)"]`,
];
17 changes: 17 additions & 0 deletions src/app/utils/pgn/get-eco-opening-code.util.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { mockPgns } from '@app/mocks/pgns';

import { getEcoOpeningCode } from './get-eco-opening-code.util';

describe('getEcoOpeningCode', () => {
it('returns `undefined` if the PGN is `undefined`', () => {
expect(getEcoOpeningCode(undefined)).toBe(undefined);
});

it('returns the ECO opening if it can be found in the PGN', () => {
expect(getEcoOpeningCode(mockPgns[0])).toBe('A01');
});

it('returns "X98" if an opening code cannot be found in the PGN', () => {
expect(getEcoOpeningCode(mockPgns[4])).toBe('X98');
});
});
2 changes: 0 additions & 2 deletions src/app/utils/pgn/get-eco-opening-code.util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@
* Parse PGN for the game's Encyclopedia of Chess Openings (ECO) code.
*
* Return a custom `'X98'` code if it cannot be found.
*
* @param pgn
*/
export function getEcoOpeningCode(pgn?: string): string | undefined {
if (!pgn) {
Expand Down
26 changes: 26 additions & 0 deletions src/app/utils/pgn/get-lichess-analysis-url.util.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { mockPgns } from '@app/mocks/pgns';

import { getLichessAnalysisUrl } from './get-lichess-analysis-url.util';

describe('getLichessAnalysisUrl', () => {
it('returns `null` if the PGN is `undefined`', () => {
expect(getLichessAnalysisUrl(undefined)).toBe(null);
});

it("returns the game's moves if the PGN contains at least one", () => {
const urlWithMoves = `
https://lichess.org/analysis/pgn/1. d4 e6 2. c4 f5 3. g3 Nf6 4. Bg2 d5 5. Nf3 c6 6. O-O Bd6
7. b3 Qe7 8. Bb2 b6 9. Nbd2 O-O 10. Ne5 Bb7 11. Rc1 c5 12. dxc5 bxc5 13. cxd5 exd5 14. Ndc4
dxc4 15. Bxb7 Bxe5 16. Bd5+ Nxd5 17. Qxd5+ Qf7 18. Qxa8 Bxb2 19. Rxc4 Nd7 20. Qxa7 Qd5 21.
Qa5 Bd4 22. Rc2 f4 23. Qd2 f3 24. e3 Qf5 25. Kh1 Qh3 26. Rg1 Ne5 27. Rxc5 Ng4 28. Rh5 Qxh5
29. h4 1-0
`;
expect(getLichessAnalysisUrl(mockPgns[0])).toBe(
urlWithMoves.replaceAll('\n', ' ').replace(/\s+/g, ' ').trim(),
);
});

it('returns `null` if the PGN does not contain any moves`', () => {
expect(getLichessAnalysisUrl(mockPgns[4])).toBe(null);
});
});
6 changes: 2 additions & 4 deletions src/app/utils/pgn/get-lichess-analysis-url.util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,13 @@
* A URL to Lichess' analysis board, with only the moves and score portion of the PGN passed in.
*
* Returns an empty string if PGN is undefined or no moves exist.
*
* @param pgn
*/
export function getLichessAnalysisUrl(pgn?: string): string | null {
if (!pgn) {
return null;
}

const moves = pgn.split('"]\n\n')[1].replaceAll('\n', ' ');
const moves = pgn.split('"]\n\n')[1]?.replaceAll('\n', ' ');

return moves.startsWith('1. ') ? 'https://lichess.org/analysis/pgn/' + moves : null;
return moves?.startsWith('1. ') ? 'https://lichess.org/analysis/pgn/' + moves : null;
}
21 changes: 21 additions & 0 deletions src/app/utils/pgn/get-opening-tallies.util.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { mockPgns } from '@app/mocks/pgns';

import { getOpeningTallies } from './get-opening-tallies.util';

describe('getOpeningTallies', () => {
it('returns `undefined` if the PGN array is `undefined` or empty', () => {
expect(getOpeningTallies(undefined)).toBe(undefined);
expect(getOpeningTallies([])).toBe(undefined);
});

it('returns a map of ECO opening codes if at least one PGN is provided', () => {
const openingMap = new Map([
['A01', 2],
['C95', 1],
['C63', 1],
['X98', 1],
]);

expect(getOpeningTallies(mockPgns)).toStrictEqual(openingMap);
});
});
2 changes: 0 additions & 2 deletions src/app/utils/pgn/get-opening-tallies.util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@ import { getEcoOpeningCode } from '@app/utils';

/**
* Return a map of ECO codes mapped to the number of occurrences in the given array.
*
* @param pgns
*/
export function getOpeningTallies(pgns?: string[]): Map<string, number> | undefined {
if (!pgns || !pgns.length) {
Expand Down
25 changes: 25 additions & 0 deletions src/app/utils/pgn/get-player-name.util.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { mockPgns } from '@app/mocks/pgns';

import { getPlayerName } from './get-player-name.util';

describe('getPlayerName', () => {
it('returns `undefined` if any of the arguments are `undefined`', () => {
expect(getPlayerName()).toBe(undefined);
expect(getPlayerName(mockPgns[0])).toBe(undefined);
expect(getPlayerName(mockPgns[0], 'first')).toBe(undefined);
expect(getPlayerName(mockPgns[0], undefined, 'White')).toBe(undefined);
});

it("returns the player's name if it can be found in the PGN", () => {
expect(getPlayerName(mockPgns[0], 'first', 'White')).toBe('J.');
expect(getPlayerName(mockPgns[0], 'first', 'Black')).toBe('G.');
expect(getPlayerName(mockPgns[0], 'last', 'White')).toBe('Whiteman');
expect(getPlayerName(mockPgns[0], 'last', 'Black')).toBe('Blackley');
expect(getPlayerName(mockPgns[0], 'full', 'White')).toBe('J. Whiteman');
expect(getPlayerName(mockPgns[0], 'full', 'Black')).toBe('G. Blackley');
});

it('returns `undefined` if unable to parse the name', () => {
expect(getPlayerName(mockPgns[4], 'full', 'White')).toBe(undefined);
});
});
28 changes: 19 additions & 9 deletions src/app/utils/pgn/get-player-name.util.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { isDefined } from '../common/is-defined.util';

/**
* Parse PGN for one of the two players' names.
*
Expand All @@ -14,16 +16,24 @@ export function getPlayerName(
return;
}

try {
const [lastName, firstName] = pgn.split(`[${color} "`)[1].split('"]')[0].split(', ');
const _firstSplit = pgn.split(`[${color} "`);
if (!_firstSplit?.length || _firstSplit.length < 2) {
return;
}

return name === 'first'
? firstName
: name === 'last'
? lastName
: `${firstName} ${lastName}`;
} catch (error) {
console.error('[LCC] Unable to parse player name:', error);
const _secondSplit = _firstSplit[1].split('"]')[0];
if (!isDefined(_secondSplit)) {
return;
}

const [lastName, firstName] = _secondSplit.split(', ');
if ((name === 'full' && !isDefined(firstName)) || !isDefined(lastName)) {
return;
}

return name === 'first'
? firstName
: name === 'last'
? lastName
: `${firstName} ${lastName}`;
}
20 changes: 20 additions & 0 deletions src/app/utils/pgn/get-ply-count.util.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { mockPgns } from '@app/mocks/pgns';

import { getPlyCount } from './get-ply-count.util';

describe('getPlyCount', () => {
it('returns `undefined` if the PGN is `undefined`', () => {
expect(getPlyCount()).toBe(undefined);
});

it("returns the player's name if it can be found in the PGN", () => {
expect(getPlyCount(mockPgns[0])).toBe(57);
expect(getPlyCount(mockPgns[1])).toBe(67);
expect(getPlyCount(mockPgns[2])).toBe(108);
expect(getPlyCount(mockPgns[3])).toBe(67);
});

it('returns `undefined` if unable to parse the ply count', () => {
expect(getPlyCount(mockPgns[4])).toBe(undefined);
});
});
9 changes: 6 additions & 3 deletions src/app/utils/pgn/get-ply-count.util.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
/**
* Parse PGN for the game's ply count.
*
* @param pgn
*/
export function getPlyCount(pgn?: string): number | undefined {
if (!pgn) {
return;
}

const plyCount = pgn.split('[PlyCount "')[1]?.split('"]')[0];
const _firstSplit = pgn.split('[PlyCount "');
if (!_firstSplit?.length || _firstSplit.length < 2) {
return;
}

const plyCount = _firstSplit[1].split('"]')[0];

return isNaN(Number(plyCount)) ? undefined : Number(plyCount);
}
22 changes: 22 additions & 0 deletions src/app/utils/pgn/get-result-tallies.util.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { mockPgns } from '@app/mocks/pgns';

import { getResultTallies } from './get-result-tallies.util';

describe('getResultTallies', () => {
it('returns `undefined` if the PGN array is `undefined` or empty', () => {
expect(getResultTallies(undefined)).toBe(undefined);
expect(getResultTallies([])).toBe(undefined);
});

it('returns a map of results if at least one PGN is provided', () => {
const resultMap = new Map([
['White wins', 1],
['Black wins', 1],
['Draw', 1],
['Inconclusive', 1],
['Unknown', 1],
]);

expect(getResultTallies(mockPgns)).toStrictEqual(resultMap);
});
});
2 changes: 0 additions & 2 deletions src/app/utils/pgn/get-result-tallies.util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@ import { getScore } from '@app/utils';

/**
* Return a map of game results mapped to the number of occurrences in the given PGN array.
*
* @param pgns
*/
export function getResultTallies(pgns?: string[]): Map<string, number> | undefined {
if (!pgns || !pgns.length) {
Expand Down
26 changes: 26 additions & 0 deletions src/app/utils/pgn/get-score.util.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { mockPgns } from '@app/mocks/pgns';

import { getScore } from './get-score.util';

describe('getScore', () => {
it('returns `undefined` if any of the arguments are `undefined`', () => {
expect(getScore()).toBe(undefined);
expect(getScore(mockPgns[0])).toBe(undefined);
expect(getScore(undefined, 'White')).toBe(undefined);
});

it("returns the player's name if it can be found in the PGN", () => {
expect(getScore(mockPgns[0], 'White')).toBe('1');
expect(getScore(mockPgns[0], 'Black')).toBe('0');
expect(getScore(mockPgns[1], 'White')).toBe('1/2');
expect(getScore(mockPgns[1], 'Black')).toBe('1/2');
expect(getScore(mockPgns[2], 'White')).toBe('0');
expect(getScore(mockPgns[2], 'Black')).toBe('1');
expect(getScore(mockPgns[3], 'White')).toBe('*');
expect(getScore(mockPgns[3], 'Black')).toBe('*');
});

it('returns `undefined` if unable to parse the name', () => {
expect(getScore(mockPgns[4], 'White')).toBe(undefined);
});
});
29 changes: 18 additions & 11 deletions src/app/utils/pgn/get-score.util.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import type { GameScore } from '@app/types';
import { isGameScore } from '@app/types/game-details.model';

import { isDefined } from '../common/is-defined.util';

/**
* Parse PGN for a player's score (`1`, `1/2`, `0` or `*`).
*
Expand All @@ -12,19 +14,24 @@ export function getScore(pgn?: string, color?: 'White' | 'Black'): GameScore | u
return;
}

try {
const [whiteScore, blackScore] = pgn.split('[Result "')[1].split('"]')[0].split('-');
if (whiteScore === '*') {
return '*';
}
const _firstSplit = pgn.split('[Result "');
if (!_firstSplit?.length || _firstSplit.length < 2) {
return;
}

const _secondSplit = _firstSplit[1].split('"]')[0];
if (!isDefined(_secondSplit)) {
return;
}

if (!isGameScore(whiteScore) || !isGameScore(blackScore)) {
return;
}
const [whiteScore, blackScore] = _secondSplit.split('-');
if (whiteScore === '*') {
return '*';
}

return color === 'White' ? whiteScore : blackScore;
} catch (error) {
console.error('[LCC] Unable to parse game score:', error);
if (!isGameScore(whiteScore) || !isGameScore(blackScore)) {
return;
}

return color === 'White' ? whiteScore : blackScore;
}
Loading

0 comments on commit bd4ed5f

Please sign in to comment.