Skip to content

Commit

Permalink
Merge pull request #16370 from fitztrev/insufficient-material
Browse files Browse the repository at this point in the history
Fix insufficient material check (KBB vs K)
  • Loading branch information
ornicar authored Nov 8, 2024
2 parents 121dd6a + ccf3557 commit 615e327
Show file tree
Hide file tree
Showing 2 changed files with 121 additions and 12 deletions.
32 changes: 20 additions & 12 deletions ui/game/src/view/status.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { FEN } from 'chessground/types';
import { Ctrl } from '../interfaces';

function bishopOnColor(expandedFen: string, offset: 0 | 1): boolean {
export function bishopOnColor(expandedFen: string, offset: 0 | 1): boolean {
if (expandedFen.length !== 64) throw new Error('Expanded FEN expected to be 64 characters');

for (let row = 0; row < 8; row++) {
for (let col = row % 2 === offset ? 0 : 1; col < 8; col += 2) {
if (/[bB]/.test(expandedFen[row * 8 + col])) return true;
Expand All @@ -10,26 +12,32 @@ function bishopOnColor(expandedFen: string, offset: 0 | 1): boolean {
return false;
}

function insufficientMaterial(variant: VariantKey, fullFen: FEN): boolean {
export function expandFen(fullFen: FEN): string {
return fullFen
.split(' ')[0]
.replace(/\d/g, n => '1'.repeat(+n))
.replace(/\//g, '');
}

export function insufficientMaterial(variant: VariantKey, fullFen: FEN): boolean {
// TODO: atomic and antichess
if (
variant === 'horde' ||
variant === 'kingOfTheHill' ||
variant === 'racingKings' ||
variant === 'crazyhouse' ||
variant === 'atomic' ||
variant === 'antichess'
variant === 'antichess' ||
variant === 'threeCheck'
)
return false;
let fen = fullFen.split(' ')[0].replace(/[^a-z]/gi, '');
if (/^[Kk]{2}$/.test(fen)) return true;
if (variant === 'threeCheck') return false;
if (/[prq]/i.test(fen)) return false;
if (/n/.test(fen)) return fen.length - fen.replace(/[a-z]/g, '').length <= 2 && !/[PBNR]/.test(fen);
if (/N/.test(fen)) return fen.length - fen.replace(/[A-Z]/g, '').length <= 2 && !/[pbnr]/.test(fen);
if (/b/i.test(fen)) {
for (let i = 8; i > 1; i--) fen = fen.replace('' + i, '1' + (i - 1));
return (!bishopOnColor(fen, 0) || !bishopOnColor(fen, 1)) && !/[pPnN]/.test(fen);
const pieces = fullFen.split(' ')[0].replace(/[^a-z]/gi, '');
if (/^[Kk]{2}$/.test(pieces)) return true;
if (/[prq]/i.test(pieces)) return false;
if (/^[KkNn]{3}$/.test(pieces)) return true;
if (/b/i.test(pieces)) {
const expandedFen = expandFen(fullFen);
return (!bishopOnColor(expandedFen, 0) || !bishopOnColor(expandedFen, 1)) && !/[pPnN]/.test(pieces);
}
return false;
}
Expand Down
101 changes: 101 additions & 0 deletions ui/game/tests/status.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import { describe, expect, test } from 'vitest';
import { bishopOnColor, expandFen, insufficientMaterial } from '../src/view/status';

describe('expand fen', () => {
test('starting position', () =>
expect(expandFen('rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1')).toBe(
'rnbqkbnrpppppppp11111111111111111111111111111111PPPPPPPPRNBQKBNR',
));
test('middlegame position', () =>
expect(expandFen('r2q1rk1/p3ppbp/2pp1np1/2n5/2P3b1/1P1BPN2/PB1N1PPP/2RQ1RK1 w HAhq - 0 1')).toBe(
'r11q1rk1p111ppbp11pp1np111n1111111P111b11P1BPN11PB1N1PPP11RQ1RK1',
));
});

describe('bishop on color', () => {
test('bishop on square', () => {
expect(bishopOnColor(expandFen('B7/8/8/8/8/8/8/8 w - - 0 1'), 0)).toBe(true);
expect(bishopOnColor(expandFen('2B5/8/8/8/8/8/8/8 w - - 0 1'), 0)).toBe(true);
expect(bishopOnColor(expandFen('3B4/8/8/8/8/8/8/8 w - - 0 1'), 1)).toBe(true);
expect(bishopOnColor(expandFen('2BB4/8/8/8/8/8/8/8 w - - 0 1'), 1)).toBe(true);
});
test('no bishops on black squares', () => {
expect(bishopOnColor(expandFen('B7/8/8/8/8/8/8/8 w - - 0 1'), 1)).toBe(false);
expect(bishopOnColor(expandFen('2B5/8/8/8/8/8/8/8 w - - 0 1'), 1)).toBe(false);
expect(bishopOnColor(expandFen('5K2/8/8/1B6/8/k7/6b1/8 w - - 0 39'), 1)).toBe(false);
});
});

describe('test insufficient material', () => {
test('K vs K', () => expect(insufficientMaterial('standard', '4k3/8/8/8/8/8/8/4K3 w - - 0 1')).toBe(true));

test('KB vs K', () =>
expect(insufficientMaterial('standard', '4k3/8/8/8/8/8/8/4KB2 w - - 0 1')).toBe(true));

test('KBB vs K (same color bishops)', () =>
expect(insufficientMaterial('standard', '4k3/8/8/8/8/8/6B1/4K2B w - - 0 1')).toBe(true));

test('KB vs KB (same color bishops)', () =>
expect(insufficientMaterial('standard', 'k7/8/1b6/8/8/8/1B6/K7 w - - 0 1')).toBe(true));
});

describe('should not be insufficient material', () => {
test.each<VariantKey[]>([
['horde'],
['kingOfTheHill'],
['racingKings'],
['crazyhouse'],
['atomic'],
['antichess'],
['threeCheck'],
])('variant %s', variant =>
expect(insufficientMaterial(variant, 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1')).toBe(
false,
),
);

test('pawn is never insufficient material', () =>
expect(insufficientMaterial('standard', '4k3/8/8/8/8/8/7P/4K3 w - - 0 1')).toBe(false));

test('rook is never insufficient material', () =>
expect(insufficientMaterial('standard', '4k3/8/8/8/8/8/7R/4K3 w - - 0 1')).toBe(false));

test('queen is never insufficient material', () =>
expect(insufficientMaterial('standard', '4k3/8/8/8/8/8/7Q/4K3 w - - 0 1')).toBe(false));

test('KBB vs K (diff color bishops)', () => {
expect(insufficientMaterial('standard', '8/8/1B6/8/1KB5/8/2k5/8 b - - 100 103')).toBe(false);
expect(insufficientMaterial('standard', '8/8/1B6/8/1KB5/8/2k5/8')).toBe(false);
});

test('KB vs KN', () =>
expect(insufficientMaterial('standard', 'kn6/8/8/8/8/8/8/KB6 w - - 0 1')).toBe(false));

test('KB vs KB (diff color bishops)', () =>
expect(insufficientMaterial('standard', 'k7/1b6/8/8/8/8/1B6/K7 w - - 0 1')).toBe(false));
});

describe('knight rules', () => {
test('KN vs K', () => expect(insufficientMaterial('standard', 'k7/8/1n6/8/8/8/8/K7 w - - 0 1')).toBe(true));
test('KNN vs K', () =>
expect(insufficientMaterial('standard', 'k7/8/1nn5/8/8/8/8/K7 w - - 0 1')).toBe(false));
});

describe('scalachess fens from AutodrawTest.scala', () => {
test.each([['5K2/8/8/1B6/8/k7/6b1/8 w - - 0 39']])('should detect insufficient material', fen => {
expect(insufficientMaterial('standard', fen)).toBe(true);
});

test.each([
['1n2k1n1/8/8/8/8/8/8/4K3 w - - 0 1'],
['7K/5k2/7P/6n1/8/8/8/8 b - - 0 40'],
['1b1b3K/8/5k1P/8/8/8/8/8 b - - 0 40'],
['b2b3K/8/5k1Q/8/8/8/8/8 b - -'],
['1b1b3K/8/5k1Q/8/8/8/8/8 b - -'],
['8/8/5N2/8/6p1/8/5K1p/7k w - - 0 37'],
['8/8/8/4N3/4k1p1/6K1/8/3b4 w - - 5 59'],
['8/8/3Q4/2bK4/B7/8/8/k7 b - - 0 67'],
])('should not detect insufficient material', fen => {
expect(insufficientMaterial('standard', fen)).toBe(false);
});
});

0 comments on commit 615e327

Please sign in to comment.