392 lines
12 KiB
Python
392 lines
12 KiB
Python
"""
|
|
RoomManager 房间管理器
|
|
管理所有游戏房间的生命周期
|
|
"""
|
|
import uuid
|
|
import random
|
|
import string
|
|
import time
|
|
import logging
|
|
from typing import Dict, List, Optional, Type, Any, Tuple
|
|
from threading import Lock
|
|
|
|
from .player import Player, HumanPlayer, AIPlayer
|
|
from .game_base import GameBase
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class Room:
|
|
"""
|
|
房间类
|
|
封装房间的所有状态和逻辑
|
|
"""
|
|
|
|
def __init__(self, room_id: str, room_code: str, game_type: str,
|
|
max_players: int = 3, config: Optional[Dict] = None):
|
|
self.id = room_id
|
|
self.code = room_code
|
|
self.game_type = game_type
|
|
self.max_players = max_players
|
|
self.config = config or {}
|
|
|
|
self.status = 'waiting' # waiting, playing, ended, closed
|
|
self.created_at = time.time()
|
|
self.started_at: Optional[float] = None
|
|
self.ended_at: Optional[float] = None
|
|
|
|
# 玩家
|
|
self.players: Dict[str, Player] = {} # player_id -> Player
|
|
self.seats: Dict[int, Optional[str]] = {} # seat -> player_id
|
|
|
|
# 房主
|
|
self.host_id: Optional[str] = None
|
|
|
|
# 游戏实例
|
|
self.game: Optional[GameBase] = None
|
|
|
|
# 最后活动时间
|
|
self.last_activity = time.time()
|
|
|
|
@property
|
|
def player_count(self) -> int:
|
|
return len(self.players)
|
|
|
|
@property
|
|
def is_full(self) -> bool:
|
|
return self.player_count >= self.max_players
|
|
|
|
@property
|
|
def is_empty(self) -> bool:
|
|
return self.player_count == 0
|
|
|
|
@property
|
|
def all_ready(self) -> bool:
|
|
"""所有人类玩家是否已准备"""
|
|
for player in self.players.values():
|
|
if not player.is_ai and not player.is_ready:
|
|
return False
|
|
return self.player_count > 0
|
|
|
|
def get_next_seat(self) -> int:
|
|
"""获取下一个可用座位号"""
|
|
for i in range(self.max_players):
|
|
if i not in self.seats or not self.seats[i]:
|
|
return i
|
|
return -1
|
|
|
|
def add_player(self, player: Player) -> bool:
|
|
"""添加玩家到房间"""
|
|
if self.is_full:
|
|
return False
|
|
|
|
seat = self.get_next_seat()
|
|
if seat == -1:
|
|
return False
|
|
|
|
# 设置房主
|
|
if self.host_id is None:
|
|
self.host_id = player.id
|
|
|
|
player.join_room(self.id, seat)
|
|
self.players[player.id] = player
|
|
self.seats[seat] = player.id
|
|
|
|
self.last_activity = time.time()
|
|
return True
|
|
|
|
def remove_player(self, player_id: str) -> bool:
|
|
"""从房间移除玩家"""
|
|
if player_id not in self.players:
|
|
return False
|
|
|
|
player = self.players[player_id]
|
|
if player.seat_number is not None:
|
|
self.seats[player.seat_number] = None
|
|
|
|
player.leave_room()
|
|
del self.players[player_id]
|
|
|
|
# 更换房主
|
|
if self.host_id == player_id and self.players:
|
|
self.host_id = next(iter(self.players.keys()))
|
|
elif not self.players:
|
|
self.host_id = None
|
|
|
|
self.last_activity = time.time()
|
|
return True
|
|
|
|
def get_player_by_socket(self, socket_id: str) -> Optional[Player]:
|
|
"""通过 socket_id 查找玩家"""
|
|
for player in self.players.values():
|
|
if player.socket_id == socket_id:
|
|
return player
|
|
return None
|
|
|
|
def start_game(self, game_class: Type[GameBase]) -> bool:
|
|
"""开始游戏"""
|
|
if self.status != 'waiting':
|
|
return False
|
|
|
|
# 斗地主需要3人
|
|
if self.game_type == 'doudizhu' and self.player_count < 3:
|
|
return False
|
|
|
|
# TestGame 需要1人
|
|
if self.game_type == 'test_game' and self.player_count < 1:
|
|
return False
|
|
|
|
# 创建游戏实例
|
|
self.game = game_class(self.id, self.game_type)
|
|
|
|
# 添加所有玩家
|
|
for player in self.players.values():
|
|
self.game.add_player(player)
|
|
|
|
# 开始游戏
|
|
self.game.start()
|
|
|
|
self.status = 'playing'
|
|
self.started_at = time.time()
|
|
self.last_activity = time.time()
|
|
|
|
return True
|
|
|
|
def end_game(self):
|
|
"""结束游戏"""
|
|
if self.status == 'playing' and self.game:
|
|
self.game.end_game()
|
|
|
|
self.status = 'ended'
|
|
self.ended_at = time.time()
|
|
self.last_activity = time.time()
|
|
|
|
def close(self):
|
|
"""关闭房间"""
|
|
self.status = 'closed'
|
|
self.players.clear()
|
|
self.seats.clear()
|
|
|
|
def to_dict(self, include_game_state: bool = True) -> Dict[str, Any]:
|
|
"""序列化为字典"""
|
|
data = {
|
|
'id': self.id,
|
|
'code': self.code,
|
|
'game_type': self.game_type,
|
|
'status': self.status,
|
|
'max_players': self.max_players,
|
|
'player_count': self.player_count,
|
|
'host_id': self.host_id,
|
|
'created_at': self.created_at,
|
|
'players': [p.to_dict() for p in self.players.values()],
|
|
}
|
|
|
|
if include_game_state and self.game:
|
|
data['game'] = self.game.to_dict()
|
|
|
|
return data
|
|
|
|
def update_activity(self):
|
|
"""更新最后活动时间"""
|
|
self.last_activity = time.time()
|
|
|
|
|
|
class RoomManager:
|
|
"""
|
|
房间管理器
|
|
单例模式,管理所有房间
|
|
"""
|
|
|
|
_instance = None
|
|
_lock = Lock()
|
|
|
|
def __new__(cls):
|
|
if cls._instance is None:
|
|
with cls._lock:
|
|
if cls._instance is None:
|
|
cls._instance = super().__new__(cls)
|
|
cls._instance._initialized = False
|
|
return cls._instance
|
|
|
|
def __init__(self):
|
|
if self._initialized:
|
|
return
|
|
|
|
self._rooms: Dict[str, Room] = {} # room_id -> Room
|
|
self._room_codes: Dict[str, str] = {} # room_code -> room_id
|
|
self._player_rooms: Dict[str, str] = {} # player_id -> room_id
|
|
|
|
self._game_registry: Dict[str, Type[GameBase]] = {} # game_type -> GameClass
|
|
|
|
self._initialized = True
|
|
|
|
def register_game(self, game_type: str, game_class: Type[GameBase]):
|
|
"""注册游戏类型"""
|
|
self._game_registry[game_type] = game_class
|
|
|
|
def _generate_room_code(self) -> str:
|
|
"""生成6位房间号"""
|
|
while True:
|
|
code = ''.join(random.choices(string.ascii_uppercase + string.digits, k=6))
|
|
if code not in self._room_codes:
|
|
return code
|
|
|
|
def create_room(self, game_type: str, max_players: int = 3,
|
|
config: Optional[Dict] = None) -> Optional[Room]:
|
|
"""创建房间"""
|
|
if game_type not in self._game_registry:
|
|
return None
|
|
|
|
room_id = f"room_{uuid.uuid4().hex[:12]}"
|
|
room_code = self._generate_room_code()
|
|
|
|
room = Room(room_id, room_code, game_type, max_players, config)
|
|
|
|
self._rooms[room_id] = room
|
|
self._room_codes[room_code] = room_id
|
|
|
|
return room
|
|
|
|
def get_room(self, room_id: Optional[str] = None,
|
|
room_code: Optional[str] = None) -> Optional[Room]:
|
|
"""获取房间"""
|
|
if room_id:
|
|
return self._rooms.get(room_id)
|
|
|
|
if room_code:
|
|
room_id = self._room_codes.get(room_code)
|
|
if room_id:
|
|
return self._rooms.get(room_id)
|
|
|
|
return None
|
|
|
|
def get_room_by_player(self, player_id: str) -> Optional[Room]:
|
|
"""通过玩家ID获取房间"""
|
|
room_id = self._player_rooms.get(player_id)
|
|
if room_id:
|
|
return self._rooms.get(room_id)
|
|
return None
|
|
|
|
def join_room(self, room_code: str, player: Player) -> Tuple[bool, str]:
|
|
"""
|
|
玩家加入房间
|
|
返回: (success, message)
|
|
"""
|
|
room = self.get_room(room_code=room_code)
|
|
if not room:
|
|
return False, "房间不存在"
|
|
|
|
if room.status != 'waiting':
|
|
return False, "游戏已开始"
|
|
|
|
if room.is_full:
|
|
return False, "房间已满"
|
|
|
|
# 如果玩家已经在其他房间,先离开
|
|
if player.id in self._player_rooms:
|
|
old_room_id = self._player_rooms[player.id]
|
|
if old_room_id != room.id:
|
|
self.leave_room(player.id)
|
|
|
|
if room.add_player(player):
|
|
self._player_rooms[player.id] = room.id
|
|
return True, "加入成功"
|
|
|
|
return False, "加入失败"
|
|
|
|
def leave_room(self, player_id: str) -> bool:
|
|
"""玩家离开房间"""
|
|
room = self.get_room_by_player(player_id)
|
|
if not room:
|
|
return False
|
|
|
|
room.remove_player(player_id)
|
|
|
|
if player_id in self._player_rooms:
|
|
del self._player_rooms[player_id]
|
|
|
|
# 如果房间空了,延迟关闭
|
|
if room.is_empty:
|
|
room.close()
|
|
self._cleanup_room(room.id)
|
|
|
|
return True
|
|
|
|
def start_game(self, room_id: str) -> Tuple[bool, str]:
|
|
"""开始游戏"""
|
|
room = self.get_room(room_id)
|
|
if not room:
|
|
return False, "房间不存在"
|
|
|
|
game_class = self._game_registry.get(room.game_type)
|
|
if not game_class:
|
|
return False, "游戏类型未注册"
|
|
|
|
if room.start_game(game_class):
|
|
return True, "游戏开始"
|
|
|
|
return False, "无法开始游戏"
|
|
|
|
def get_all_rooms(self, game_type: Optional[str] = None,
|
|
status: Optional[str] = None) -> List[Room]:
|
|
"""获取所有房间"""
|
|
rooms = list(self._rooms.values())
|
|
|
|
if game_type:
|
|
rooms = [r for r in rooms if r.game_type == game_type]
|
|
|
|
if status:
|
|
rooms = [r for r in rooms if r.status == status]
|
|
|
|
return rooms
|
|
|
|
def _cleanup_room(self, room_id: str):
|
|
"""清理房间"""
|
|
if room_id in self._rooms:
|
|
room = self._rooms[room_id]
|
|
if room.code in self._room_codes:
|
|
del self._room_codes[room.code]
|
|
del self._rooms[room_id]
|
|
|
|
def cleanup_inactive_rooms(self, max_inactive_minutes: int = 30):
|
|
"""清理不活跃房间
|
|
|
|
清理策略:
|
|
- 所有人类玩家都断开且游戏已结束:5分钟后清理
|
|
- 所有人类玩家都断开但游戏进行中:10分钟后清理
|
|
- 有活跃玩家但长期无活动:30分钟后清理
|
|
"""
|
|
import time as _time
|
|
now = _time.time()
|
|
to_remove = []
|
|
|
|
for room_id, room in list(self._rooms.items()):
|
|
# 计算人类玩家连接状态
|
|
human_players = [p for p in room.players.values() if not p.is_ai]
|
|
all_disconnected = len(human_players) > 0 and all(not p.is_connected for p in human_players)
|
|
|
|
inactive_time = now - room.last_activity
|
|
|
|
if all_disconnected:
|
|
# 所有人类玩家都断开了
|
|
if room.status in ('ended', 'closed'):
|
|
# 游戏已结束,5分钟后清理
|
|
if inactive_time > 5 * 60:
|
|
to_remove.append(room_id)
|
|
logger.info(f"清理已结束的空房间: {room.code} (断开 {int(inactive_time//60)} 分钟)")
|
|
else:
|
|
# 游戏进行中,10分钟后清理
|
|
if inactive_time > 10 * 60:
|
|
to_remove.append(room_id)
|
|
logger.info(f"清理进行中的空房间: {room.code} (断开 {int(inactive_time//60)} 分钟)")
|
|
else:
|
|
# 有活跃玩家,使用标准超时
|
|
if inactive_time > max_inactive_minutes * 60:
|
|
to_remove.append(room_id)
|
|
logger.info(f"清理不活跃房间: {room.code} (无活动 {int(inactive_time//60)} 分钟)")
|
|
|
|
for room_id in to_remove:
|
|
self._cleanup_room(room_id)
|
|
|
|
return len(to_remove)
|