Files
nanocode/happy-games/games/guandan/game.js
T
Ubuntu 32b43f85bd a
2026-07-03 16:58:28 +08:00

629 lines
22 KiB
JavaScript

/**
* 掼蛋游戏核心逻辑
* 简化版: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 };
}