feat: add Baccarat casino game with AI opponents and cinematic reveals
- Implement core game logic in BaccaratLogic.js including shoe management, hand evaluation, and betting resolution with standard rules (5% banker commission, 8-to-1 tie payout). - Create BaccaratGame.js Phaser scene with full UI: betting panel, chip selection, seat portraits, and animated card dealing. - Add BaccaratAI.js for opponent bet sizing and type selection (weighted toward Banker). - Introduce "Equation Ribbon" reveal sequence that walks through each card's value, sums them, and performs a mod-10 reduction with visual flair. - Register Baccarat in the server game registry and frontend scene dispatch.
This commit is contained in:
parent
e46cd1cc2f
commit
78466530a3
|
|
@ -0,0 +1,18 @@
|
|||
import { BET } from './BaccaratLogic.js';
|
||||
|
||||
// ─── Bet sizing ───────────────────────────────────────────────────────────────
|
||||
export function chooseBet(player) {
|
||||
const options = [5, 10, 15, 25];
|
||||
const raw = options[Math.floor(Math.random() * options.length)];
|
||||
return Math.min(raw, player.chips, 100);
|
||||
}
|
||||
|
||||
// ─── Bet-type selection ─────────────────────────────────────────────────────
|
||||
// Weighted toward Banker (the lower house-edge wager), with the occasional
|
||||
// long-shot Tie bet for flavor.
|
||||
export function chooseBetType() {
|
||||
const r = Math.random();
|
||||
if (r < 0.45) return BET.BANKER;
|
||||
if (r < 0.85) return BET.PLAYER;
|
||||
return BET.TIE;
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,197 @@
|
|||
// Baccarat (Punto Banco) pure game logic — no Phaser dependencies
|
||||
|
||||
import { SUITS, RANKS } from '../cards/Deck.js';
|
||||
|
||||
// ─── Bet types ──────────────────────────────────────────────────────────────
|
||||
export const BET = { PLAYER: 'player', BANKER: 'banker', TIE: 'tie' };
|
||||
|
||||
// Banker win commission and tie payout (standard table odds).
|
||||
const BANKER_COMMISSION = 0.05;
|
||||
const TIE_PAYOUT = 8;
|
||||
|
||||
// ─── Shoe ─────────────────────────────────────────────────────────────────────
|
||||
// Baccarat point values: A=1, 2–9 face value, 10/J/Q/K = 0.
|
||||
const RANK_VALUE = {
|
||||
'2':2,'3':3,'4':4,'5':5,'6':6,'7':7,'8':8,'9':9,'T':0,'J':0,'Q':0,'K':0,'A':1,
|
||||
};
|
||||
|
||||
function makeCard(rank, suit) {
|
||||
return {
|
||||
rank, suit,
|
||||
value: RANK_VALUE[rank],
|
||||
label: rank === 'T' ? '10' : rank,
|
||||
isRed: suit === 'h' || suit === 'd',
|
||||
suitSymbol: { s:'♠', h:'♥', d:'♦', c:'♣' }[suit],
|
||||
key: `${rank}${suit}`,
|
||||
};
|
||||
}
|
||||
|
||||
export function buildShoe(numDecks = 8) {
|
||||
const cards = [];
|
||||
for (let d = 0; d < numDecks; d++) {
|
||||
for (const suit of SUITS) {
|
||||
for (const rank of RANKS) {
|
||||
cards.push(makeCard(rank, suit));
|
||||
}
|
||||
}
|
||||
}
|
||||
for (let i = cards.length - 1; i > 0; i--) {
|
||||
const j = Math.floor(Math.random() * (i + 1));
|
||||
[cards[i], cards[j]] = [cards[j], cards[i]];
|
||||
}
|
||||
return cards;
|
||||
}
|
||||
|
||||
// ─── Hand evaluation ──────────────────────────────────────────────────────────
|
||||
// A baccarat hand's value is the sum of its cards modulo 10.
|
||||
export function handValue(cards) {
|
||||
return cards.reduce((sum, c) => sum + c.value, 0) % 10;
|
||||
}
|
||||
|
||||
// A "natural" is an 8 or 9 on the first two cards — no further draws.
|
||||
export function isNatural(cards) {
|
||||
return cards.length === 2 && (handValue(cards) === 8 || handValue(cards) === 9);
|
||||
}
|
||||
|
||||
// ─── State creation ───────────────────────────────────────────────────────────
|
||||
export function createInitialState(opponents, chips) {
|
||||
const players = [
|
||||
{
|
||||
seat: 0, name: 'You', isHuman: true, active: true, opponent: null,
|
||||
chips, bet: 0, betType: null,
|
||||
result: null, chipsWon: 0,
|
||||
},
|
||||
];
|
||||
|
||||
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,
|
||||
chips: 1000, bet: 0, betType: null,
|
||||
result: null, chipsWon: 0,
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
phase: 'betting',
|
||||
playerHand: [],
|
||||
bankerHand: [],
|
||||
outcome: null, // 'player' | 'banker' | 'tie'
|
||||
players,
|
||||
roundNumber: 0,
|
||||
};
|
||||
}
|
||||
|
||||
// ─── Round start ──────────────────────────────────────────────────────────────
|
||||
export function prepareRound(gs) {
|
||||
const players = gs.players.map(p => ({
|
||||
...p,
|
||||
bet: 0, betType: null,
|
||||
result: null, chipsWon: 0,
|
||||
}));
|
||||
return {
|
||||
...gs,
|
||||
phase: 'betting',
|
||||
playerHand: [],
|
||||
bankerHand: [],
|
||||
outcome: null,
|
||||
players,
|
||||
roundNumber: gs.roundNumber + 1,
|
||||
};
|
||||
}
|
||||
|
||||
export function applyBet(gs, seat, amount, betType) {
|
||||
const players = gs.players.map(p =>
|
||||
p.seat === seat ? { ...p, bet: amount, betType } : p
|
||||
);
|
||||
return { ...gs, players };
|
||||
}
|
||||
|
||||
// ─── Deal a coup ────────────────────────────────────────────────────────────
|
||||
// Deals the Player and Banker hands per the fixed drawing rules. The shoe is
|
||||
// mutated. Returns new gs with both hands and the outcome populated. The deal
|
||||
// order popped from the shoe is P1, B1, P2, B2, then any third cards.
|
||||
export function dealCoup(gs, shoe) {
|
||||
const playerHand = [];
|
||||
const bankerHand = [];
|
||||
|
||||
playerHand.push(shoe.pop()); // P1
|
||||
bankerHand.push(shoe.pop()); // B1
|
||||
playerHand.push(shoe.pop()); // P2
|
||||
bankerHand.push(shoe.pop()); // B2
|
||||
|
||||
const playerNatural = isNatural(playerHand);
|
||||
const bankerNatural = isNatural(bankerHand);
|
||||
|
||||
if (!playerNatural && !bankerNatural) {
|
||||
let playerThird = null;
|
||||
|
||||
// Player draws on 0–5, stands on 6–7.
|
||||
if (handValue(playerHand) <= 5) {
|
||||
const card = shoe.pop();
|
||||
playerHand.push(card);
|
||||
playerThird = card.value;
|
||||
}
|
||||
|
||||
// Banker drawing rules.
|
||||
const bankerTotal = handValue(bankerHand);
|
||||
let bankerDraws;
|
||||
if (playerThird === null) {
|
||||
// Player stood: banker plays like the player (draw on 0–5).
|
||||
bankerDraws = bankerTotal <= 5;
|
||||
} else if (bankerTotal <= 2) {
|
||||
bankerDraws = true;
|
||||
} else if (bankerTotal === 3) {
|
||||
bankerDraws = playerThird !== 8;
|
||||
} else if (bankerTotal === 4) {
|
||||
bankerDraws = playerThird >= 2 && playerThird <= 7;
|
||||
} else if (bankerTotal === 5) {
|
||||
bankerDraws = playerThird >= 4 && playerThird <= 7;
|
||||
} else if (bankerTotal === 6) {
|
||||
bankerDraws = playerThird >= 6 && playerThird <= 7;
|
||||
} else {
|
||||
bankerDraws = false; // 7 stands
|
||||
}
|
||||
|
||||
if (bankerDraws) bankerHand.push(shoe.pop());
|
||||
}
|
||||
|
||||
const playerTotal = handValue(playerHand);
|
||||
const bankerTotal = handValue(bankerHand);
|
||||
const outcome = playerTotal > bankerTotal ? 'player'
|
||||
: bankerTotal > playerTotal ? 'banker'
|
||||
: 'tie';
|
||||
|
||||
return { ...gs, phase: 'resolved', playerHand, bankerHand, outcome };
|
||||
}
|
||||
|
||||
// ─── Resolve bets ─────────────────────────────────────────────────────────────
|
||||
// Returns the net chip delta for a single wager given the coup outcome.
|
||||
export function betPayout(betType, bet, outcome) {
|
||||
if (betType === BET.PLAYER) {
|
||||
if (outcome === 'player') return bet;
|
||||
if (outcome === 'tie') return 0; // bet returned
|
||||
return -bet;
|
||||
}
|
||||
if (betType === BET.BANKER) {
|
||||
if (outcome === 'banker') return bet - Math.floor(bet * BANKER_COMMISSION);
|
||||
if (outcome === 'tie') return 0; // bet returned
|
||||
return -bet;
|
||||
}
|
||||
// Tie
|
||||
if (outcome === 'tie') return bet * TIE_PAYOUT;
|
||||
return -bet;
|
||||
}
|
||||
|
||||
export function resolveRound(gs) {
|
||||
const players = gs.players.map(p => {
|
||||
if (!p.active || !p.betType || p.bet === 0) return p;
|
||||
const net = betPayout(p.betType, p.bet, gs.outcome);
|
||||
let result;
|
||||
if (net > 0) result = 'win';
|
||||
else if (net < 0) result = 'lose';
|
||||
else result = 'push';
|
||||
return { ...p, chips: p.chips + net, chipsWon: net, result };
|
||||
});
|
||||
return { ...gs, phase: 'resolved', players };
|
||||
}
|
||||
|
|
@ -27,6 +27,7 @@ import HeartsGame from './games/hearts/HeartsGame.js';
|
|||
import CatanGame from './games/catan/CatanGame.js';
|
||||
import NertsGame from './games/nerts/NertsGame.js';
|
||||
import BingoGame from './games/bingo/BingoGame.js';
|
||||
import BaccaratGame from './games/baccarat/BaccaratGame.js';
|
||||
|
||||
const config = {
|
||||
type: Phaser.AUTO,
|
||||
|
|
@ -67,6 +68,7 @@ const config = {
|
|||
CatanGame,
|
||||
NertsGame,
|
||||
BingoGame,
|
||||
BaccaratGame,
|
||||
],
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ export default class GameRoomScene extends Phaser.Scene {
|
|||
}
|
||||
|
||||
create() {
|
||||
const slugDispatch = { backgammon: 'Backgammon', holdem: 'HoldemGame', blackjack: 'BlackjackGame', parchisi: 'ParchisiGame', yatzi: 'YatziGame', skipbo: 'SkipBoGame', phase10: 'Phase10Game', chinesecheckers: 'ChineseCheckersGame', gofish: 'GoFishGame', uno: 'UnoGame', craps: 'CrapsGame', roulette: 'RouletteGame', mexicantrain: 'MexicanTrainGame', hearts: 'HeartsGame', catan: 'CatanGame', nerts: 'NertsGame', bingo: 'BingoGame' };
|
||||
const slugDispatch = { backgammon: 'Backgammon', holdem: 'HoldemGame', blackjack: 'BlackjackGame', parchisi: 'ParchisiGame', yatzi: 'YatziGame', skipbo: 'SkipBoGame', phase10: 'Phase10Game', chinesecheckers: 'ChineseCheckersGame', gofish: 'GoFishGame', uno: 'UnoGame', craps: 'CrapsGame', roulette: 'RouletteGame', mexicantrain: 'MexicanTrainGame', hearts: 'HeartsGame', catan: 'CatanGame', nerts: 'NertsGame', bingo: 'BingoGame', baccarat: 'BaccaratGame' };
|
||||
if (slugDispatch[this.game.slug]) {
|
||||
this.scene.start(slugDispatch[this.game.slug], {
|
||||
game: this.game,
|
||||
|
|
|
|||
|
|
@ -40,3 +40,4 @@ registerGame({ slug: 'hearts', name: 'Hearts', category: 'cards', cardGame: true
|
|||
registerGame({ slug: 'catan', name: 'Settlers of Catan', category: 'tabletop', cardGame: true, minPlayers: 3, maxPlayers: 4, minOpponents: 2, maxOpponents: 3 });
|
||||
registerGame({ slug: 'nerts', name: 'Nerts', category: 'cards', cardGame: true, minPlayers: 2, maxPlayers: 4, minOpponents: 1, maxOpponents: 3 });
|
||||
registerGame({ slug: 'bingo', name: 'Bingo', category: 'casino', minPlayers: 2, maxPlayers: 11, minOpponents: 1, maxOpponents: 10 });
|
||||
registerGame({ slug: 'baccarat', name: 'Baccarat', category: 'casino', cardGame: true, minPlayers: 2, maxPlayers: 7, minOpponents: 1, maxOpponents: 6 });
|
||||
|
|
|
|||
Loading…
Reference in New Issue