feat: add Blackjack game and update game registration
- Introduce BlackjackGame as a new playable game in the client - Register Blackjack in the server game registry with updated player limits (1-5 players, 0-4 opponents) - Wire up Blackjack in main.js and GameRoomScene for routing - Fix OpponentSelectScene to handle games with minOpponents=0 - Remove incomplete ParchisiLogic.js implementation
This commit is contained in:
parent
1d28f27a7d
commit
712956a841
|
|
@ -0,0 +1,57 @@
|
||||||
|
import { handValue, canDouble, canSplit } from './BlackjackLogic.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);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Action selection (basic strategy) ───────────────────────────────────────
|
||||||
|
|
||||||
|
export function chooseAction(player, dealerUpcard) {
|
||||||
|
const hand = player.activeHand === 1 ? player.hand2 : player.hand;
|
||||||
|
const { score, soft } = handValue(hand);
|
||||||
|
const upVal = Math.min(dealerUpcard.value, 10); // treat 10/J/Q/K as 10
|
||||||
|
|
||||||
|
// Split decisions
|
||||||
|
if (canSplit(player)) {
|
||||||
|
const rank = player.hand[0].rank;
|
||||||
|
if (rank === 'A' || rank === '8') return 'split';
|
||||||
|
if (rank === '5' || rank === 'T' || rank === 'J' || rank === 'Q' || rank === 'K') {
|
||||||
|
// never split 5s or 10-values
|
||||||
|
} else if (upVal >= 2 && upVal <= 6) {
|
||||||
|
return 'split';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Double decisions
|
||||||
|
if (canDouble(player)) {
|
||||||
|
if (!soft) {
|
||||||
|
if (score === 11) return 'double';
|
||||||
|
if (score === 10 && upVal <= 9) return 'double';
|
||||||
|
if (score === 9 && upVal >= 3 && upVal <= 6) return 'double';
|
||||||
|
} else {
|
||||||
|
// Soft doubles
|
||||||
|
if ((score === 17 || score === 18) && upVal >= 3 && upVal <= 6) return 'double';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Soft totals
|
||||||
|
if (soft) {
|
||||||
|
if (score >= 19) return 'stand';
|
||||||
|
if (score === 18) return upVal >= 9 ? 'hit' : 'stand';
|
||||||
|
return 'hit';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hard totals
|
||||||
|
if (score >= 17) return 'stand';
|
||||||
|
if (score >= 13 && upVal <= 6) return 'stand';
|
||||||
|
if (score === 12 && upVal >= 4 && upVal <= 6) return 'stand';
|
||||||
|
return 'hit';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function chooseInsurance() {
|
||||||
|
return false; // AI always declines insurance
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,351 @@
|
||||||
|
// Blackjack pure game logic — no Phaser dependencies
|
||||||
|
|
||||||
|
import { SUITS, RANKS } from '../cards/Deck.js';
|
||||||
|
|
||||||
|
// ─── Shoe ─────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
const RANK_VALUE = {
|
||||||
|
'2':2,'3':3,'4':4,'5':5,'6':6,'7':7,'8':8,'9':9,'T':10,'J':10,'Q':10,'K':10,'A':11,
|
||||||
|
};
|
||||||
|
|
||||||
|
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 = 6) {
|
||||||
|
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 ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export function handValue(cards) {
|
||||||
|
let total = 0;
|
||||||
|
let aces = 0;
|
||||||
|
for (const c of cards) {
|
||||||
|
if (c.rank === 'A') { aces++; total += 11; }
|
||||||
|
else total += Math.min(c.value, 10);
|
||||||
|
}
|
||||||
|
while (total > 21 && aces > 0) { total -= 10; aces--; }
|
||||||
|
return { score: total, soft: aces > 0 && total <= 21 };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isBlackjack(hand) {
|
||||||
|
return hand.length === 2 && handValue(hand).score === 21;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isBust(hand) {
|
||||||
|
return handValue(hand).score > 21;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function canDouble(player) {
|
||||||
|
const hand = player.activeHand === 1 ? player.hand2 : player.hand;
|
||||||
|
return (hand ?? []).length === 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function canSplit(player) {
|
||||||
|
return (
|
||||||
|
player.hand.length === 2 &&
|
||||||
|
player.hand[0].rank === player.hand[1].rank &&
|
||||||
|
!player.hand2
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── State creation ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export function createInitialState(opponents, chips) {
|
||||||
|
const players = [
|
||||||
|
{
|
||||||
|
seat: 0, name: 'You', isHuman: true, active: true, opponent: null,
|
||||||
|
chips, bet: 0, bet2: null,
|
||||||
|
hand: [], hand2: null,
|
||||||
|
doubled: false, doubled2: false,
|
||||||
|
insuranceBet: 0,
|
||||||
|
activeHand: 0,
|
||||||
|
status: 'waiting', status2: null,
|
||||||
|
result: null, result2: null, chipsWon: 0,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
for (let i = 0; i < 4; i++) {
|
||||||
|
const opp = opponents[i] ?? null;
|
||||||
|
players.push({
|
||||||
|
seat: i + 1, name: opp?.name ?? '', isHuman: false, active: !!opp, opponent: opp,
|
||||||
|
chips: 1000, bet: 0, bet2: null,
|
||||||
|
hand: [], hand2: null,
|
||||||
|
doubled: false, doubled2: false,
|
||||||
|
insuranceBet: 0,
|
||||||
|
activeHand: 0,
|
||||||
|
status: 'waiting', status2: null,
|
||||||
|
result: null, result2: null, chipsWon: 0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
phase: 'betting',
|
||||||
|
dealer: { hand: [], revealed: false },
|
||||||
|
players,
|
||||||
|
currentSeat: 0,
|
||||||
|
insurancePending: false,
|
||||||
|
roundNumber: 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Round start ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export function prepareRound(gs) {
|
||||||
|
const players = gs.players.map(p => ({
|
||||||
|
...p,
|
||||||
|
bet: 0, bet2: null,
|
||||||
|
hand: [], hand2: null,
|
||||||
|
doubled: false, doubled2: false,
|
||||||
|
insuranceBet: 0,
|
||||||
|
activeHand: 0,
|
||||||
|
status: p.active ? 'waiting' : 'waiting',
|
||||||
|
status2: null,
|
||||||
|
result: null, result2: null, chipsWon: 0,
|
||||||
|
}));
|
||||||
|
return {
|
||||||
|
...gs,
|
||||||
|
phase: 'betting',
|
||||||
|
dealer: { hand: [], revealed: false },
|
||||||
|
players,
|
||||||
|
currentSeat: 0,
|
||||||
|
insurancePending: false,
|
||||||
|
roundNumber: gs.roundNumber + 1,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function applyBet(gs, seat, amount) {
|
||||||
|
const players = gs.players.map(p => p.seat === seat ? { ...p, bet: amount } : p);
|
||||||
|
return { ...gs, players };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deal all cards into state from the shoe (shoe is mutated).
|
||||||
|
// Returns new gs with hands populated, phase set appropriately.
|
||||||
|
export function dealAllCards(gs, shoe) {
|
||||||
|
const players = gs.players.map(p => ({ ...p }));
|
||||||
|
const dealer = { hand: [], revealed: false };
|
||||||
|
|
||||||
|
// Standard deal order: each player card 1, dealer upcard, each player card 2, dealer hole
|
||||||
|
const activePlayers = players.filter(p => p.active);
|
||||||
|
|
||||||
|
// Round 1
|
||||||
|
for (const p of activePlayers) p.hand.push(shoe.pop());
|
||||||
|
dealer.hand.push(shoe.pop()); // upcard (index 0)
|
||||||
|
|
||||||
|
// Round 2
|
||||||
|
for (const p of activePlayers) p.hand.push(shoe.pop());
|
||||||
|
dealer.hand.push(shoe.pop()); // hole card (index 1)
|
||||||
|
|
||||||
|
// Detect player blackjacks immediately
|
||||||
|
for (const p of activePlayers) {
|
||||||
|
if (isBlackjack(p.hand)) p.status = 'blackjack';
|
||||||
|
}
|
||||||
|
|
||||||
|
const firstActiveSeat = activePlayers[0]?.seat ?? 0;
|
||||||
|
const dealerUpcard = dealer.hand[0];
|
||||||
|
const insurancePending = dealerUpcard.rank === 'A';
|
||||||
|
|
||||||
|
return {
|
||||||
|
...gs,
|
||||||
|
phase: insurancePending ? 'insurance' : 'player_turn',
|
||||||
|
dealer,
|
||||||
|
players,
|
||||||
|
currentSeat: firstActiveSeat,
|
||||||
|
insurancePending,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Insurance ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export function applyInsurance(gs, seat, accept) {
|
||||||
|
const players = gs.players.map(p => {
|
||||||
|
if (p.seat !== seat) return p;
|
||||||
|
const insuranceBet = accept ? Math.floor(p.bet / 2) : 0;
|
||||||
|
return { ...p, insuranceBet };
|
||||||
|
});
|
||||||
|
return { ...gs, players };
|
||||||
|
}
|
||||||
|
|
||||||
|
// After all insurance decisions: check dealer blackjack
|
||||||
|
export function resolveInsurance(gs) {
|
||||||
|
const dealerBJ = isBlackjack(gs.dealer.hand);
|
||||||
|
const players = gs.players.map(p => {
|
||||||
|
if (!p.active || p.insuranceBet === 0) return p;
|
||||||
|
// Insurance bet: pays 2:1 if dealer has BJ, else lost
|
||||||
|
const insuranceResult = dealerBJ ? p.insuranceBet * 2 : -p.insuranceBet;
|
||||||
|
return { ...p, chipsWon: (p.chipsWon ?? 0) + insuranceResult };
|
||||||
|
});
|
||||||
|
return { ...gs, players, insurancePending: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Player actions ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export function applyHit(gs, seat, shoe) {
|
||||||
|
const players = gs.players.map(p => {
|
||||||
|
if (p.seat !== seat) return p;
|
||||||
|
const isH2 = p.activeHand === 1;
|
||||||
|
const newCard = shoe.pop();
|
||||||
|
if (isH2) {
|
||||||
|
const hand2 = [...p.hand2, newCard];
|
||||||
|
const bust = isBust(hand2);
|
||||||
|
return { ...p, hand2, status2: bust ? 'bust' : 'playing' };
|
||||||
|
} else {
|
||||||
|
const hand = [...p.hand, newCard];
|
||||||
|
const bust = isBust(hand);
|
||||||
|
return { ...p, hand, status: bust ? 'bust' : 'playing' };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return { ...gs, players };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function applyStand(gs, seat) {
|
||||||
|
const players = gs.players.map(p => {
|
||||||
|
if (p.seat !== seat) return p;
|
||||||
|
if (p.activeHand === 1) return { ...p, status2: 'standing' };
|
||||||
|
// If split exists and hand 1 not yet played, move to hand 2
|
||||||
|
if (p.hand2 !== null && p.status2 === null) return { ...p, status: 'standing', activeHand: 1 };
|
||||||
|
return { ...p, status: 'standing' };
|
||||||
|
});
|
||||||
|
return { ...gs, players };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function applyDouble(gs, seat, shoe) {
|
||||||
|
const players = gs.players.map(p => {
|
||||||
|
if (p.seat !== seat) return p;
|
||||||
|
const isH2 = p.activeHand === 1;
|
||||||
|
const newCard = shoe.pop();
|
||||||
|
if (isH2) {
|
||||||
|
const hand2 = [...p.hand2, newCard];
|
||||||
|
const bust = isBust(hand2);
|
||||||
|
return { ...p, hand2, bet2: p.bet2 * 2, doubled2: true, status2: bust ? 'bust' : 'standing' };
|
||||||
|
} else {
|
||||||
|
const hand = [...p.hand, newCard];
|
||||||
|
const bust = isBust(hand);
|
||||||
|
const doubled = true;
|
||||||
|
// If split exists and not yet played hand 2
|
||||||
|
if (p.hand2 !== null && p.status2 === null) {
|
||||||
|
return { ...p, hand, bet: p.bet * 2, doubled, status: bust ? 'bust' : 'standing', activeHand: 1 };
|
||||||
|
}
|
||||||
|
return { ...p, hand, bet: p.bet * 2, doubled, status: bust ? 'bust' : 'standing' };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return { ...gs, players };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function applySplit(gs, seat, shoe) {
|
||||||
|
const players = gs.players.map(p => {
|
||||||
|
if (p.seat !== seat) return p;
|
||||||
|
const [c1, c2] = p.hand;
|
||||||
|
const hand = [c1, shoe.pop()];
|
||||||
|
const hand2 = [c2, shoe.pop()];
|
||||||
|
const isAceSplit = c1.rank === 'A';
|
||||||
|
// Ace splits: each hand gets one card only, immediately stands
|
||||||
|
return {
|
||||||
|
...p,
|
||||||
|
hand, hand2,
|
||||||
|
bet2: p.bet,
|
||||||
|
doubled: false, doubled2: false,
|
||||||
|
status: isAceSplit ? 'standing' : 'playing',
|
||||||
|
status2: isAceSplit ? 'standing' : null,
|
||||||
|
activeHand: 0,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
return { ...gs, players };
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Dealer turn ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
// Returns new gs with dealer hand fully played out. shoe is mutated.
|
||||||
|
export function runDealerTurn(gs, shoe) {
|
||||||
|
const dealer = { ...gs.dealer, revealed: true };
|
||||||
|
while (true) {
|
||||||
|
const { score } = handValue(dealer.hand);
|
||||||
|
if (score >= 17) break;
|
||||||
|
dealer.hand = [...dealer.hand, shoe.pop()];
|
||||||
|
}
|
||||||
|
return { ...gs, phase: 'resolved', dealer };
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Resolve round ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export function resolveRound(gs) {
|
||||||
|
const { score: dealerScore } = handValue(gs.dealer.hand);
|
||||||
|
const dealerBJ = isBlackjack(gs.dealer.hand);
|
||||||
|
const dealerBust = dealerScore > 21;
|
||||||
|
|
||||||
|
const players = gs.players.map(p => {
|
||||||
|
if (!p.active) return p;
|
||||||
|
|
||||||
|
function resolveHand(hand, bet, status, doubled) {
|
||||||
|
if (!hand || hand.length === 0) return { result: null, net: 0 };
|
||||||
|
const { score: playerScore } = handValue(hand);
|
||||||
|
const playerBJ = isBlackjack(hand);
|
||||||
|
|
||||||
|
if (status === 'bust') return { result: 'lose', net: -bet };
|
||||||
|
if (playerBJ && dealerBJ) return { result: 'push', net: 0 };
|
||||||
|
if (playerBJ) return { result: 'blackjack', net: Math.floor(bet * 1.5) }; // 3:2
|
||||||
|
if (dealerBJ) return { result: 'lose', net: -bet };
|
||||||
|
if (dealerBust) return { result: 'win', net: bet };
|
||||||
|
if (playerScore > dealerScore) return { result: 'win', net: bet };
|
||||||
|
if (playerScore === dealerScore) return { result: 'push', net: 0 };
|
||||||
|
return { result: 'lose', net: -bet };
|
||||||
|
}
|
||||||
|
|
||||||
|
const h1 = resolveHand(p.hand, p.bet, p.status, p.doubled);
|
||||||
|
const h2 = p.hand2 ? resolveHand(p.hand2, p.bet2, p.status2, p.doubled2) : { result: null, net: 0 };
|
||||||
|
|
||||||
|
const totalNet = h1.net + h2.net + (p.chipsWon ?? 0); // chipsWon may have insurance delta
|
||||||
|
const newChips = p.chips + totalNet;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...p,
|
||||||
|
chips: newChips,
|
||||||
|
result: h1.result,
|
||||||
|
result2: h2.result,
|
||||||
|
chipsWon: totalNet,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return { ...gs, phase: 'resolved', players };
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Turn advancement ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
// Returns the seat number of the next player who still needs to act, or null if done.
|
||||||
|
export function nextActiveSeat(gs) {
|
||||||
|
const activePlayers = gs.players.filter(p => p.active);
|
||||||
|
const currentIdx = activePlayers.findIndex(p => p.seat === gs.currentSeat);
|
||||||
|
for (let i = currentIdx + 1; i < activePlayers.length; i++) {
|
||||||
|
const p = activePlayers[i];
|
||||||
|
if (p.status === 'playing' || p.status === 'waiting') return p.seat;
|
||||||
|
}
|
||||||
|
return null; // dealer's turn
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isPlayerDone(player) {
|
||||||
|
// Hand 1 is done
|
||||||
|
const h1done = ['standing','bust','blackjack','done'].includes(player.status);
|
||||||
|
// Hand 2 (if split): must also be done
|
||||||
|
const h2done = player.hand2 === null || ['standing','bust','blackjack','done'].includes(player.status2);
|
||||||
|
return h1done && h2done;
|
||||||
|
}
|
||||||
|
|
@ -1,496 +0,0 @@
|
||||||
// Parchisi pure game logic — no Phaser dependencies
|
|
||||||
|
|
||||||
export const PLAYER_COLORS = ['yellow', 'green', 'red', 'blue'];
|
|
||||||
|
|
||||||
// 52-square outer path as [row, col] pairs, traced clockwise starting at Yellow entry (8,0)
|
|
||||||
// LEFT arm bottom → left col up → top → junction(6,6) → TOP arm left col → top → right → junction(6,8)
|
|
||||||
// → RIGHT arm top → right col → bottom → junction(8,8) → BOTTOM arm right → bottom → left → junction(8,6) → close
|
|
||||||
export const OUTER_PATH = [
|
|
||||||
// Left arm bottom row (cols 0→5, row 8) — Yellow entry at index 0
|
|
||||||
[8,0],[8,1],[8,2],[8,3],[8,4],[8,5],
|
|
||||||
// Left col up (col 5, rows 7→6) — then junction
|
|
||||||
[7,5],[6,5],
|
|
||||||
// junction
|
|
||||||
[6,6],
|
|
||||||
// Top arm left col (col 6, rows 5→0)
|
|
||||||
[5,6],[4,6],[3,6],[2,6],[1,6],[0,6],
|
|
||||||
// top row (row 0, cols 7→8)
|
|
||||||
[0,7],[0,8],
|
|
||||||
// Top arm right col (col 8, rows 1→5)
|
|
||||||
[1,8],[2,8],[3,8],[4,8],[5,8],
|
|
||||||
// junction
|
|
||||||
[6,8],
|
|
||||||
// Right arm top row (row 6, cols 9→14) — Red entry at index 26 → wait, let me recount
|
|
||||||
// Actually need to recount entries. Let me redo this carefully.
|
|
||||||
// After junction (6,8): right arm top row cols 9→13, then col 14 rows 6→8
|
|
||||||
[6,9],[6,10],[6,11],[6,12],[6,13],
|
|
||||||
// right col (col 14, rows 7→8)
|
|
||||||
[7,14],[8,14],
|
|
||||||
// junction
|
|
||||||
[8,13],[8,12],[8,11],[8,10],[8,9],
|
|
||||||
// junction
|
|
||||||
[8,8],
|
|
||||||
// Bottom arm right col (col 8, rows 9→14)
|
|
||||||
[9,8],[10,8],[11,8],[12,8],[13,8],[14,8],
|
|
||||||
// bottom row (row 14, cols 7→6)
|
|
||||||
[14,7],[14,6],
|
|
||||||
// Bottom arm left col (col 6, rows 13→9)
|
|
||||||
[13,6],[12,6],[11,6],[10,6],[9,6],
|
|
||||||
// junction
|
|
||||||
[8,6],
|
|
||||||
// Left arm bottom row back to start (already at [8,0])
|
|
||||||
// But we need rows 8 cols back... no: left arm row 8 goes col 0→5, we started there.
|
|
||||||
// Actually from (8,6) we go: left arm col 5 down? No...
|
|
||||||
// From junction (8,6): we need to get back to (8,0) via row 8 going left?
|
|
||||||
// But row 8 cols 0-5 is already covered (indices 0-5).
|
|
||||||
// The path is circular so index 51 should neighbor index 0.
|
|
||||||
// Let me redo the path properly below.
|
|
||||||
];
|
|
||||||
|
|
||||||
// The above rough draft needs to be carefully constructed. Let me redo it clean:
|
|
||||||
// Trace clockwise from (8,0):
|
|
||||||
// 1. Left arm: row 8 from col 0→5 (6 cells, indices 0-5)
|
|
||||||
// 2. Left col up: col 5 rows 7→6 (2 cells, indices 6-7) — midpoint safe at index 6
|
|
||||||
// 3. Junction: (6,6) (index 8) -- but wait, (6,5) is NOT a junction, (6,6) is
|
|
||||||
// Actually left col up to row 6: col 5, rows 7,6 → lands at (6,5) then turn
|
|
||||||
// Wait: from (8,5) we go up the col: (7,5),(6,5) then into center arm at row 6
|
|
||||||
// junction is (6,6) - but we need to reach it: (6,5)→(6,6) yes
|
|
||||||
// Let me re-examine: The left arm spans rows 6-8, cols 0-5 (3 rows, 6 cols)
|
|
||||||
// Path through left arm:
|
|
||||||
// Enter at (8,0), go right to (8,5) — bottom row of arm
|
|
||||||
// Go up col 5: (7,5),(6,5)
|
|
||||||
// Junction corner (6,6)
|
|
||||||
// Into top arm
|
|
||||||
|
|
||||||
// This gives indices:
|
|
||||||
// 0:(8,0) 1:(8,1) 2:(8,2) 3:(8,3) 4:(8,4) 5:(8,5) 6:(7,5) 7:(6,5) 8:(6,6)
|
|
||||||
// Top arm: rows 0-5, cols 6-8
|
|
||||||
// From (6,6): go up col 6: (5,6)(4,6)(3,6)(2,6)(1,6)(0,6) — 6 cells
|
|
||||||
// Go right on row 0: (0,7)(0,8) — 2 cells
|
|
||||||
// Go down col 8: (1,8)(2,8)(3,8)(4,8)(5,8) — 5 cells (skipping (0,8) already done)
|
|
||||||
// Junction (6,8)
|
|
||||||
// Indices: 9:(5,6) 10:(4,6) 11:(3,6) 12:(2,6) 13:(1,6) 14:(0,6) — Green entry at 13?
|
|
||||||
// Wait Green entry should be the first cell of the top arm entering.
|
|
||||||
// Green enters at (0,6) which would be index 14...
|
|
||||||
// But plan says Green entry at absolute index 13 → (1,6)?
|
|
||||||
// Let me recount: 0-8 = 9 cells so far. Top arm:
|
|
||||||
// 9:(5,6) 10:(4,6) 11:(3,6) 12:(2,6) 13:(1,6) 14:(0,6) 15:(0,7) 16:(0,8)
|
|
||||||
// 17:(1,8) 18:(2,8) 19:(3,8) 20:(4,8) 21:(5,8) 22:(6,8)
|
|
||||||
// Right arm: rows 6-8, cols 9-14
|
|
||||||
// From (6,8): enter at (6,9) going right: (6,9)(6,10)(6,11)(6,12)(6,13)(6,14)
|
|
||||||
// Go down col 14: (7,14)(8,14)
|
|
||||||
// Junction (8,13)? No... junction is (8,8).
|
|
||||||
// Hmm the junctions in the plan were (6,6),(6,8),(8,6),(8,8).
|
|
||||||
// After going down col 14 to (8,14), we go left on row 8: (8,13)(8,12)(8,11)(8,10)(8,9)
|
|
||||||
// Then junction (8,8)
|
|
||||||
// Red entry should be at index 26 = (6,14)? Let me count:
|
|
||||||
// 23:(6,9) 24:(6,10) 25:(6,11) 26:(6,12)...
|
|
||||||
// Hmm, Red entry at 26 should be when the path first enters the right arm from the top.
|
|
||||||
// From the plan: Red entry at absolute index 26 → need to recount carefully.
|
|
||||||
|
|
||||||
// I'll just define the correct 52-cell path here directly:
|
|
||||||
|
|
||||||
const _OUTER_PATH_CORRECT = [
|
|
||||||
// Yellow entry (index 0): left arm bottom row L→R
|
|
||||||
[8,0],[8,1],[8,2],[8,3],[8,4],[8,5],
|
|
||||||
// up left arm right col
|
|
||||||
[7,5],[6,5],
|
|
||||||
// corner junction into top arm
|
|
||||||
[6,6],
|
|
||||||
// top arm: up col 6
|
|
||||||
[5,6],[4,6],[3,6],[2,6],[1,6],
|
|
||||||
// Green entry (index 13): (0,6) — top-left of top arm
|
|
||||||
[0,6],
|
|
||||||
// top arm: across row 0
|
|
||||||
[0,7],[0,8],
|
|
||||||
// top arm: down col 8
|
|
||||||
[1,8],[2,8],[3,8],[4,8],[5,8],
|
|
||||||
// corner junction into right arm
|
|
||||||
[6,8],
|
|
||||||
// right arm: across row 6 R direction
|
|
||||||
[6,9],[6,10],[6,11],[6,12],[6,13],
|
|
||||||
// Red entry (index 26): top-right entry = (6,14)? count: 0-5=6, 6-7=2, 8=1, 9-13=5, 14=1, 15-16=2, 17-21=5, 22=1, 23-27=5
|
|
||||||
// Let me recount: indices 0-5 (6), 6-7 (2), 8 (1) = 9 so far
|
|
||||||
// 9-13 (5), 14 (1), 15-16 (2), 17-21 (5), 22 (1) = 14 more = 23 total
|
|
||||||
// 23-27 (5) = right arm row...
|
|
||||||
// Hmm Red at 26: from index 23=(6,9): 23,24,25,26=(6,12)? That doesn't feel right for top-right corner.
|
|
||||||
// Plan says Red: 26 → (6,14). So (6,14) should be at index 26.
|
|
||||||
// Count again: 0:(8,0)..5:(8,5) = 6; 6:(7,5),7:(6,5) = 2; 8:(6,6) = 1; total=9
|
|
||||||
// 9:(5,6)..13:(1,6) = 5; 14:(0,6) = 1; total=15
|
|
||||||
// 15:(0,7),16:(0,8) = 2; total=17
|
|
||||||
// 17:(1,8)..21:(5,8) = 5; total=22
|
|
||||||
// 22:(6,8) = 1; total=23
|
|
||||||
// 23:(6,9)..27:(6,13) = 5; total=28
|
|
||||||
// So (6,14) would be index 28, not 26.
|
|
||||||
// The safe square at index 26 is the midpoint of the right arm approach.
|
|
||||||
// Let me check: the plan says "Safe squares at absolute path indices: [0, 6, 13, 19, 26, 32, 39, 45]"
|
|
||||||
// index 6=(7,5), 13=(1,6), 19=(3,8) - midpoint of top arm
|
|
||||||
// For right arm midpoint at 26: right arm goes 23-29? midpoint would be 26=(6,11)
|
|
||||||
// Red entry at absolute 26 must mean (6,11)? But plan says (6,14)...
|
|
||||||
// Let me re-examine the plan more carefully.
|
|
||||||
// "Entry indices: Red: 26 → (6,14)"
|
|
||||||
// So either my path is off by a few, or the junction isn't counted.
|
|
||||||
// Let me try removing the junction squares from the path (only arm squares):
|
|
||||||
// Without (6,6) as a separate cell (absorbed into transition):
|
|
||||||
// 0-5: left bottom, 6-7: left col up, 8-12: top arm up col6, 13:(0,6) Green, 14-15: top row, 16-20: down col8
|
|
||||||
// 21-25: right arm row6, 26:(6,14)? That works if right arm starts at 21!
|
|
||||||
// So: no (6,8) junction either — junction is just the corner cell (6,8) = last of col8?
|
|
||||||
// Actually (5,8) is last of col 8 down...
|
|
||||||
// Let me try: top arm down col 8 is (1,8)-(5,8) = 5 cells (17-21), then (6,8) = junction = 22
|
|
||||||
// Then right arm: (6,9)-(6,13) = 5 cells (23-27), (6,14) = 28. Still 28.
|
|
||||||
|
|
||||||
// Alternative: top arm only goes (0,6)(0,7)(0,8) across, then immediately down to (5,8),
|
|
||||||
// and the "left col up" doesn't include (6,5) — maybe just (7,5) and then (6,6) directly?
|
|
||||||
// i.e. (8,5)→(7,5)→(6,6) skipping (6,5)?
|
|
||||||
// That would be: 0-5 (6), 6:(7,5), 7:(6,6), 8-12:(5→1,6), 13:(0,6)=Green, 14:(0,7), 15:(0,8),
|
|
||||||
// 16-20:(1-5,8), 21:(6,8), 22-26:(6,9-13), 27:(6,14)... still 27 not 26.
|
|
||||||
|
|
||||||
// Another option: right arm entry (Red) is at first cell entering the right arm from top junction:
|
|
||||||
// If (6,8) is index 21, then (6,9) = 22... Red at 26 would be (6,13). Hmm.
|
|
||||||
|
|
||||||
// OR: the arms are only 2 rows wide instead of 3, meaning the "left col up" piece
|
|
||||||
// is just one cell: (7,5) and the arm is only rows 7-8 at the sides.
|
|
||||||
// No, the board is clearly defined as 3-wide arms (3 cells).
|
|
||||||
|
|
||||||
// Let me just accept that my plan's entry indices may have been approximate, and instead
|
|
||||||
// define the path correctly and compute the correct entry indices from it.
|
|
||||||
// The entry point for each player is where they first enter the outer path from their home base.
|
|
||||||
// Yellow → bottom-left corner → enters at leftmost cell of left arm bottom row = (8,0)
|
|
||||||
// Green → top-left corner → enters at top of top arm = (0,6)
|
|
||||||
// Red → top-right corner → enters at top-right = (6,14)? or (0,8)?
|
|
||||||
// Red home is top-right (rows 0-5, cols 9-14). Red enters the path at...
|
|
||||||
// looking at standard Parchisi: each player enters on the cell nearest their home base.
|
|
||||||
// Red (top-right) → enters at right arm row = (6,14)? or at (0,8)?
|
|
||||||
// Standard Parchisi: Red enters at the top of the right arm.
|
|
||||||
// Right arm is rows 6-8, cols 9-14. Red's home is rows 0-5, cols 9-14.
|
|
||||||
// Red enters path at (6,14) going LEFT then down then left on right arm.
|
|
||||||
// But wait — the path is clockwise. From Red's home (top-right), going clockwise means
|
|
||||||
// entering at the top of the right arm and going DOWN the right col then left.
|
|
||||||
// So Red enters at (6,14) and goes: (6,14)→(7,14)→(8,14)→(8,13)→...→(8,9)→(8,8)→...
|
|
||||||
|
|
||||||
// Let me retrace the full clockwise path:
|
|
||||||
// Standard clockwise from top-left perspective:
|
|
||||||
// Actually "clockwise" depends on orientation. Let me define it by the standard Parchisi board.
|
|
||||||
// In standard Parchisi (viewed from above):
|
|
||||||
// - Yellow starts bottom-left, moves UP the left arm, then across the top, down the right arm, across the bottom
|
|
||||||
// - So Yellow path: (8,0) right to (8,5), up to (6,5), across to (6,6), up col 6 to (0,6), across row 0 to (0,8), down col 8 to (6,8), right to (6,14)...
|
|
||||||
// Wait, that's going LEFT across the bottom, which is counter-clockwise...
|
|
||||||
|
|
||||||
// Let me think of it differently. Standard Parchisi: each player's tokens move in a consistent direction.
|
|
||||||
// Yellow (bottom-left) moves: up the left column → across the top → down the right column → across the bottom → back to start
|
|
||||||
// This is COUNTER-clockwise when viewed from above (standard for most Parchisi implementations).
|
|
||||||
|
|
||||||
// Actually, Parchisi traditionally moves tokens clockwise on the board.
|
|
||||||
// Let me just define a consistent path and make sure all 4 players have proper entry points.
|
|
||||||
|
|
||||||
// DEFINITIVE PATH (clockwise from Yellow's perspective at bottom-left):
|
|
||||||
// Yellow enters at (8,0), moves right along bottom of left arm, then UP the arm's right col to (6,5),
|
|
||||||
// corner (6,6), up the left col of top arm to (0,6)=Green entry, across top to (0,8),
|
|
||||||
// down right col of top arm to (6,8)=corner, right along top of right arm to (6,14)=Red entry,
|
|
||||||
// DOWN the right col to (8,14), left along bottom of right arm to (8,9), corner (8,8),
|
|
||||||
// down left col of bottom arm to (14,8)=Blue entry, left along bottom to (14,6),
|
|
||||||
// up right col of bottom arm to (8,6)=corner, and back.
|
|
||||||
|
|
||||||
// That gives a path. Let me count:
|
|
||||||
[8,14],[8,13],[8,12],[8,11],[8,10],[8,9], // placeholder - will be reordered
|
|
||||||
];
|
|
||||||
|
|
||||||
// ─── Final correct OUTER_PATH ───────────────────────────────────────────────
|
|
||||||
// Traced clockwise. Yellow at index 0, Green at 13, Red at 26, Blue at 39.
|
|
||||||
const OUTER_PATH_FINAL = (() => {
|
|
||||||
const path = [];
|
|
||||||
// Yellow entry (0): left arm, bottom row left→right
|
|
||||||
for (let c = 0; c <= 5; c++) path.push([8, c]); // 0-5
|
|
||||||
// left arm, right col up (row 7→6)
|
|
||||||
path.push([7, 5]); // 6 (safe)
|
|
||||||
// corner into top arm
|
|
||||||
path.push([6, 5]); // 7
|
|
||||||
path.push([6, 6]); // 8
|
|
||||||
// top arm, left col down→up (row 5→1)
|
|
||||||
for (let r = 5; r >= 1; r--) path.push([r, 6]); // 9-13 (13=Green entry)
|
|
||||||
// Green entry (13): (1,6) — first top-arm cell nearest green home
|
|
||||||
// top arm, top row left→right
|
|
||||||
path.push([0, 6]); // 14
|
|
||||||
path.push([0, 7]); // 15
|
|
||||||
path.push([0, 8]); // 16
|
|
||||||
// top arm, right col top→bottom (row 1→5)
|
|
||||||
for (let r = 1; r <= 5; r++) path.push([r, 8]); // 17-21
|
|
||||||
// corner into right arm
|
|
||||||
path.push([6, 8]); // 22
|
|
||||||
path.push([6, 9]); // 23
|
|
||||||
// right arm, top row left→right
|
|
||||||
for (let c = 10; c <= 14; c++) path.push([6, c]); // 24-28 (26=Red entry?)
|
|
||||||
// Hmm still not getting Red at 26. Let me check index 26: 0-5(6) 6(1) 7(1) 8(1) 9-13(5) 14(1) 15(1) 16(1) 17-21(5) 22(1) 23(1) 24-28(5)
|
|
||||||
// index 26 = path[26]. count: 0-5=6, 6=1(7tot), 7=1(8), 8=1(9), 9-13=5(14), 14=1(15), 15=1(16), 16=1(17), 17-21=5(22), 22=1(23), 23=1(24), 24=1(25), 25=1(26)=[6,11]
|
|
||||||
// So Red at 26 = (6,11) which is the middle of the right arm. That's the SAFE square, not entry.
|
|
||||||
// Something is off. Let me try: right arm entry is where Red EXITS their home base.
|
|
||||||
// Red home is top-right (rows 0-5, cols 9-14). They enter the path via... the right col of the top arm?
|
|
||||||
// Or they enter at the right side of the right arm top row?
|
|
||||||
// Standard Parchisi: Red enters at (6,14) = rightmost cell of right arm's top row. = index?
|
|
||||||
// From my count above, [6,14] would be index 28.
|
|
||||||
// The plan may have a different path construction. Let me just use whatever makes the game work,
|
|
||||||
// recalculating the entry indices from the actual path.
|
|
||||||
return path;
|
|
||||||
})();
|
|
||||||
|
|
||||||
// Build the actual path separately and correctly
|
|
||||||
function buildOuterPath() {
|
|
||||||
const p = [];
|
|
||||||
// Index 0 — Yellow entry
|
|
||||||
for (let c = 0; c <= 5; c++) p.push([8, c]); // 0-5
|
|
||||||
p.push([7, 5]); // 6
|
|
||||||
p.push([6, 5]); // 7
|
|
||||||
p.push([6, 6]); // 8
|
|
||||||
for (let r = 5; r >= 1; r--) p.push([r, 6]); // 9-13
|
|
||||||
p.push([0, 6]); // 14
|
|
||||||
p.push([0, 7]); // 15
|
|
||||||
p.push([0, 8]); // 16
|
|
||||||
for (let r = 1; r <= 5; r++) p.push([r, 8]); // 17-21
|
|
||||||
p.push([6, 8]); // 22
|
|
||||||
for (let c = 9; c <= 14; c++) p.push([6, c]); // 23-28
|
|
||||||
p.push([7, 14]); // 29
|
|
||||||
p.push([8, 14]); // 30
|
|
||||||
p.push([8, 13]); // 31
|
|
||||||
for (let c = 12; c >= 9; c--) p.push([8, c]); // 32-35
|
|
||||||
p.push([8, 8]); // 36
|
|
||||||
for (let r = 9; r <= 14; r++) p.push([r, 8]); // 37-42
|
|
||||||
p.push([14, 7]); // 43
|
|
||||||
p.push([14, 6]); // 44
|
|
||||||
for (let r = 13; r >= 9; r--) p.push([r, 6]); // 45-49
|
|
||||||
p.push([8, 6]); // 50
|
|
||||||
p.push([8, 7]); // 51
|
|
||||||
return p;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const OUTER_PATH = buildOuterPath();
|
|
||||||
|
|
||||||
// Entry absolute indices (where each player's token appears when entering from home)
|
|
||||||
// Yellow (seat 0): index 0 = (8,0)
|
|
||||||
// Green (seat 1): index 13 = (1,6) — top of left col going up
|
|
||||||
// Red (seat 2): enters from top-right; by symmetry 26 steps from Yellow = index 26
|
|
||||||
// Index 26 = (6,11) — center of right arm top row (safe square midpoint)
|
|
||||||
// Actually Red enters at right arm, from (0,8) side going right: after (6,8) junction
|
|
||||||
// Red home is top-right, they enter path at first cell adjacent to their home.
|
|
||||||
// In standard Parchisi, Red enters at (6,14) — rightmost of right arm.
|
|
||||||
// But (6,14) = index 28. Let me just use correct actual indices:
|
|
||||||
export const ENTRY_INDEX = [0, 13, 28, 41];
|
|
||||||
// Yellow: 0=(8,0), Green: 13=(1,6), Red: 28=(6,14), Blue: 41=(14,8)
|
|
||||||
|
|
||||||
// Recheck: index 41 from path above:
|
|
||||||
// 0-5(6), 6(7),7(8),8(9), 9-13(14), 14(15),15(16),16(17), 17-21(22), 22(23),
|
|
||||||
// 23-28(29), 29(30),30(31),31(32), 32-35(36), 36(37),
|
|
||||||
// 37-42(43), 43(44),44(45), 45-49(50), 50(51),51(52)
|
|
||||||
// Total = 52 ✓
|
|
||||||
// Index 41 = 37 is (9,8), 38=(10,8),39=(11,8),40=(12,8),41=(13,8)...
|
|
||||||
// Blue should enter at (14,8). Let me recount:
|
|
||||||
// 37=(9,8),38=(10,8),39=(11,8),40=(12,8),41=(13,8),42=(14,8)
|
|
||||||
// So Blue entry (14,8) = index 42. Fix:
|
|
||||||
// And also check Green: 9=(5,6),10=(4,6),11=(3,6),12=(2,6),13=(1,6) ✓
|
|
||||||
// Red: 23=(6,9),24=(6,10),25=(6,11),26=(6,12),27=(6,13),28=(6,14) ✓
|
|
||||||
|
|
||||||
// Corrected entry indices:
|
|
||||||
// Yellow:0, Green:13 ✓, Red:28, Blue:42
|
|
||||||
|
|
||||||
// Verify path length = 52:
|
|
||||||
// 0-5: 6, 6-8: 3, 9-13: 5, 14-16: 3, 17-21: 5, 22-28: 7, 29-31: 3, 32-35: 4, 36: 1, 37-42: 6, 43-44: 2, 45-49: 5, 50-51: 2
|
|
||||||
// = 6+3+5+3+5+7+3+4+1+6+2+5+2 = 52 ✓
|
|
||||||
|
|
||||||
// Safe squares at absolute path indices (entry points + midpoints of each arm)
|
|
||||||
// Entry: 0,13,28,42
|
|
||||||
// Midpoints: left arm midpoint ≈ index 6=(7,5), top arm ≈ 19=(3,8), right arm ≈ 32=(8,13), bottom arm ≈ 45=(13,6)
|
|
||||||
export const SAFE_INDICES = new Set([0, 6, 13, 19, 28, 32, 42, 45]);
|
|
||||||
|
|
||||||
// Home columns — 5 squares each player can uniquely enter (relative pos 52-56)
|
|
||||||
// These are the [row,col] squares leading to center (7,7)
|
|
||||||
export const HOME_COL = [
|
|
||||||
// Yellow (seat 0): row 7, cols 1→5 (moving right toward center)
|
|
||||||
[[7,1],[7,2],[7,3],[7,4],[7,5]],
|
|
||||||
// Green (seat 1): col 7, rows 1→5 (moving down toward center)
|
|
||||||
[[1,7],[2,7],[3,7],[4,7],[5,7]],
|
|
||||||
// Red (seat 2): row 7, cols 13→9 (moving left toward center)
|
|
||||||
[[7,13],[7,12],[7,11],[7,10],[7,9]],
|
|
||||||
// Blue (seat 3): col 7, rows 13→9 (moving up toward center)
|
|
||||||
[[13,7],[12,7],[11,7],[10,7],[9,7]],
|
|
||||||
];
|
|
||||||
|
|
||||||
// Relative position at which a token transitions from outer path to home column
|
|
||||||
// When token.pos === this value, the NEXT move step enters the home column
|
|
||||||
// Yellow: just before completing the circuit (51 steps from entry 0, home col is at col 0-5 left arm)
|
|
||||||
// Actually: Yellow enters home col from (8,6) or (8,7)?
|
|
||||||
// Yellow home col: row 7, cols 1-5 going right. The outer path near Yellow's entry:
|
|
||||||
// At index 50=(8,6), 51=(8,7). Yellow's entry is 0=(8,0).
|
|
||||||
// Relative pos 50 = abs (0+50)%52=50=(8,6), rel 51 = abs 51=(8,7)
|
|
||||||
// After completing 51 relative steps (abs index 51), the next step enters home col at (7,7)?
|
|
||||||
// No: home col starts at (7,1). The token at (8,7) moves up to (7,7)=center, but that's the finish.
|
|
||||||
// The home col should be entered from (8,6) or similar.
|
|
||||||
// Yellow home col squares: (7,1)-(7,5). Entry from outer path via (8,6)?
|
|
||||||
// rel pos 50 = (8,6), next step up = (7,6) which isn't in home col...
|
|
||||||
// Actually Yellow enters home col from left: at abs (8,0) = rel 0, after 51 relative steps = abs (8,7)=rel 51
|
|
||||||
// Then home col entry: after outer path, token moves from (8,0 side) into row 7...
|
|
||||||
// In standard Parchisi Yellow's home col is (7,1)(7,2)(7,3)(7,4)(7,5) entered from the left.
|
|
||||||
// Yellow passes through index 51=(8,7). Next natural step would be (7,7)=center (finish).
|
|
||||||
// But the home col squares (7,1)-(7,5) are BEFORE (7,7).
|
|
||||||
// Yellow needs to enter home col from (7,0) which isn't on the outer path.
|
|
||||||
// OR: Yellow's home col entry is from (8,6) turning up into the left arm of row 7.
|
|
||||||
// Outer path index 50=(8,6). After rel pos 50, token moves from (8,6) UP to (7,6) then along row 7?
|
|
||||||
// (7,6) is not in home col... (7,5) is the first home col square.
|
|
||||||
// This means HOME_COL_ENTRY_POS[0] = 50 (token at rel 50=(8,6), next step goes into home col).
|
|
||||||
|
|
||||||
// Let me define HOME_COL_ENTRY_POS as the last OUTER PATH relative position BEFORE entering home col:
|
|
||||||
// After moving past this position, if steps go beyond it, they enter the home column.
|
|
||||||
// Yellow (0): rel pos 50 = (8,6) → home col via (8,6)→(7,5)... but these aren't adjacent!
|
|
||||||
// (8,6) to (7,5) is diagonal. This is wrong.
|
|
||||||
|
|
||||||
// OK let me think about this differently.
|
|
||||||
// In standard Parchisi, the home column is directly in front of each player's home base.
|
|
||||||
// Yellow's home is bottom-left. Their home column is row 7, cols 1-5 (leftmost non-home col).
|
|
||||||
// Yellow travels counter-clockwise: starts at (8,0), goes right, up, across, down, and completes
|
|
||||||
// nearly a full circuit, arriving at (8,6). From (8,6) the token turns and enters the home column
|
|
||||||
// by going UP: (7,6)→(7,5)? No, that's still wrong directionally.
|
|
||||||
|
|
||||||
// I think the home col for Yellow should actually be col 1, rows 8→7→... no.
|
|
||||||
// Let me look at this from a standard board description:
|
|
||||||
// Yellow home col: (7,1),(7,2),(7,3),(7,4),(7,5) — these are in the LEFT ARM of the cross.
|
|
||||||
// Left arm spans rows 6-8, cols 0-5. Row 7 in the left arm.
|
|
||||||
// Yellow's home col is the MIDDLE ROW of the left arm, going from col 1 to col 5 (toward center).
|
|
||||||
// To enter: Yellow must travel almost fully around the board, approaching from the right side of the left arm.
|
|
||||||
// After outer path index 50=(8,6) and 51=(8,7): wait that puts us in the center area.
|
|
||||||
//
|
|
||||||
// I think I need to restructure the outer path to NOT include row 7 of the center (8,7).
|
|
||||||
// After (8,6) (index 50), the path should go to (8,7) is wrong — that's the junction area.
|
|
||||||
//
|
|
||||||
// Let me reconsider the path ending. After junction (8,6) from the bottom arm,
|
|
||||||
// what's left before Yellow's entry at (8,0)?
|
|
||||||
// The path needs to close: from (8,6) back to (8,0).
|
|
||||||
// Row 8, col 6→5→4→3→2→1→0. But (8,5) is already index 5, and (8,0) is index 0.
|
|
||||||
// So the path is NOT a simple row traversal — it must go AROUND.
|
|
||||||
// Yellow enters at (8,0) heading RIGHT. After nearly a full circuit, they approach from the left,
|
|
||||||
// which means (8,0) is approached FROM (8,1) going left, i.e., from the loop closing.
|
|
||||||
// This means the segment after junction (8,6) goes: col 5 in row 8 going left.
|
|
||||||
// But we need: after (8,6) go to... LEFT ARM'S MIDDLE ROW (7,6)?
|
|
||||||
// No wait. From the junction (8,6), going toward Yellow's home means going into the LEFT ARM via the top row (row 6).
|
|
||||||
// From (8,6) → (7,6)? No, from junction at row 8 col 6, moving into left arm means going UP along col 5: no...
|
|
||||||
//
|
|
||||||
// I'm overcomplicating this. Let me look at it from first principles:
|
|
||||||
// The outer path is a closed loop. The left arm occupies rows 6-8, cols 0-5.
|
|
||||||
// The bottom row of the left arm (row 8, cols 0-5) is where Yellow enters.
|
|
||||||
// The TOP row of the left arm (row 6, cols 0-5) connects to the center column junction.
|
|
||||||
// The MIDDLE row of the left arm (row 7, cols 0-5) is Yellow's HOME COLUMN (for arriving tokens).
|
|
||||||
//
|
|
||||||
// The outer path uses rows 8 and 6 of the left arm (not row 7).
|
|
||||||
// Yellow tokens travel: row 8 left→right, then up the right column (col 5), then into the center
|
|
||||||
// via row 6... but wait, they've already traversed the left arm going right on row 8.
|
|
||||||
// After nearly a full circuit, Yellow approaches the left arm from the CENTER side:
|
|
||||||
// Coming from the bottom arm via junction (8,6), the path must enter the left arm from the RIGHT.
|
|
||||||
// But row 8 of left arm is already the ENTRY row (heading right at start).
|
|
||||||
// So the closing segment of the path (after Blue's arm) should use ROW 6 of the left arm going LEFT:
|
|
||||||
// (8,6) junction → (7,6)? → hmm.
|
|
||||||
//
|
|
||||||
// Actually in standard boards the outer path uses the OUTER EDGE of each arm:
|
|
||||||
// Left arm: goes along row 8 (bottom edge, left→right) and row 6 (top edge, right→left) going different directions.
|
|
||||||
// So the path enters from (8,0) going right to (8,5), then goes up and eventually loops back via row 6 (cols 5→0).
|
|
||||||
// Row 6 would be the "return" path through the left arm.
|
|
||||||
// The home column (row 7, cols 1-5) is in the MIDDLE and only accessible by turning off from the outer path.
|
|
||||||
//
|
|
||||||
// So the correct path structure for the left arm:
|
|
||||||
// GOING (start): row 8, cols 0→5 (Yellow enters here)
|
|
||||||
// After nearly full circuit RETURNING: row 6, cols 5→0 (but this is NOT the outer path for Yellow —
|
|
||||||
// these are only traversed when nearly done)
|
|
||||||
// Yellow's home col entry: when they reach col 5 in the "return" segment, they turn into row 7.
|
|
||||||
//
|
|
||||||
// So let me rebuild: after the bottom arm, instead of (8,6)→(8,7) I should have:
|
|
||||||
// junction (8,6) → row 6, cols 5→1 → then home col entry (row 7)
|
|
||||||
// Wait, the junction from bottom arm leads to LEFT ARM TOP ROW (row 6), not (8,7).
|
|
||||||
//
|
|
||||||
// Let me redo the FULL path completely:
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─── DEFINITIVE IMPLEMENTATION ───────────────────────────────────────────────
|
|
||||||
|
|
||||||
// Outer path: 56 squares (standard Parchisi has 52 outer + 4 junction/corner squares)
|
|
||||||
// Actually let me just hard-code the correct path based on standard Parchisi topology:
|
|
||||||
|
|
||||||
const _PATH = [];
|
|
||||||
|
|
||||||
// Yellow entry = index 0 = (8,0)
|
|
||||||
// LEFT ARM bottom row: row 8, cols 0→5
|
|
||||||
for (let c = 0; c <= 5; c++) _PATH.push([8, c]); // indices 0-5
|
|
||||||
// Left arm right col going up: col 5, rows 7→6
|
|
||||||
_PATH.push([7, 5], [6, 5]); // 6, 7 (6=safe)
|
|
||||||
// Junction into top arm: (6,6)
|
|
||||||
_PATH.push([6, 6]); // 8
|
|
||||||
// TOP ARM left col going up: col 6, rows 5→0
|
|
||||||
for (let r = 5; r >= 0; r--) _PATH.push([r, 6]); // 9-14 (13=Green entry=(1,6); 14=(0,6))
|
|
||||||
// Top row going right: row 0, cols 7→8
|
|
||||||
_PATH.push([0, 7], [0, 8]); // 15-16
|
|
||||||
// Top arm right col going down: col 8, rows 1→5
|
|
||||||
for (let r = 1; r <= 5; r++) _PATH.push([r, 8]); // 17-21 (19=(3,8)=safe)
|
|
||||||
// Junction into right arm: (6,8)
|
|
||||||
_PATH.push([6, 8]); // 22
|
|
||||||
// RIGHT ARM top row going right: row 6, cols 9→13
|
|
||||||
for (let c = 9; c <= 13; c++) _PATH.push([6, c]); // 23-27
|
|
||||||
// Red entry at (6,14): index 28
|
|
||||||
_PATH.push([6, 14]); // 28 Red entry (safe)
|
|
||||||
// Right arm right col going down: col 14, rows 7→8
|
|
||||||
_PATH.push([7, 14], [8, 14]); // 29-30
|
|
||||||
// Right arm bottom row going left: row 8, cols 13→9
|
|
||||||
for (let c = 13; c >= 9; c--) _PATH.push([8, c]); // 31-35 (32=(8,13)=safe)
|
|
||||||
// Junction into bottom arm: (8,8)
|
|
||||||
_PATH.push([8, 8]); // 36
|
|
||||||
// BOTTOM ARM right col going down: col 8, rows 9→13
|
|
||||||
for (let r = 9; r <= 13; r++) _PATH.push([r, 8]); // 37-41 (45 candidate)
|
|
||||||
// Blue entry at (14,8): index 42
|
|
||||||
_PATH.push([14, 8]); // 42 Blue entry (safe)
|
|
||||||
// Bottom row going left: row 14, cols 7→6
|
|
||||||
_PATH.push([14, 7], [14, 6]); // 43-44
|
|
||||||
// Bottom arm left col going up: col 6, rows 13→9
|
|
||||||
for (let r = 13; r >= 9; r--) _PATH.push([r, 6]); // 45-49 (45=(13,6)=safe)
|
|
||||||
// Junction into left arm: (8,6)
|
|
||||||
_PATH.push([8, 6]); // 50
|
|
||||||
// Left arm top row going left: row 6, cols 5→1 — these lead to Yellow's home col entry
|
|
||||||
// Actually after (8,6) we need to get back to close near (8,0).
|
|
||||||
// The path closes: from junction (8,6) we move along... row 8 going LEFT: (8,5) is already index 5.
|
|
||||||
// This is the problem. We can't use row 8 again.
|
|
||||||
// The path must use row 6 of the left arm (the top edge going left):
|
|
||||||
_PATH.push([7, 6]); // 51 — last cell before home col entry for Yellow
|
|
||||||
|
|
||||||
// This gives 52 cells (0-51). After relative pos 51, Yellow's token enters the home column.
|
|
||||||
// HOME_COL_ENTRY_POS for Yellow: after relative pos 50 (which is abs (0+50)%52=50=(8,6))
|
|
||||||
// When token is at rel pos 50, moving 1+ more steps:
|
|
||||||
// rel 51 = (7,6) — but this is not in Yellow's home col either! Home col is row 7, cols 1-5.
|
|
||||||
// (7,6) would be the junction. Hmm.
|
|
||||||
|
|
||||||
// I think the standard approach is:
|
|
||||||
// After index 50 (or wherever), the token EXITS the outer path and enters home col.
|
|
||||||
// HOME_COL_ENTRY_POS[seat] is the relative pos such that:
|
|
||||||
// if token.pos <= HCEP and token.pos + dice > HCEP, it enters home col.
|
|
||||||
// The home col entry happens AT the threshold, not after.
|
|
||||||
// So Yellow: HOME_COL_ENTRY_POS = 51 means: when at rel 51 and rolling a 1-5, they enter home col.
|
|
||||||
// OR: the token at rel 51=(7,6) can move to home col square (7,5) with 1 step (going left).
|
|
||||||
// This makes sense! (7,6) → (7,5) = first home col square.
|
|
||||||
|
|
||||||
// So the corrected home cols (approaching from the junction side):
|
|
||||||
// Yellow: at (7,6), moves LEFT into (7,5),(7,4),(7,3),(7,2),(7,1)... wait that's going away from center.
|
|
||||||
// (7,1) closer to home base (further from center). We want to move toward (7,7)=center.
|
|
||||||
// So Yellow home col should be: (7,5),(7,4),(7,3),(7,2),(7,1) going left? But center is at (7,7).
|
|
||||||
// To get to (7,7) from (7,6): one step right = (7,7). But home col should be 5 squares before center.
|
|
||||||
// Yellow home col = (7,1),(7,2),(7,3),(7,4),(7,5) going RIGHT toward (7,7).
|
|
||||||
// Yellow enters from (7,0) (their home corner side). But (7,0) isn't on the outer path either.
|
|
||||||
//
|
|
||||||
// I think the standard Parchisi board has the home col on the OPPOSITE side of the arm from entry.
|
|
||||||
// Yellow enters the arm from the bottom (row 8 going right). After traveling the full circuit,
|
|
||||||
// they come back and enter the ARM from the TOP (row 6 going left toward col 0).
|
|
||||||
// Their home column is in the MIDDLE ROW (row 7) going RIGHT toward center.
|
|
||||||
// Entry into home col: when path cell is (6,1) (top of arm near home), token turns down to (7,1) then right.
|
|
||||||
//
|
|
||||||
// This is getting very complex. Let me just hard-code a working 52-cell path and matching home cols
|
|
||||||
// that are geometrically consistent, even if they don't match every cell in the plan exactly.
|
|
||||||
// The key is that the GAME LOGIC works correctly.
|
|
||||||
|
|
||||||
// ─── FINAL DEFINITIVE PATH AND DATA ─────────────────────────────────────────
|
|
||||||
// I'll use a simpler approach: home col entry is triggered when the outer path
|
|
||||||
// reaches a specific cell adjacent to the home column corridor.
|
|
||||||
|
|
||||||
export { }; // temporary export placeholder
|
|
||||||
|
|
@ -13,6 +13,7 @@ import LobbyScene from './scenes/LobbyScene.js';
|
||||||
import GameRoomScene from './scenes/GameRoomScene.js';
|
import GameRoomScene from './scenes/GameRoomScene.js';
|
||||||
import BackgammonGame from './games/backgammon/BackgammonGame.js';
|
import BackgammonGame from './games/backgammon/BackgammonGame.js';
|
||||||
import HoldemGame from './games/holdem/HoldemGame.js';
|
import HoldemGame from './games/holdem/HoldemGame.js';
|
||||||
|
import BlackjackGame from './games/blackjack/BlackjackGame.js';
|
||||||
|
|
||||||
const config = {
|
const config = {
|
||||||
type: Phaser.AUTO,
|
type: Phaser.AUTO,
|
||||||
|
|
@ -39,6 +40,7 @@ const config = {
|
||||||
GameRoomScene,
|
GameRoomScene,
|
||||||
BackgammonGame,
|
BackgammonGame,
|
||||||
HoldemGame,
|
HoldemGame,
|
||||||
|
BlackjackGame,
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ export default class GameRoomScene extends Phaser.Scene {
|
||||||
}
|
}
|
||||||
|
|
||||||
create() {
|
create() {
|
||||||
const slugDispatch = { backgammon: 'Backgammon', holdem: 'HoldemGame' };
|
const slugDispatch = { backgammon: 'Backgammon', holdem: 'HoldemGame', blackjack: 'BlackjackGame' };
|
||||||
if (slugDispatch[this.game.slug]) {
|
if (slugDispatch[this.game.slug]) {
|
||||||
this.scene.start(slugDispatch[this.game.slug], {
|
this.scene.start(slugDispatch[this.game.slug], {
|
||||||
game: this.game,
|
game: this.game,
|
||||||
|
|
|
||||||
|
|
@ -58,14 +58,19 @@ export default class OpponentSelectScene extends Phaser.Scene {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const min = this.gameDef.minOpponents ?? 1;
|
||||||
this.startBtn = new Button(this, cx, 1048, 'Start Game', () => this.startGame(), { width: 280 });
|
this.startBtn = new Button(this, cx, 1048, 'Start Game', () => this.startGame(), { width: 280 });
|
||||||
this.startBtn.setEnabled(false);
|
this.startBtn.setEnabled(min === 0);
|
||||||
|
|
||||||
new Button(this, cx, 978, 'Back', () => this.scene.start('GameMenu'), {
|
new Button(this, cx, 978, 'Back', () => this.scene.start('GameMenu'), {
|
||||||
variant: 'ghost',
|
variant: 'ghost',
|
||||||
width: 280,
|
width: 280,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
for (let i = opponents.length - 1; i > 0; i--) {
|
||||||
|
const j = Math.floor(Math.random() * (i + 1));
|
||||||
|
[opponents[i], opponents[j]] = [opponents[j], opponents[i]];
|
||||||
|
}
|
||||||
this.buildOpponentGrid(opponents);
|
this.buildOpponentGrid(opponents);
|
||||||
|
|
||||||
this.buildOptionSection('Playfield', 630, this.cache.json.get('playfields')?.playfields ?? [],
|
this.buildOptionSection('Playfield', 630, this.cache.json.get('playfields')?.playfields ?? [],
|
||||||
|
|
|
||||||
|
|
@ -27,5 +27,5 @@ export function getGame(slug) {
|
||||||
// Built-in placeholders so the menu has something to show.
|
// Built-in placeholders so the menu has something to show.
|
||||||
registerGame({ slug: 'backgammon', name: 'Backgammon', category: 'tabletop', minPlayers: 2, maxPlayers: 2, minOpponents: 1, maxOpponents: 1, multiplayerOnly: false });
|
registerGame({ slug: 'backgammon', name: 'Backgammon', category: 'tabletop', minPlayers: 2, maxPlayers: 2, minOpponents: 1, maxOpponents: 1, multiplayerOnly: false });
|
||||||
registerGame({ slug: 'parchisi', name: 'Parchisi', category: 'tabletop', minPlayers: 2, maxPlayers: 4, multiplayerOnly: false });
|
registerGame({ slug: 'parchisi', name: 'Parchisi', category: 'tabletop', minPlayers: 2, maxPlayers: 4, multiplayerOnly: false });
|
||||||
registerGame({ slug: 'blackjack', name: 'Blackjack', category: 'casino', cardGame: true, minPlayers: 1, maxPlayers: 6, multiplayerOnly: false });
|
registerGame({ slug: 'blackjack', name: 'Blackjack', category: 'casino', cardGame: true, minPlayers: 1, maxPlayers: 5, minOpponents: 0, maxOpponents: 4, multiplayerOnly: false });
|
||||||
registerGame({ slug: 'holdem', name: "Texas Hold 'Em", category: 'casino', cardGame: true, minPlayers: 2, maxPlayers: 8, minOpponents: 3, maxOpponents: 3, multiplayerOnly: false });
|
registerGame({ slug: 'holdem', name: "Texas Hold 'Em", category: 'casino', cardGame: true, minPlayers: 2, maxPlayers: 8, minOpponents: 3, maxOpponents: 3, multiplayerOnly: false });
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue