feat(server): moderator announces every move and attempt to both players

All move-event announcements in translator.ts and all attempted-move
announcements in commit.ts now use audience 'both' so the moderator
panel is a complete shared transcript for both players.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
claude (blind_chess)
2026-05-18 19:54:34 -04:00
parent be8ecd96b6
commit 41b3ab93bb
3 changed files with 28 additions and 13 deletions
+6 -6
View File
@@ -37,7 +37,7 @@ export function handleCommit(game: Game, color: Color, msg: CommitInput): Commit
const piece = game.chess.get(msg.from) as { color: Color; type: Piece['type'] } | false; const piece = game.chess.get(msg.from) as { color: Color; type: Piece['type'] } | false;
if (!piece || piece.color !== color) { if (!piece || piece.color !== color) {
return announceWith(game, 'no_such_piece', color); return announceWith(game, 'no_such_piece');
} }
const pseudo = geometricMoves( const pseudo = geometricMoves(
@@ -46,12 +46,12 @@ export function handleCommit(game: Game, color: Color, msg: CommitInput): Commit
ownSquares(game, color), ownSquares(game, color),
); );
if (pseudo.length === 0) { if (pseudo.length === 0) {
return announceWith(game, 'no_legal_moves', color); return announceWith(game, 'no_legal_moves');
} }
const legal = chessJsLegalFrom(game, msg.from); const legal = chessJsLegalFrom(game, msg.from);
if (legal.length === 0) { if (legal.length === 0) {
return announceWith(game, 'wont_help', color); return announceWith(game, 'wont_help');
} }
game.armed = { color, from: msg.from }; game.armed = { color, from: msg.from };
@@ -77,7 +77,7 @@ function tryMove(
} }
if (!move) { if (!move) {
return announceWith(game, 'illegal_move', color); return announceWith(game, 'illegal_move');
} }
game.armed = null; game.armed = null;
@@ -110,10 +110,10 @@ function tryMove(
function announceWith( function announceWith(
game: Game, game: Game,
text: 'no_such_piece' | 'no_legal_moves' | 'wont_help' | 'illegal_move', text: 'no_such_piece' | 'no_legal_moves' | 'wont_help' | 'illegal_move',
color: Color,
): CommitResult { ): CommitResult {
const ply = game.chess.history().length; const ply = game.chess.history().length;
const a = announce(text, color, ply); // Feature 1: attempted moves are announced to both players.
const a = announce(text, 'both', ply);
game.announcements.push(a); game.announcements.push(a);
return { kind: 'announce', announcements: [a] }; return { kind: 'announce', announcements: [a] };
} }
+8 -7
View File
@@ -33,21 +33,22 @@ export function translateMove(game: Game, move: Move): Announcement[] {
const isQueensideCastle = move.isQueensideCastle(); const isQueensideCastle = move.isQueensideCastle();
const isProm = !!move.promotion; const isProm = !!move.promotion;
// To opponent: the move event itself. // To both players: the move event itself (Feature 1 — the moderator
// announces every move aloud; both players hear it).
if (isKingsideCastle) { if (isKingsideCastle) {
out.push(announce(`${moverWord}_castled_kingside` as ModeratorText, opp, ply)); out.push(announce(`${moverWord}_castled_kingside` as ModeratorText, 'both', ply));
} else if (isQueensideCastle) { } else if (isQueensideCastle) {
out.push(announce(`${moverWord}_castled_queenside` as ModeratorText, opp, ply)); out.push(announce(`${moverWord}_castled_queenside` as ModeratorText, 'both', ply));
} else if (isCap && isEp) { } else if (isCap && isEp) {
out.push(announce(`${moverWord}_moved_captured_ep` as ModeratorText, opp, ply)); out.push(announce(`${moverWord}_moved_captured_ep` as ModeratorText, 'both', ply));
} else if (isCap) { } else if (isCap) {
out.push(announce(`${moverWord}_moved_captured` as ModeratorText, opp, ply)); out.push(announce(`${moverWord}_moved_captured` as ModeratorText, 'both', ply));
} else { } else {
out.push(announce(`${moverWord}_moved` as ModeratorText, opp, ply)); out.push(announce(`${moverWord}_moved` as ModeratorText, 'both', ply));
} }
if (isProm) { if (isProm) {
out.push(announce(`${moverWord}_promoted` as ModeratorText, opp, ply, { promotedTo: move.promotion })); out.push(announce(`${moverWord}_promoted` as ModeratorText, 'both', ply, { promotedTo: move.promotion }));
} }
// To both: state changes. // To both: state changes.
@@ -74,6 +74,20 @@ describe('hierarchy decision table', () => {
expect(game.armed).toBeNull(); expect(game.armed).toBeNull();
expect(game.chess.history()).toContain('e4'); expect(game.chess.history()).toContain('e4');
}); });
it('attempted-move announcements are audience: both', () => {
const r = handleCommit(game, 'w', { from: 'e4' }); // empty square -> no_such_piece
expect(r.kind).toBe('announce');
if (r.kind === 'announce') expect(r.announcements[0]!.audience).toBe('both');
});
it('applied-move announcements are audience: both', () => {
const r = handleCommit(game, 'w', { from: 'e2', to: 'e4' });
expect(r.kind).toBe('applied');
if (r.kind === 'applied') {
for (const a of r.announcements) expect(a.audience).toBe('both');
}
});
}); });
describe('touch-move enforcement', () => { describe('touch-move enforcement', () => {