```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_X = CX;
|
||||||
const DEALER_Y = 420; // ~1/3 down the table ellipse (top ≈190, bottom ≈890)
|
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 = [
|
const SEAT_POS = [
|
||||||
{ x: CX, y: 860, portraitR: 72, portraitX: CX - 230, portraitY: 860 }, // Human
|
{ x: CX, y: 860, portraitR: 72, portraitX: CX - 230, portraitY: 860, betX: 1110, betY: 770 }, // 0 Human (bottom centre)
|
||||||
{ x: 380, y: 775, portraitR: 58, portraitX: 200, portraitY: 650 }, // Opp 1 bottom-left (outside ellipse)
|
{ x: 1490, y: 745, portraitR: 58, portraitX: 1600, portraitY: 800 }, // 1 right-bottom
|
||||||
{ x: 150, y: 490, portraitR: 58 }, // Opp 2 left (already outside ellipse)
|
{ x: 430, y: 745, portraitR: 58, portraitX: 320, portraitY: 800 }, // 2 left-bottom
|
||||||
{ x: 1770, y: 490, portraitR: 58 }, // Opp 3 right (already outside ellipse)
|
{ x: 1670, y: 595, portraitR: 58, portraitX: 1805, portraitY: 610 }, // 3 right-mid
|
||||||
{ x: 1540, y: 775, portraitR: 58, portraitX: 1720, portraitY: 650 }, // Opp 4 bottom-right (outside ellipse)
|
{ 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 };
|
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.add.rectangle(CX, GAME_HEIGHT / 2, GAME_WIDTH, GAME_HEIGHT, COLORS.bg).setDepth(D.bg);
|
||||||
this.buildPlayfield();
|
this.buildPlayfield();
|
||||||
this.buildTable();
|
this.buildTable();
|
||||||
|
this.buildTableMarkings();
|
||||||
this.buildDealerArea();
|
this.buildDealerArea();
|
||||||
this.buildSeats();
|
this.buildSeats();
|
||||||
this.buildBettingUI();
|
this.buildBettingUI();
|
||||||
|
|
@ -116,16 +122,77 @@ export default class BlackjackGame extends Phaser.Scene {
|
||||||
g.strokeEllipse(CX, 540, 1440, 640);
|
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 ───────────────────────────────────────────────────────────
|
// ── Dealer area ───────────────────────────────────────────────────────────
|
||||||
buildDealerArea() {
|
buildDealerArea() {
|
||||||
this.add.text(CX, 60, 'Blackjack', {
|
this.add.text(CX, 60, 'Blackjack', {
|
||||||
fontFamily: 'Righteous', fontSize: '52px', color: COLORS.textHex,
|
fontFamily: 'Righteous', fontSize: '52px', color: COLORS.textHex,
|
||||||
}).setOrigin(0.5).setDepth(D.ui);
|
}).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, '', {
|
this.dealerScoreTxt = this.add.text(CX, DEALER_Y - CARD_H / 2 - 22, '', {
|
||||||
fontFamily: '"Julius Sans One"', fontSize: '22px', color: COLORS.textHex,
|
fontFamily: '"Julius Sans One"', fontSize: '22px', color: COLORS.textHex,
|
||||||
}).setOrigin(0.5).setDepth(D.ui);
|
}).setOrigin(0.5).setDepth(D.ui);
|
||||||
|
|
@ -133,7 +200,7 @@ export default class BlackjackGame extends Phaser.Scene {
|
||||||
|
|
||||||
// ── Seat labels & portraits ───────────────────────────────────────────────
|
// ── Seat labels & portraits ───────────────────────────────────────────────
|
||||||
buildSeats() {
|
buildSeats() {
|
||||||
for (let seat = 0; seat < 5; seat++) {
|
for (let seat = 0; seat < SEAT_POS.length; seat++) {
|
||||||
const pos = SEAT_POS[seat];
|
const pos = SEAT_POS[seat];
|
||||||
const player = seat === 0
|
const player = seat === 0
|
||||||
? { name: 'You', isHuman: true, active: true }
|
? { name: 'You', isHuman: true, active: true }
|
||||||
|
|
@ -141,45 +208,31 @@ export default class BlackjackGame extends Phaser.Scene {
|
||||||
|
|
||||||
if (!player.active) continue;
|
if (!player.active) continue;
|
||||||
|
|
||||||
let nameX, nameY, chipX, chipY, labelAnchorX;
|
// Portrait centre
|
||||||
if (seat === 0) {
|
const px = pos.portraitX ?? pos.x;
|
||||||
// Human: labels to the left of portrait, right-aligned
|
const py = pos.portraitY ?? (seat === 0 ? pos.y : pos.y - CARD_H / 2 - pos.portraitR - 68);
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
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,
|
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,
|
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
|
// Semi-transparent backing behind both text lines, depth below portrait images/videos
|
||||||
{
|
{
|
||||||
const maxTW = Math.max(this.nameTxts[seat].width, 130);
|
const maxTW = Math.max(this.nameTxts[seat].width, 130);
|
||||||
const rectW = maxTW + 20;
|
const rectW = maxTW + 20;
|
||||||
const rectH = (chipY - nameY) + 36;
|
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);
|
const bg = this.add.graphics().setDepth(D.ui - 1);
|
||||||
bg.fillStyle(0x000000, 0.60);
|
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, '', {
|
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);
|
}).setOrigin(0.5).setDepth(D.ui);
|
||||||
|
|
||||||
// Portraits
|
// 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) {
|
if (seat === 0) {
|
||||||
this.portraits[seat] = createPlayerPortrait(this, px, py, pos.portraitR, D.ui, 'BlackjackGame');
|
this.portraits[seat] = createPlayerPortrait(this, px, py, pos.portraitR, D.ui, 'BlackjackGame');
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -408,7 +459,7 @@ export default class BlackjackGame extends Phaser.Scene {
|
||||||
// ── Render ────────────────────────────────────────────────────────────────
|
// ── Render ────────────────────────────────────────────────────────────────
|
||||||
renderAll() {
|
renderAll() {
|
||||||
this.renderDealer();
|
this.renderDealer();
|
||||||
for (let seat = 0; seat < 5; seat++) {
|
for (let seat = 0; seat < SEAT_POS.length; seat++) {
|
||||||
const p = this.gs.players[seat];
|
const p = this.gs.players[seat];
|
||||||
if (!p.active) continue;
|
if (!p.active) continue;
|
||||||
this.renderSeatCards(seat);
|
this.renderSeatCards(seat);
|
||||||
|
|
@ -505,7 +556,7 @@ export default class BlackjackGame extends Phaser.Scene {
|
||||||
renderBetAreas() {
|
renderBetAreas() {
|
||||||
for (const g of Object.values(this.betGraphics)) g.destroy();
|
for (const g of Object.values(this.betGraphics)) g.destroy();
|
||||||
this.betGraphics = {};
|
this.betGraphics = {};
|
||||||
for (let seat = 0; seat < 5; seat++) {
|
for (let seat = 0; seat < SEAT_POS.length; seat++) {
|
||||||
const p = this.gs.players[seat];
|
const p = this.gs.players[seat];
|
||||||
if (!p.active || p.bet === 0) continue;
|
if (!p.active || p.bet === 0) continue;
|
||||||
const { x: betX, y: betY } = this.betCirclePos(seat);
|
const { x: betX, y: betY } = this.betCirclePos(seat);
|
||||||
|
|
@ -534,16 +585,17 @@ export default class BlackjackGame extends Phaser.Scene {
|
||||||
this.betGraphics = {};
|
this.betGraphics = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Returns the {x,y} of a seat's bet chip circle, placed 40% of the way
|
// Returns the {x,y} of a seat's bet chip circle, placed just inboard of the
|
||||||
// from the portrait toward the table centre (CX, 540).
|
// card spot (toward the table centre at CX, 540).
|
||||||
betCirclePos(seat) {
|
betCirclePos(seat) {
|
||||||
const pos = SEAT_POS[seat];
|
const pos = SEAT_POS[seat];
|
||||||
const px = pos.portraitX ?? pos.x;
|
if (pos.betX !== undefined && pos.betY !== undefined) {
|
||||||
const py = pos.portraitY ?? (seat === 0 ? pos.y : pos.y - CARD_H / 2 - pos.portraitR - 68);
|
return { x: pos.betX, y: pos.betY };
|
||||||
const t = 0.40;
|
}
|
||||||
|
const t = 0.22;
|
||||||
return {
|
return {
|
||||||
x: Math.round(px + t * (CX - px)),
|
x: Math.round(pos.x + t * (CX - pos.x)),
|
||||||
y: Math.round(py + t * (540 - py)),
|
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);
|
this.gs = applyBet(this.gs, 0, this.pendingBet);
|
||||||
|
|
||||||
// AI bets
|
// AI bets
|
||||||
for (let seat = 1; seat <= 4; seat++) {
|
for (let seat = 1; seat < SEAT_POS.length; seat++) {
|
||||||
const p = this.gs.players[seat];
|
const p = this.gs.players[seat];
|
||||||
if (!p.active) continue;
|
if (!p.active) continue;
|
||||||
const bet = chooseBet(p);
|
const bet = chooseBet(p);
|
||||||
|
|
@ -956,7 +1008,7 @@ export default class BlackjackGame extends Phaser.Scene {
|
||||||
|
|
||||||
offerInsurance() {
|
offerInsurance() {
|
||||||
// AI declines immediately
|
// 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];
|
const p = this.gs.players[seat];
|
||||||
if (p.active) this.gs = applyInsurance(this.gs, seat, false);
|
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;
|
const opp = opponents[i] ?? null;
|
||||||
players.push({
|
players.push({
|
||||||
seat: i + 1, name: opp?.name ?? '', isHuman: false, active: !!opp, opponent: opp,
|
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.
|
// 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: '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: 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: '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: '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 });
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue