/** * 斗地主游戏核心逻辑 * 本地实现,完整游戏流程 */ // ========== 牌组定义 ========== const SUITS = ['♠', '♥', '♣', '♦']; const RANKS = ['3', '4', '5', '6', '7', '8', '9', '10', 'J', 'Q', 'K', 'A', '2']; // 生成完整牌组 function generateDeck() { const deck = []; let id = 0; for (const rank of RANKS) { for (const suit of SUITS) { deck.push({ id: id++, rank: rank, suit: suit, value: RANKS.indexOf(rank) + 3, isJoker: false }); } } deck.push({ id: id++, rank: '小王', suit: '🃏', value: 16, isJoker: true }); deck.push({ id: id++, rank: '大王', suit: '🃏', value: 17, isJoker: true }); return deck; } // 洗牌 function shuffleDeck(deck) { const shuffled = [...deck]; for (let i = shuffled.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]]; } return shuffled; } // ========== 牌型识别 ========== const CARD_TYPES = { SINGLE: 'single', // 单张 PAIR: 'pair', // 对子 TRIPLE1: 'triple1', // 三带一 STRAIGHT: 'straight', // 顺子 BOMB: 'bomb', // 炸弹 JOKER: 'joker' // 王炸 }; // 识别牌型 function identifyCardType(cards) { if (!cards || cards.length === 0) return null; const sorted = [...cards].sort((a, b) => a.value - b.value); const n = cards.length; // 王炸 if (n === 2 && cards.every(c => c.isJoker)) { return { type: CARD_TYPES.JOKER, value: 100, cards: cards, name: '王炸' }; } // 单张 if (n === 1) { return { type: CARD_TYPES.SINGLE, value: cards[0].value, cards: cards, name: '单张' }; } // 对子 if (n === 2 && sorted[0].value === sorted[1].value) { return { type: CARD_TYPES.PAIR, value: sorted[0].value, cards: cards, name: '对子' }; } // 炸弹 if (n === 4 && sorted.every(c => c.value === sorted[0].value)) { return { type: CARD_TYPES.BOMB, value: sorted[0].value, cards: cards, name: '炸弹' }; } // 三带一 if (n === 4) { const valueCount = {}; cards.forEach(c => valueCount[c.value] = (valueCount[c.value] || 0) + 1); const values = Object.values(valueCount); if (values.includes(3) && values.includes(1)) { const tripleValue = parseInt(Object.keys(valueCount).find(k => valueCount[k] === 3)); return { type: CARD_TYPES.TRIPLE1, value: tripleValue, cards: cards, name: '三带一' }; } } // 顺子 (>=5张连续,不能含2和王) if (n >= 5) { const hasInvalid = cards.some(c => c.value >= 15); // 2=15, 王=16,17 if (!hasInvalid) { const isStraight = sorted.every((c, i) => { if (i === 0) return true; return c.value === sorted[i-1].value + 1; }); if (isStraight) { return { type: CARD_TYPES.STRAIGHT, value: sorted[0].value, length: n, cards: cards, name: '顺子' }; } } } return null; } // 比较牌型大小 function canBeat(desktop, newType) { if (!desktop) return true; // 王炸最大 if (newType.type === CARD_TYPES.JOKER) return true; if (desktop.type === CARD_TYPES.JOKER) return false; // 炸弹 > 非炸弹 if (newType.type === CARD_TYPES.BOMB && desktop.type !== CARD_TYPES.BOMB) return true; if (desktop.type === CARD_TYPES.BOMB && newType.type !== CARD_TYPES.BOMB) return false; // 同类型比较 if (newType.type !== desktop.type) return false; if (newType.length && desktop.length && newType.length !== desktop.length) return false; return newType.value > desktop.value; } // ========== AI逻辑 ========== class DoudizhuAI { constructor(playerId) { this.id = playerId; } // 评估手牌强度(用于叫地主) evaluateHand(cards) { let score = 0; // 大牌加分 cards.forEach(c => { if (c.value >= 15) score += 3; // 2, 王 else if (c.value >= 14) score += 2; // A else if (c.value >= 13) score += 1; // K }); // 炸弹加分 const valueCount = {}; cards.forEach(c => valueCount[c.value] = (valueCount[c.value] || 0) + 1); Object.values(valueCount).forEach(count => { if (count === 4) score += 5; }); // 王炸 if (cards.filter(c => c.isJoker).length === 2) score += 8; return score; } // 决定是否叫地主 wantToBeLord(cards) { const score = this.evaluateHand(cards); return score >= 8; // 分数>=8才当地主 } // 找出所有可出的牌型 findAllValidPlays(cards, desktop) { const validPlays = []; const n = cards.length; // 辅助函数:生成所有组合 const getCombinations = (arr, k) => { if (k === 0) return [[]]; if (arr.length < k) return []; const result = []; for (let i = 0; i <= arr.length - k; i++) { const subCombos = getCombinations(arr.slice(i + 1), k - 1); subCombos.forEach(sub => result.push([arr[i], ...sub])); } return result; }; // 尝试不同数量的牌 for (let k = 1; k <= Math.min(5, n); k++) { const combos = getCombinations(cards, k); combos.forEach(combo => { const cardType = identifyCardType(combo); if (cardType && canBeat(desktop, cardType)) { validPlays.push({ cards: combo, type: cardType }); } }); } // 炸弹单独处理(可以管任何非炸弹) const valueCount = {}; cards.forEach(c => valueCount[c.value] = (valueCount[c.value] || 0) + 1); Object.entries(valueCount).forEach(([value, count]) => { if (count === 4) { const bombCards = cards.filter(c => c.value == value); const bombType = identifyCardType(bombCards); if (bombType && canBeat(desktop, bombType)) { validPlays.push({ cards: bombCards, type: bombType }); } } }); // 王炸 const jokers = cards.filter(c => c.isJoker); if (jokers.length === 2) { const jokerType = identifyCardType(jokers); if (jokerType && canBeat(desktop, jokerType)) { validPlays.push({ cards: jokers, type: jokerType }); } } return validPlays; } // 选择最佳出牌 chooseBestPlay(validPlays, cards, isLord, teammateCards) { if (validPlays.length === 0) return null; // 排序策略 validPlays.sort((a, b) => { // 优先出能走完的 const aRemaining = cards.length - a.cards.length; const bRemaining = cards.length - b.cards.length; if (aRemaining === 0) return -1; if (bRemaining === 0) return 1; // 优先出小牌 return a.type.value - b.type.value; }); // 选择第一个(最小且合法的) return validPlays[0]; } // AI决策 makeDecision(cards, desktop, isLord, teammateCards) { const validPlays = this.findAllValidPlays(cards, desktop); if (validPlays.length === 0) { return { action: 'pass' }; } const bestPlay = this.chooseBestPlay(validPlays, cards, isLord, teammateCards); if (bestPlay) { return { action: 'play', cards: bestPlay.cards, type: bestPlay.type }; } return { action: 'pass' }; } } // ========== 游戏主类 ========== class DoudizhuGame { constructor() { this.players = [ { id: 0, cards: [], isLord: false, name: '玩家', ai: null }, { id: 1, cards: [], isLord: false, name: '农民甲', ai: new DoudizhuAI(1) }, { id: 2, cards: [], isLord: false, name: '农民乙', ai: new DoudizhuAI(2) } ]; this.deck = []; this.bottomCards = []; this.currentPlayer = 0; this.gamePhase = 'init'; this.desktopCards = null; this.lastAction = null; this.passCount = 0; } init() { this.deck = shuffleDeck(generateDeck()); this.players.forEach(p => { p.cards = []; p.isLord = false; }); this.bottomCards = []; this.desktopCards = null; this.passCount = 0; this.gamePhase = 'dealing'; } deal() { // 发51张,每人17张 for (let i = 0; i < 51; i++) { this.players[i % 3].cards.push(this.deck[i]); } // 剩余3张底牌 this.bottomCards = this.deck.slice(51, 54); // 排序 this.players.forEach(p => { p.cards.sort((a, b) => a.value - b.value); }); this.gamePhase = 'calling'; return { players: this.players.map(p => ({ id: p.id, cardCount: p.cards.length, cards: p.id === 0 ? p.cards : null // 只有玩家能看到自己的牌 })), bottomCards: this.bottomCards }; } // 叫地主 callLord(playerId, want) { if (want) { this.players[playerId].isLord = true; this.players[playerId].cards.push(...this.bottomCards); this.players[playerId].cards.sort((a, b) => a.value - b.value); this.currentPlayer = playerId; this.gamePhase = 'playing'; return { success: true, lordId: playerId }; } return { success: false }; } // AI叫地主 aiCallLord() { for (let i = 1; i <= 2; i++) { const ai = this.players[i].ai; if (ai.wantToBeLord(this.players[i].cards)) { return this.callLord(i, true); } } // 都没叫,随机选一个 const randomLord = Math.floor(Math.random() * 3); return this.callLord(randomLord, true); } // 出牌 playCards(playerId, cards) { const player = this.players[playerId]; // 验证牌是否在手中 const cardIds = cards.map(c => c.id); const hasAllCards = cardIds.every(id => player.cards.some(c => c.id === id) ); if (!hasAllCards) return { success: false, error: '牌不在手中' }; // 识别牌型 const cardType = identifyCardType(cards); if (!cardType) return { success: false, error: '非法牌型' }; // 验证能否管上 if (this.desktopCards && !canBeat(this.desktopCards, cardType)) { return { success: false, error: '管不上' }; } // 执行出牌 player.cards = player.cards.filter(c => !cardIds.includes(c.id)); this.desktopCards = cardType; this.lastAction = 'play'; this.passCount = 0; // 检查胜利 if (player.cards.length === 0) { return { success: true, cardType: cardType, remainingCards: player.cards.length, gameOver: true, winner: playerId }; } // 下一个玩家 this.currentPlayer = (playerId + 1) % 3; return { success: true, cardType: cardType, remainingCards: player.cards.length, nextPlayer: this.currentPlayer }; } // 不出 pass(playerId) { this.passCount++; this.lastAction = 'pass'; // 如果连续两人不出,清空桌面 if (this.passCount >= 2) { this.desktopCards = null; this.passCount = 0; this.currentPlayer = (playerId + 1) % 3; return { success: true, clearDesktop: true, nextPlayer: this.currentPlayer }; } this.currentPlayer = (playerId + 1) % 3; return { success: true, nextPlayer: this.currentPlayer }; } // AI决策 aiPlay() { const aiPlayer = this.players[this.currentPlayer]; const ai = aiPlayer.ai; const teammateCards = this.currentPlayer === 1 ? this.players[2].cards.length : this.players[1].cards.length; const decision = ai.makeDecision( aiPlayer.cards, this.desktopCards, aiPlayer.isLord, teammateCards ); if (decision.action === 'play') { return this.playCards(this.currentPlayer, decision.cards); } else { return this.pass(this.currentPlayer); } } // 获取游戏状态 getState() { return { phase: this.gamePhase, currentPlayer: this.currentPlayer, desktopCards: this.desktopCards, players: this.players.map(p => ({ id: p.id, name: p.name, isLord: p.isLord, cardCount: p.cards.length, cards: p.id === 0 ? p.cards : null })) }; } } // 导出 if (typeof module !== 'undefined' && module.exports) { module.exports = { DoudizhuGame, DoudizhuAI, identifyCardType, canBeat, CARD_TYPES }; }