/** * 掼蛋游戏核心逻辑 * 简化版:4人2v2,108张牌,27张/人,有百搭,无进贡,无亮主 */ // ========== 牌组定义 ========== const SUITS = ['♠', '♥', '♣', '♦']; const RANKS = ['2', '3', '4', '5', '6', '7', '8', '9', '10', 'J', 'Q', 'K', 'A']; // 生成两副牌共108张 function generateDeck() { const deck = []; let id = 0; // 两副牌 for (let deckNum = 0; deckNum < 2; deckNum++) { for (const rank of RANKS) { for (const suit of SUITS) { deck.push({ id: id++, rank: rank, suit: suit, value: getCardValue(rank), isJoker: false, deckNum: deckNum }); } } // 大小王(每副牌2张) deck.push({ id: id++, rank: '小王', suit: '🃏', value: 15, isJoker: true, deckNum: deckNum }); deck.push({ id: id++, rank: '大王', suit: '🃏', value: 16, isJoker: true, deckNum: deckNum }); } return deck; } // 获取牌点数(用于比较大小) function getCardValue(rank) { const values = { '2': 2, '3': 3, '4': 4, '5': 5, '6': 6, '7': 7, '8': 8, '9': 9, '10': 10, 'J': 11, 'Q': 12, 'K': 13, 'A': 14 }; return values[rank] || 0; } // 洗牌 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', // 对子 TRIPLE: 'triple', // 三张 TRIPLE_WITH_PAIR: 'triple_pair', // 三带二 STRAIGHT: 'straight', // 顺子(5+连续) CONSECUTIVE_PAIRS: 'consecutive_pairs', // 三连对(3+连续对子) STEEL_PLATE: 'steel_plate', // 钢板(2个连续三张) BOMB_4: 'bomb4', // 4张炸 BOMB_5: 'bomb5', // 5张炸 BOMB_6: 'bomb6', // 6张炸 BOMB_7: 'bomb7', // 7张炸 BOMB_8: 'bomb8', // 8张炸 JOKER_BOMB: 'joker_bomb' // 天王炸(4王) }; // ========== 游戏状态 ========== class GuandanGame { constructor() { this.players = [ { id: 0, cards: [], team: 0, name: '玩家', isAI: false, rank: null }, { id: 1, cards: [], team: 1, name: '对手甲', isAI: true, rank: null }, { id: 2, cards: [], team: 0, name: '队友', isAI: true, rank: null }, { id: 3, cards: [], team: 1, name: '对手乙', isAI: true, rank: null } ]; this.currentLevel = '2'; // 当前级牌 this.wildCard = null; // 百搭牌(红桃级牌) this.currentPlayer = 0; // 当前出牌玩家 this.lastPlay = null; // 上轮出牌 this.lastPlayer = null; // 上轮出牌玩家 this.passCount = 0; // 连续不出计数 this.gameState = 'waiting'; // waiting/dealing/playing/ended this.roundPlays = []; // 本轮出牌记录 this.finishedPlayers = []; // 已出完牌的玩家 this.teamLevels = { 0: '2', 1: '2' }; // 两队当前级数 } // 开始新游戏 startGame() { this.currentLevel = this.teamLevels[0]; // 使用我方当前级数 this.wildCard = { suit: '♥', rank: this.currentLevel }; // 生成并洗牌 const deck = shuffleDeck(generateDeck()); // 发牌,每人27张 for (let i = 0; i < 4; i++) { this.players[i].cards = deck.slice(i * 27, (i + 1) * 27); this.players[i].cards.sort((a, b) => this.compareCards(a, b)); this.players[i].rank = null; } this.currentPlayer = Math.floor(Math.random() * 4); // 随机起始 this.lastPlay = null; this.lastPlayer = null; this.passCount = 0; this.gameState = 'playing'; this.roundPlays = []; this.finishedPlayers = []; return { success: true, firstPlayer: this.currentPlayer, wildCard: this.wildCard }; } // 比较两张牌(用于排序) compareCards(a, b) { // 王最大 if (a.isJoker && b.isJoker) return b.value - a.value; if (a.isJoker) return -1; if (b.isJoker) return 1; // 级牌次之 const aIsLevel = a.rank === this.currentLevel; const bIsLevel = b.rank === this.currentLevel; if (aIsLevel && bIsLevel) { // 红桃级牌(百搭)最大 if (a.suit === '♥') return -1; if (b.suit === '♥') return 1; return 0; } if (aIsLevel) return -1; if (bIsLevel) return 1; // 然后按点数 if (b.value !== a.value) { return b.value - a.value; } // 同点数按花色 const suitOrder = { '♠': 0, '♥': 1, '♣': 2, '♦': 3 }; return suitOrder[a.suit] - suitOrder[b.suit]; } // 识别牌型(核心逻辑) identifyCardType(cards) { if (!cards || cards.length === 0) return null; const count = cards.length; // 统计点数和百搭数量 let wildCount = 0; const rankCounts = {}; for (const card of cards) { if (this.isWildCard(card)) { wildCount++; } else { rankCounts[card.rank] = (rankCounts[card.rank] || 0) + 1; } } const ranks = Object.keys(rankCounts); // 天王炸(4张王) if (count === 4 && cards.every(c => c.isJoker)) { return { type: CARD_TYPES.JOKER_BOMB, value: 100, length: 4 }; } // 炸弹(4-8张相同点数,可配百搭) if (count >= 4 && count <= 8) { // 检查是否能组成炸弹 for (const rank of ranks) { const need = count - rankCounts[rank]; if (need <= wildCount && need >= 0) { return { type: CARD_TYPES[`BOMB_${count}`], value: getCardValue(rank) + (count - 4) * 20, length: count, mainRank: rank }; } } } // 单张 if (count === 1) { return { type: CARD_TYPES.SINGLE, value: this.getCardPower(cards[0]), length: 1 }; } // 对子 if (count === 2) { // 两张真牌同点数 if (ranks.length === 1 && wildCount === 0) { return { type: CARD_TYPES.PAIR, value: getCardValue(ranks[0]), length: 2, mainRank: ranks[0] }; } // 一真一百搭 if (ranks.length === 1 && wildCount === 1) { return { type: CARD_TYPES.PAIR, value: getCardValue(ranks[0]), length: 2, mainRank: ranks[0] }; } // 两张百搭 if (ranks.length === 0 && wildCount === 2) { return { type: CARD_TYPES.PAIR, value: getCardValue(this.currentLevel), length: 2, mainRank: this.currentLevel }; } } // 三张 if (count === 3) { // 全真 if (ranks.length === 1 && wildCount === 0) { return { type: CARD_TYPES.TRIPLE, value: getCardValue(ranks[0]), length: 3, mainRank: ranks[0] }; } // 配百搭 if (ranks.length === 1 && wildCount > 0) { return { type: CARD_TYPES.TRIPLE, value: getCardValue(ranks[0]), length: 3, mainRank: ranks[0] }; } } // 三带二(5张) if (count === 5) { // 找三张部分 for (const rank of ranks) { const tripleWild = Math.max(0, 3 - rankCounts[rank]); const remainingWild = wildCount - tripleWild; const remainingCards = count - rankCounts[rank] - tripleWild; // 检查剩余2张能否组成对子 const remainingRanks = ranks.filter(r => r !== rank); if (remainingRanks.length === 1 && rankCounts[remainingRanks[0]] + remainingWild === 2) { return { type: CARD_TYPES.TRIPLE_WITH_PAIR, value: getCardValue(rank), length: 5, mainRank: rank }; } } } // 顺子(5+连续单张,不可含级牌、王) if (count >= 5) { const straightResult = this.checkStraight(cards, rankCounts, wildCount); if (straightResult) { return { type: CARD_TYPES.STRAIGHT, value: straightResult.minValue, length: count, minRank: straightResult.minRank }; } } // 三连对(6张=3对连续) if (count === 6) { const result = this.checkConsecutivePairs(cards, rankCounts, wildCount, 3); if (result) { return { type: CARD_TYPES.CONSECUTIVE_PAIRS, value: result.minValue, length: 6, minRank: result.minRank }; } } // 钢板(6张=2个连续三张) if (count === 6) { const result = this.checkSteelPlate(cards, rankCounts, wildCount); if (result) { return { type: CARD_TYPES.STEEL_PLATE, value: result.minValue, length: 6, minRank: result.minRank }; } } return null; // 非法牌型 } // 检查是否为百搭牌 isWildCard(card) { return card.suit === '♥' && card.rank === this.currentLevel; } // 获取牌的实际威力值 getCardPower(card) { if (card.isJoker) return 100 + card.value; // 王 if (this.isWildCard(card)) return 90; // 百搭 if (card.rank === this.currentLevel) return 80; // 其他级牌 return card.value; } // 检查顺子 checkStraight(cards, rankCounts, wildCount) { const count = cards.length; const validRanks = ['3', '4', '5', '6', '7', '8', '9', '10', 'J', 'Q', 'K', 'A']; // 顺子不能含级牌和王 for (const card of cards) { if (card.isJoker) return null; if (card.rank === this.currentLevel && !this.isWildCard(card)) return null; } // 尝试找到连续序列 const rankValues = {}; for (const rank of validRanks) { rankValues[rank] = getCardValue(rank); } // 收集已有的牌点数 const hasRanks = new Set(); for (const rank of Object.keys(rankCounts)) { if (rankCounts[rank] > 0) hasRanks.add(rank); } // 尝试每个可能的起始点 for (let i = 0; i <= validRanks.length - count; i++) { const sequence = validRanks.slice(i, i + count); let needWild = 0; for (const rank of sequence) { if (!hasRanks.has(rank)) needWild++; } if (needWild <= wildCount) { return { minValue: getCardValue(sequence[0]), minRank: sequence[0] }; } } return null; } // 检查三连对 checkConsecutivePairs(cards, rankCounts, wildCount, pairCount) { const validRanks = ['3', '4', '5', '6', '7', '8', '9', '10', 'J', 'Q', 'K', 'A']; const count = pairCount * 2; // 不能含级牌和王 for (const card of cards) { if (card.isJoker) return null; if (card.rank === this.currentLevel && !this.isWildCard(card)) return null; } for (let i = 0; i <= validRanks.length - pairCount; i++) { const sequence = validRanks.slice(i, i + pairCount); let needWild = 0; for (const rank of sequence) { const has = rankCounts[rank] || 0; needWild += Math.max(0, 2 - has); } if (needWild <= wildCount) { return { minValue: getCardValue(sequence[0]), minRank: sequence[0] }; } } return null; } // 检查钢板(2个连续三张) checkSteelPlate(cards, rankCounts, wildCount) { const validRanks = ['3', '4', '5', '6', '7', '8', '9', '10', 'J', 'Q', 'K', 'A']; for (let i = 0; i <= validRanks.length - 2; i++) { const rank1 = validRanks[i]; const rank2 = validRanks[i + 1]; const need1 = Math.max(0, 3 - (rankCounts[rank1] || 0)); const need2 = Math.max(0, 3 - (rankCounts[rank2] || 0)); const totalNeed = need1 + need2; if (totalNeed <= wildCount) { return { minValue: getCardValue(rank1), minRank: rank1 }; } } return null; } // 验证出牌是否合法 validatePlay(playerId, cardIds) { const player = this.players[playerId]; const cards = cardIds.map(id => player.cards.find(c => c.id === id)).filter(Boolean); if (cards.length !== cardIds.length) { return { success: false, message: '牌不存在' }; } const cardType = this.identifyCardType(cards); if (!cardType) { return { success: false, message: '非法牌型' }; } // 如果是新一轮或没人出牌,任意合法牌型都可出 if (!this.lastPlay || this.passCount >= 3) { return { success: true, cardType, cards }; } // 必须牌型相同、长度相同 if (cardType.type !== this.lastPlay.type || cardType.length !== this.lastPlay.length) { // 炸弹可以管任何牌 if (this.isBomb(cardType.type)) { return { success: true, cardType, cards, isBomb: true }; } return { success: false, message: '牌型或长度不匹配' }; } // 比较大小 if (cardType.value > this.lastPlay.value) { return { success: true, cardType, cards }; } return { success: false, message: '牌不够大' }; } // 判断是否为炸弹 isBomb(type) { return [CARD_TYPES.BOMB_4, CARD_TYPES.BOMB_5, CARD_TYPES.BOMB_6, CARD_TYPES.BOMB_7, CARD_TYPES.BOMB_8, CARD_TYPES.JOKER_BOMB].includes(type); } // 执行出牌 playCards(playerId, cardIds) { const validation = this.validatePlay(playerId, cardIds); if (!validation.success) { return validation; } const player = this.players[playerId]; // 从手牌移除 player.cards = player.cards.filter(c => !cardIds.includes(c.id)); // 更新游戏状态 this.lastPlay = validation.cardType; this.lastPlayer = playerId; this.passCount = 0; this.roundPlays.push({ player: playerId, cards: validation.cards, type: validation.cardType }); // 检查是否出完 if (player.cards.length === 0 && !this.finishedPlayers.includes(playerId)) { this.finishedPlayers.push(playerId); player.rank = this.finishedPlayers.length; } // 检查游戏是否结束 if (this.finishedPlayers.length >= 3) { this.endGame(); } else { // 下一个玩家 this.currentPlayer = (playerId + 1) % 4; } return { success: true, cardType: validation.cardType, cards: validation.cards, isFinished: player.cards.length === 0, nextPlayer: this.currentPlayer }; } // 不出 pass(playerId) { if (playerId !== this.currentPlayer) { return { success: false, message: '不是你的回合' }; } // 如果是首轮或自己上轮出的牌,不能不出 if (this.lastPlayer === playerId || !this.lastPlay) { return { success: false, message: '必须出牌' }; } this.passCount++; // 连续3人不出,本轮结束 if (this.passCount >= 3) { this.roundPlays = []; this.passCount = 0; // 最后出牌者获得出牌权 this.currentPlayer = this.lastPlayer; return { success: true, roundEnd: true, nextPlayer: this.currentPlayer }; } this.currentPlayer = (playerId + 1) % 4; return { success: true, nextPlayer: this.currentPlayer }; } // AI决策 makeAIDecision(playerId) { const player = this.players[playerId]; // 如果是新一轮,出最小的牌 if (!this.lastPlay || this.passCount >= 3) { return this.findSmallestPlay(player); } // 尝试找能管上的牌 const play = this.findPlayToBeat(player, this.lastPlay); if (play) { return { action: 'play', cards: play }; } // 不出 return { action: 'pass' }; } // 找最小的出牌(AI用) findSmallestPlay(player) { // 优先出单张、对子 const cards = player.cards; // 尝试出单张(最小的非级牌非王) const normalCards = cards.filter(c => !c.isJoker && c.rank !== this.currentLevel); if (normalCards.length > 0) { const minCard = normalCards.reduce((min, c) => this.getCardPower(c) < this.getCardPower(min) ? c : min ); return { action: 'play', cards: [minCard] }; } // 出级牌单张 const levelCards = cards.filter(c => c.rank === this.currentLevel && !this.isWildCard(c)); if (levelCards.length > 0) { return { action: 'play', cards: [levelCards[0]] }; } // 出百搭 const wildCards = cards.filter(c => this.isWildCard(c)); if (wildCards.length > 0) { return { action: 'play', cards: [wildCards[0]] }; } // 出对子 // ...简化处理,出第一张 return { action: 'play', cards: [cards[0]] }; } // 找能管上的牌(AI用)- 简化版 findPlayToBeat(player, lastPlay) { // 这里简化处理,只检查是否有炸弹 const cards = player.cards; // 检查是否有更大的炸弹 const rankCounts = {}; let wildCount = 0; for (const card of cards) { if (this.isWildCard(card)) { wildCount++; } else { rankCounts[card.rank] = (rankCounts[card.rank] || 0) + 1; } } // 找4张炸弹 for (const rank of Object.keys(rankCounts)) { if (rankCounts[rank] + wildCount >= 4) { const needWild = Math.max(0, 4 - rankCounts[rank]); const playCards = cards.filter(c => c.rank === rank && !this.isWildCard(c)).slice(0, 4 - needWild); // 添加百搭 if (needWild > 0) { playCards.push(...cards.filter(c => this.isWildCard(c)).slice(0, needWild)); } return playCards; } } return null; } // 结束游戏 endGame() { this.gameState = 'ended'; // 给最后一个玩家排名 for (const player of this.players) { if (player.rank === null) { player.rank = 4; } } // 计算升级 const team0Ranks = this.players.filter(p => p.team === 0).map(p => p.rank).sort((a, b) => a - b); const team1Ranks = this.players.filter(p => p.team === 1).map(p => p.rank).sort((a, b) => a - b); let upgrade = 0; let winner = null; // 判断哪队获胜(有头游的队伍) const headPlayer = this.players.find(p => p.rank === 1); winner = headPlayer.team; const winnerRanks = winner === 0 ? team0Ranks : team1Ranks; // 计算升级数 if (winnerRanks[0] === 1 && winnerRanks[1] === 2) { upgrade = 3; // 双上 } else if (winnerRanks[0] === 1 && winnerRanks[1] === 3) { upgrade = 2; // 单上 } else { upgrade = 1; // 险胜 } return { winner: winner, upgrade: upgrade, ranks: this.players.map(p => ({ name: p.name, rank: p.rank, team: p.team })) }; } // 开始下一局 nextRound(winner, upgrade) { // 更新级数 const currentLevelIndex = RANKS.indexOf(this.teamLevels[winner]); const newLevelIndex = Math.min(currentLevelIndex + upgrade, RANKS.length - 1); this.teamLevels[winner] = RANKS[newLevelIndex]; // 检查是否打过A if (this.teamLevels[winner] === 'A' && upgrade === 3) { return { gameOver: true, finalWinner: winner }; } // 重新开始 return this.startGame(); } } // 导出 if (typeof module !== 'undefined' && module.exports) { module.exports = { GuandanGame, CARD_TYPES, generateDeck, shuffleDeck }; }