fix(bot): finalize game on bot checkmate; harden driver dispatch
Extract endGame/finalizeIfEnded to game-end.ts so driver.ts can call finalizeIfEnded after an applied move (fix: bot checkmate was not setting game.status='finished'). Wrap entire dispatch() call in try/catch for exception safety. Move lastSeenAnnouncementCount advance to after successful dispatch so retry attempts see FSM rejection announcements. Add checkmate-finalize test; lock retry-cap at 5 calls. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -11,6 +11,7 @@ import { legalCandidates } from './candidates.js';
|
||||
import { handleCommit } from '../commit.js';
|
||||
import { buildView } from '../view.js';
|
||||
import { announce } from '../translator.js';
|
||||
import { finalizeIfEnded } from '../game-end.js';
|
||||
|
||||
const RETRY_CAP = 5;
|
||||
|
||||
@@ -76,27 +77,31 @@ export class BotDriver {
|
||||
|
||||
for (let attempt = 0; attempt < RETRY_CAP; attempt++) {
|
||||
const input = this.buildBrainInput(attemptHistory);
|
||||
let action: BrainAction;
|
||||
let outcome: { kind: 'done' } | { kind: 'retry'; entry: AttemptHistoryEntry };
|
||||
try {
|
||||
action = await this.brain.decide(input);
|
||||
const action = await this.brain.decide(input);
|
||||
outcome = this.dispatch(action);
|
||||
} catch {
|
||||
// Brain exception → bot resigns. CasualBrain only throws on zero
|
||||
// candidates (impossible if shouldDecide passed).
|
||||
// Brain exception OR programming error in dispatch. Safe failure: resign.
|
||||
this.botResign();
|
||||
return;
|
||||
}
|
||||
|
||||
const outcome = this.dispatch(action);
|
||||
if (outcome.kind === 'done') return;
|
||||
if (outcome.kind === 'done') {
|
||||
this.lastSeenAnnouncementCount = this.game.announcements.length;
|
||||
return;
|
||||
}
|
||||
attemptHistory.push(outcome.entry);
|
||||
}
|
||||
this.lastSeenAnnouncementCount = this.game.announcements.length;
|
||||
this.botResign();
|
||||
}
|
||||
|
||||
private buildBrainInput(attemptHistory: AttemptHistoryEntry[]): BrainInput {
|
||||
const view = buildView(this.game, this.color);
|
||||
const sliceStart = this.lastSeenAnnouncementCount;
|
||||
this.lastSeenAnnouncementCount = this.game.announcements.length;
|
||||
// NOTE: do NOT advance lastSeenAnnouncementCount here. The caller advances
|
||||
// it once the decision cycle terminates successfully — otherwise retried
|
||||
// attempts would not see the FSM's rejection announcements in their input.
|
||||
const newAnnouncements = this.game.announcements
|
||||
.slice(sliceStart)
|
||||
.filter((a) => a.audience === 'both' || a.audience === this.color);
|
||||
@@ -121,7 +126,10 @@ export class BotDriver {
|
||||
const result = handleCommit(this.game, this.color, {
|
||||
from: action.from, to: action.to, promotion: action.promotion,
|
||||
});
|
||||
if (result.kind === 'applied') return { kind: 'done' };
|
||||
if (result.kind === 'applied') {
|
||||
finalizeIfEnded(this.game, result.announcements);
|
||||
return { kind: 'done' };
|
||||
}
|
||||
if (result.kind === 'announce') {
|
||||
const text = result.announcements[0]!.text;
|
||||
if (text === 'wont_help' || text === 'illegal_move'
|
||||
|
||||
Reference in New Issue
Block a user