Show explicit blackjack settlements across the stack
- Replace round payout multipliers with per-player settlement amounts - Update blackjack panel to display wager, payout, and net results
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import type { GamePlugin, GameResult, GameOverResult } from "../types";
|
||||
import type { GamePlugin, GameResult, GameOverResult, RoundSettlement } from "../types";
|
||||
import type {
|
||||
BlackjackState, BlackjackAction, BlackjackPlayerView,
|
||||
BlackjackSpectatorView, PlayerHandView, PlayerSeatView,
|
||||
@@ -174,7 +174,49 @@ function resolveHand(hand: PlayerHand, dealerHand: Card[]): PlayerHand {
|
||||
return { ...hand, result: "push", resultReason: "Push" };
|
||||
}
|
||||
|
||||
/** Transition to dealer turn + resolve all hands. Returns round payouts. */
|
||||
function calculateHandPayout(hand: Pick<PlayerHand, "bet" | "result">, betAmount: number): number {
|
||||
if (betAmount <= 0 || hand.result === null) return 0;
|
||||
|
||||
switch (hand.result) {
|
||||
case "blackjack":
|
||||
return Math.round(hand.bet * betAmount * 2.5);
|
||||
case "win":
|
||||
return hand.bet * betAmount * 2;
|
||||
case "push":
|
||||
return hand.bet * betAmount;
|
||||
case "lose":
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
function calculateHandNet(hand: Pick<PlayerHand, "bet" | "result">, betAmount: number): number | null {
|
||||
if (hand.result === null) return null;
|
||||
const wager = hand.bet * betAmount;
|
||||
return calculateHandPayout(hand, betAmount) - wager;
|
||||
}
|
||||
|
||||
function calculateSeatRoundSettlement(seat: PlayerSeat, betAmount: number): RoundSettlement {
|
||||
const wager = seat.hands.reduce((sum, hand) => sum + (hand.bet * betAmount), 0);
|
||||
const payout = seat.hands.reduce((sum, hand) => sum + calculateHandPayout(hand, betAmount), 0);
|
||||
|
||||
return {
|
||||
wager,
|
||||
payout,
|
||||
net: payout - wager,
|
||||
};
|
||||
}
|
||||
|
||||
function calculateRoundSettlements(seats: Record<string, PlayerSeat>, betAmount: number): Record<string, RoundSettlement> {
|
||||
const settlements: Record<string, RoundSettlement> = {};
|
||||
|
||||
for (const [playerId, seat] of Object.entries(seats)) {
|
||||
settlements[playerId] = calculateSeatRoundSettlement(seat, betAmount);
|
||||
}
|
||||
|
||||
return settlements;
|
||||
}
|
||||
|
||||
/** Transition to dealer turn + resolve all hands. */
|
||||
function finishPlayerTurns(state: BlackjackState): BlackjackState {
|
||||
const anyStood = Object.values(state.seats).some(
|
||||
seat => seat.hands.some(h => h.status === "stood" || h.status === "blackjack"),
|
||||
@@ -191,23 +233,14 @@ function finishPlayerTurns(state: BlackjackState): BlackjackState {
|
||||
|
||||
const resolvedSeats: Record<string, PlayerSeat> = {};
|
||||
for (const [id, seat] of Object.entries(state.seats)) {
|
||||
// First resolve the hands
|
||||
const resolvedHands = seat.hands.map(h => resolveHand(h, dealerHand));
|
||||
|
||||
// Calculate round payouts as multipliers
|
||||
const roundPayout = calculateRoundPayouts({ [id]: { ...seat, hands: resolvedHands } });
|
||||
const multiplier = roundPayout[id] ?? 0;
|
||||
// Calculate total bet amount for this player
|
||||
const roundBetTotal = seat.hands.reduce((sum, h) => sum + (h.bet * state.betAmount), 0);
|
||||
// Calculate actual payout amount and net profit
|
||||
const roundPayoutAmount = Math.round(multiplier * state.betAmount);
|
||||
const roundNetPnl = roundBetTotal > 0 ? roundPayoutAmount - roundBetTotal : 0;
|
||||
|
||||
const settlement = calculateSeatRoundSettlement({ ...seat, hands: resolvedHands }, state.betAmount);
|
||||
|
||||
resolvedSeats[id] = {
|
||||
...seat,
|
||||
activeHandIndex: -1,
|
||||
hands: resolvedHands,
|
||||
cumulativePnl: (seat.cumulativePnl ?? 0) + roundNetPnl,
|
||||
cumulativePnl: (seat.cumulativePnl ?? 0) + settlement.net,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -221,34 +254,6 @@ function finishPlayerTurns(state: BlackjackState): BlackjackState {
|
||||
};
|
||||
}
|
||||
|
||||
/** Calculate round payouts as multipliers of betAmount. */
|
||||
function calculateRoundPayouts(seats: Record<string, PlayerSeat>): Record<string, number> {
|
||||
const payouts: Record<string, number> = {};
|
||||
|
||||
for (const [playerId, seat] of Object.entries(seats)) {
|
||||
let playerPayout = 0;
|
||||
for (const hand of seat.hands) {
|
||||
switch (hand.result) {
|
||||
case "blackjack":
|
||||
playerPayout += hand.bet * 2.5; // 3:2 payout
|
||||
break;
|
||||
case "win":
|
||||
playerPayout += hand.bet * 2; // 1:1 payout
|
||||
break;
|
||||
case "push":
|
||||
playerPayout += hand.bet * 1; // refund
|
||||
break;
|
||||
// "lose" / null → 0
|
||||
}
|
||||
}
|
||||
if (playerPayout > 0) {
|
||||
payouts[playerId] = playerPayout;
|
||||
}
|
||||
}
|
||||
|
||||
return payouts;
|
||||
}
|
||||
|
||||
/** Build the dealer hand for views — hole card hidden during player_turns/betting. */
|
||||
function dealerVisibleHand(state: BlackjackState): Card[] {
|
||||
if (state.phase === "resolved") return state.dealerHand;
|
||||
@@ -257,7 +262,7 @@ function dealerVisibleHand(state: BlackjackState): Card[] {
|
||||
}
|
||||
|
||||
/** Convert internal PlayerHand to a view. */
|
||||
function toHandView(hand: PlayerHand): PlayerHandView {
|
||||
function toHandView(hand: PlayerHand, betAmount: number): PlayerHandView {
|
||||
return {
|
||||
cards: hand.cards,
|
||||
value: handValue(hand.cards),
|
||||
@@ -265,16 +270,25 @@ function toHandView(hand: PlayerHand): PlayerHandView {
|
||||
result: hand.result,
|
||||
resultReason: hand.resultReason,
|
||||
bet: hand.bet,
|
||||
wager: hand.bet * betAmount,
|
||||
payout: hand.result === null ? null : calculateHandPayout(hand, betAmount),
|
||||
net: calculateHandNet(hand, betAmount),
|
||||
fromSplit: hand.fromSplit,
|
||||
};
|
||||
}
|
||||
|
||||
/** Convert internal PlayerSeat to a view. */
|
||||
function toSeatView(seat: PlayerSeat): PlayerSeatView {
|
||||
function toSeatView(seat: PlayerSeat, betAmount: number): PlayerSeatView {
|
||||
const settlement = seat.hands.every(hand => hand.result !== null)
|
||||
? calculateSeatRoundSettlement(seat, betAmount)
|
||||
: null;
|
||||
|
||||
return {
|
||||
hands: seat.hands.map(toHandView),
|
||||
hands: seat.hands.map(hand => toHandView(hand, betAmount)),
|
||||
activeHandIndex: seat.activeHandIndex,
|
||||
hasBet: seat.hasBet,
|
||||
totalWager: seat.hands.reduce((sum, hand) => sum + (hand.bet * betAmount), 0),
|
||||
roundNet: settlement?.net ?? null,
|
||||
cumulativePnl: seat.cumulativePnl,
|
||||
};
|
||||
}
|
||||
@@ -425,7 +439,7 @@ export const blackjackPlugin: GamePlugin<BlackjackState, BlackjackAction> = {
|
||||
const visibleDealer = dealerVisibleHand(state);
|
||||
const seatsView: Record<string, PlayerSeatView> = {};
|
||||
for (const [id, seat] of Object.entries(state.seats)) {
|
||||
seatsView[id] = toSeatView(seat);
|
||||
seatsView[id] = toSeatView(seat, state.betAmount);
|
||||
}
|
||||
|
||||
const activeId = state.activePlayerIndex >= 0
|
||||
@@ -465,7 +479,7 @@ export const blackjackPlugin: GamePlugin<BlackjackState, BlackjackAction> = {
|
||||
const visibleDealer = dealerVisibleHand(state);
|
||||
const seatsView: Record<string, PlayerSeatView> = {};
|
||||
for (const [id, seat] of Object.entries(state.seats)) {
|
||||
seatsView[id] = toSeatView(seat);
|
||||
seatsView[id] = toSeatView(seat, state.betAmount);
|
||||
}
|
||||
|
||||
const activeId = state.activePlayerIndex >= 0
|
||||
@@ -559,7 +573,7 @@ function handlePlaceBet(state: BlackjackState, playerId: string): GameResult<Bla
|
||||
const dealtState = dealRound(newState);
|
||||
if (dealtState.phase === "resolved") {
|
||||
// All players got blackjack — round is already over
|
||||
return { ok: true, state: dealtState, roundPayouts: calculateRoundPayouts(dealtState.seats) };
|
||||
return { ok: true, state: dealtState, roundSettlements: calculateRoundSettlements(dealtState.seats, dealtState.betAmount) };
|
||||
}
|
||||
return { ok: true, state: dealtState };
|
||||
}
|
||||
@@ -602,7 +616,7 @@ function handleHit(state: BlackjackState, playerId: string): GameResult<Blackjac
|
||||
}
|
||||
|
||||
if (newState.phase === "resolved") {
|
||||
return { ok: true, state: newState, roundPayouts: calculateRoundPayouts(newState.seats) };
|
||||
return { ok: true, state: newState, roundSettlements: calculateRoundSettlements(newState.seats, newState.betAmount) };
|
||||
}
|
||||
return { ok: true, state: newState };
|
||||
}
|
||||
@@ -627,7 +641,7 @@ function handleStand(state: BlackjackState, playerId: string): GameResult<Blackj
|
||||
newState = advanceTurn(newState);
|
||||
|
||||
if (newState.phase === "resolved") {
|
||||
return { ok: true, state: newState, roundPayouts: calculateRoundPayouts(newState.seats) };
|
||||
return { ok: true, state: newState, roundSettlements: calculateRoundSettlements(newState.seats, newState.betAmount) };
|
||||
}
|
||||
return { ok: true, state: newState };
|
||||
}
|
||||
@@ -707,7 +721,7 @@ function handleSplit(state: BlackjackState, playerId: string): GameResult<Blackj
|
||||
}
|
||||
|
||||
if (newState.phase === "resolved") {
|
||||
return { ok: true, state: newState, roundPayouts: calculateRoundPayouts(newState.seats) };
|
||||
return { ok: true, state: newState, roundSettlements: calculateRoundSettlements(newState.seats, newState.betAmount) };
|
||||
}
|
||||
return { ok: true, state: newState };
|
||||
}
|
||||
@@ -749,7 +763,7 @@ function handleDoubleDown(state: BlackjackState, playerId: string): GameResult<B
|
||||
newState = advanceTurn(newState);
|
||||
|
||||
if (newState.phase === "resolved") {
|
||||
return { ok: true, state: newState, roundPayouts: calculateRoundPayouts(newState.seats) };
|
||||
return { ok: true, state: newState, roundSettlements: calculateRoundSettlements(newState.seats, newState.betAmount) };
|
||||
}
|
||||
return { ok: true, state: newState };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user