Files
liars-bar-game/server/game/engine.js
T

360 lines
11 KiB
JavaScript

// 遊戲引擎核心 (Liar's Bar)
// 規則與 Python 版 (game.py) 對齊:
// - 每手每人 5 張,牌組依玩家人數等比擴充(Q:K:A 等量 + 約 10% Joker),支援 2-8 人
// - 每次可出 1-3 張,宣稱皆為目標牌;Joker 為萬能牌
// - 下一位玩家可選擇出牌或質疑「上一組」出的牌
// - 質疑:整組牌全為目標牌/Joker 則質疑失敗(質疑者開槍),否則質疑成功(出牌者開槍)
// - 質疑後本手結束,存活玩家重新發牌並選新目標牌
// - 特殊:輪到某玩家時若其他存活玩家已無牌,該玩家人餘牌視為自動打出並受系統質疑
// - 僅存一位存活者時結束
const HAND_SIZE = 5;
const REVOLVER_CHAMBERS = 6;
const TARGETS = ['Q', 'K', 'A'];
const MAX_PLAY_PER_TURN = 3;
class GameEngine {
constructor() {
this.players = new Map(); // id -> { id, hand, revolver, alive }
this.currentPlayerId = null;
this.targetCard = null;
this.playGroups = []; // 本手連續出牌組 [{ playerId, cards }]
this.roundNumber = 0;
this.isGameOver = false;
this.winnerId = null;
this.lastShooterId = null; // 上一手開槍者(存活時作為下一手起始)
}
// ---------- 初始化 ----------
init(playerIds) {
this.players.clear();
this.currentPlayerId = null;
this.targetCard = null;
this.playGroups = [];
this.roundNumber = 0;
this.isGameOver = false;
this.winnerId = null;
this.lastShooterId = null;
for (const id of playerIds) {
this.players.set(id, {
id,
hand: [],
revolver: this.createRevolver(),
alive: true
});
}
this.roundNumber = 1;
this.dealCards(this.getAlivePlayers().length);
this.setTargetCard();
this.currentPlayerId = this.pickRandomAlivePlayer();
return this.getGameState();
}
// 依存活人數建立等比牌組(每人 5 張)
createDeck(playerCount) {
const total = playerCount * HAND_SIZE;
const jokerCount = Math.max(1, Math.round(total * 0.1));
const normalTotal = total - jokerCount;
const base = Math.floor(normalTotal / 3);
let remainder = normalTotal % 3;
const counts = { Q: base, K: base, A: base };
for (const key of TARGETS) {
if (remainder > 0) { counts[key] += 1; remainder -= 1; }
}
const deck = [];
for (const key of Object.keys(counts)) {
for (let i = 0; i < counts[key]; i++) deck.push(key);
}
for (let i = 0; i < jokerCount; i++) deck.push('JOKER');
this.shuffle(deck);
return deck;
}
// ---------- 基本工具 ----------
shuffle(deck) {
for (let i = deck.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[deck[i], deck[j]] = [deck[j], deck[i]];
}
return deck;
}
createRevolver() {
const chambers = Array(REVOLVER_CHAMBERS).fill(false);
chambers[Math.floor(Math.random() * REVOLVER_CHAMBERS)] = true;
return { chambers, currentChamber: 0 };
}
setTargetCard() {
this.targetCard = TARGETS[Math.floor(Math.random() * TARGETS.length)];
}
getAlivePlayers() {
return Array.from(this.players.values()).filter((p) => p.alive);
}
getPlayer(playerId) {
return this.players.get(playerId);
}
pickRandomAlivePlayer() {
const alive = this.getAlivePlayers();
return alive.length ? alive[Math.floor(Math.random() * alive.length)].id : null;
}
// 目前玩家的下一位「有牌且存活」的玩家
getNextAlivePlayerWithCards() {
const ids = Array.from(this.players.keys());
if (ids.length === 0) return null;
const curIdx = ids.indexOf(this.currentPlayerId);
for (let i = 1; i <= ids.length; i++) {
const player = this.players.get(ids[(curIdx + i) % ids.length]);
if (player && player.alive && player.hand.length > 0) return player.id;
}
// 所有人手上都無牌:回傳目前玩家(由呼叫端處理)
return this.currentPlayerId;
}
isAllOthersEmpty(playerId) {
return this.getAlivePlayers()
.filter((p) => p.id !== playerId)
.every((p) => p.hand.length === 0);
}
// ---------- 回合流程 ----------
// 出牌:cardIndices 為 1-3 個手牌索引(此輪宣稱皆為目標牌)
playCard(playerId, cardIndices) {
if (this.isGameOver) return { error: '遊戲已結束' };
const player = this.getPlayer(playerId);
if (!player || !player.alive) return { error: '玩家已死亡' };
if (playerId !== this.currentPlayerId) return { error: '還沒輪到你出牌' };
if (!Array.isArray(cardIndices) || cardIndices.length < 1 || cardIndices.length > MAX_PLAY_PER_TURN) {
return { error: `每次只能出 1-${MAX_PLAY_PER_TURN} 張` };
}
const unique = new Set(cardIndices);
if (unique.size !== cardIndices.length) {
return { error: '不能重複選擇同一張牌' };
}
for (const idx of cardIndices) {
if (!Number.isInteger(idx) || idx < 0 || idx >= player.hand.length) {
return { error: '無效的選牌索引' };
}
}
const played = cardIndices.map((idx) => player.hand[idx]);
// 由大到小移除,避免索引位移
[...cardIndices].sort((a, b) => b - a).forEach((idx) => player.hand.splice(idx, 1));
this.playGroups.push({ playerId: player.id, cards: played });
this.currentPlayerId = this.getNextAlivePlayerWithCards();
const baseResult = {
success: true,
playedGroup: { playerId: player.id, cards: played },
nextPlayerId: this.currentPlayerId
};
// 特殊規則:輪到 nextPlayer 時其他存活玩家已無牌 → 系統質疑(自動打出剩餘手牌)
const allAliveEmpty = this.getAlivePlayers().every((p) => p.hand.length === 0);
const othersEmpty = !allAliveEmpty
&& this.currentPlayerId !== null
&& this.getPlayer(this.currentPlayerId).hand.length > 0
&& this.isAllOthersEmpty(this.currentPlayerId);
if (othersEmpty) {
const sys = this.systemChallenge(this.currentPlayerId);
baseResult.systemChallenge = sys;
} else if (allAliveEmpty) {
// 所有人同時無牌(理論上不會發生)→ 視為無效質疑,重開一手
baseResult.autoRoundReset = true;
this.startNewRound();
}
return baseResult;
}
// 系統質疑:自動打出某玩家剩餘手牌並質疑
systemChallenge(playerId) {
const player = this.getPlayer(playerId);
const cards = [...player.hand];
player.hand = [];
const valid = cards.length > 0 && cards.every((c) => c === this.targetCard || c === 'JOKER');
const isBluff = !valid;
let shooter = null;
let shot = null;
if (isBluff) {
// 剩餘手牌有假牌 → 出牌者開槍
shooter = playerId;
shot = this.fireRevolver(playerId);
} else {
// 全是真牌 → 系統質疑失敗(與 Python 一致:無人開槍)
shot = { playerId, hasBullet: false, alive: player.alive };
}
this.clearTable();
this.startNewRound();
return {
systemChallenge: true,
playerId,
autoPlayedCards: cards,
isBluff,
shooter,
shot
};
}
// 質疑上一組出的牌(由下一位玩家執行)
challenge(challengerId) {
if (this.isGameOver) return { error: '遊戲已結束' };
const challenger = this.getPlayer(challengerId);
if (!challenger || !challenger.alive) return { error: '挑戰者已死亡' };
if (challengerId !== this.currentPlayerId) return { error: '還沒輪到你質疑' };
const last = this.playGroups[this.playGroups.length - 1];
if (!last) return { error: '還沒有出牌可以質疑' };
const valid = last.cards.length > 0 && last.cards.every((c) => c === this.targetCard || c === 'JOKER');
const isBluff = !valid;
const result = {
challengerId,
playerId: last.playerId,
isBluff,
challengedGroup: last.cards
};
if (isBluff) {
// 質疑成功 → 出牌者開槍
result.shooter = last.playerId;
result.shot = this.fireRevolver(last.playerId);
} else {
// 質疑失敗 → 質疑者開槍
result.shooter = challengerId;
result.shot = this.fireRevolver(challengerId);
}
this.clearTable();
// 質疑後本手結束,重開一手(若遊戲還未結束)
this.startNewRound();
return result;
}
// 重開一手:重新發牌、選目標牌、決定起始玩家
startNewRound() {
const alive = this.getAlivePlayers();
if (alive.length <= 1) {
this.isGameOver = true;
this.winnerId = alive.length === 1 ? alive[0].id : null;
return;
}
this.roundNumber += 1;
this.clearTable();
this.setTargetCard();
this.dealCards(alive.length);
// 起始玩家:上一手開槍者(若存活),否則隨機
if (this.lastShooterId && this.getPlayer(this.lastShooterId) && this.getPlayer(this.lastShooterId).alive) {
this.currentPlayerId = this.lastShooterId;
} else {
this.currentPlayerId = this.pickRandomAlivePlayer();
}
}
clearTable() {
this.playGroups = [];
}
dealCards(aliveCount) {
for (const p of this.players.values()) p.hand = [];
const deck = this.createDeck(aliveCount);
for (let round = 0; round < HAND_SIZE; round++) {
for (const id of this.players.keys()) {
const p = this.players.get(id);
if (p.alive && deck.length > 0) p.hand.push(deck.pop());
}
}
}
// ---------- 射擊 / 結束 ----------
fireRevolver(playerId) {
const player = this.getPlayer(playerId);
if (!player) return null;
const revolver = player.revolver;
const hasBullet = revolver.chambers[revolver.currentChamber];
revolver.currentChamber = (revolver.currentChamber + 1) % REVOLVER_CHAMBERS;
if (hasBullet) {
player.alive = false;
player.revolver = this.createRevolver(); // 死亡後重置左輪手槍
}
this.lastShooterId = playerId;
this.checkGameOver();
return { playerId, hasBullet, alive: player.alive, isGameOver: this.isGameOver };
}
checkGameOver() {
if (this.getAlivePlayers().length <= 1) {
this.isGameOver = true;
const alive = this.getAlivePlayers();
this.winnerId = alive.length === 1 ? alive[0].id : null;
}
}
removePlayer(playerId) {
const player = this.getPlayer(playerId);
if (player) {
player.alive = false;
player.hand = [];
this.checkGameOver();
}
}
// ---------- 對外狀態 ----------
getGameState() {
const state = {
players: [],
currentPlayerId: this.currentPlayerId,
targetCard: this.targetCard,
playedCards: [],
playedGroups: this.playGroups.map((g) => ({ playerId: g.playerId, cardCount: g.cards.length })),
roundNumber: this.roundNumber,
isGameOver: this.isGameOver,
winnerId: this.winnerId
};
for (const [id, player] of this.players) {
state.players.push({
id,
handSize: player.hand.length,
alive: player.alive,
revolverChambers: player.revolver.currentChamber
});
}
// 資訊邊界:公開狀態只顯示「誰出了幾張」,不洩漏真實牌值;
// 真實牌值只在質疑(challenge / systemChallenge)時透過 challenged 事件揭露。
return state;
}
getPlayerHand(playerId) {
const player = this.getPlayer(playerId);
if (!player) return [];
return [...player.hand];
}
isBluff(cards) {
return !(cards.length > 0 && cards.every((c) => c === this.targetCard || c === 'JOKER'));
}
}
module.exports = GameEngine;