Add cumulative PnL tracking to Blackjack game
Some checks failed
Deploy to Production / test (push) Failing after 32s

- Added cumulativePnl field to PlayerSeat and PlayerSeatView types
- Added myCumulativePnl to PlayerView for UI display
- Track net profit/loss across rounds in the game state
- Update round result banner to show both round net and total balance
- Add player seat PnL indicator with color coding (green/red)
- Preserve cumulativePnl when players stay seated through rounds
- Initialize new players with cumulativePnl = 0
- Added comprehensive tests for cumulative PnL tracking
This commit is contained in:
syntaxbullet
2026-04-06 14:31:58 +02:00
parent 2b89fb7ede
commit 966bad98d3
4 changed files with 177 additions and 8 deletions

View File

@@ -23,6 +23,8 @@ interface PlayerSeatView {
hands: PlayerHandView[];
activeHandIndex: number;
hasBet: boolean;
/** Cumulative PnL from all previous rounds. */
cumulativePnl: number;
}
interface BlackjackViewBase {
@@ -42,6 +44,8 @@ interface PlayerView extends BlackjackViewBase {
canAct: boolean;
canSplit: boolean;
canDoubleDown: boolean;
/** Cumulative PnL for the current player. */
myCumulativePnl: number;
}
function isPlayerView(state: unknown): state is PlayerView {
@@ -207,6 +211,16 @@ function Seat({ seat, playerName, isMe, isActivePlayer, activeHandIdx, betAmount
const hasHands = seat.hands.length > 0;
const compact = seat.hands.length > 2;
// Calculate current round PnL for the display
const currentRoundPnl = seat.hands.reduce((sum, h) => {
if (!h.result) return sum; // Not yet resolved
const handWin = h.bet * betAmount;
if (h.result === "win" || h.result === "blackjack") return sum + handWin;
if (h.result === "push") return sum; // Push returns bet
return sum - handWin; // Lose
}, 0);
const totalPnl = seat.cumulativePnl + currentRoundPnl;
return (
<div className={`flex flex-col items-center gap-1.5 transition-all ${
isActivePlayer ? "scale-105" : ""
@@ -256,6 +270,21 @@ function Seat({ seat, playerName, isMe, isActivePlayer, activeHandIdx, betAmount
{hasHands && betAmount > 0 && (
<BetChip bet={seat.hands.reduce((sum, h) => sum + h.bet, 0)} betAmount={betAmount} />
)}
{/* Cumulative PnL indicator */}
{(seat.cumulativePnl !== 0 || hasHands) && (
<div className={`flex items-center gap-1 text-xs font-bold rounded px-2 py-0.5 ${
totalPnl > 0
? "bg-emerald-500/20 text-emerald-400"
: totalPnl < 0
? "bg-red-500/20 text-red-400"
: "bg-blue-500/20 text-blue-300"
}`}>
{totalPnl > 0 && <ChevronsUp className="w-3.5 h-3.5" />}
{totalPnl < 0 && <ChevronsUp className="w-3.5 h-3.5 rotate-180" />}
<span>{totalPnl > 0 ? "+" : ""}{totalPnl} AU</span>
</div>
)}
</div>
);
}
@@ -318,24 +347,37 @@ function DealerArea({ dealerHand, visibleValue, fullValue }: {
// ── Round result banner ──
function RoundResultBanner({ roundNumber, roundResult, myPlayerId, betAmount }: {
function RoundResultBanner({ roundNumber, roundResult, myPlayerId, betAmount, myCumulativePnl }: {
roundNumber: number;
roundResult: GameUIProps["roundResult"];
myPlayerId: string;
betAmount: number;
myCumulativePnl: number;
}) {
const myPayout = roundResult?.payouts[myPlayerId];
const myPayout = roundResult?.payouts?.[myPlayerId];
// Calculate this round's net result
const roundNet = myPayout?.net ?? 0;
// Current balance after this round (includes round result + cumulative from before)
const currentBalance = myCumulativePnl;
return (
<div className="flex items-center justify-center gap-3 bg-black/30 rounded-xl px-4 py-2">
<span className="text-xs text-white/50">Round {roundNumber}</span>
{myPayout && betAmount > 0 && (
<span className={`text-xs font-semibold ${
myPayout.net > betAmount ? "text-emerald-400" : myPayout.net === betAmount ? "text-blue-400" : "text-white/60"
<div className="flex items-center gap-2">
{myPayout && betAmount > 0 && (
<span className={`text-xs font-semibold ${
myPayout.net > betAmount ? "text-emerald-400" : myPayout.net === betAmount ? "text-blue-400" : "text-white/60"
}`}>
Round: {myPayout.net > 0 ? "+" : ""}{myPayout.net} AU
</span>
)}
<div className="w-px h-4 bg-white/20" />
<span className={`text-xs font-bold ${
currentBalance > 0 ? "text-emerald-400" : currentBalance < 0 ? "text-red-400" : "text-blue-300"
}`}>
+{myPayout.net} AU
Total: {currentBalance > 0 ? "+" : ""}{currentBalance} AU
</span>
)}
</div>
</div>
);
}
@@ -358,6 +400,15 @@ export function BlackjackGame({ state, myPlayerId, isSpectator, onAction, player
const mySeat = view.seats[myPlayerId];
const myBetPlaced = mySeat?.hasBet ?? false;
// Calculate current balance (if betting) or just track profit/loss
const balance = betAmount > 0 && mySeat?.hands.length === 0
? 0 // Not yet bet for this round
: mySeat?.hands.reduce((sum, h) => sum + (h.result === "win" || h.result === "blackjack"
? h.bet * betAmount
: h.result === "push" ? h.bet * betAmount
: 0), 0) - (myBetPlaced ? mySeat.hands.reduce((sum, h) => sum + h.bet * betAmount, 0) : 0);
const myPnl = (mySeat?.cumulativePnl ?? 0) + balance;
// Determine who can sit (spectators during betting phase)
const canSitDown = isSpectator && isBetting && view.turnOrder.length < 6;
@@ -529,6 +580,7 @@ export function BlackjackGame({ state, myPlayerId, isSpectator, onAction, player
roundResult={roundResult}
myPlayerId={myPlayerId}
betAmount={betAmount}
myCumulativePnl={(view as PlayerView)?.myCumulativePnl ?? 0}
/>
)}