471 lines
17 KiB
Python
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()
|
|
]
|
|
}
|