""" 斗地主游戏服务端实现 """ 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() ] }