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

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)