fix(offline): hotseat finished-game medals by final score (fix all-last-place on a tie)
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Has been skipped
CI / integration (pull_request) Has been skipped
CI / ui (pull_request) Successful in 1m6s
CI / conformance (pull_request) Successful in 10s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m42s
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Has been skipped
CI / integration (pull_request) Has been skipped
CI / ui (pull_request) Successful in 1m6s
CI / conformance (pull_request) Successful in 10s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m42s
Reported: a hotseat game ended by 6 scoreless passes deducts each rack (correct rule, same as online: -8/-14/-8 here), which tied two seats for the lead. The engine's winner() returns -1 on a tie, so seatMedal read it as a full draw and gave EVERY seat the last-place medal. - result.ts: seatMedal now ranks by the FINAL score (competition ranking — a tie for the lead shares the trophy), not the single-winner flag. A resigned / host-excluded seat places last with no medal and does not push the others down. - model.ts: Seat.resigned (offline-only); engine.resignedOf getter; the local source surfaces it on the game view. - The scoreless rack deduction is unchanged (standard rules, owner-confirmed).
This commit is contained in:
@@ -240,6 +240,10 @@ export class LocalGame {
|
||||
handOf(player: number): number[] {
|
||||
return [...this.hands[player]];
|
||||
}
|
||||
/** resignedOf reports whether a seat has resigned or been excluded by the host. */
|
||||
resignedOf(player: number): boolean {
|
||||
return this.resigned[player] ?? false;
|
||||
}
|
||||
/** winnerIndex is the finished game's winner, or -1 (tie / in progress / aborted). */
|
||||
get winnerIndex(): number {
|
||||
return winner(this.over, this.reason, this.scores, this.resigned);
|
||||
|
||||
@@ -101,6 +101,19 @@ describe('LocalSource hotseat', () => {
|
||||
expect((st as StateView).game.status).toBe('finished');
|
||||
});
|
||||
|
||||
it('exposes a host-excluded seat as resigned in the view (for the medal ranking)', async () => {
|
||||
const src = await hotseat('local:hR', [
|
||||
{ kind: 'human', name: 'A' },
|
||||
{ kind: 'human', name: 'B' },
|
||||
{ kind: 'human', name: 'C' },
|
||||
], '4321');
|
||||
await src.hostAction('local:hR', '4321', 'resign', 1); // exclude seat 1 (game continues: 2 active)
|
||||
const st = await src.gameState('local:hR');
|
||||
expect(st.game.seats[1].resigned).toBe(true);
|
||||
expect(st.game.seats[0].resigned).toBe(false);
|
||||
expect(st.game.seats[2].resigned).toBe(false);
|
||||
});
|
||||
|
||||
it('lets the host terminate the game, deleting it', async () => {
|
||||
const src = await hotseat('local:h6', [
|
||||
{ kind: 'human', name: 'A' },
|
||||
|
||||
@@ -464,6 +464,7 @@ export class LocalSource implements GameLoopSource {
|
||||
score: game.scoreOf(i),
|
||||
hintsUsed: 0,
|
||||
isWinner: game.isOver && i === game.winnerIndex,
|
||||
resigned: game.resignedOf(i),
|
||||
}));
|
||||
return {
|
||||
id: record.id,
|
||||
|
||||
Reference in New Issue
Block a user