feat: Initial project structure for WebTriviasMulti
- Backend: FastAPI + Python-SocketIO + SQLAlchemy - Models for categories, questions, game sessions, events - AI services for answer validation and question generation (Claude) - Room management with Redis - Game logic with stealing mechanics - Admin API for question management - Frontend: React + Vite + TypeScript + Tailwind - 5 visual themes (DRRR, Retro, Minimal, RGB, Anime 90s) - Real-time game with Socket.IO - Achievement system - Replay functionality - Sound effects per theme - Docker Compose for deployment - Design documentation Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
173
backend/app/services/room_manager.py
Normal file
173
backend/app/services/room_manager.py
Normal file
@@ -0,0 +1,173 @@
|
||||
import json
|
||||
import random
|
||||
import string
|
||||
from typing import Optional
|
||||
import redis.asyncio as redis
|
||||
from app.config import get_settings
|
||||
|
||||
settings = get_settings()
|
||||
|
||||
|
||||
class RoomManager:
|
||||
def __init__(self):
|
||||
self.redis: Optional[redis.Redis] = None
|
||||
|
||||
async def connect(self):
|
||||
if not self.redis:
|
||||
self.redis = await redis.from_url(settings.redis_url)
|
||||
|
||||
async def disconnect(self):
|
||||
if self.redis:
|
||||
await self.redis.close()
|
||||
|
||||
def _generate_room_code(self) -> str:
|
||||
"""Generate a 6-character room code."""
|
||||
return ''.join(random.choices(string.ascii_uppercase + string.digits, k=6))
|
||||
|
||||
async def create_room(self, player_name: str, socket_id: str) -> dict:
|
||||
"""Create a new game room."""
|
||||
await self.connect()
|
||||
|
||||
# Generate unique room code
|
||||
room_code = self._generate_room_code()
|
||||
while await self.redis.exists(f"room:{room_code}"):
|
||||
room_code = self._generate_room_code()
|
||||
|
||||
# Create room state
|
||||
room_state = {
|
||||
"code": room_code,
|
||||
"status": "waiting",
|
||||
"host": player_name,
|
||||
"teams": {
|
||||
"A": [],
|
||||
"B": []
|
||||
},
|
||||
"current_team": None,
|
||||
"current_player_index": {"A": 0, "B": 0},
|
||||
"current_question": None,
|
||||
"can_steal": False,
|
||||
"scores": {"A": 0, "B": 0},
|
||||
"questions_used": [],
|
||||
"board": {}
|
||||
}
|
||||
|
||||
# Save room state
|
||||
await self.redis.setex(
|
||||
f"room:{room_code}",
|
||||
3600 * 3, # 3 hours TTL
|
||||
json.dumps(room_state)
|
||||
)
|
||||
|
||||
# Add player to room
|
||||
await self.add_player(room_code, player_name, "A", socket_id)
|
||||
|
||||
return room_state
|
||||
|
||||
async def get_room(self, room_code: str) -> Optional[dict]:
|
||||
"""Get room state by code."""
|
||||
await self.connect()
|
||||
data = await self.redis.get(f"room:{room_code}")
|
||||
if data:
|
||||
return json.loads(data)
|
||||
return None
|
||||
|
||||
async def update_room(self, room_code: str, room_state: dict) -> bool:
|
||||
"""Update room state."""
|
||||
await self.connect()
|
||||
await self.redis.setex(
|
||||
f"room:{room_code}",
|
||||
3600 * 3,
|
||||
json.dumps(room_state)
|
||||
)
|
||||
return True
|
||||
|
||||
async def add_player(
|
||||
self,
|
||||
room_code: str,
|
||||
player_name: str,
|
||||
team: str,
|
||||
socket_id: str
|
||||
) -> Optional[dict]:
|
||||
"""Add a player to a room."""
|
||||
room = await self.get_room(room_code)
|
||||
if not room:
|
||||
return None
|
||||
|
||||
# Check if team is full
|
||||
if len(room["teams"][team]) >= 4:
|
||||
return None
|
||||
|
||||
# Check if name is taken
|
||||
for t in ["A", "B"]:
|
||||
for p in room["teams"][t]:
|
||||
if p["name"].lower() == player_name.lower():
|
||||
return None
|
||||
|
||||
# Add player
|
||||
player = {
|
||||
"name": player_name,
|
||||
"team": team,
|
||||
"position": len(room["teams"][team]),
|
||||
"socket_id": socket_id
|
||||
}
|
||||
room["teams"][team].append(player)
|
||||
|
||||
# Save player mapping
|
||||
await self.redis.setex(
|
||||
f"player:{socket_id}",
|
||||
3600 * 3,
|
||||
json.dumps({"name": player_name, "room": room_code, "team": team})
|
||||
)
|
||||
|
||||
await self.update_room(room_code, room)
|
||||
return room
|
||||
|
||||
async def remove_player(self, socket_id: str) -> Optional[dict]:
|
||||
"""Remove a player from their room."""
|
||||
await self.connect()
|
||||
|
||||
# Get player info
|
||||
player_data = await self.redis.get(f"player:{socket_id}")
|
||||
if not player_data:
|
||||
return None
|
||||
|
||||
player_info = json.loads(player_data)
|
||||
room_code = player_info["room"]
|
||||
team = player_info["team"]
|
||||
|
||||
# Get room
|
||||
room = await self.get_room(room_code)
|
||||
if not room:
|
||||
return None
|
||||
|
||||
# Remove player from team
|
||||
room["teams"][team] = [
|
||||
p for p in room["teams"][team] if p["socket_id"] != socket_id
|
||||
]
|
||||
|
||||
# Update positions
|
||||
for i, p in enumerate(room["teams"][team]):
|
||||
p["position"] = i
|
||||
|
||||
# Delete player mapping
|
||||
await self.redis.delete(f"player:{socket_id}")
|
||||
|
||||
# If room is empty, delete it
|
||||
if not room["teams"]["A"] and not room["teams"]["B"]:
|
||||
await self.redis.delete(f"room:{room_code}")
|
||||
return None
|
||||
|
||||
await self.update_room(room_code, room)
|
||||
return room
|
||||
|
||||
async def get_player(self, socket_id: str) -> Optional[dict]:
|
||||
"""Get player info by socket ID."""
|
||||
await self.connect()
|
||||
data = await self.redis.get(f"player:{socket_id}")
|
||||
if data:
|
||||
return json.loads(data)
|
||||
return None
|
||||
|
||||
|
||||
# Singleton instance
|
||||
room_manager = RoomManager()
|
||||
Reference in New Issue
Block a user