Files
Ubuntu 32b43f85bd a
2026-07-03 16:58:28 +08:00

448 lines
14 KiB
JavaScript

/**
* 斗地主游戏核心逻辑
* 本地实现,完整游戏流程
*/
// ========== 牌组定义 ==========
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 };
}