// Pure Craps rules. No Phaser dependency. // // Scope: Pass Line, Don't Pass (bar 12), Field, Place (4·5·6·8·9·10), // Come / Don't Come, and free Odds behind Pass/Don't and Come/Don't Come. // // A "round" is one shooter sequence. The shooter rolls a come-out; if a point // is set the game enters the point phase and the shooter keeps rolling until // the point repeats (pass wins) or a 7 shows (seven-out → shooter rotates). // // State is treated as immutable: every mutating helper returns a fresh object. export const POINT_NUMBERS = [4, 5, 6, 8, 9, 10]; // Bet types and where they live on the layout. export const BET = { PASS: 'pass', DONT_PASS: 'dontpass', FIELD: 'field', PLACE: 'place', COME: 'come', DONT_COME: 'dontcome', }; // Chip denominations offered in the UI. export const CHIP_AMOUNTS = [5, 25, 50, 100]; // ─── Setup ─────────────────────────────────────────────────────────────────── export function createInitialState(opponents = [], humanChips = 2000, humanName = 'You') { const players = [ { name: humanName, isAI: false, avatar: null, chips: humanChips, bets: [], lastDelta: 0 }, ...opponents.map((o) => ({ name: o.name ?? o.id ?? 'Bot', isAI: true, avatar: o, chips: 2000, bets: [], lastDelta: 0, })), ]; return { players, shooterIndex: 0, phase: 'betting', // betting → comeout → point → (betting | gameover) point: null, dice: [1, 1], rollCount: 0, _nextBetId: 1, }; } export function cloneState(state) { return JSON.parse(JSON.stringify(state)); } // ─── Bet placement ─────────────────────────────────────────────────────────-- // Which base bet types may be opened right now. export function legalBetTypes(state) { const out = new Set([BET.FIELD, BET.PLACE]); if (state.point === null) { out.add(BET.PASS); out.add(BET.DONT_PASS); } else { out.add(BET.COME); out.add(BET.DONT_COME); } return out; } // A line/come bet that can still receive odds (point/come-point established). export function oddsEligibleBets(player, state) { return player.bets.filter((b) => { if (b.type === BET.PASS || b.type === BET.DONT_PASS) return state.point !== null; if (b.type === BET.COME || b.type === BET.DONT_COME) return b.comePoint != null; return false; }); } export function maxOddsFor(bet) { // Keep it simple: allow up to 3× the flat bet behind the line. return bet.amount * 3; } // Place a flat bet. Stake is removed from the player's bankroll immediately. export function placeBet(state, playerIndex, { type, number = null, amount }) { const player = state.players[playerIndex]; if (amount <= 0 || amount > player.chips) return state; if (!legalBetTypes(state).has(type)) return state; if (type === BET.PLACE && !POINT_NUMBERS.includes(number)) return state; const s = cloneState(state); const p = s.players[playerIndex]; // Stack onto an existing matching flat bet rather than duplicating it. const existing = p.bets.find((b) => b.type === type && (type === BET.PLACE ? b.number === number : b.comePoint == null && b.number == null)); if (existing && type !== BET.COME && type !== BET.DONT_COME) { existing.amount += amount; } else { p.bets.push({ id: s._nextBetId++, type, number: type === BET.PLACE ? number : null, comePoint: null, amount, oddsAmount: 0, // Place bets sit "off" during a come-out; everything else is working. working: type === BET.PLACE ? state.point !== null : true, }); } p.chips -= amount; return s; } // Add odds behind an eligible line/come bet. export function addOdds(state, playerIndex, betId, amount) { const s = cloneState(state); const p = s.players[playerIndex]; const bet = p.bets.find((b) => b.id === betId); if (!bet) return state; const cap = maxOddsFor(bet) - bet.oddsAmount; const add = Math.min(amount, cap, p.chips); if (add <= 0) return state; bet.oddsAmount += add; p.chips -= add; return s; } // Return un-resolved stakes to the bankroll (only sensible during a betting // window). Used by "Clear" and when leaving the table. export function refundBets(state, playerIndex, predicate = () => true) { const s = cloneState(state); const p = s.players[playerIndex]; const kept = []; for (const b of p.bets) { if (predicate(b)) p.chips += b.amount + b.oddsAmount; else kept.push(b); } p.bets = kept; return s; } // ─── Dice ──────────────────────────────────────────────────────────────────── export function rollDice(state) { const s = cloneState(state); s.dice = [1 + Math.floor(Math.random() * 6), 1 + Math.floor(Math.random() * 6)]; s.rollCount += 1; return s; } // Force specific dice (testing / scripted demos). export function setDice(state, dice) { const s = cloneState(state); s.dice = [...dice]; return s; } // ─── Payout tables (return winnings only, not the returned stake) ───────────── function floor(n) { return Math.floor(n); } function passOddsWin(amount, number) { if (number === 4 || number === 10) return amount * 2; // 2:1 if (number === 5 || number === 9) return floor(amount * 3 / 2); // 3:2 if (number === 6 || number === 8) return floor(amount * 6 / 5); // 6:5 return 0; } function dontOddsWin(amount, number) { if (number === 4 || number === 10) return floor(amount / 2); // 1:2 if (number === 5 || number === 9) return floor(amount * 2 / 3); // 2:3 if (number === 6 || number === 8) return floor(amount * 5 / 6); // 5:6 return 0; } function placeWin(amount, number) { if (number === 4 || number === 10) return floor(amount * 9 / 5); if (number === 5 || number === 9) return floor(amount * 7 / 5); if (number === 6 || number === 8) return floor(amount * 7 / 6); return 0; } function fieldWin(amount, total) { if (total === 2) return amount * 2; // 2 pays double if (total === 12) return amount * 3; // 12 pays triple return amount; // 3,4,9,10,11 pay 1:1 } // ─── Resolution ────────────────────────────────────────────────────────────-- // Resolve every bet against the dice already set on `state`. // Returns { state, total, payouts, pointEstablished, pointMade, sevenOut, newComeOut }. // // payouts entry: { playerIndex, betType, number, result, amount, delta } // result ∈ 'win' | 'lose' | 'push'; delta is the net bankroll change for the bet. export function resolveRoll(state) { const s = cloneState(state); const total = s.dice[0] + s.dice[1]; const point = s.point; const isComeOut = point === null; const payouts = []; let pointEstablished = null; let pointMade = false; let sevenOut = false; for (let pi = 0; pi < s.players.length; pi++) { const p = s.players[pi]; const kept = []; for (const bet of p.bets) { const resolved = resolveBet(bet, { total, point, isComeOut }); if (resolved.result === 'open') { // Bet survives (possibly mutated, e.g. a come bet that travelled). kept.push(resolved.bet ?? bet); continue; } // Credit returned stake + winnings; delta is net of the staked amount. const stake = bet.amount + bet.oddsAmount; let credited = 0; if (resolved.result === 'win') credited = stake + resolved.winnings; else if (resolved.result === 'push') credited = stake; p.chips += credited; const delta = credited - stake; p.lastDelta += delta; payouts.push({ playerIndex: pi, betType: bet.type, number: bet.number ?? bet.comePoint ?? null, result: resolved.result, amount: stake, delta, }); // A winning Place bet stays up and working; everything else clears. if (resolved.keep) kept.push(resolved.bet ?? bet); } p.bets = kept; } // Phase / point transitions driven by the line outcome. if (isComeOut) { if (POINT_NUMBERS.includes(total)) { pointEstablished = total; s.point = total; s.phase = 'point'; // Place bets switch on once a point exists. for (const p of s.players) for (const b of p.bets) { if (b.type === BET.PLACE) b.working = true; } } else { s.phase = 'comeout'; } } else if (total === point) { pointMade = true; s.point = null; s.phase = 'comeout'; for (const p of s.players) for (const b of p.bets) { if (b.type === BET.PLACE) b.working = false; } } else if (total === 7) { sevenOut = true; s.point = null; s.phase = 'comeout'; for (const p of s.players) for (const b of p.bets) { if (b.type === BET.PLACE) b.working = false; } } const newComeOut = pointMade || sevenOut; if (sevenOut) { s.shooterIndex = (s.shooterIndex + 1) % s.players.length; } return { state: s, total, payouts, pointEstablished, pointMade, sevenOut, newComeOut }; } // Resolve a single bet. Returns one of: // { result:'open', bet } — unresolved (bet may have mutated) // { result:'win', winnings, keep } — paid; keep=true leaves it on the felt // { result:'lose' } | { result:'push' } function resolveBet(bet, { total, point, isComeOut }) { switch (bet.type) { case BET.FIELD: { if ([2, 3, 4, 9, 10, 11, 12].includes(total)) { return { result: 'win', winnings: fieldWin(bet.amount, total) }; } return { result: 'lose' }; } case BET.PASS: { if (isComeOut) { if (total === 7 || total === 11) return { result: 'win', winnings: bet.amount }; if (total === 2 || total === 3 || total === 12) return { result: 'lose' }; return { result: 'open' }; // point established, ride it } if (total === point) { const winnings = bet.amount + passOddsWin(bet.oddsAmount, point); return { result: 'win', winnings }; } if (total === 7) return { result: 'lose' }; return { result: 'open' }; } case BET.DONT_PASS: { if (isComeOut) { if (total === 2 || total === 3) return { result: 'win', winnings: bet.amount }; if (total === 12) return { result: 'push' }; // bar 12 if (total === 7 || total === 11) return { result: 'lose' }; return { result: 'open' }; } if (total === 7) { const winnings = bet.amount + dontOddsWin(bet.oddsAmount, point); return { result: 'win', winnings }; } if (total === point) return { result: 'lose' }; return { result: 'open' }; } case BET.PLACE: { if (!bet.working) return { result: 'open' }; if (total === bet.number) { return { result: 'win', winnings: placeWin(bet.amount, bet.number), keep: true }; } if (total === 7) return { result: 'lose' }; return { result: 'open' }; } case BET.COME: case BET.DONT_COME: { const isDont = bet.type === BET.DONT_COME; if (bet.comePoint == null) { // This roll is the come bet's own come-out. if (total === 7 || total === 11) return isDont ? { result: 'lose' } : { result: 'win', winnings: bet.amount }; if (total === 2 || total === 3) return isDont ? { result: 'win', winnings: bet.amount } : { result: 'lose' }; if (total === 12) return isDont ? { result: 'push' } : { result: 'lose' }; // Travels to its number. return { result: 'open', bet: { ...bet, comePoint: total } }; } // Come point established. if (total === bet.comePoint) { if (isDont) return { result: 'lose' }; return { result: 'win', winnings: bet.amount + passOddsWin(bet.oddsAmount, bet.comePoint) }; } if (total === 7) { if (isDont) return { result: 'win', winnings: bet.amount + dontOddsWin(bet.oddsAmount, bet.comePoint) }; return { result: 'lose' }; } return { result: 'open' }; } default: return { result: 'open' }; } } // ─── Round / table helpers ─────────────────────────────────────────────────-- export function clearLastDeltas(state) { const s = cloneState(state); for (const p of s.players) p.lastDelta = 0; return s; } export function totalAtRisk(player) { return player.bets.reduce((sum, b) => sum + b.amount + b.oddsAmount, 0); } export function hasActiveBets(player) { return player.bets.length > 0; } // Human net result vs. the chips they walked in with, for match history. export function getNetResult(player, startingChips) { if (player.chips > startingChips) return 'win'; if (player.chips < startingChips) return 'loss'; return 'draw'; }