Files
nanocode/net-games/doudizhu/game.py
T
Ubuntu 32b43f85bd a
2026-07-03 16:58:28 +08:00

471 lines
17 KiB
Python

"""
斗地主游戏服务端实现
"""
import random
import logging
from typing import Dict, List, Optional, Any, Tuple
from enum import Enum
import sys
import os
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
logger = logging.getLogger(__name__)
from engine.game_base import GameBase, GamePhase
from engine.player import Player
from doudizhu.game_logic import (
Deck, Card, identify_card_type, can_beat,
evaluate_hand_strength, cards_to_dicts, find_cards_by_ids,
CardType
)
class DoudizhuPhase(Enum):
"""斗地主游戏阶段"""
DEALING = "dealing" # 发牌中
CALLING = "calling" # 叫地主
PLAYING = "playing" # 出牌中
class DoudizhuGame(GameBase):
"""
斗地主游戏
3人游戏,支持 AI 和真人玩家
"""
def __init__(self, room_id: str, game_type: str = "doudizhu"):
super().__init__(room_id, game_type)
# 斗地主特定状态
self.deck: List[Card] = []
self.bottom_cards: List[Card] = []
self.lord_id: Optional[str] = None
self.doudizhu_phase = DoudizhuPhase.DEALING
self.desktop_cards: Optional[Dict] = None # 桌面上的牌型
self.last_played_id: Optional[str] = None # 最后出牌的玩家
self.pass_count = 0 # 连续不出次数
# 玩家手牌
self.player_cards: Dict[str, List[Card]] = {}
# 叫地主相关
self.calling_order: List[str] = [] # 叫地主顺序
self.calling_index = 0 # 当前叫地主索引
self.call_records: List[Dict] = [] # 叫地主记录
@property
def min_players(self) -> int:
"""最少玩家数"""
return 3
@property
def max_players(self) -> int:
"""最多玩家数"""
return 3
@property
def player_count(self) -> int:
return len(self.players)
def can_start(self) -> bool:
"""检查是否可以开始游戏"""
return self.player_count == self.max_players
def start(self):
"""开始游戏 - 发牌阶段"""
if not self.can_start():
raise ValueError("需要3名玩家才能开始")
# 初始化玩家手牌
self.player_cards = {pid: [] for pid in self.players}
# 创建并洗牌
self.deck = Deck.shuffle(Deck.create_full_deck())
# 发牌:每人17张
for i in range(51):
player_id = self.player_order[i % 3]
self.player_cards[player_id].append(self.deck[i])
# 剩余3张作为底牌
self.bottom_cards = self.deck[51:54]
# 排序手牌
for pid in self.player_cards:
self.player_cards[pid] = Deck.sort_cards(self.player_cards[pid])
# 初始化 state.player_states(供客户端使用)
for pid in self.players:
self.state.player_states[pid] = {
'cards': cards_to_dicts(self.player_cards[pid]),
'card_count': len(self.player_cards[pid]),
'score': 0
}
# 进入叫地主阶段
self.doudizhu_phase = DoudizhuPhase.CALLING
self.calling_order = self.player_order.copy()
self.calling_index = 0
# 随机决定谁先叫地主
random.shuffle(self.calling_order)
# 设置第一个叫地主的玩家
self.state.next_turn(self.calling_order[0])
self.state.phase = GamePhase.PLAYING
self.is_started = True
# 更新游戏状态数据
self._update_state_data()
def _update_state_data(self):
"""更新游戏状态数据"""
# desktop_cards 需要移除 cards 字段以便 JSON 序列化
desktop_cards_clean = None
if self.desktop_cards:
desktop_cards_clean = {k: v for k, v in self.desktop_cards.items() if k != 'cards'}
self.state.data = {
'phase': self.doudizhu_phase.value,
'current_turn': self.state.current_turn_player_id,
'lord_id': self.lord_id,
'bottom_cards': cards_to_dicts(self.bottom_cards) if self.lord_id else [],
'desktop_cards': desktop_cards_clean,
'last_played_id': self.last_played_id,
'pass_count': self.pass_count,
'player_cards_count': {
pid: len(cards) for pid, cards in self.player_cards.items()
},
}
def get_player_view(self, player_id: str) -> Dict[str, Any]:
"""
获取玩家的视角(只能看到自己的牌)
"""
view = self.to_dict()
# 只包含自己的手牌
for pid in self.players:
if pid == player_id:
view['my_cards'] = cards_to_dicts(self.player_cards.get(pid, []))
else:
view['my_cards'] = []
return view
def handle_action(self, player_id: str, action: str, data: Dict) -> Dict[str, Any]:
"""处理玩家操作"""
# 检查玩家是否存在
if player_id not in self.players:
return {'success': False, 'message': '玩家不存在'}
# 检查是否是当前回合
if self.state.current_turn_player_id != player_id:
return {'success': False, 'message': '不是你的回合'}
if self.doudizhu_phase == DoudizhuPhase.CALLING:
return self._handle_calling_action(player_id, action, data)
elif self.doudizhu_phase == DoudizhuPhase.PLAYING:
return self._handle_playing_action(player_id, action, data)
else:
return {'success': False, 'message': '无效的游戏阶段'}
def _handle_calling_action(self, player_id: str, action: str, data: Dict) -> Dict[str, Any]:
"""处理叫地主阶段的操作"""
if action not in ['call', 'pass']:
return {'success': False, 'message': '无效的操作,只能是 call 或 pass'}
want_to_call = data.get('want', False)
# 记录叫地主
self.call_records.append({
'player_id': player_id,
'action': action,
'want': want_to_call
})
if action == 'call' and want_to_call:
# 成为地主
self.lord_id = player_id
logger.info(f"Player {player_id} calling lord, before: {len(self.player_cards[player_id])} cards")
self.player_cards[player_id].extend(self.bottom_cards)
self.player_cards[player_id] = Deck.sort_cards(self.player_cards[player_id])
logger.info(f"After adding bottom cards: {len(self.player_cards[player_id])} cards")
# 更新 state.player_states
self.state.player_states[player_id] = {
'cards': cards_to_dicts(self.player_cards[player_id]),
'card_count': len(self.player_cards[player_id]),
'score': 0
}
# 设置地主为第一个出牌
self.doudizhu_phase = DoudizhuPhase.PLAYING
self.state.next_turn(player_id)
self._update_state_data()
return {
'success': True,
'message': f'{self.players[player_id].name} 成为地主',
'lord_id': player_id,
'bottom_cards': cards_to_dicts(self.bottom_cards),
'phase': 'playing'
}
# 下一个叫地主
self.calling_index += 1
# 如果都没叫,随机选一个人当地主
if self.calling_index >= len(self.calling_order):
lord_id = random.choice(self.calling_order)
self.lord_id = lord_id
self.player_cards[lord_id].extend(self.bottom_cards)
self.player_cards[lord_id] = Deck.sort_cards(self.player_cards[lord_id])
# 更新 state.player_states
self.state.player_states[lord_id] = {
'cards': cards_to_dicts(self.player_cards[lord_id]),
'card_count': len(self.player_cards[lord_id]),
'score': 0
}
self.doudizhu_phase = DoudizhuPhase.PLAYING
self.state.next_turn(lord_id)
self._update_state_data()
return {
'success': True,
'message': f'{self.players[lord_id].name} 成为地主(随机)',
'lord_id': lord_id,
'bottom_cards': cards_to_dicts(self.bottom_cards),
'phase': 'playing'
}
# 轮到下一个
next_caller = self.calling_order[self.calling_index]
self.state.next_turn(next_caller)
self._update_state_data()
return {
'success': True,
'message': f'轮到 {self.players[next_caller].name} 叫地主',
'next_caller': next_caller
}
def _handle_playing_action(self, player_id: str, action: str, data: Dict) -> Dict[str, Any]:
"""处理出牌阶段的操作"""
import logging
logger = logging.getLogger(__name__)
if action not in ['play', 'pass']:
return {'success': False, 'message': '无效的操作,只能是 play 或 pass'}
if action == 'pass':
return self._handle_pass(player_id)
# 出牌
card_ids = data.get('card_ids', [])
logger.info(f"Player {player_id} trying to play cards: {card_ids}")
logger.info(f"Player cards: {[c.id for c in self.player_cards[player_id]]}")
if not card_ids:
return {'success': False, 'message': '请选择要出的牌'}
# 查找牌
cards = find_cards_by_ids(self.player_cards[player_id], card_ids)
logger.info(f"Found cards: {cards}")
if len(cards) != len(card_ids):
return {'success': False, 'message': f'无效的牌ID: 期望{card_ids}, 找到{[c.id for c in cards]}'}
# 识别牌型
card_type = identify_card_type(cards)
if not card_type:
return {'success': False, 'message': '非法牌型'}
# 验证能否管上
if self.desktop_cards and not can_beat(self.desktop_cards, card_type):
return {'success': False, 'message': '管不上'}
# 检查牌是否都在手中
player_card_ids = {c.id for c in self.player_cards[player_id]}
if not set(card_ids).issubset(player_card_ids):
return {'success': False, 'message': '牌不在手中'}
# 执行出牌
self.player_cards[player_id] = [
c for c in self.player_cards[player_id]
if c.id not in card_ids
]
# 更新 state.player_states
self.state.player_states[player_id] = {
'cards': cards_to_dicts(self.player_cards[player_id]),
'card_count': len(self.player_cards[player_id]),
'score': 0
}
self.desktop_cards = card_type
self.last_played_id = player_id
self.pass_count = 0
# 记录历史
self.state.add_history('play', player_id, {
'cards': cards_to_dicts(cards),
'card_type': card_type['name']
})
# 检查胜利
if len(self.player_cards[player_id]) == 0:
self.end_game(player_id)
self._update_state_data()
return {
'success': True,
'message': f'{self.players[player_id].name} 获胜!',
'winner': player_id,
'played_cards': cards_to_dicts(cards),
'card_type': {k: v for k, v in card_type.items() if k != 'cards'},
'game_over': True
}
# 下一个玩家
self._next_player()
self._update_state_data()
return {
'success': True,
'message': f'{self.players[player_id].name} 出了 {card_type["name"]}',
'played_cards': cards_to_dicts(cards),
'card_type': {k: v for k, v in card_type.items() if k != 'cards'},
'next_player': self.state.current_turn_player_id,
'remaining_cards': len(self.player_cards[player_id])
}
def _handle_pass(self, player_id: str) -> Dict[str, Any]:
"""处理不出"""
# 如果自己是最后出牌的,不能不出
if self.last_played_id == player_id:
return {'success': False, 'message': '你必须出牌'}
self.pass_count += 1
# 记录历史
self.state.add_history('pass', player_id, {})
# 如果连续两人不出,清空桌面,轮到下家
if self.pass_count >= 2:
self.desktop_cards = None
self.pass_count = 0
self._next_player() # 关键:清空后轮到下家出牌
self._update_state_data()
return {
'success': True,
'message': f'{self.players[player_id].name} 不出,桌面清空',
'clear_desktop': True,
'next_player': self.state.current_turn_player_id
}
self._next_player()
self._update_state_data()
return {
'success': True,
'message': f'{self.players[player_id].name} 不出',
'next_player': self.state.current_turn_player_id
}
def _next_player(self):
"""切换到下一个玩家"""
current_idx = self.player_order.index(self.state.current_turn_player_id)
next_idx = (current_idx + 1) % 3
self.state.next_turn(self.player_order[next_idx])
def get_valid_actions(self, player_id: str) -> List[str]:
"""获取可用操作"""
if self.state.current_turn_player_id != player_id:
return []
if self.doudizhu_phase == DoudizhuPhase.CALLING:
return ['call', 'pass']
elif self.doudizhu_phase == DoudizhuPhase.PLAYING:
actions = ['pass']
# 检查是否可以出牌
if self.last_played_id != player_id:
actions.append('play')
else:
# 自己是最后出牌的,必须出牌
actions = ['play']
return actions
return []
def get_current_player(self):
"""获取当前回合玩家"""
if self.state.current_turn_player_id:
return self.players.get(self.state.current_turn_player_id)
return None
def check_winner(self) -> Optional[str]:
"""检查获胜者"""
return self.state.winner_id
def to_dict(self) -> Dict[str, Any]:
"""序列化游戏状态"""
data = super().to_dict()
# 添加斗地主特定数据
# desktop_cards 需要正确处理cards字段以便 JSON 序列化
desktop_cards_clean = None
if self.desktop_cards:
desktop_cards_clean = {
k: (cards_to_dicts(v) if k == 'cards' else v)
for k, v in self.desktop_cards.items()
}
player_cards_count = {
pid: len(cards) for pid, cards in self.player_cards.items()
}
logger.info(f"to_dict: player_cards_count = {player_cards_count}")
data['doudizhu'] = {
'phase': self.doudizhu_phase.value if self.doudizhu_phase else None,
'lord_id': self.lord_id,
'bottom_cards': cards_to_dicts(self.bottom_cards) if self.lord_id else [],
'desktop_cards': desktop_cards_clean,
'last_played_id': self.last_played_id,
'pass_count': self.pass_count,
'player_cards_count': player_cards_count,
}
return data
def get_game_result(self) -> Dict[str, Any]:
"""获取游戏结果"""
if not self.is_ended:
return {'ended': False}
winner = self.players.get(self.state.winner_id)
is_lord_win = self.state.winner_id == self.lord_id
return {
'ended': True,
'winner_id': self.state.winner_id,
'winner_name': winner.name if winner else 'Unknown',
'is_lord_win': is_lord_win,
'lord_id': self.lord_id,
'players': [
{
'id': pid,
'name': p.name,
'is_lord': pid == self.lord_id,
'remaining_cards': len(self.player_cards.get(pid, []))
}
for pid, p in self.players.items()
]
}