Show explicit blackjack settlements across the stack
All checks were successful
CI / Deploy / test (push) Successful in 1m18s
CI / Deploy / deploy (push) Successful in 1m4s

- Replace round payout multipliers with per-player settlement amounts
- Update blackjack panel to display wager, payout, and net results
This commit is contained in:
syntaxbullet
2026-04-10 11:03:58 +02:00
parent f796cac6be
commit de15cb4206
11 changed files with 754 additions and 674 deletions

View File

@@ -88,32 +88,30 @@ export class GameServer {
this.publishRoomListUpdate();
});
this.roomManager.emitter.on("round:settled", async ({ roomId, roundPayouts }) => {
this.roomManager.emitter.on("round:settled", async ({ roomId, roundSettlements }) => {
const room = this.roomManager.getRoom(roomId);
if (!room || room.betAmount <= 0) return;
const betAmount = room.betAmount;
const gameName = gameRegistry.get(room.gameSlug)?.name ?? "Game";
const payoutDetails: Record<string, { net: number }> = {};
const settlementDetails: typeof roundSettlements = {};
for (const [playerId, multiplier] of Object.entries(roundPayouts)) {
// roundPayout contains multipliers (e.g., win=2, blackjack=2.5, push=1)
const grossAmount = Math.floor(betAmount * multiplier);
const netProfit = grossAmount - betAmount;
for (const [playerId, settlement] of Object.entries(roundSettlements)) {
try {
await economyService.modifyUserBalance(
playerId,
BigInt(grossAmount),
TransactionType.GAME_WIN,
`${gameName} round payout (room ${roomId.slice(0, 8)})`,
);
payoutDetails[playerId] = { net: netProfit };
if (settlement.payout > 0) {
await economyService.modifyUserBalance(
playerId,
BigInt(settlement.payout),
TransactionType.GAME_WIN,
`${gameName} round payout (room ${roomId.slice(0, 8)})`,
);
}
settlementDetails[playerId] = settlement;
} catch (err) {
logger.error("web", `Round payout failed for ${playerId} in room ${roomId}: ${err}`);
}
}
if (Object.keys(payoutDetails).length > 0) {
this.publish(`room:${roomId}`, { type: "ROUND_SETTLED", roomId, payouts: payoutDetails });
if (Object.keys(settlementDetails).length > 0) {
this.publish(`room:${roomId}`, { type: "ROUND_SETTLED", roomId, settlements: settlementDetails });
}
});

View File

@@ -1,6 +1,7 @@
import mitt from "mitt";
import { gameRegistry } from "@shared/games/registry";
import type { Room, RoomSummary } from "./types";
import type { RoundSettlement } from "@shared/games/types";
const ROOM_CONFIG = {
WAITING_CLEANUP_MS: 60_000,
@@ -9,7 +10,7 @@ const ROOM_CONFIG = {
} as const;
type ActionResult =
| { ok: true; state: unknown; gameOver: { winner: string | null; reason: string } | null; roundPayouts?: Record<string, number> }
| { ok: true; state: unknown; gameOver: { winner: string | null; reason: string } | null; roundSettlements?: Record<string, RoundSettlement> }
| { ok: false; error: string };
type CreateResult = { ok: true; roomId: string } | { ok: false; error: string };
@@ -24,7 +25,7 @@ type RoomEvents = {
"game:started": { roomId: string; spectatorView: unknown; playerViews: Map<string, unknown> };
"game:updated": { roomId: string; spectatorView: unknown; playerViews: Map<string, unknown> };
"game:ended": { roomId: string; winner: string | null; reason: string; payouts?: Record<string, number> };
"round:settled": { roomId: string; roundPayouts: Record<string, number> };
"round:settled": { roomId: string; roundSettlements: Record<string, RoundSettlement> };
"player:left": { roomId: string; playerId: string };
"room:deleted": { roomId: string };
"room:list:changed": void;
@@ -138,8 +139,8 @@ export class RoomManager {
this.emitter.emit("game:updated", { roomId, spectatorView, playerViews });
// Emit round payouts for mid-game settlement (continuous-play games)
if (result.roundPayouts && !gameOver) {
this.emitter.emit("round:settled", { roomId, roundPayouts: result.roundPayouts });
if (result.roundSettlements && !gameOver) {
this.emitter.emit("round:settled", { roomId, roundSettlements: result.roundSettlements });
}
if (gameOver) {
@@ -147,7 +148,7 @@ export class RoomManager {
this.emitter.emit("room:list:changed");
}
return { ok: true, state: room.state, gameOver, roundPayouts: result.roundPayouts };
return { ok: true, state: room.state, gameOver, roundSettlements: result.roundSettlements };
}
leaveRoom(roomId: string, playerId: string): void {

View File

@@ -1,3 +1,4 @@
import type { RoundSettlement } from "@shared/games/types";
import { z } from "zod";
export interface Room {
@@ -59,6 +60,6 @@ export type GameWsServerMessage =
| { type: "GAME_ENDED"; roomId: string; winner: string | null; reason: string; payout?: { amount: number; refunded?: boolean } }
| { type: "ROOM_CREATED"; roomId: string; gameSlug: string }
| { type: "JOIN_RESULT"; roomId: string; joinedAs: "player" | "spectator"; roomStatus: "waiting" | "playing" | "finished"; players: PlayerInfo[]; spectators: PlayerInfo[]; state?: unknown; roomOptions?: { betAmount?: number; timeControl?: string } }
| { type: "ROUND_SETTLED"; roomId: string; payouts: Record<string, { net: number }> }
| { type: "ROUND_SETTLED"; roomId: string; settlements: Record<string, RoundSettlement> }
| { type: "SESSION_REPLACED"; roomId: string }
| { type: "ERROR"; message: string };