feat: add MMGame backend and frontend source code
This commit is contained in:
@@ -0,0 +1,694 @@
|
||||
import json
|
||||
import random
|
||||
import uuid
|
||||
import asyncio
|
||||
from datetime import datetime
|
||||
from fastapi import APIRouter, Depends, HTTPException, WebSocket, WebSocketDisconnect, Query
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select
|
||||
from pydantic import BaseModel
|
||||
from app.database import get_db
|
||||
from app.models.game import GameSession, SessionPlayer, ChatMessage, ClueState, Vote, GameStatus, MessageType
|
||||
from app.scripts.sample import get_script_by_id, get_script_by_title
|
||||
from app.agents.npc import NPCAgent
|
||||
from app.agents.dm import DMAgent
|
||||
|
||||
router = APIRouter(prefix="/api/games", tags=["games"])
|
||||
|
||||
active_connections: dict[str, list[WebSocket]] = {}
|
||||
game_locks: dict[str, asyncio.Lock] = {}
|
||||
|
||||
|
||||
def get_lock(game_id: str) -> asyncio.Lock:
|
||||
if game_id not in game_locks:
|
||||
game_locks[game_id] = asyncio.Lock()
|
||||
return game_locks[game_id]
|
||||
|
||||
|
||||
async def broadcast(game_id: str, message: dict):
|
||||
if game_id in active_connections:
|
||||
data = json.dumps(message, ensure_ascii=False)
|
||||
for ws in active_connections[game_id][:]:
|
||||
try:
|
||||
await ws.send_text(data)
|
||||
except Exception:
|
||||
active_connections[game_id].remove(ws)
|
||||
|
||||
|
||||
async def broadcast_to_role(game_id: str, target_role_id: str, message: dict):
|
||||
if game_id in active_connections:
|
||||
data = json.dumps(message, ensure_ascii=False)
|
||||
for ws in active_connections[game_id][:]:
|
||||
try:
|
||||
role_id = getattr(ws, "role_id", None)
|
||||
if role_id is None or role_id == target_role_id:
|
||||
await ws.send_text(data)
|
||||
except Exception:
|
||||
active_connections[game_id].remove(ws)
|
||||
|
||||
|
||||
class CreateGameRequest(BaseModel):
|
||||
script_id: str
|
||||
human_role_id: str = ""
|
||||
|
||||
|
||||
class JoinGameRequest(BaseModel):
|
||||
role_id: str
|
||||
|
||||
|
||||
class ChatRequest(BaseModel):
|
||||
type: str = "public"
|
||||
content: str
|
||||
target_role_id: str | None = None
|
||||
sender_role_id: str = ""
|
||||
|
||||
|
||||
class VoteRequest(BaseModel):
|
||||
target_role_id: str
|
||||
reason: str = ""
|
||||
voter_role_id: str = ""
|
||||
|
||||
|
||||
@router.post("/create")
|
||||
async def create_game(req: CreateGameRequest, db: AsyncSession = Depends(get_db)):
|
||||
script = get_script_by_id(req.script_id)
|
||||
if not script:
|
||||
raise HTTPException(404, "剧本不存在")
|
||||
game = GameSession(
|
||||
id=str(uuid.uuid4()),
|
||||
script_id=req.script_id,
|
||||
status=GameStatus.waiting.value,
|
||||
phase=script["phases"][0]["name"] if script["phases"] else "",
|
||||
config={"script": script},
|
||||
)
|
||||
db.add(game)
|
||||
await db.commit()
|
||||
|
||||
for role in script["roles"]:
|
||||
is_human = role["id"] == req.human_role_id
|
||||
sp = SessionPlayer(
|
||||
id=str(uuid.uuid4()),
|
||||
session_id=game.id,
|
||||
role_id=role["id"],
|
||||
role_name=role["name"],
|
||||
is_human=is_human,
|
||||
avatar_url="",
|
||||
)
|
||||
db.add(sp)
|
||||
|
||||
first_phase_clues = script["phases"][0].get("clues", []) if script["phases"] else []
|
||||
for clue_id in first_phase_clues:
|
||||
cs = ClueState(id=str(uuid.uuid4()), session_id=game.id, clue_id=clue_id, status="released")
|
||||
db.add(cs)
|
||||
|
||||
await db.commit()
|
||||
|
||||
return {"game_id": game.id}
|
||||
|
||||
|
||||
@router.post("/{game_id}/join")
|
||||
async def join_game(game_id: str, req: JoinGameRequest, db: AsyncSession = Depends(get_db)):
|
||||
result = await db.execute(select(GameSession).where(GameSession.id == game_id))
|
||||
game = result.scalar_one_or_none()
|
||||
if not game:
|
||||
raise HTTPException(404, "游戏不存在")
|
||||
player = SessionPlayer(
|
||||
id=str(uuid.uuid4()),
|
||||
session_id=game_id,
|
||||
role_id=req.role_id,
|
||||
is_human=True,
|
||||
)
|
||||
db.add(player)
|
||||
await db.commit()
|
||||
|
||||
script = get_script_by_id(game.script_id) or {}
|
||||
for role in script.get("roles", []):
|
||||
if role["id"] == req.role_id:
|
||||
return {
|
||||
"player_id": player.id,
|
||||
"role_id": req.role_id,
|
||||
"role_name": role["name"],
|
||||
"public_profile": role["publicProfile"],
|
||||
"secret_profile": role["secretProfile"],
|
||||
"secret": role.get("secret", ""),
|
||||
"goal": role["goal"],
|
||||
}
|
||||
return {"player_id": player.id, "role_id": req.role_id}
|
||||
|
||||
|
||||
@router.post("/{game_id}/start")
|
||||
async def start_game(game_id: str, db: AsyncSession = Depends(get_db)):
|
||||
result = await db.execute(select(GameSession).where(GameSession.id == game_id))
|
||||
game = result.scalar_one_or_none()
|
||||
if not game:
|
||||
raise HTTPException(404, "游戏不存在")
|
||||
|
||||
game.status = GameStatus.playing.value
|
||||
game.started_at = datetime.utcnow()
|
||||
await db.commit()
|
||||
|
||||
script = game.config.get("script", {}) if game.config else {}
|
||||
background = script.get("background", "")
|
||||
title = script.get("title", "")
|
||||
|
||||
dm_msg = ChatMessage(
|
||||
id=str(uuid.uuid4()),
|
||||
session_id=game_id,
|
||||
sender_role_id="dm",
|
||||
sender_name="DM",
|
||||
message_type=MessageType.system.value,
|
||||
content=f"欢迎来到【{title}】。\n{background}\n\n请各位玩家阅读角色信息,做好准备后,在公屏中发送「准备好了」开始游戏。",
|
||||
phase_index=game.phase_index,
|
||||
)
|
||||
db.add(dm_msg)
|
||||
await db.commit()
|
||||
|
||||
await broadcast(game_id, {
|
||||
"type": "game_started",
|
||||
"data": {
|
||||
"phase": game.phase,
|
||||
"phase_index": game.phase_index,
|
||||
"message": {
|
||||
"id": dm_msg.id,
|
||||
"sender_name": "DM",
|
||||
"sender_role_id": "dm",
|
||||
"message_type": "system",
|
||||
"content": dm_msg.content,
|
||||
"created_at": dm_msg.created_at.isoformat(),
|
||||
}
|
||||
}
|
||||
})
|
||||
return {"status": "started", "phase": game.phase}
|
||||
|
||||
|
||||
@router.post("/{game_id}/chat")
|
||||
async def send_chat(game_id: str, req: ChatRequest, db: AsyncSession = Depends(get_db)):
|
||||
result = await db.execute(select(GameSession).where(GameSession.id == game_id))
|
||||
game = result.scalar_one_or_none()
|
||||
if not game:
|
||||
raise HTTPException(404, "游戏不存在")
|
||||
|
||||
sender_name = req.sender_role_id
|
||||
players_result = await db.execute(
|
||||
select(SessionPlayer).where(SessionPlayer.session_id == game_id, SessionPlayer.role_id == req.sender_role_id)
|
||||
)
|
||||
player = players_result.scalar_one_or_none()
|
||||
script = game.config.get("script", {}) if game.config else {}
|
||||
for role in script.get("roles", []):
|
||||
if role["id"] == req.sender_role_id:
|
||||
sender_name = role["name"]
|
||||
break
|
||||
|
||||
msg = ChatMessage(
|
||||
id=str(uuid.uuid4()),
|
||||
session_id=game_id,
|
||||
sender_role_id=req.sender_role_id,
|
||||
sender_name=sender_name,
|
||||
message_type=req.type,
|
||||
content=req.content,
|
||||
target_role_id=req.target_role_id,
|
||||
phase_index=game.phase_index,
|
||||
)
|
||||
db.add(msg)
|
||||
|
||||
npc_responses = await _generate_npc_responses_inline(game_id, req, script, sender_name, game.phase, game.phase_index, db)
|
||||
for npc_msg_data in npc_responses:
|
||||
if npc_msg_data["_msg"] is not None:
|
||||
db.add(npc_msg_data["_msg"])
|
||||
await db.commit()
|
||||
|
||||
msg_data = {
|
||||
"type": "chat_message",
|
||||
"data": {
|
||||
"id": msg.id,
|
||||
"sender_role_id": req.sender_role_id,
|
||||
"sender_name": sender_name,
|
||||
"message_type": req.type,
|
||||
"content": req.content,
|
||||
"target_role_id": req.target_role_id,
|
||||
"phase_index": game.phase_index,
|
||||
"created_at": msg.created_at.isoformat(),
|
||||
}
|
||||
}
|
||||
|
||||
if req.type == "private" and req.target_role_id:
|
||||
await broadcast_to_role(game_id, req.sender_role_id, msg_data)
|
||||
await broadcast_to_role(game_id, req.target_role_id, msg_data)
|
||||
else:
|
||||
await broadcast(game_id, msg_data)
|
||||
|
||||
for npc in npc_responses:
|
||||
npc_broadcast = {k: v for k, v in npc.items() if k != "_msg"}
|
||||
if npc["data"]["message_type"] == "private" and npc["data"].get("target_role_id"):
|
||||
await broadcast_to_role(game_id, req.sender_role_id, npc_broadcast)
|
||||
await broadcast_to_role(game_id, npc["data"]["target_role_id"], npc_broadcast)
|
||||
else:
|
||||
await broadcast(game_id, npc_broadcast)
|
||||
|
||||
return {
|
||||
"status": "ok",
|
||||
"message_id": msg.id,
|
||||
"npc_responses": [
|
||||
{k: v for k, v in n.items() if k != "_msg"}
|
||||
for n in npc_responses
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
@router.post("/{game_id}/phase/advance")
|
||||
async def advance_phase(game_id: str, db: AsyncSession = Depends(get_db)):
|
||||
result = await db.execute(select(GameSession).where(GameSession.id == game_id))
|
||||
game = result.scalar_one_or_none()
|
||||
if not game:
|
||||
raise HTTPException(404, "游戏不存在")
|
||||
|
||||
script = game.config.get("script", {}) if game.config else {}
|
||||
phases = script.get("phases", [])
|
||||
next_idx = game.phase_index + 1
|
||||
|
||||
if next_idx >= len(phases):
|
||||
return {"status": "already_at_end"}
|
||||
|
||||
game.phase_index = next_idx
|
||||
game.phase = phases[next_idx]["name"]
|
||||
await db.commit()
|
||||
|
||||
phase = phases[next_idx]
|
||||
msg = ChatMessage(
|
||||
id=str(uuid.uuid4()),
|
||||
session_id=game_id,
|
||||
sender_role_id="dm",
|
||||
sender_name="DM",
|
||||
message_type=MessageType.system.value,
|
||||
content=f"【{phase['name']}】\n{phase['description']}\n\n{phase.get('publicInfo', '')}",
|
||||
phase_index=next_idx,
|
||||
)
|
||||
db.add(msg)
|
||||
await db.commit()
|
||||
|
||||
clues_to_release = phase.get("clues", [])
|
||||
for clue_id in clues_to_release:
|
||||
existing = await db.execute(
|
||||
select(ClueState).where(ClueState.session_id == game_id, ClueState.clue_id == clue_id)
|
||||
)
|
||||
if not existing.scalar_one_or_none():
|
||||
cs = ClueState(id=str(uuid.uuid4()), session_id=game_id, clue_id=clue_id, status="released")
|
||||
db.add(cs)
|
||||
await db.commit()
|
||||
|
||||
await broadcast(game_id, {
|
||||
"type": "phase_change",
|
||||
"data": {
|
||||
"phase": game.phase,
|
||||
"phase_index": next_idx,
|
||||
"message": {
|
||||
"id": msg.id,
|
||||
"sender_name": "DM",
|
||||
"sender_role_id": "dm",
|
||||
"message_type": "system",
|
||||
"content": msg.content,
|
||||
"created_at": msg.created_at.isoformat(),
|
||||
},
|
||||
"new_clues": clues_to_release,
|
||||
}
|
||||
})
|
||||
return {"status": "ok", "phase": game.phase, "phase_index": next_idx}
|
||||
|
||||
|
||||
@router.post("/{game_id}/clue/{clue_id}/reveal")
|
||||
async def reveal_clue(game_id: str, clue_id: str, db: AsyncSession = Depends(get_db)):
|
||||
result = await db.execute(
|
||||
select(ClueState).where(ClueState.session_id == game_id, ClueState.clue_id == clue_id)
|
||||
)
|
||||
clue_state = result.scalar_one_or_none()
|
||||
if not clue_state:
|
||||
raise HTTPException(404, "线索不存在或未发放")
|
||||
|
||||
clue_state.status = "revealed"
|
||||
await db.commit()
|
||||
|
||||
game_result = await db.execute(select(GameSession).where(GameSession.id == game_id))
|
||||
game = game_result.scalar_one()
|
||||
script = game.config.get("script", {}) if game.config else {}
|
||||
clue_info = None
|
||||
for c in script.get("clues", []):
|
||||
if c["id"] == clue_id:
|
||||
clue_info = c
|
||||
break
|
||||
|
||||
await broadcast(game_id, {
|
||||
"type": "clue_revealed",
|
||||
"data": {"clue_id": clue_id, "clue": clue_info}
|
||||
})
|
||||
return {"status": "revealed", "clue": clue_info}
|
||||
|
||||
|
||||
@router.post("/{game_id}/vote")
|
||||
async def submit_vote(game_id: str, req: VoteRequest, db: AsyncSession = Depends(get_db)):
|
||||
voter = await db.execute(
|
||||
select(SessionPlayer).where(SessionPlayer.session_id == game_id, SessionPlayer.role_id == req.voter_role_id)
|
||||
)
|
||||
voter_player = voter.scalar_one_or_none()
|
||||
if not voter_player:
|
||||
raise HTTPException(404, "投票者不存在")
|
||||
|
||||
target = await db.execute(
|
||||
select(SessionPlayer).where(SessionPlayer.session_id == game_id, SessionPlayer.role_id == req.target_role_id)
|
||||
)
|
||||
target_player = target.scalar_one_or_none()
|
||||
if not target_player:
|
||||
raise HTTPException(404, "投票目标不存在")
|
||||
|
||||
vote = Vote(
|
||||
id=str(uuid.uuid4()),
|
||||
session_id=game_id,
|
||||
voter_id=voter_player.id,
|
||||
target_id=target_player.id,
|
||||
reason=req.reason,
|
||||
)
|
||||
db.add(vote)
|
||||
await db.commit()
|
||||
|
||||
await broadcast(game_id, {
|
||||
"type": "vote_cast",
|
||||
"data": {
|
||||
"voter_role_id": req.voter_role_id,
|
||||
"target_role_id": req.target_role_id,
|
||||
"voter_name": voter_player.role_name,
|
||||
}
|
||||
})
|
||||
return {"status": "ok"}
|
||||
|
||||
|
||||
@router.post("/{game_id}/vote/end")
|
||||
async def end_vote(game_id: str, db: AsyncSession = Depends(get_db)):
|
||||
game_result = await db.execute(select(GameSession).where(GameSession.id == game_id))
|
||||
game = game_result.scalar_one_or_none()
|
||||
if not game:
|
||||
raise HTTPException(404, "游戏不存在")
|
||||
script = game.config.get("script", {}) if game.config else {}
|
||||
|
||||
existing_votes_result = await db.execute(select(Vote).where(Vote.session_id == game_id))
|
||||
existing_votes = existing_votes_result.scalars().all()
|
||||
existing_voter_ids = {v.voter_id for v in existing_votes}
|
||||
|
||||
players_result = await db.execute(select(SessionPlayer).where(SessionPlayer.session_id == game_id))
|
||||
players_list = players_result.scalars().all()
|
||||
players = {p.id: p for p in players_list}
|
||||
|
||||
clues_result = await db.execute(select(ClueState).where(ClueState.session_id == game_id, ClueState.status == "released"))
|
||||
released_clue_states = clues_result.scalars().all()
|
||||
released_clue_ids = {c.clue_id for c in released_clue_states}
|
||||
all_clues = {c["id"]: c for c in script.get("clues", [])}
|
||||
known_clues = [all_clues[cid]["description"] for cid in released_clue_ids if cid in all_clues]
|
||||
|
||||
npc_players = [p for p in players_list if not p.is_human and p.id not in existing_voter_ids]
|
||||
all_roles = script.get("roles", [])
|
||||
role_dict = {r["id"]: r for r in all_roles}
|
||||
|
||||
for npc in npc_players:
|
||||
targets = [p for p in players_list if p.id != npc.id]
|
||||
if not targets:
|
||||
continue
|
||||
|
||||
npc_role = role_dict.get(npc.role_id)
|
||||
if npc_role:
|
||||
agent = NPCAgent(npc_role, script.get("background", ""), "")
|
||||
vote_reason = await agent.respond("投票阶段", [], "请投票选出你最怀疑的人,并说明理由",
|
||||
known_clues=known_clues, all_roles=all_roles)
|
||||
vote_content = vote_reason.get("content", "")
|
||||
target_id = random.choice(targets).id
|
||||
for t in targets:
|
||||
if t.role_name and t.role_name in vote_content:
|
||||
target_id = t.id
|
||||
break
|
||||
else:
|
||||
target_id = random.choice(targets).id
|
||||
|
||||
vote = Vote(
|
||||
id=str(uuid.uuid4()),
|
||||
session_id=game_id,
|
||||
voter_id=npc.id,
|
||||
target_id=target_id,
|
||||
reason="",
|
||||
)
|
||||
db.add(vote)
|
||||
|
||||
target_player = players[target_id]
|
||||
await broadcast(game_id, {
|
||||
"type": "vote_cast",
|
||||
"data": {
|
||||
"voter_role_id": npc.role_id,
|
||||
"voter_name": npc.role_name,
|
||||
"target_role_id": target_player.role_id,
|
||||
"target_name": target_player.role_name,
|
||||
}
|
||||
})
|
||||
await db.commit()
|
||||
|
||||
votes_result = await db.execute(select(Vote).where(Vote.session_id == game_id))
|
||||
votes = votes_result.scalars().all()
|
||||
|
||||
tally: dict[str, list[str]] = {}
|
||||
for v in votes:
|
||||
target_role = players[v.target_id].role_id if v.target_id in players else "unknown"
|
||||
voter_role = players[v.voter_id].role_id if v.voter_id in players else "unknown"
|
||||
if target_role not in tally:
|
||||
tally[target_role] = []
|
||||
tally[target_role].append(voter_role)
|
||||
|
||||
max_votes = 0
|
||||
accused_role_id = ""
|
||||
for role_id, voters in tally.items():
|
||||
if len(voters) > max_votes:
|
||||
max_votes = len(voters)
|
||||
accused_role_id = role_id
|
||||
|
||||
accused_name = accused_role_id
|
||||
for role in script.get("roles", []):
|
||||
if role["id"] == accused_role_id:
|
||||
accused_name = role["name"]
|
||||
break
|
||||
|
||||
truth = script.get("truth", "")
|
||||
|
||||
game.status = GameStatus.completed.value
|
||||
game.completed_at = datetime.utcnow()
|
||||
await db.commit()
|
||||
|
||||
result_msg = ChatMessage(
|
||||
id=str(uuid.uuid4()),
|
||||
session_id=game_id,
|
||||
sender_role_id="dm",
|
||||
sender_name="DM",
|
||||
message_type=MessageType.system.value,
|
||||
content=f"投票结束!最多票指向:{accused_name}({max_votes}票)\n\n真相:{truth}",
|
||||
phase_index=game.phase_index,
|
||||
)
|
||||
db.add(result_msg)
|
||||
await db.commit()
|
||||
|
||||
await broadcast(game_id, {
|
||||
"type": "vote_result",
|
||||
"data": {
|
||||
"tally": {role_id: voters for role_id, voters in tally.items()},
|
||||
"accused_role_id": accused_role_id,
|
||||
"accused_name": accused_name,
|
||||
"truth": truth,
|
||||
"game_id": game_id,
|
||||
"message": {
|
||||
"id": result_msg.id,
|
||||
"sender_name": "DM",
|
||||
"sender_role_id": "dm",
|
||||
"message_type": "system",
|
||||
"content": result_msg.content,
|
||||
"created_at": result_msg.created_at.isoformat(),
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
if game_id in active_connections:
|
||||
del active_connections[game_id]
|
||||
|
||||
return {"status": "completed", "accused": accused_name, "truth": truth}
|
||||
|
||||
|
||||
@router.get("/{game_id}/state")
|
||||
async def get_game_state(game_id: str, db: AsyncSession = Depends(get_db)):
|
||||
result = await db.execute(select(GameSession).where(GameSession.id == game_id))
|
||||
game = result.scalar_one_or_none()
|
||||
if not game:
|
||||
raise HTTPException(404, "游戏不存在")
|
||||
clues_result = await db.execute(select(ClueState).where(ClueState.session_id == game_id))
|
||||
clues = clues_result.scalars().all()
|
||||
players_result = await db.execute(select(SessionPlayer).where(SessionPlayer.session_id == game_id))
|
||||
players = players_result.scalars().all()
|
||||
msgs_result = await db.execute(
|
||||
select(ChatMessage).where(ChatMessage.session_id == game_id).order_by(ChatMessage.created_at)
|
||||
)
|
||||
msgs = msgs_result.scalars().all()
|
||||
|
||||
script = game.config.get("script", {}) if game.config else {}
|
||||
return {
|
||||
"id": game.id,
|
||||
"script_id": game.script_id,
|
||||
"status": game.status,
|
||||
"phase": game.phase,
|
||||
"phase_index": game.phase_index,
|
||||
"script": script,
|
||||
"players": [
|
||||
{"id": p.id, "role_id": p.role_id, "role_name": p.role_name, "is_human": p.is_human, "status": p.status}
|
||||
for p in players
|
||||
],
|
||||
"clues": [{"clue_id": c.clue_id, "status": c.status} for c in clues],
|
||||
"messages": [
|
||||
{
|
||||
"id": m.id, "sender_role_id": m.sender_role_id, "sender_name": m.sender_name,
|
||||
"message_type": m.message_type, "content": m.content, "target_role_id": m.target_role_id,
|
||||
"clue_id": m.clue_id, "phase_index": m.phase_index,
|
||||
"created_at": m.created_at.isoformat(),
|
||||
}
|
||||
for m in msgs
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
@router.get("/{game_id}/replay")
|
||||
async def get_replay(game_id: str, db: AsyncSession = Depends(get_db)):
|
||||
result = await db.execute(select(GameSession).where(GameSession.id == game_id))
|
||||
game = result.scalar_one_or_none()
|
||||
if not game:
|
||||
raise HTTPException(404, "游戏不存在")
|
||||
msgs_result = await db.execute(
|
||||
select(ChatMessage).where(ChatMessage.session_id == game_id).order_by(ChatMessage.created_at)
|
||||
)
|
||||
msgs = msgs_result.scalars().all()
|
||||
votes_result = await db.execute(
|
||||
select(Vote).where(Vote.session_id == game_id)
|
||||
)
|
||||
votes = votes_result.scalars().all()
|
||||
players_result = await db.execute(select(SessionPlayer).where(SessionPlayer.session_id == game_id))
|
||||
players = {p.id: p for p in players_result.scalars().all()}
|
||||
script = game.config.get("script", {}) if game.config else {}
|
||||
|
||||
vote_details = []
|
||||
for v in votes:
|
||||
voter = players.get(v.voter_id)
|
||||
target = players.get(v.target_id)
|
||||
vote_details.append({
|
||||
"voter_name": voter.role_name if voter else "unknown",
|
||||
"target_name": target.role_name if target else "unknown",
|
||||
"reason": v.reason,
|
||||
})
|
||||
|
||||
return {
|
||||
"game_id": game_id,
|
||||
"script_title": script.get("title", game.script_id),
|
||||
"truth": script.get("truth", ""),
|
||||
"roles": [
|
||||
{"id": r["id"], "name": r["name"], "publicProfile": r["publicProfile"],
|
||||
"secretProfile": r["secretProfile"], "goal": r["goal"]}
|
||||
for r in script.get("roles", [])
|
||||
],
|
||||
"messages": [
|
||||
{"id": m.id, "sender_name": m.sender_name, "content": m.content,
|
||||
"type": m.message_type, "time": m.created_at.isoformat()}
|
||||
for m in msgs
|
||||
],
|
||||
"votes": vote_details,
|
||||
}
|
||||
|
||||
|
||||
@router.websocket("/ws/{game_id}")
|
||||
async def game_websocket(websocket: WebSocket, game_id: str, role_id: str = Query("")):
|
||||
await websocket.accept()
|
||||
websocket.role_id = role_id
|
||||
if game_id not in active_connections:
|
||||
active_connections[game_id] = []
|
||||
active_connections[game_id].append(websocket)
|
||||
try:
|
||||
while True:
|
||||
data = await websocket.receive_text()
|
||||
msg = json.loads(data)
|
||||
msg_type = msg.get("type", "")
|
||||
msg_data = msg.get("data", {})
|
||||
|
||||
if msg_type == "ping":
|
||||
await websocket.send_text(json.dumps({"type": "pong"}))
|
||||
except WebSocketDisconnect:
|
||||
if game_id in active_connections and websocket in active_connections[game_id]:
|
||||
active_connections[game_id].remove(websocket)
|
||||
except Exception:
|
||||
if game_id in active_connections and websocket in active_connections[game_id]:
|
||||
active_connections[game_id].remove(websocket)
|
||||
|
||||
|
||||
async def _generate_npc_responses_inline(game_id: str, req, script: dict, sender_name: str, phase_name: str, phase_index: int, db: AsyncSession) -> list[dict]:
|
||||
npc_responses = []
|
||||
try:
|
||||
npc_roles = [r for r in script.get("roles", []) if r["id"] != req.sender_role_id]
|
||||
random.shuffle(npc_roles)
|
||||
|
||||
if not npc_roles:
|
||||
return npc_responses
|
||||
|
||||
recent_msgs_result = await db.execute(
|
||||
select(ChatMessage).where(ChatMessage.session_id == game_id).order_by(ChatMessage.created_at.desc()).limit(10)
|
||||
)
|
||||
recent_msgs = recent_msgs_result.scalars().all()
|
||||
recent_list = [{"sender_name": m.sender_name, "content": m.content} for m in recent_msgs]
|
||||
|
||||
clues_result = await db.execute(select(ClueState).where(ClueState.session_id == game_id, ClueState.status == "released"))
|
||||
released_clue_states = clues_result.scalars().all()
|
||||
released_clue_ids = {c.clue_id for c in released_clue_states}
|
||||
all_clues = {c["id"]: c for c in script.get("clues", [])}
|
||||
known_clues = [all_clues[cid]["description"] for cid in released_clue_ids if cid in all_clues]
|
||||
|
||||
phases = script.get("phases", [])
|
||||
current_phase = phases[phase_index] if phase_index < len(phases) else None
|
||||
phase_goal = current_phase["description"] if current_phase else ""
|
||||
|
||||
all_roles = script.get("roles", [])
|
||||
|
||||
for npc_role in npc_roles[:2]:
|
||||
agent = NPCAgent(npc_role, script.get("background", ""), "")
|
||||
is_private = req.type == "private" and req.target_role_id == npc_role["id"]
|
||||
response = await agent.respond(phase_name, recent_list, req.content, is_private,
|
||||
known_clues=known_clues, phase_goal=phase_goal, all_roles=all_roles)
|
||||
content = response.get("content", "")
|
||||
action = response.get("action", "")
|
||||
|
||||
if not content or action == "remain_silent":
|
||||
continue
|
||||
|
||||
now = datetime.utcnow()
|
||||
npc_msg = ChatMessage(
|
||||
id=str(uuid.uuid4()),
|
||||
session_id=game_id,
|
||||
sender_role_id=npc_role["id"],
|
||||
sender_name=npc_role["name"],
|
||||
message_type="private" if is_private else "public",
|
||||
content=content,
|
||||
target_role_id=req.sender_role_id if is_private else None,
|
||||
phase_index=phase_index,
|
||||
created_at=now,
|
||||
)
|
||||
|
||||
npc_responses.append({
|
||||
"_msg": npc_msg,
|
||||
"type": "chat_message",
|
||||
"data": {
|
||||
"id": npc_msg.id,
|
||||
"sender_role_id": npc_role["id"],
|
||||
"sender_name": npc_role["name"],
|
||||
"message_type": npc_msg.message_type,
|
||||
"content": content,
|
||||
"target_role_id": npc_msg.target_role_id,
|
||||
"phase_index": phase_index,
|
||||
"created_at": npc_msg.created_at.isoformat(),
|
||||
}
|
||||
})
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return npc_responses
|
||||
Reference in New Issue
Block a user