fix(bot): blind Casual no longer resigns prematurely under check
The blind-mode CasualBrain heuristic ignored the moderator's '<own>_in_check' announcement and scored moves on capture/advance/development signals uncorrelated with check resolution. chess.js rejected every non-resolving attempt, BotDriver's RETRY_CAP=5 fired, and the bot resigned. 100-game blind self-play: 100% resignations at avg ply 26. Fix: - CasualBrain.detectOwnCheck() scans newAnnouncements for the own-color in_check tag; when set, heuristicPick() applies a +5000 boost to king moves so they're tried first. Information stays within the public moderator vocabulary — no oracle access, view-filter invariant intact. - RETRY_CAP raised 5 -> 25. Vanilla never hits the cap (chess.js verbose moves are guaranteed legal); blind needs more budget to find a legal move through pseudo-legal candidates. - BotDriver.botResign() now logs '[bot resign]' with gameId/color/mode/ply/ reason/detail. Previously silent — operator had no signal in journald. Verification (100-game blind Casual-vs-Casual self-play): - avgPly: 26 -> 90 (3.5x) - Resignations: 100% -> 17% - Checkmates: 0% -> 42% - Threefold draws: 0% -> 41% Vanilla regression check (80 games combined): 0 resigns either way, strength unchanged (Casual still wins 98% vs random). 78 tests pass (was 75; +2 new check-resolution tests, +1 retry-cap test updated to match new cap). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -115,20 +115,44 @@ describe('BotDriver', () => {
|
||||
expect(game.chess.turn()).toBe('w');
|
||||
});
|
||||
|
||||
it('retry cap (5): after 5 wont_help, driver resigns the bot', async () => {
|
||||
it('retry on illegal_move: switching from after rejection still succeeds', async () => {
|
||||
// Black bishop b8 has legal moves (c7, d6, e5xpawn, a7) but b8→f4 is
|
||||
// illegal because the white pawn on e5 blocks the diagonal. Black king on a1.
|
||||
// The FSM ARMS the b8 piece during its first attempt then rejects with
|
||||
// illegal_move; on the retry the brain switches to a king move, which used
|
||||
// to trip "must_move_touched_piece" and resign the bot.
|
||||
const fen = '1b6/8/8/4P3/8/8/8/k6K b - - 0 1';
|
||||
game = makeGame({ mode: 'blind', fen });
|
||||
brain = new StubBrain();
|
||||
driver = new BotDriver({ game, brain, color: 'b' });
|
||||
await driver.init();
|
||||
brain.enqueue(
|
||||
{ type: 'commit', from: 'b8', to: 'f4' }, // illegal: blocked by white pawn
|
||||
{ type: 'commit', from: 'a1', to: 'b1' }, // legal king move
|
||||
);
|
||||
await driver.onStateChange();
|
||||
expect(brain.decide).toHaveBeenCalledTimes(2);
|
||||
expect(game.status).toBe('active');
|
||||
expect(game.chess.turn()).toBe('w');
|
||||
expect(game.armed).toBeNull();
|
||||
});
|
||||
|
||||
it('retry cap (25): after RETRY_CAP rejected attempts, driver resigns the bot', async () => {
|
||||
const fen = '4k2K/4b3/8/8/8/8/8/4R3 b - - 0 1';
|
||||
game = makeGame({ fen });
|
||||
brain = new StubBrain();
|
||||
driver = new BotDriver({ game, brain, color: 'b' });
|
||||
await driver.init();
|
||||
for (let i = 0; i < 6; i++) {
|
||||
// Enqueue more than RETRY_CAP repeated illegal moves; driver should
|
||||
// exhaust the retry budget and resign.
|
||||
for (let i = 0; i < 30; i++) {
|
||||
brain.enqueue({ type: 'commit', from: 'e7', to: 'd6' });
|
||||
}
|
||||
await driver.onStateChange();
|
||||
expect(game.status).toBe('finished');
|
||||
expect(game.endReason).toBe('resign');
|
||||
expect(game.winner).toBe('w');
|
||||
expect(brain.decide).toHaveBeenCalledTimes(5);
|
||||
expect(brain.decide).toHaveBeenCalledTimes(25);
|
||||
});
|
||||
|
||||
it('respond-draw: when drawOffer is from opponent, driver fires decide and dispatches', async () => {
|
||||
|
||||
Reference in New Issue
Block a user