feat(holdem): support up to 7 AI opponents with dynamic elliptical seating
- Update Holdem game configuration to allow 3-7 AI opponents (previously fixed at 3) - Implement dynamic seat layout using an elliptical distribution for balanced positioning - Add computeSeatLayout() to calculate positions based on opponent count, with special handling for side/diagonal corners - Adjust portrait, card, and UI element positioning to adapt to different table sizes - Update AI personality definitions to include 7 distinct profiles with varied aggression/bluff rates - Modify AI decision logic to limp more frequently in unraised preflop scenarios - Update hand summary modal to scale rows dynamically to fit 7+ players - Update server game registry to reflect new max opponent count
This commit is contained in:
parent
24a48c13eb
commit
a05d8b6c96
|
|
@ -1,10 +1,14 @@
|
||||||
import { evaluateHand, callAmount, canCheck, minRaise } from './HoldemLogic.js';
|
import { evaluateHand, callAmount, canCheck, minRaise } from './HoldemLogic.js';
|
||||||
|
|
||||||
// Per-seat personalities indexed by seat number (1–3 for AI seats)
|
// Per-seat personalities indexed by seat number (1–7 for AI seats)
|
||||||
const PERSONALITIES = {
|
const PERSONALITIES = {
|
||||||
1: { style: 'aggressive', raiseFactor: 0.55, bluffRate: 0.22 }, // Marcus
|
1: { style: 'aggressive', raiseFactor: 0.55, bluffRate: 0.22 },
|
||||||
2: { style: 'tight', raiseFactor: 0.28, bluffRate: 0.10 }, // Sofia
|
2: { style: 'tight', raiseFactor: 0.28, bluffRate: 0.10 },
|
||||||
3: { style: 'loose', raiseFactor: 0.44, bluffRate: 0.27 }, // third AI
|
3: { style: 'loose', raiseFactor: 0.44, bluffRate: 0.27 },
|
||||||
|
4: { style: 'aggressive', raiseFactor: 0.62, bluffRate: 0.30 },
|
||||||
|
5: { style: 'tight', raiseFactor: 0.22, bluffRate: 0.06 },
|
||||||
|
6: { style: 'loose', raiseFactor: 0.50, bluffRate: 0.18 },
|
||||||
|
7: { style: 'aggressive', raiseFactor: 0.48, bluffRate: 0.14 },
|
||||||
};
|
};
|
||||||
|
|
||||||
// Chen formula approximation: 0–20 score for pre-flop hand strength
|
// Chen formula approximation: 0–20 score for pre-flop hand strength
|
||||||
|
|
@ -65,7 +69,11 @@ export function chooseAction(state, seat) {
|
||||||
if (Math.random() < pers.bluffRate) strength = Math.min(1, strength + 0.40);
|
if (Math.random() < pers.bluffRate) strength = Math.min(1, strength + 0.40);
|
||||||
|
|
||||||
// Fold threshold: fold if strength is below the price of calling
|
// Fold threshold: fold if strength is below the price of calling
|
||||||
const foldThreshold = odds * 0.88;
|
let foldThreshold = odds * 0.88;
|
||||||
|
// When the pot hasn't been raised pre-flop it only costs the big blind to see
|
||||||
|
// the flop, so opponents limp in with almost anything instead of folding.
|
||||||
|
const unraisedPreflop = state.phase === 'preflop' && state.roundBet <= (state.blind?.big ?? state.roundBet);
|
||||||
|
if (unraisedPreflop) foldThreshold *= 0.25;
|
||||||
|
|
||||||
if (isCheck) {
|
if (isCheck) {
|
||||||
// Check or bet
|
// Check or bet
|
||||||
|
|
|
||||||
|
|
@ -21,14 +21,13 @@ const CARD_W = 100;
|
||||||
const CARD_H = 140;
|
const CARD_H = 140;
|
||||||
const CARD_R = 8; // corner radius
|
const CARD_R = 8; // corner radius
|
||||||
|
|
||||||
// Seat screen positions: [human, left-AI, top-AI, right-AI]
|
// Opponents are spaced evenly around a seating ellipse so the table stays
|
||||||
// portraitX/portraitY override the default world position when set.
|
// balanced for any count (3–7 opponents). The human owns the bottom of the ring
|
||||||
const SEAT_POS = [
|
// (fixed bottom-centre). Per-game positions are computed in computeSeatLayout().
|
||||||
{ x: CX, y: 900, portraitR: 85, portraitX: CX - 210, portraitY: 900 }, // 0 — Human: portrait left of cards
|
const SEAT_ELLIPSE = { cx: CX, cy: 540, rx: 760, ry: 330 };
|
||||||
{ x: 220, y: 470, portraitR: 78 }, // 1 — AI left
|
// Diagonal corner seats (top/bottom on either side) are pulled this far toward
|
||||||
{ x: CX, y: 140, portraitR: 70, portraitX: CX - 175, portraitY: 140 }, // 2 — AI top: portrait left of cards
|
// the table centre so they don't hug the screen edges.
|
||||||
{ x: 1700, y: 470, portraitR: 78 }, // 3 — AI right
|
const CORNER_NUDGE = 60;
|
||||||
];
|
|
||||||
|
|
||||||
// Depth layers
|
// Depth layers
|
||||||
const D = { bg: -1, table: 0, cards: 10, chips: 20, ui: 30, modal: 50 };
|
const D = { bg: -1, table: 0, cards: 10, chips: 20, ui: 30, modal: 50 };
|
||||||
|
|
@ -175,31 +174,107 @@ export default class HoldemGame extends Phaser.Scene {
|
||||||
|
|
||||||
// ── Seat containers ─────────────────────────────────────────────────────────
|
// ── Seat containers ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
// Build per-game seat positions: human bottom-centre, opponents fanned evenly
|
||||||
|
// across the top arc of the seating ellipse (matches the legacy left/top/right
|
||||||
|
// look at 3 opponents, fans out to 7).
|
||||||
|
computeSeatLayout() {
|
||||||
|
const numOpp = Math.min(this.opponents.length, 7);
|
||||||
|
const portraitR = numOpp >= 5 ? 58 : 72;
|
||||||
|
const { cx, cy, rx, ry } = SEAT_ELLIPSE;
|
||||||
|
const seats = [
|
||||||
|
{ x: CX, y: 900, portraitR: 85, portraitX: CX - 210, portraitY: 900 }, // human
|
||||||
|
];
|
||||||
|
// Spread opponents evenly around the whole ellipse. The human owns the
|
||||||
|
// bottom slot (270°); opponents fill the remaining ring symmetrically.
|
||||||
|
const cardHalfSpan = (CARD_W * 0.7 + 8) / 2 + CARD_W / 2; // half-width of an opponent's card pair
|
||||||
|
const n = numOpp + 1;
|
||||||
|
for (let i = 1; i <= numOpp; i++) {
|
||||||
|
const deg = 270 - (i * 360) / n;
|
||||||
|
const rad = (deg * Math.PI) / 180;
|
||||||
|
const ex = cx + rx * Math.cos(rad); // outer (on-ellipse) anchor
|
||||||
|
const ey = cy - ry * Math.sin(rad);
|
||||||
|
|
||||||
|
// Seats near the horizontal axis are "side" seats: cards pulled toward the
|
||||||
|
// table interior, portrait beside them on the outer edge (vertically
|
||||||
|
// centred), name/chips stacked below the portrait.
|
||||||
|
if (Math.abs(ey - CY) <= 90) {
|
||||||
|
const interiorSign = ex < CX ? 1 : -1;
|
||||||
|
const cardX = ex + interiorSign * (portraitR + 14 + cardHalfSpan);
|
||||||
|
seats.push({
|
||||||
|
x: cardX, y: ey, portraitR,
|
||||||
|
portraitX: ex, portraitY: ey,
|
||||||
|
side: ex < CX ? 'left' : 'right', interiorSign,
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pull diagonal corner seats inward (seats on the vertical centre axis stay put).
|
||||||
|
const ax = ex + (Math.abs(ex - cx) > 1 ? (ex < cx ? CORNER_NUDGE : -CORNER_NUDGE) : 0);
|
||||||
|
|
||||||
|
const flipped = ey > CY; // bottom-of-table seats: portrait below, cards toward centre
|
||||||
|
let portraitX, portraitY;
|
||||||
|
if (flipped) {
|
||||||
|
portraitX = ax;
|
||||||
|
portraitY = ey + CARD_H / 2 + portraitR + 8;
|
||||||
|
} else {
|
||||||
|
// Portrait sits above the cards; if that clips the top edge, place it
|
||||||
|
// beside the cards (toward the nearest screen edge) at card height.
|
||||||
|
const aboveY = ey - CARD_H / 2 - portraitR - 8;
|
||||||
|
if (aboveY >= portraitR + 4) {
|
||||||
|
portraitX = ax;
|
||||||
|
portraitY = aboveY;
|
||||||
|
} else {
|
||||||
|
const spacing = CARD_W * 0.7 + 8;
|
||||||
|
const sideSign = ax <= CX ? -1 : 1;
|
||||||
|
portraitX = ax + sideSign * (spacing / 2 + portraitR + 12);
|
||||||
|
portraitY = ey;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
seats.push({ x: ax, y: ey, portraitR, portraitX, portraitY, flipped });
|
||||||
|
}
|
||||||
|
this.seatPos = seats;
|
||||||
|
}
|
||||||
|
|
||||||
buildSeatContainers() {
|
buildSeatContainers() {
|
||||||
for (let seat = 0; seat < 4; seat++) {
|
this.computeSeatLayout();
|
||||||
const { x, y } = SEAT_POS[seat];
|
for (let seat = 0; seat < this.seatPos.length; seat++) {
|
||||||
|
const sp = this.seatPos[seat];
|
||||||
|
const { x, y, flipped, side } = sp;
|
||||||
const isHuman = seat === 0;
|
const isHuman = seat === 0;
|
||||||
const container = this.add.container(x, y).setDepth(D.cards);
|
const container = this.add.container(x, y).setDepth(D.cards);
|
||||||
|
|
||||||
|
// Label positions (local to the seat container). Side seats stack their
|
||||||
|
// name/chips directly below the portrait, which sits beside the cards;
|
||||||
|
// other seats keep them on the table-centre side of the cards.
|
||||||
|
let nameX = 0, chipX = 0, betX = 0, nameY, chipY, betY;
|
||||||
|
if (isHuman) {
|
||||||
|
nameY = -CARD_H / 2 - 48; chipY = -CARD_H / 2 - 24; betY = -CARD_H / 2 - 80;
|
||||||
|
} else if (side) {
|
||||||
|
nameX = chipX = sp.portraitX - x;
|
||||||
|
nameY = sp.portraitR + 26; chipY = sp.portraitR + 52;
|
||||||
|
betX = sp.interiorSign * 140; betY = 0;
|
||||||
|
} else if (flipped) {
|
||||||
|
nameY = -CARD_H / 2 - 28; chipY = -CARD_H / 2 - 52; betY = -CARD_H / 2 - 80;
|
||||||
|
} else {
|
||||||
|
nameY = CARD_H / 2 + 28; chipY = CARD_H / 2 + 52; betY = CARD_H / 2 + 80;
|
||||||
|
}
|
||||||
|
|
||||||
// Name label
|
// Name label
|
||||||
const nameY = isHuman ? -CARD_H / 2 - 48 : CARD_H / 2 + 28;
|
const name = this.add.text(nameX, nameY, '—', {
|
||||||
const name = this.add.text(0, nameY, '—', {
|
|
||||||
fontFamily: '"Julius Sans One"',
|
fontFamily: '"Julius Sans One"',
|
||||||
fontSize: '18px',
|
fontSize: '18px',
|
||||||
color: COLORS.textHex,
|
color: COLORS.textHex,
|
||||||
}).setOrigin(0.5);
|
}).setOrigin(0.5);
|
||||||
|
|
||||||
// Chip count
|
// Chip count
|
||||||
const chipY = isHuman ? -CARD_H / 2 - 24 : CARD_H / 2 + 52;
|
const chipTxt = this.add.text(chipX, chipY, '$0', {
|
||||||
const chipTxt = this.add.text(0, chipY, '$0', {
|
|
||||||
fontFamily: '"Julius Sans One"',
|
fontFamily: '"Julius Sans One"',
|
||||||
fontSize: '20px',
|
fontSize: '20px',
|
||||||
color: '#f0e8d0',
|
color: '#f0e8d0',
|
||||||
}).setOrigin(0.5);
|
}).setOrigin(0.5);
|
||||||
|
|
||||||
// Bet display (shown at table edge between seat and center)
|
// Bet display (shown between the seat and the table centre)
|
||||||
const betY = isHuman ? -CARD_H / 2 - 80 : (seat === 2 ? CARD_H / 2 + 80 : (y > CY ? -CARD_H / 2 - 80 : CARD_H / 2 + 80));
|
const betTxt = this.add.text(betX, betY, '', {
|
||||||
const betTxt = this.add.text(0, betY, '', {
|
|
||||||
fontFamily: '"Julius Sans One"',
|
fontFamily: '"Julius Sans One"',
|
||||||
fontSize: '18px',
|
fontSize: '18px',
|
||||||
color: '#ffd700',
|
color: '#ffd700',
|
||||||
|
|
@ -225,7 +300,7 @@ export default class HoldemGame extends Phaser.Scene {
|
||||||
this.seatContainers.push({ container, name, chipTxt, betTxt, cardObjs, dealerChip, dealerTxt });
|
this.seatContainers.push({ container, name, chipTxt, betTxt, cardObjs, dealerChip, dealerTxt });
|
||||||
|
|
||||||
// Portrait (world-space — outside the container)
|
// Portrait (world-space — outside the container)
|
||||||
const { x: px, y: py, portraitR, portraitX, portraitY: portraitYOverride } = SEAT_POS[seat];
|
const { x: px, y: py, portraitR, portraitX, portraitY: portraitYOverride } = this.seatPos[seat];
|
||||||
const portraitWorldX = portraitX ?? px;
|
const portraitWorldX = portraitX ?? px;
|
||||||
const portraitY = portraitYOverride ?? (isHuman
|
const portraitY = portraitYOverride ?? (isHuman
|
||||||
? py - CARD_H / 2 - 48 - portraitR - 8 // above name label
|
? py - CARD_H / 2 - 48 - portraitR - 8 // above name label
|
||||||
|
|
@ -345,8 +420,10 @@ export default class HoldemGame extends Phaser.Scene {
|
||||||
|
|
||||||
if (check) {
|
if (check) {
|
||||||
this.callBtn.setLabel('Check');
|
this.callBtn.setLabel('Check');
|
||||||
|
this.raiseBtn.setLabel('Bet');
|
||||||
} else {
|
} else {
|
||||||
this.callBtn.setLabel(`Call $${toCall}`);
|
this.callBtn.setLabel(`Call $${toCall}`);
|
||||||
|
this.raiseBtn.setLabel('Raise');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -406,7 +483,7 @@ export default class HoldemGame extends Phaser.Scene {
|
||||||
// ── Game flow ────────────────────────────────────────────────────────────────
|
// ── Game flow ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
beginGame() {
|
beginGame() {
|
||||||
this.gs = createInitialState(this.opponents.slice(0, 3), this.buyIn);
|
this.gs = createInitialState(this.opponents.slice(0, 7), this.buyIn);
|
||||||
this.dealNextHand();
|
this.dealNextHand();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -521,13 +598,13 @@ export default class HoldemGame extends Phaser.Scene {
|
||||||
|
|
||||||
// Returns world coords of the "bet zone" in front of a seat (between seat and table centre).
|
// Returns world coords of the "bet zone" in front of a seat (between seat and table centre).
|
||||||
betZone(seat) {
|
betZone(seat) {
|
||||||
const { x, y } = SEAT_POS[seat];
|
const { x, y } = this.seatPos[seat];
|
||||||
const dx = CX - x;
|
const dx = CX - x;
|
||||||
const dy = CY - y;
|
const dy = CY - y;
|
||||||
const len = Math.sqrt(dx * dx + dy * dy);
|
const len = Math.sqrt(dx * dx + dy * dy);
|
||||||
// Seats 0 and 2 sit on the vertical centre axis; shift their bet zone right
|
// Seats on the vertical centre axis shift their bet zone right so chip
|
||||||
// so chip stacks don't overlap the name/chip-count text centred at CX.
|
// stacks don't overlap the name/chip-count text centred at CX.
|
||||||
const xOffset = (seat === 0 || seat === 2) ? 160 : 0;
|
const xOffset = Math.abs(x - CX) < 120 ? 160 : 0;
|
||||||
return { x: x + (dx / len) * 140 + xOffset, y: y + (dy / len) * 100 };
|
return { x: x + (dx / len) * 140 + xOffset, y: y + (dy / len) * 100 };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -544,7 +621,7 @@ export default class HoldemGame extends Phaser.Scene {
|
||||||
animateFold(seat, onComplete) {
|
animateFold(seat, onComplete) {
|
||||||
playSound(this, SFX.CARD_PLACE);
|
playSound(this, SFX.CARD_PLACE);
|
||||||
const sc = this.seatContainers[seat];
|
const sc = this.seatContainers[seat];
|
||||||
const { x: sx, y: sy } = SEAT_POS[seat];
|
const { x: sx, y: sy } = this.seatPos[seat];
|
||||||
const { x: tx, y: ty } = this.betZone(seat);
|
const { x: tx, y: ty } = this.betZone(seat);
|
||||||
|
|
||||||
const tempCards = sc.cardObjs
|
const tempCards = sc.cardObjs
|
||||||
|
|
@ -569,8 +646,8 @@ export default class HoldemGame extends Phaser.Scene {
|
||||||
|
|
||||||
if (tempCards.length === 0) { onComplete(); return; }
|
if (tempCards.length === 0) { onComplete(); return; }
|
||||||
|
|
||||||
// Slight rotation: clockwise for left seats, counter for right
|
// Slight rotation: tilt toward the table edge based on which side the seat is on
|
||||||
const angle = seat === 1 ? -14 : seat === 3 ? 14 : seat === 0 ? 10 : -10;
|
const angle = seat === 0 ? 10 : (sx < CX ? -14 : sx > CX ? 14 : -10);
|
||||||
|
|
||||||
this.tweens.add({
|
this.tweens.add({
|
||||||
targets: tempCards,
|
targets: tempCards,
|
||||||
|
|
@ -590,7 +667,7 @@ export default class HoldemGame extends Phaser.Scene {
|
||||||
}
|
}
|
||||||
|
|
||||||
animateChips(seat, onComplete) {
|
animateChips(seat, onComplete) {
|
||||||
const { x: sx, y: sy } = SEAT_POS[seat];
|
const { x: sx, y: sy } = this.seatPos[seat];
|
||||||
const { x: tx, y: ty } = this.betZone(seat);
|
const { x: tx, y: ty } = this.betZone(seat);
|
||||||
|
|
||||||
playChipBet(this);
|
playChipBet(this);
|
||||||
|
|
@ -764,8 +841,13 @@ export default class HoldemGame extends Phaser.Scene {
|
||||||
|
|
||||||
// Animate hole cards flying from the deck (centre) to each seat, one at a time.
|
// Animate hole cards flying from the deck (centre) to each seat, one at a time.
|
||||||
animateDeal(onComplete) {
|
animateDeal(onComplete) {
|
||||||
// Deal order: one pass around the table, then a second pass (standard poker)
|
// Deal order: opponents around the table (seats 1..n-1) then the human (0),
|
||||||
const seatOrder = [1, 2, 3, 0].filter((s) => !this.gs.players[s]?.eliminated);
|
// one pass, then a second pass (standard poker)
|
||||||
|
const n = this.gs.players.length;
|
||||||
|
const order = [];
|
||||||
|
for (let s = 1; s < n; s++) order.push(s);
|
||||||
|
order.push(0);
|
||||||
|
const seatOrder = order.filter((s) => !this.gs.players[s]?.eliminated);
|
||||||
const sequence = [...seatOrder, ...seatOrder]; // card 0 then card 1
|
const sequence = [...seatOrder, ...seatOrder]; // card 0 then card 1
|
||||||
|
|
||||||
let done = 0;
|
let done = 0;
|
||||||
|
|
@ -774,7 +856,7 @@ export default class HoldemGame extends Phaser.Scene {
|
||||||
|
|
||||||
sequence.forEach((seat, idx) => {
|
sequence.forEach((seat, idx) => {
|
||||||
const cardIdx = idx < seatOrder.length ? 0 : 1;
|
const cardIdx = idx < seatOrder.length ? 0 : 1;
|
||||||
const { x: sx, y: sy } = SEAT_POS[seat];
|
const { x: sx, y: sy } = this.seatPos[seat];
|
||||||
const isHuman = seat === 0;
|
const isHuman = seat === 0;
|
||||||
const spacing = isHuman ? CARD_W + 12 : CARD_W * 0.7 + 8;
|
const spacing = isHuman ? CARD_W + 12 : CARD_W * 0.7 + 8;
|
||||||
const targetX = sx + (cardIdx === 0 ? -spacing / 2 : spacing / 2);
|
const targetX = sx + (cardIdx === 0 ? -spacing / 2 : spacing / 2);
|
||||||
|
|
@ -999,9 +1081,11 @@ export default class HoldemGame extends Phaser.Scene {
|
||||||
// ── Action badge ─────────────────────────────────────────────────────────────
|
// ── Action badge ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
showActionBadge(seat, action) {
|
showActionBadge(seat, action) {
|
||||||
const pos = SEAT_POS[seat];
|
const pos = this.seatPos[seat];
|
||||||
const isHuman = seat === 0;
|
const isHuman = seat === 0;
|
||||||
const badgeY = isHuman ? pos.y - CARD_H / 2 - 110 : pos.y + CARD_H / 2 + 70;
|
const badgeY = isHuman
|
||||||
|
? pos.y - CARD_H / 2 - 110
|
||||||
|
: pos.flipped ? pos.y - CARD_H / 2 - 70 : pos.y + CARD_H / 2 + 70;
|
||||||
const labels = { fold: 'FOLD', check: 'CHECK', call: 'CALL', raise: `RAISE $${action.amount ?? ''}`, allin: 'ALL IN' };
|
const labels = { fold: 'FOLD', check: 'CHECK', call: 'CALL', raise: `RAISE $${action.amount ?? ''}`, allin: 'ALL IN' };
|
||||||
const colors = { fold: '#e06c75', check: '#9e9080', call: COLORS.accentHex, raise: '#ffd700', allin: '#ff6b35' };
|
const colors = { fold: '#e06c75', check: '#9e9080', call: COLORS.accentHex, raise: '#ffd700', allin: '#ff6b35' };
|
||||||
|
|
||||||
|
|
@ -1079,12 +1163,14 @@ export default class HoldemGame extends Phaser.Scene {
|
||||||
const divider = this.add.rectangle(CX, 262, 1600, 1, COLORS.muted, 0.4).setDepth(D.modal + 1);
|
const divider = this.add.rectangle(CX, 262, 1600, 1, COLORS.muted, 0.4).setDepth(D.modal + 1);
|
||||||
S.push(divider);
|
S.push(divider);
|
||||||
|
|
||||||
// Player rows — one per seat, staggered
|
// Player rows — one per seat, staggered. Pitch shrinks to fit up to 8 rows
|
||||||
|
// between the divider and the Continue button.
|
||||||
const rowStartY = 305;
|
const rowStartY = 305;
|
||||||
const rowH = 155;
|
const rowH = Math.min(155, 700 / players.length);
|
||||||
|
const scale = Math.min(1, rowH / 155);
|
||||||
players.forEach((player, seat) => {
|
players.forEach((player, seat) => {
|
||||||
const rowY = rowStartY + seat * rowH;
|
const rowY = rowStartY + seat * rowH;
|
||||||
this.buildSummaryRow(player, rowY, winnings, 500 + seat * 190, community, S);
|
this.buildSummaryRow(player, rowY, winnings, 500 + seat * 190, community, S, rowH, scale);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Continue button — fades in after rows have appeared
|
// Continue button — fades in after rows have appeared
|
||||||
|
|
@ -1096,11 +1182,12 @@ export default class HoldemGame extends Phaser.Scene {
|
||||||
this.tweens.add({ targets: continueBtn, alpha: 1, duration: 300, delay: 1100 });
|
this.tweens.add({ targets: continueBtn, alpha: 1, duration: 300, delay: 1100 });
|
||||||
}
|
}
|
||||||
|
|
||||||
buildSummaryRow(player, rowY, winnings, delay, community, S) {
|
buildSummaryRow(player, rowY, winnings, delay, community, S, rowH = 155, scale = 1) {
|
||||||
const isWinner = (winnings.get(player.seat) ?? 0) > 0;
|
const isWinner = (winnings.get(player.seat) ?? 0) > 0;
|
||||||
|
|
||||||
// Row panel
|
// Row panel
|
||||||
const panel = this.add.rectangle(CX, rowY, 1720, 140, COLORS.panel)
|
const panelH = Math.max(70, rowH - 14);
|
||||||
|
const panel = this.add.rectangle(CX, rowY, 1720, panelH, COLORS.panel)
|
||||||
.setStrokeStyle(2, isWinner ? 0xffd700 : 0x2a2a3a)
|
.setStrokeStyle(2, isWinner ? 0xffd700 : 0x2a2a3a)
|
||||||
.setDepth(D.modal + 1).setAlpha(0);
|
.setDepth(D.modal + 1).setAlpha(0);
|
||||||
S.push(panel);
|
S.push(panel);
|
||||||
|
|
@ -1108,7 +1195,7 @@ export default class HoldemGame extends Phaser.Scene {
|
||||||
|
|
||||||
// Portrait backing
|
// Portrait backing
|
||||||
const portX = 145;
|
const portX = 145;
|
||||||
const portR = 52;
|
const portR = Math.round(52 * scale);
|
||||||
const backing = this.add.circle(portX, rowY, portR + 3, 0x1a1a2e)
|
const backing = this.add.circle(portX, rowY, portR + 3, 0x1a1a2e)
|
||||||
.setDepth(D.modal + 1).setAlpha(0);
|
.setDepth(D.modal + 1).setAlpha(0);
|
||||||
S.push(backing);
|
S.push(backing);
|
||||||
|
|
@ -1152,7 +1239,7 @@ export default class HoldemGame extends Phaser.Scene {
|
||||||
} else {
|
} else {
|
||||||
const initial = (auth.user?.username ?? 'You').charAt(0).toUpperCase();
|
const initial = (auth.user?.username ?? 'You').charAt(0).toUpperCase();
|
||||||
const initTxt = this.add.text(portX, rowY, initial, {
|
const initTxt = this.add.text(portX, rowY, initial, {
|
||||||
fontFamily: 'Righteous', fontSize: '46px', color: COLORS.accentHex,
|
fontFamily: 'Righteous', fontSize: `${Math.round(46 * scale)}px`, color: COLORS.accentHex,
|
||||||
}).setOrigin(0.5).setDepth(D.modal + 2).setAlpha(0);
|
}).setOrigin(0.5).setDepth(D.modal + 2).setAlpha(0);
|
||||||
S.push(initTxt);
|
S.push(initTxt);
|
||||||
this.tweens.add({ targets: initTxt, alpha: 1, duration: 350, delay });
|
this.tweens.add({ targets: initTxt, alpha: 1, duration: 350, delay });
|
||||||
|
|
@ -1187,15 +1274,14 @@ export default class HoldemGame extends Phaser.Scene {
|
||||||
this.tweens.add({ targets: statusTxt, alpha: 1, duration: 350, delay });
|
this.tweens.add({ targets: statusTxt, alpha: 1, duration: 350, delay });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hole cards (0.82× scale → 82×115)
|
// Hole cards (0.82× scale → 82×115, shrinks further when rows are compact)
|
||||||
const CW = Math.round(CARD_W * 0.82);
|
const cardScale = 0.82 * scale;
|
||||||
const CH = Math.round(CARD_H * 0.82);
|
|
||||||
const cardBaseX = 490;
|
const cardBaseX = 490;
|
||||||
const cardOffsets = [-48, 48];
|
const cardOffsets = [-48 * scale, 48 * scale];
|
||||||
if (player.hand?.length === 2) {
|
if (player.hand?.length === 2) {
|
||||||
player.hand.forEach((card, ci) => {
|
player.hand.forEach((card, ci) => {
|
||||||
const cx = cardBaseX + cardOffsets[ci];
|
const cx = cardBaseX + cardOffsets[ci];
|
||||||
const cont = this.add.container(cx, rowY).setDepth(D.modal + 2).setScale(0.82).setAlpha(0);
|
const cont = this.add.container(cx, rowY).setDepth(D.modal + 2).setScale(cardScale).setAlpha(0);
|
||||||
if (player.folded) {
|
if (player.folded) {
|
||||||
this.renderCard(cont, card, false);
|
this.renderCard(cont, card, false);
|
||||||
this.tweens.add({ targets: cont, alpha: 0.28, duration: 350, delay });
|
this.tweens.add({ targets: cont, alpha: 0.28, duration: 350, delay });
|
||||||
|
|
|
||||||
|
|
@ -254,7 +254,7 @@ export function createInitialState(opponentDefs, buyIn) {
|
||||||
eliminated: false,
|
eliminated: false,
|
||||||
hasActedThisRound: false,
|
hasActedThisRound: false,
|
||||||
},
|
},
|
||||||
...opponentDefs.slice(0, 3).map((opp, i) => ({
|
...opponentDefs.slice(0, 7).map((opp, i) => ({
|
||||||
seat: i + 1,
|
seat: i + 1,
|
||||||
name: opp.name,
|
name: opp.name,
|
||||||
chips: buyIn,
|
chips: buyIn,
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@ export function getGame(slug) {
|
||||||
registerGame({ slug: 'backgammon', name: 'Backgammon', category: 'tabletop', minPlayers: 2, maxPlayers: 2, minOpponents: 1, maxOpponents: 1 });
|
registerGame({ slug: 'backgammon', name: 'Backgammon', category: 'tabletop', minPlayers: 2, maxPlayers: 2, minOpponents: 1, maxOpponents: 1 });
|
||||||
registerGame({ slug: 'parchisi', name: 'Parchisi', category: 'tabletop', minPlayers: 1, maxPlayers: 4, minOpponents: 3, maxOpponents: 3 });
|
registerGame({ slug: 'parchisi', name: 'Parchisi', category: 'tabletop', minPlayers: 1, maxPlayers: 4, minOpponents: 3, maxOpponents: 3 });
|
||||||
registerGame({ slug: 'blackjack', name: 'Blackjack', category: 'casino', cardGame: true, minPlayers: 1, maxPlayers: 7, minOpponents: 0, maxOpponents: 6 });
|
registerGame({ slug: 'blackjack', name: 'Blackjack', category: 'casino', cardGame: true, minPlayers: 1, maxPlayers: 7, minOpponents: 0, maxOpponents: 6 });
|
||||||
registerGame({ slug: 'holdem', name: "Texas Hold 'Em", category: 'casino', cardGame: true, minPlayers: 2, maxPlayers: 8, minOpponents: 3, maxOpponents: 3 });
|
registerGame({ slug: 'holdem', name: "Texas Hold 'Em", category: 'casino', cardGame: true, minPlayers: 2, maxPlayers: 8, minOpponents: 3, maxOpponents: 7 });
|
||||||
registerGame({ slug: 'yatzi', name: 'Yatzi', category: 'tabletop', minPlayers: 1, maxPlayers: 4, minOpponents: 1, maxOpponents: 3 });
|
registerGame({ slug: 'yatzi', name: 'Yatzi', category: 'tabletop', minPlayers: 1, maxPlayers: 4, minOpponents: 1, maxOpponents: 3 });
|
||||||
registerGame({ slug: 'skipbo', name: 'Skip-Bo', category: 'cards', cardGame: true, minPlayers: 1, maxPlayers: 4, minOpponents: 1, maxOpponents: 3 });
|
registerGame({ slug: 'skipbo', name: 'Skip-Bo', category: 'cards', cardGame: true, minPlayers: 1, maxPlayers: 4, minOpponents: 1, maxOpponents: 3 });
|
||||||
registerGame({ slug: 'phase10', name: 'Phase 10', category: 'cards', cardGame: true, minPlayers: 1, maxPlayers: 4, minOpponents: 1, maxOpponents: 3 });
|
registerGame({ slug: 'phase10', name: 'Phase 10', category: 'cards', cardGame: true, minPlayers: 1, maxPlayers: 4, minOpponents: 1, maxOpponents: 3 });
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue