695 lines
25 KiB
Python
695 lines
25 KiB
Python
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
|