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:
312
backend/app/sockets/game_events.py
Normal file
312
backend/app/sockets/game_events.py
Normal file
@@ -0,0 +1,312 @@
|
||||
import socketio
|
||||
from datetime import datetime
|
||||
from app.services.room_manager import room_manager
|
||||
from app.services.game_manager import game_manager
|
||||
|
||||
|
||||
def register_socket_events(sio: socketio.AsyncServer):
|
||||
"""Register all Socket.IO event handlers."""
|
||||
|
||||
@sio.event
|
||||
async def connect(sid, environ):
|
||||
print(f"Client connected: {sid}")
|
||||
|
||||
@sio.event
|
||||
async def disconnect(sid):
|
||||
print(f"Client disconnected: {sid}")
|
||||
# Remove player from room
|
||||
room = await room_manager.remove_player(sid)
|
||||
if room:
|
||||
await sio.emit(
|
||||
"player_left",
|
||||
{"room": room},
|
||||
room=room["code"]
|
||||
)
|
||||
|
||||
@sio.event
|
||||
async def create_room(sid, data):
|
||||
"""Create a new game room."""
|
||||
player_name = data.get("player_name", "Player")
|
||||
|
||||
room = await room_manager.create_room(player_name, sid)
|
||||
|
||||
# Join socket room
|
||||
sio.enter_room(sid, room["code"])
|
||||
|
||||
await sio.emit("room_created", {"room": room}, to=sid)
|
||||
|
||||
@sio.event
|
||||
async def join_room(sid, data):
|
||||
"""Join an existing room."""
|
||||
room_code = data.get("room_code", "").upper()
|
||||
player_name = data.get("player_name", "Player")
|
||||
team = data.get("team", "A")
|
||||
|
||||
room = await room_manager.add_player(room_code, player_name, team, sid)
|
||||
|
||||
if not room:
|
||||
await sio.emit(
|
||||
"error",
|
||||
{"message": "Could not join room. It may be full or the name is taken."},
|
||||
to=sid
|
||||
)
|
||||
return
|
||||
|
||||
# Join socket room
|
||||
sio.enter_room(sid, room_code)
|
||||
|
||||
# Notify all players
|
||||
await sio.emit("player_joined", {"room": room}, room=room_code)
|
||||
|
||||
@sio.event
|
||||
async def change_team(sid, data):
|
||||
"""Switch player to another team."""
|
||||
player = await room_manager.get_player(sid)
|
||||
if not player:
|
||||
return
|
||||
|
||||
room_code = player["room"]
|
||||
new_team = data.get("team")
|
||||
|
||||
room = await room_manager.get_room(room_code)
|
||||
if not room or len(room["teams"][new_team]) >= 4:
|
||||
await sio.emit(
|
||||
"error",
|
||||
{"message": "Cannot change team. It may be full."},
|
||||
to=sid
|
||||
)
|
||||
return
|
||||
|
||||
# Remove from current team
|
||||
current_team = player["team"]
|
||||
room["teams"][current_team] = [
|
||||
p for p in room["teams"][current_team] if p["socket_id"] != sid
|
||||
]
|
||||
|
||||
# Add to new team
|
||||
room["teams"][new_team].append({
|
||||
"name": player["name"],
|
||||
"team": new_team,
|
||||
"position": len(room["teams"][new_team]),
|
||||
"socket_id": sid
|
||||
})
|
||||
|
||||
await room_manager.update_room(room_code, room)
|
||||
await sio.emit("team_changed", {"room": room}, room=room_code)
|
||||
|
||||
@sio.event
|
||||
async def start_game(sid, data):
|
||||
"""Start the game (host only)."""
|
||||
player = await room_manager.get_player(sid)
|
||||
if not player:
|
||||
return
|
||||
|
||||
room_code = player["room"]
|
||||
room = await room_manager.get_room(room_code)
|
||||
|
||||
if not room:
|
||||
return
|
||||
|
||||
# Check if player is host
|
||||
if room["host"] != player["name"]:
|
||||
await sio.emit(
|
||||
"error",
|
||||
{"message": "Only the host can start the game."},
|
||||
to=sid
|
||||
)
|
||||
return
|
||||
|
||||
# Check minimum players
|
||||
if not room["teams"]["A"] or not room["teams"]["B"]:
|
||||
await sio.emit(
|
||||
"error",
|
||||
{"message": "Both teams need at least one player."},
|
||||
to=sid
|
||||
)
|
||||
return
|
||||
|
||||
# Get board from data or generate
|
||||
board = data.get("board", {})
|
||||
|
||||
updated_room = await game_manager.start_game(room_code, board)
|
||||
|
||||
if updated_room:
|
||||
await sio.emit("game_started", {"room": updated_room}, room=room_code)
|
||||
|
||||
@sio.event
|
||||
async def select_question(sid, data):
|
||||
"""Select a question from the board."""
|
||||
player = await room_manager.get_player(sid)
|
||||
if not player:
|
||||
return
|
||||
|
||||
room_code = player["room"]
|
||||
question_id = data.get("question_id")
|
||||
category_id = data.get("category_id")
|
||||
|
||||
room = await game_manager.select_question(room_code, question_id, category_id)
|
||||
|
||||
if room:
|
||||
# Get current player info
|
||||
current_player = await game_manager.get_current_player(room)
|
||||
|
||||
await sio.emit(
|
||||
"question_selected",
|
||||
{
|
||||
"room": room,
|
||||
"question_id": question_id,
|
||||
"current_player": current_player
|
||||
},
|
||||
room=room_code
|
||||
)
|
||||
|
||||
@sio.event
|
||||
async def submit_answer(sid, data):
|
||||
"""Submit an answer to the current question."""
|
||||
player = await room_manager.get_player(sid)
|
||||
if not player:
|
||||
return
|
||||
|
||||
room_code = player["room"]
|
||||
answer = data.get("answer", "")
|
||||
question = data.get("question", {})
|
||||
is_steal = data.get("is_steal", False)
|
||||
|
||||
result = await game_manager.submit_answer(
|
||||
room_code, question, answer, is_steal
|
||||
)
|
||||
|
||||
if "error" in result:
|
||||
await sio.emit("error", {"message": result["error"]}, to=sid)
|
||||
return
|
||||
|
||||
await sio.emit(
|
||||
"answer_result",
|
||||
{
|
||||
"player_name": player["name"],
|
||||
"team": player["team"],
|
||||
"answer": answer,
|
||||
"valid": result["valid"],
|
||||
"reason": result["reason"],
|
||||
"points_earned": result["points_earned"],
|
||||
"was_steal": is_steal,
|
||||
"room": result["room"]
|
||||
},
|
||||
room=room_code
|
||||
)
|
||||
|
||||
@sio.event
|
||||
async def steal_decision(sid, data):
|
||||
"""Decide whether to attempt stealing."""
|
||||
player = await room_manager.get_player(sid)
|
||||
if not player:
|
||||
return
|
||||
|
||||
room_code = player["room"]
|
||||
attempt = data.get("attempt", False)
|
||||
question_id = data.get("question_id")
|
||||
|
||||
if not attempt:
|
||||
# Pass on steal
|
||||
room = await game_manager.pass_steal(room_code, question_id)
|
||||
if room:
|
||||
await sio.emit(
|
||||
"steal_passed",
|
||||
{"room": room, "team": player["team"]},
|
||||
room=room_code
|
||||
)
|
||||
else:
|
||||
# Will attempt steal - just notify, answer comes separately
|
||||
room = await room_manager.get_room(room_code)
|
||||
await sio.emit(
|
||||
"steal_attempted",
|
||||
{
|
||||
"team": player["team"],
|
||||
"player_name": player["name"],
|
||||
"room": room
|
||||
},
|
||||
room=room_code
|
||||
)
|
||||
|
||||
@sio.event
|
||||
async def chat_message(sid, data):
|
||||
"""Send a chat message to team."""
|
||||
player = await room_manager.get_player(sid)
|
||||
if not player:
|
||||
return
|
||||
|
||||
room_code = player["room"]
|
||||
message = data.get("message", "")[:500] # Limit message length
|
||||
|
||||
# Get all team members' socket IDs
|
||||
room = await room_manager.get_room(room_code)
|
||||
if not room:
|
||||
return
|
||||
|
||||
team_sockets = [
|
||||
p["socket_id"] for p in room["teams"][player["team"]]
|
||||
]
|
||||
|
||||
# Send only to team members
|
||||
for socket_id in team_sockets:
|
||||
await sio.emit(
|
||||
"chat_message",
|
||||
{
|
||||
"player_name": player["name"],
|
||||
"team": player["team"],
|
||||
"message": message,
|
||||
"timestamp": datetime.utcnow().isoformat()
|
||||
},
|
||||
to=socket_id
|
||||
)
|
||||
|
||||
@sio.event
|
||||
async def emoji_reaction(sid, data):
|
||||
"""Send an emoji reaction visible to all."""
|
||||
player = await room_manager.get_player(sid)
|
||||
if not player:
|
||||
return
|
||||
|
||||
room_code = player["room"]
|
||||
emoji = data.get("emoji", "")
|
||||
|
||||
# Validate emoji
|
||||
allowed_emojis = ["👏", "😮", "😂", "🔥", "💀", "🎉", "😭", "🤔"]
|
||||
if emoji not in allowed_emojis:
|
||||
return
|
||||
|
||||
await sio.emit(
|
||||
"emoji_reaction",
|
||||
{
|
||||
"player_name": player["name"],
|
||||
"team": player["team"],
|
||||
"emoji": emoji
|
||||
},
|
||||
room=room_code
|
||||
)
|
||||
|
||||
@sio.event
|
||||
async def timer_expired(sid, data):
|
||||
"""Handle timer expiration."""
|
||||
player = await room_manager.get_player(sid)
|
||||
if not player:
|
||||
return
|
||||
|
||||
room_code = player["room"]
|
||||
room = await room_manager.get_room(room_code)
|
||||
|
||||
if not room:
|
||||
return
|
||||
|
||||
# Treat as wrong answer
|
||||
if room["can_steal"]:
|
||||
# Steal timer expired - pass
|
||||
question_id = room["current_question"]
|
||||
room = await game_manager.pass_steal(room_code, question_id)
|
||||
await sio.emit("time_up", {"room": room, "was_steal": True}, room=room_code)
|
||||
else:
|
||||
# Answer timer expired - enable steal
|
||||
room["can_steal"] = True
|
||||
room["current_team"] = "B" if room["current_team"] == "A" else "A"
|
||||
await room_manager.update_room(room_code, room)
|
||||
await sio.emit("time_up", {"room": room, "was_steal": False}, room=room_code)
|
||||
Reference in New Issue
Block a user