```text
feat(blackjack): expand player capacity to 7 and improve table visuals
- Increase maximum players from 5 to 7 (6 opponents) by updating seat positions, logic loops, and server registry.
- Add traditional felt table markings including curved text for rules ("Blackjack pays 3 to 2", "Insurance pays 2 to 1") and card/bet spot outlines.
- Refactor seat label rendering to use a consistent centered layout under portraits.
- Adjust bet circle positioning logic for the new seat layout.
```
This commit is contained in:
parent
1a7decfa0e
commit
b2ef3cecf7
|
|
@ -24,12 +24,17 @@ const CARD_SPREAD = 28; // horizontal offset between stacked cards
|
|||
const DEALER_X = CX;
|
||||
const DEALER_Y = 420; // ~1/3 down the table ellipse (top ≈190, bottom ≈890)
|
||||
|
||||
// 7 seats fanned around the oval: seat 0 (human) anchored bottom-centre,
|
||||
// 6 opponents fanned 3-per-side (filled outward-alternating so the table stays
|
||||
// balanced when fewer than 6 opponents are chosen).
|
||||
const SEAT_POS = [
|
||||
{ x: CX, y: 860, portraitR: 72, portraitX: CX - 230, portraitY: 860 }, // Human
|
||||
{ x: 380, y: 775, portraitR: 58, portraitX: 200, portraitY: 650 }, // Opp 1 bottom-left (outside ellipse)
|
||||
{ x: 150, y: 490, portraitR: 58 }, // Opp 2 left (already outside ellipse)
|
||||
{ x: 1770, y: 490, portraitR: 58 }, // Opp 3 right (already outside ellipse)
|
||||
{ x: 1540, y: 775, portraitR: 58, portraitX: 1720, portraitY: 650 }, // Opp 4 bottom-right (outside ellipse)
|
||||
{ x: CX, y: 860, portraitR: 72, portraitX: CX - 230, portraitY: 860, betX: 1110, betY: 770 }, // 0 Human (bottom centre)
|
||||
{ x: 1490, y: 745, portraitR: 58, portraitX: 1600, portraitY: 800 }, // 1 right-bottom
|
||||
{ x: 430, y: 745, portraitR: 58, portraitX: 320, portraitY: 800 }, // 2 left-bottom
|
||||
{ x: 1670, y: 595, portraitR: 58, portraitX: 1805, portraitY: 610 }, // 3 right-mid
|
||||
{ x: 250, y: 595, portraitR: 58, portraitX: 115, portraitY: 610 }, // 4 left-mid
|
||||
{ x: 1585, y: 375, portraitR: 58, portraitX: 1705, portraitY: 335, labelDX: 10 }, // 5 right-top
|
||||
{ x: 335, y: 375, portraitR: 58, portraitX: 215, portraitY: 335, labelDX: -10 }, // 6 left-top
|
||||
];
|
||||
|
||||
const CHIP_COLORS = { 5: 0xe05c5c, 25: 0x5cb85c, 50: 0x4a90d9, 100: 0x2c2c2c };
|
||||
|
|
@ -82,6 +87,7 @@ export default class BlackjackGame extends Phaser.Scene {
|
|||
this.add.rectangle(CX, GAME_HEIGHT / 2, GAME_WIDTH, GAME_HEIGHT, COLORS.bg).setDepth(D.bg);
|
||||
this.buildPlayfield();
|
||||
this.buildTable();
|
||||
this.buildTableMarkings();
|
||||
this.buildDealerArea();
|
||||
this.buildSeats();
|
||||
this.buildBettingUI();
|
||||
|
|
@ -116,16 +122,77 @@ export default class BlackjackGame extends Phaser.Scene {
|
|||
g.strokeEllipse(CX, 540, 1440, 640);
|
||||
}
|
||||
|
||||
// ── Traditional felt markings ───────────────────────────────────────────────
|
||||
// White card-deal outlines + bet circles for every seat (drawn for all 7 so
|
||||
// unoccupied seats still show open spots), plus curved felt lettering.
|
||||
buildTableMarkings() {
|
||||
const g = this.add.graphics().setDepth(D.table + 1);
|
||||
|
||||
for (let seat = 0; seat < SEAT_POS.length; seat++) {
|
||||
const pos = SEAT_POS[seat];
|
||||
g.lineStyle(2, 0xffffff, 0.5);
|
||||
g.strokeRoundedRect(
|
||||
pos.x - CARD_W / 2 - 4, pos.y - CARD_H / 2 - 4,
|
||||
CARD_W + 8, CARD_H + 8, CARD_R + 2,
|
||||
);
|
||||
const { x: bx, y: by } = this.betCirclePos(seat);
|
||||
g.lineStyle(2, 0xffffff, 0.55);
|
||||
g.strokeCircle(bx, by, 30);
|
||||
}
|
||||
|
||||
// Curved insurance line + label (concentric with the text arc)
|
||||
const insBaseY = 512, insRadius = 820;
|
||||
const insCx = CX, insCy = insBaseY - insRadius;
|
||||
const insSpan = 0.34;
|
||||
g.lineStyle(3, 0xffffff, 0.45);
|
||||
g.beginPath();
|
||||
g.arc(insCx, insCy, insRadius + 16, Math.PI / 2 - insSpan, Math.PI / 2 + insSpan, false);
|
||||
g.strokePath();
|
||||
this.drawArcText('INSURANCE PAYS 2 TO 1', insCx, insBaseY, insRadius, {
|
||||
fontSize: 20, color: COLORS.textHex, advanceFactor: 0.95,
|
||||
});
|
||||
|
||||
this.drawArcText('BLACKJACK PAYS 3 TO 2', CX, 610, 760, {
|
||||
fontSize: 30, color: COLORS.goldHex, bold: true, advanceFactor: 0.98,
|
||||
});
|
||||
|
||||
this.drawArcText('DEALER MUST STAND ON ALL 17s · DRAW TO 16', CX, 668, 720, {
|
||||
fontSize: 19, color: COLORS.mutedHex, advanceFactor: 0.86,
|
||||
});
|
||||
}
|
||||
|
||||
// Draws a string along a downward-bulging arc, glyph by glyph (Phaser has no
|
||||
// native curved text). (centerX, baseY) is where the middle of the text sits;
|
||||
// the line curves upward toward both ends.
|
||||
drawArcText(text, centerX, baseY, radius, opts = {}) {
|
||||
const fontSize = opts.fontSize ?? 24;
|
||||
const anglePer = ((opts.advanceFactor ?? 0.92) * fontSize) / radius;
|
||||
const cy = baseY - radius; // circle centre, above the text
|
||||
const start = -anglePer * (text.length - 1) / 2;
|
||||
const style = {
|
||||
fontFamily: opts.fontFamily ?? '"Julius Sans One"',
|
||||
fontSize: `${fontSize}px`,
|
||||
color: opts.color ?? COLORS.textHex,
|
||||
...(opts.bold ? { fontStyle: 'bold' } : {}),
|
||||
};
|
||||
const depth = opts.depth ?? (D.table + 1);
|
||||
for (let i = 0; i < text.length; i++) {
|
||||
const ch = text[i];
|
||||
if (ch === ' ') continue;
|
||||
const a = start + i * anglePer; // offset from straight-down
|
||||
this.add.text(centerX + radius * Math.sin(a), cy + radius * Math.cos(a), ch, style)
|
||||
.setOrigin(0.5)
|
||||
.setRotation(a)
|
||||
.setDepth(depth);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Dealer area ───────────────────────────────────────────────────────────
|
||||
buildDealerArea() {
|
||||
this.add.text(CX, 60, 'Blackjack', {
|
||||
fontFamily: 'Righteous', fontSize: '52px', color: COLORS.textHex,
|
||||
}).setOrigin(0.5).setDepth(D.ui);
|
||||
|
||||
this.add.text(CX, 110, 'Dealer stands on all 17s · Blackjack pays 3:2', {
|
||||
fontFamily: '"Julius Sans One"', fontSize: '18px', color: COLORS.mutedHex,
|
||||
}).setOrigin(0.5).setDepth(D.ui);
|
||||
|
||||
this.dealerScoreTxt = this.add.text(CX, DEALER_Y - CARD_H / 2 - 22, '', {
|
||||
fontFamily: '"Julius Sans One"', fontSize: '22px', color: COLORS.textHex,
|
||||
}).setOrigin(0.5).setDepth(D.ui);
|
||||
|
|
@ -133,7 +200,7 @@ export default class BlackjackGame extends Phaser.Scene {
|
|||
|
||||
// ── Seat labels & portraits ───────────────────────────────────────────────
|
||||
buildSeats() {
|
||||
for (let seat = 0; seat < 5; seat++) {
|
||||
for (let seat = 0; seat < SEAT_POS.length; seat++) {
|
||||
const pos = SEAT_POS[seat];
|
||||
const player = seat === 0
|
||||
? { name: 'You', isHuman: true, active: true }
|
||||
|
|
@ -141,45 +208,31 @@ export default class BlackjackGame extends Phaser.Scene {
|
|||
|
||||
if (!player.active) continue;
|
||||
|
||||
let nameX, nameY, chipX, chipY, labelAnchorX;
|
||||
if (seat === 0) {
|
||||
// Human: labels to the left of portrait, right-aligned
|
||||
const px = pos.portraitX;
|
||||
const py = pos.portraitY;
|
||||
nameX = px - pos.portraitR - 14; nameY = py - 14;
|
||||
chipX = px - pos.portraitR - 14; chipY = py + 14;
|
||||
labelAnchorX = 1;
|
||||
} else {
|
||||
// Opponents: labels beside their portrait circle
|
||||
const px = pos.portraitX ?? pos.x;
|
||||
const py = pos.portraitY ?? (pos.y - CARD_H / 2 - pos.portraitR - 68);
|
||||
const isLeft = pos.x < CX;
|
||||
const side = isLeft ? 1 : -1;
|
||||
const labelX = px + side * (pos.portraitR + 14);
|
||||
nameX = labelX; nameY = py - 14;
|
||||
chipX = labelX; chipY = py + 14;
|
||||
labelAnchorX = isLeft ? 0 : 1;
|
||||
}
|
||||
// Portrait centre
|
||||
const px = pos.portraitX ?? pos.x;
|
||||
const py = pos.portraitY ?? (seat === 0 ? pos.y : pos.y - CARD_H / 2 - pos.portraitR - 68);
|
||||
|
||||
this.nameTxts[seat] = this.add.text(nameX, nameY, player.name, {
|
||||
// Name + bankroll stacked, centred under the portrait
|
||||
const labelX = px + (pos.labelDX ?? 0);
|
||||
const nameY = py + pos.portraitR + 18;
|
||||
const chipY = nameY + 24;
|
||||
|
||||
this.nameTxts[seat] = this.add.text(labelX, nameY, player.name, {
|
||||
fontFamily: '"Julius Sans One"', fontSize: '20px', color: COLORS.textHex,
|
||||
}).setOrigin(labelAnchorX, 0.5).setDepth(D.ui);
|
||||
}).setOrigin(0.5).setDepth(D.ui);
|
||||
|
||||
this.chipTxts[seat] = this.add.text(chipX, chipY, '', {
|
||||
this.chipTxts[seat] = this.add.text(labelX, chipY, '', {
|
||||
fontFamily: '"Julius Sans One"', fontSize: '18px', color: COLORS.mutedHex,
|
||||
}).setOrigin(labelAnchorX, 0.5).setDepth(D.ui);
|
||||
}).setOrigin(0.5).setDepth(D.ui);
|
||||
|
||||
// Semi-transparent backing behind both text lines, depth below portrait images/videos
|
||||
{
|
||||
const maxTW = Math.max(this.nameTxts[seat].width, 130);
|
||||
const rectW = maxTW + 20;
|
||||
const rectH = (chipY - nameY) + 36;
|
||||
// For right-anchored text the anchor is the RIGHT edge; for left-anchored it's the LEFT edge
|
||||
const rectCX = labelAnchorX === 1 ? nameX - maxTW / 2 : nameX + maxTW / 2;
|
||||
const rectCY = (nameY + chipY) / 2;
|
||||
const bg = this.add.graphics().setDepth(D.ui - 1);
|
||||
bg.fillStyle(0x000000, 0.60);
|
||||
bg.fillRoundedRect(rectCX - rectW / 2, rectCY - rectH / 2, rectW, rectH, 6);
|
||||
bg.fillRoundedRect(labelX - rectW / 2, (nameY + chipY) / 2 - rectH / 2, rectW, rectH, 6);
|
||||
}
|
||||
|
||||
this.scoreTxts[seat] = this.add.text(pos.x, pos.y - CARD_H / 2 - 11, '', {
|
||||
|
|
@ -187,8 +240,6 @@ export default class BlackjackGame extends Phaser.Scene {
|
|||
}).setOrigin(0.5).setDepth(D.ui);
|
||||
|
||||
// Portraits
|
||||
const px = pos.portraitX ?? pos.x;
|
||||
const py = pos.portraitY ?? (seat === 0 ? pos.y : pos.y - CARD_H / 2 - pos.portraitR - 68);
|
||||
if (seat === 0) {
|
||||
this.portraits[seat] = createPlayerPortrait(this, px, py, pos.portraitR, D.ui, 'BlackjackGame');
|
||||
} else {
|
||||
|
|
@ -408,7 +459,7 @@ export default class BlackjackGame extends Phaser.Scene {
|
|||
// ── Render ────────────────────────────────────────────────────────────────
|
||||
renderAll() {
|
||||
this.renderDealer();
|
||||
for (let seat = 0; seat < 5; seat++) {
|
||||
for (let seat = 0; seat < SEAT_POS.length; seat++) {
|
||||
const p = this.gs.players[seat];
|
||||
if (!p.active) continue;
|
||||
this.renderSeatCards(seat);
|
||||
|
|
@ -505,7 +556,7 @@ export default class BlackjackGame extends Phaser.Scene {
|
|||
renderBetAreas() {
|
||||
for (const g of Object.values(this.betGraphics)) g.destroy();
|
||||
this.betGraphics = {};
|
||||
for (let seat = 0; seat < 5; seat++) {
|
||||
for (let seat = 0; seat < SEAT_POS.length; seat++) {
|
||||
const p = this.gs.players[seat];
|
||||
if (!p.active || p.bet === 0) continue;
|
||||
const { x: betX, y: betY } = this.betCirclePos(seat);
|
||||
|
|
@ -534,16 +585,17 @@ export default class BlackjackGame extends Phaser.Scene {
|
|||
this.betGraphics = {};
|
||||
}
|
||||
|
||||
// Returns the {x,y} of a seat's bet chip circle, placed 40% of the way
|
||||
// from the portrait toward the table centre (CX, 540).
|
||||
// Returns the {x,y} of a seat's bet chip circle, placed just inboard of the
|
||||
// card spot (toward the table centre at CX, 540).
|
||||
betCirclePos(seat) {
|
||||
const pos = SEAT_POS[seat];
|
||||
const px = pos.portraitX ?? pos.x;
|
||||
const py = pos.portraitY ?? (seat === 0 ? pos.y : pos.y - CARD_H / 2 - pos.portraitR - 68);
|
||||
const t = 0.40;
|
||||
if (pos.betX !== undefined && pos.betY !== undefined) {
|
||||
return { x: pos.betX, y: pos.betY };
|
||||
}
|
||||
const t = 0.22;
|
||||
return {
|
||||
x: Math.round(px + t * (CX - px)),
|
||||
y: Math.round(py + t * (540 - py)),
|
||||
x: Math.round(pos.x + t * (CX - pos.x)),
|
||||
y: Math.round(pos.y + t * (540 - pos.y)),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -637,7 +689,7 @@ export default class BlackjackGame extends Phaser.Scene {
|
|||
this.gs = applyBet(this.gs, 0, this.pendingBet);
|
||||
|
||||
// AI bets
|
||||
for (let seat = 1; seat <= 4; seat++) {
|
||||
for (let seat = 1; seat < SEAT_POS.length; seat++) {
|
||||
const p = this.gs.players[seat];
|
||||
if (!p.active) continue;
|
||||
const bet = chooseBet(p);
|
||||
|
|
@ -956,7 +1008,7 @@ export default class BlackjackGame extends Phaser.Scene {
|
|||
|
||||
offerInsurance() {
|
||||
// AI declines immediately
|
||||
for (let seat = 1; seat <= 4; seat++) {
|
||||
for (let seat = 1; seat < SEAT_POS.length; seat++) {
|
||||
const p = this.gs.players[seat];
|
||||
if (p.active) this.gs = applyInsurance(this.gs, seat, false);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -85,7 +85,7 @@ export function createInitialState(opponents, chips) {
|
|||
},
|
||||
];
|
||||
|
||||
for (let i = 0; i < 4; i++) {
|
||||
for (let i = 0; i < 6; i++) {
|
||||
const opp = opponents[i] ?? null;
|
||||
players.push({
|
||||
seat: i + 1, name: opp?.name ?? '', isHuman: false, active: !!opp, opponent: opp,
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ export function getGame(slug) {
|
|||
// Built-in catalog so the menu has something to show.
|
||||
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: 'blackjack', name: 'Blackjack', category: 'casino', cardGame: true, minPlayers: 1, maxPlayers: 5, minOpponents: 0, maxOpponents: 4 });
|
||||
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: '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 });
|
||||
|
|
|
|||
Loading…
Reference in New Issue