Port the FastAPI backend to Go 1.26, preserving all API functionality: - auth (register/login/guest/logout/profile/history) with JWT + bcrypt - scripts (list/detail) backed by embedded sample_data.json - games (create/join/start/chat/phase/clue/vote/state/replay) + WebSocket - rule-based NPC agent plus DeepSeek/Ollama AI backends - SQLite persistence via modernc.org/sqlite (pure Go, no cgo) Frontend and documents are unchanged.
867 lines
23 KiB
Go
867 lines
23 KiB
Go
package handlers
|
||
|
||
import (
|
||
"database/sql"
|
||
"encoding/json"
|
||
"fmt"
|
||
"math/rand"
|
||
"net/http"
|
||
"strings"
|
||
|
||
"github.com/go-chi/chi/v5"
|
||
|
||
"mmgame/internal/agents"
|
||
"mmgame/internal/database"
|
||
"mmgame/internal/models"
|
||
"mmgame/internal/scripts"
|
||
"mmgame/internal/util"
|
||
)
|
||
|
||
func CreateGame(w http.ResponseWriter, r *http.Request) {
|
||
var req struct {
|
||
ScriptID string `json:"script_id"`
|
||
HumanRoleID string `json:"human_role_id"`
|
||
}
|
||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||
writeError(w, 422, "请求格式错误")
|
||
return
|
||
}
|
||
script := scripts.GetByID(req.ScriptID)
|
||
if script == nil {
|
||
writeError(w, 404, "剧本不存在")
|
||
return
|
||
}
|
||
gameID := newID()
|
||
phase := ""
|
||
if len(script.Phases) > 0 {
|
||
phase = script.Phases[0].Name
|
||
}
|
||
configJSON, _ := json.Marshal(map[string]interface{}{"script": script})
|
||
_, err := database.DB.Exec(
|
||
`INSERT INTO game_sessions (id, script_id, status, phase, phase_index, config, created_at)
|
||
VALUES (?, ?, ?, ?, 0, ?, ?)`,
|
||
gameID, req.ScriptID, models.GameStatusWaiting, phase, string(configJSON), util.NowStr())
|
||
if err != nil {
|
||
writeError(w, 500, "服务器错误")
|
||
return
|
||
}
|
||
|
||
for _, role := range script.Roles {
|
||
isHuman := role.ID == req.HumanRoleID
|
||
if _, err := database.DB.Exec(
|
||
`INSERT INTO session_players (id, session_id, role_id, role_name, is_human, is_ready, avatar_url, status, joined_at)
|
||
VALUES (?, ?, ?, ?, ?, 0, '', ?, ?)`,
|
||
newID(), gameID, role.ID, role.Name, isHuman, models.PlayerStatusActive, util.NowStr()); err != nil {
|
||
writeError(w, 500, "服务器错误")
|
||
return
|
||
}
|
||
}
|
||
|
||
if len(script.Phases) > 0 {
|
||
for _, clueID := range script.Phases[0].Clues {
|
||
if _, err := database.DB.Exec(
|
||
`INSERT INTO clues_state (id, session_id, clue_id, status, created_at)
|
||
VALUES (?, ?, ?, 'released', ?)`,
|
||
newID(), gameID, clueID, util.NowStr()); err != nil {
|
||
writeError(w, 500, "服务器错误")
|
||
return
|
||
}
|
||
}
|
||
}
|
||
|
||
writeJSON(w, 200, map[string]string{"game_id": gameID})
|
||
}
|
||
|
||
func JoinGame(w http.ResponseWriter, r *http.Request) {
|
||
gameID := chi.URLParam(r, "game_id")
|
||
var req struct {
|
||
RoleID string `json:"role_id"`
|
||
}
|
||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||
writeError(w, 422, "请求格式错误")
|
||
return
|
||
}
|
||
var exists int
|
||
err := database.DB.QueryRow(`SELECT 1 FROM game_sessions WHERE id = ?`, gameID).Scan(&exists)
|
||
if err != nil {
|
||
writeError(w, 404, "游戏不存在")
|
||
return
|
||
}
|
||
|
||
playerID := newID()
|
||
if _, err := database.DB.Exec(
|
||
`INSERT INTO session_players (id, session_id, role_id, is_human, is_ready, status, joined_at)
|
||
VALUES (?, ?, ?, 1, 0, ?, ?)`,
|
||
playerID, gameID, req.RoleID, models.PlayerStatusActive, util.NowStr()); err != nil {
|
||
writeError(w, 500, "服务器错误")
|
||
return
|
||
}
|
||
|
||
var scriptID string
|
||
_ = database.DB.QueryRow(`SELECT script_id FROM game_sessions WHERE id = ?`, gameID).Scan(&scriptID)
|
||
script := scripts.GetByID(scriptID)
|
||
if script != nil {
|
||
for _, role := range script.Roles {
|
||
if role.ID == req.RoleID {
|
||
writeJSON(w, 200, map[string]interface{}{
|
||
"player_id": playerID,
|
||
"role_id": req.RoleID,
|
||
"role_name": role.Name,
|
||
"public_profile": role.PublicProfile,
|
||
"secret_profile": role.SecretProfile,
|
||
"secret": role.Secret,
|
||
"goal": role.Goal,
|
||
})
|
||
return
|
||
}
|
||
}
|
||
}
|
||
writeJSON(w, 200, map[string]interface{}{"player_id": playerID, "role_id": req.RoleID})
|
||
}
|
||
|
||
func StartGame(w http.ResponseWriter, r *http.Request) {
|
||
gameID := chi.URLParam(r, "game_id")
|
||
game, err := getGame(gameID)
|
||
if err != nil {
|
||
writeError(w, 404, "游戏不存在")
|
||
return
|
||
}
|
||
|
||
_, err = database.DB.Exec(
|
||
`UPDATE game_sessions SET status = ?, started_at = ? WHERE id = ?`,
|
||
models.GameStatusPlaying, util.NowStr(), gameID)
|
||
if err != nil {
|
||
writeError(w, 500, "服务器错误")
|
||
return
|
||
}
|
||
|
||
script := scriptFromConfig(game.Config)
|
||
background := ""
|
||
title := ""
|
||
if script != nil {
|
||
background = script.Background
|
||
title = script.Title
|
||
}
|
||
content := fmt.Sprintf("欢迎来到【%s】。\n%s\n\n请各位玩家阅读角色信息,做好准备后,在公屏中发送「准备好了」开始游戏。", title, background)
|
||
msgID := newID()
|
||
now := util.NowStr()
|
||
if _, err := database.DB.Exec(
|
||
`INSERT INTO chat_messages (id, session_id, sender_role_id, sender_name, message_type, content, phase_index, created_at)
|
||
VALUES (?, ?, 'dm', 'DM', ?, ?, ?, ?)`,
|
||
msgID, gameID, models.MessageTypeSystem, content, game.PhaseIndex, now); err != nil {
|
||
writeError(w, 500, "服务器错误")
|
||
return
|
||
}
|
||
|
||
broadcast(gameID, map[string]interface{}{
|
||
"type": "game_started",
|
||
"data": map[string]interface{}{
|
||
"phase": game.Phase,
|
||
"phase_index": game.PhaseIndex,
|
||
"message": map[string]interface{}{
|
||
"id": msgID,
|
||
"sender_name": "DM",
|
||
"sender_role_id": "dm",
|
||
"message_type": models.MessageTypeSystem,
|
||
"content": content,
|
||
"created_at": now,
|
||
},
|
||
},
|
||
})
|
||
writeJSON(w, 200, map[string]interface{}{"status": "started", "phase": game.Phase})
|
||
}
|
||
|
||
type npcResponse struct {
|
||
msg *models.ChatMessage
|
||
broadcast map[string]interface{}
|
||
}
|
||
|
||
func generateNPCResponses(gameID string, senderRoleID, content, msgType string, targetRoleID *string, script *scripts.Script, phaseName string, phaseIndex int) []npcResponse {
|
||
var responses []npcResponse
|
||
if script == nil {
|
||
return responses
|
||
}
|
||
|
||
var npcRoles []*scripts.Role
|
||
for i := range script.Roles {
|
||
if script.Roles[i].ID != senderRoleID {
|
||
npcRoles = append(npcRoles, &script.Roles[i])
|
||
}
|
||
}
|
||
rand.Shuffle(len(npcRoles), func(i, j int) { npcRoles[i], npcRoles[j] = npcRoles[j], npcRoles[i] })
|
||
if len(npcRoles) == 0 {
|
||
return responses
|
||
}
|
||
|
||
recentMsgs := queryMessages(gameID)
|
||
recentList := []map[string]string{}
|
||
if len(recentMsgs) > 10 {
|
||
recentMsgs = recentMsgs[len(recentMsgs)-10:]
|
||
}
|
||
for _, m := range recentMsgs {
|
||
recentList = append(recentList, map[string]string{"sender_name": m.SenderName, "content": m.Content})
|
||
}
|
||
|
||
knownClues := []string{}
|
||
for _, c := range queryClues(gameID) {
|
||
if c.Status == "released" && script != nil {
|
||
for _, sc := range script.Clues {
|
||
if sc.ID == c.ClueID {
|
||
knownClues = append(knownClues, sc.Description)
|
||
break
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
var phaseGoal string
|
||
if phaseIndex < len(script.Phases) {
|
||
phaseGoal = script.Phases[phaseIndex].Description
|
||
}
|
||
|
||
allRoles := []*scripts.Role{}
|
||
for i := range script.Roles {
|
||
allRoles = append(allRoles, &script.Roles[i])
|
||
}
|
||
|
||
count := 2
|
||
if len(npcRoles) < count {
|
||
count = len(npcRoles)
|
||
}
|
||
for _, npcRole := range npcRoles[:count] {
|
||
agent := agents.NewNPCAgent(npcRole, script.Background, "")
|
||
isPrivate := msgType == "private" && targetRoleID != nil && *targetRoleID == npcRole.ID
|
||
response := agent.Respond(phaseName, recentList, content, isPrivate, knownClues, phaseGoal, allRoles)
|
||
reply, _ := response["content"].(string)
|
||
action, _ := response["action"].(string)
|
||
|
||
if reply == "" || action == "remain_silent" {
|
||
continue
|
||
}
|
||
|
||
var npcTarget *string
|
||
if isPrivate {
|
||
npcTarget = &senderRoleID
|
||
}
|
||
npcMsgType := models.MessageTypePublic
|
||
if isPrivate {
|
||
npcMsgType = models.MessageTypePrivate
|
||
}
|
||
now := util.NowStr()
|
||
npcMsg := &models.ChatMessage{
|
||
ID: newID(),
|
||
SessionID: gameID,
|
||
SenderRoleID: npcRole.ID,
|
||
SenderName: npcRole.Name,
|
||
MessageType: npcMsgType,
|
||
Content: reply,
|
||
TargetRoleID: npcTarget,
|
||
PhaseIndex: phaseIndex,
|
||
CreatedAt: util.ParseTime(now),
|
||
}
|
||
responses = append(responses, npcResponse{
|
||
msg: npcMsg,
|
||
broadcast: map[string]interface{}{
|
||
"type": "chat_message",
|
||
"data": map[string]interface{}{
|
||
"id": npcMsg.ID,
|
||
"sender_role_id": npcRole.ID,
|
||
"sender_name": npcRole.Name,
|
||
"message_type": npcMsgType,
|
||
"content": reply,
|
||
"target_role_id": npcTarget,
|
||
"phase_index": phaseIndex,
|
||
"created_at": now,
|
||
},
|
||
},
|
||
})
|
||
}
|
||
return responses
|
||
}
|
||
|
||
func SendChat(w http.ResponseWriter, r *http.Request) {
|
||
gameID := chi.URLParam(r, "game_id")
|
||
var req struct {
|
||
Type string `json:"type"`
|
||
Content string `json:"content"`
|
||
TargetRoleID *string `json:"target_role_id"`
|
||
SenderRoleID string `json:"sender_role_id"`
|
||
}
|
||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||
writeError(w, 422, "请求格式错误")
|
||
return
|
||
}
|
||
game, err := getGame(gameID)
|
||
if err != nil {
|
||
writeError(w, 404, "游戏不存在")
|
||
return
|
||
}
|
||
|
||
senderName := req.SenderRoleID
|
||
script := scriptFromConfig(game.Config)
|
||
if script != nil {
|
||
for _, role := range script.Roles {
|
||
if role.ID == req.SenderRoleID {
|
||
senderName = role.Name
|
||
break
|
||
}
|
||
}
|
||
}
|
||
|
||
msgType := req.Type
|
||
if msgType == "" {
|
||
msgType = models.MessageTypePublic
|
||
}
|
||
msgID := newID()
|
||
now := util.NowStr()
|
||
if _, err := database.DB.Exec(
|
||
`INSERT INTO chat_messages (id, session_id, sender_role_id, sender_name, message_type, content, target_role_id, phase_index, created_at)
|
||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||
msgID, gameID, req.SenderRoleID, senderName, msgType, req.Content, req.TargetRoleID, game.PhaseIndex, now); err != nil {
|
||
writeError(w, 500, "服务器错误")
|
||
return
|
||
}
|
||
|
||
npcResponses := generateNPCResponses(gameID, req.SenderRoleID, req.Content, msgType, req.TargetRoleID, script, game.Phase, game.PhaseIndex)
|
||
for _, n := range npcResponses {
|
||
if n.msg != nil {
|
||
if _, err := database.DB.Exec(
|
||
`INSERT INTO chat_messages (id, session_id, sender_role_id, sender_name, message_type, content, target_role_id, phase_index, created_at)
|
||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||
n.msg.ID, n.msg.SessionID, n.msg.SenderRoleID, n.msg.SenderName, n.msg.MessageType,
|
||
n.msg.Content, n.msg.TargetRoleID, n.msg.PhaseIndex, util.TimeStr(n.msg.CreatedAt)); err != nil {
|
||
continue
|
||
}
|
||
}
|
||
}
|
||
|
||
msgData := map[string]interface{}{
|
||
"type": "chat_message",
|
||
"data": map[string]interface{}{
|
||
"id": msgID,
|
||
"sender_role_id": req.SenderRoleID,
|
||
"sender_name": senderName,
|
||
"message_type": msgType,
|
||
"content": req.Content,
|
||
"target_role_id": req.TargetRoleID,
|
||
"phase_index": game.PhaseIndex,
|
||
"created_at": now,
|
||
},
|
||
}
|
||
|
||
if msgType == "private" && req.TargetRoleID != nil {
|
||
broadcastToRole(gameID, req.SenderRoleID, msgData)
|
||
broadcastToRole(gameID, *req.TargetRoleID, msgData)
|
||
} else {
|
||
broadcast(gameID, msgData)
|
||
}
|
||
|
||
for _, n := range npcResponses {
|
||
b := n.broadcast
|
||
data, _ := b["data"].(map[string]interface{})
|
||
if data != nil {
|
||
if data["message_type"] == "private" {
|
||
target, _ := data["target_role_id"].(string)
|
||
if target != "" {
|
||
broadcastToRole(gameID, req.SenderRoleID, b)
|
||
broadcastToRole(gameID, target, b)
|
||
continue
|
||
}
|
||
}
|
||
}
|
||
broadcast(gameID, b)
|
||
}
|
||
|
||
npcList := []map[string]interface{}{}
|
||
for _, n := range npcResponses {
|
||
npcList = append(npcList, n.broadcast)
|
||
}
|
||
writeJSON(w, 200, map[string]interface{}{
|
||
"status": "ok",
|
||
"message_id": msgID,
|
||
"npc_responses": npcList,
|
||
})
|
||
}
|
||
|
||
func AdvancePhase(w http.ResponseWriter, r *http.Request) {
|
||
gameID := chi.URLParam(r, "game_id")
|
||
game, err := getGame(gameID)
|
||
if err != nil {
|
||
writeError(w, 404, "游戏不存在")
|
||
return
|
||
}
|
||
|
||
script := scriptFromConfig(game.Config)
|
||
var phases []scripts.Phase
|
||
if script != nil {
|
||
phases = script.Phases
|
||
}
|
||
nextIdx := game.PhaseIndex + 1
|
||
if nextIdx >= len(phases) {
|
||
writeJSON(w, 200, map[string]string{"status": "already_at_end"})
|
||
return
|
||
}
|
||
|
||
_, err = database.DB.Exec(
|
||
`UPDATE game_sessions SET phase_index = ?, phase = ? WHERE id = ?`,
|
||
nextIdx, phases[nextIdx].Name, gameID)
|
||
if err != nil {
|
||
writeError(w, 500, "服务器错误")
|
||
return
|
||
}
|
||
|
||
phase := &phases[nextIdx]
|
||
content := fmt.Sprintf("【%s】\n%s\n\n%s", phase.Name, phase.Description, phase.PublicInfo)
|
||
msgID := newID()
|
||
now := util.NowStr()
|
||
if _, err := database.DB.Exec(
|
||
`INSERT INTO chat_messages (id, session_id, sender_role_id, sender_name, message_type, content, phase_index, created_at)
|
||
VALUES (?, ?, 'dm', 'DM', ?, ?, ?, ?)`,
|
||
msgID, gameID, models.MessageTypeSystem, content, nextIdx, now); err != nil {
|
||
writeError(w, 500, "服务器错误")
|
||
return
|
||
}
|
||
|
||
cluesToRelease := phase.Clues
|
||
for _, clueID := range cluesToRelease {
|
||
var existingID string
|
||
err := database.DB.QueryRow(
|
||
`SELECT id FROM clues_state WHERE session_id = ? AND clue_id = ?`, gameID, clueID).Scan(&existingID)
|
||
if err == sql.ErrNoRows {
|
||
if _, err := database.DB.Exec(
|
||
`INSERT INTO clues_state (id, session_id, clue_id, status, created_at)
|
||
VALUES (?, ?, ?, 'released', ?)`,
|
||
newID(), gameID, clueID, util.NowStr()); err != nil {
|
||
continue
|
||
}
|
||
}
|
||
}
|
||
|
||
broadcast(gameID, map[string]interface{}{
|
||
"type": "phase_change",
|
||
"data": map[string]interface{}{
|
||
"phase": phase.Name,
|
||
"phase_index": nextIdx,
|
||
"message": map[string]interface{}{
|
||
"id": msgID,
|
||
"sender_name": "DM",
|
||
"sender_role_id": "dm",
|
||
"message_type": models.MessageTypeSystem,
|
||
"content": content,
|
||
"created_at": now,
|
||
},
|
||
"new_clues": cluesToRelease,
|
||
},
|
||
})
|
||
writeJSON(w, 200, map[string]interface{}{
|
||
"status": "ok",
|
||
"phase": phase.Name,
|
||
"phase_index": nextIdx,
|
||
})
|
||
}
|
||
|
||
func RevealClue(w http.ResponseWriter, r *http.Request) {
|
||
gameID := chi.URLParam(r, "game_id")
|
||
clueID := chi.URLParam(r, "clue_id")
|
||
|
||
var stateID string
|
||
err := database.DB.QueryRow(
|
||
`SELECT id FROM clues_state WHERE session_id = ? AND clue_id = ?`, gameID, clueID).Scan(&stateID)
|
||
if err == sql.ErrNoRows {
|
||
writeError(w, 404, "线索不存在或未发放")
|
||
return
|
||
}
|
||
if err != nil {
|
||
writeError(w, 500, "服务器错误")
|
||
return
|
||
}
|
||
if _, err := database.DB.Exec(`UPDATE clues_state SET status = 'revealed' WHERE id = ?`, stateID); err != nil {
|
||
writeError(w, 500, "服务器错误")
|
||
return
|
||
}
|
||
|
||
game, _ := getGame(gameID)
|
||
script := scriptFromConfig(game.Config)
|
||
var clueInfo interface{}
|
||
if script != nil {
|
||
for _, c := range script.Clues {
|
||
if c.ID == clueID {
|
||
clueInfo = c
|
||
break
|
||
}
|
||
}
|
||
}
|
||
|
||
broadcast(gameID, map[string]interface{}{
|
||
"type": "clue_revealed",
|
||
"data": map[string]interface{}{"clue_id": clueID, "clue": clueInfo},
|
||
})
|
||
writeJSON(w, 200, map[string]interface{}{"status": "revealed", "clue": clueInfo})
|
||
}
|
||
|
||
func SubmitVote(w http.ResponseWriter, r *http.Request) {
|
||
gameID := chi.URLParam(r, "game_id")
|
||
var req struct {
|
||
TargetRoleID string `json:"target_role_id"`
|
||
Reason string `json:"reason"`
|
||
VoterRoleID string `json:"voter_role_id"`
|
||
}
|
||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||
writeError(w, 422, "请求格式错误")
|
||
return
|
||
}
|
||
|
||
var voterID, voterName string
|
||
err := database.DB.QueryRow(
|
||
`SELECT id, role_name FROM session_players WHERE session_id = ? AND role_id = ?`,
|
||
gameID, req.VoterRoleID).Scan(&voterID, &voterName)
|
||
if err != nil {
|
||
writeError(w, 404, "投票者不存在")
|
||
return
|
||
}
|
||
|
||
var targetID string
|
||
err = database.DB.QueryRow(
|
||
`SELECT id FROM session_players WHERE session_id = ? AND role_id = ?`,
|
||
gameID, req.TargetRoleID).Scan(&targetID)
|
||
if err != nil {
|
||
writeError(w, 404, "投票目标不存在")
|
||
return
|
||
}
|
||
|
||
if _, err := database.DB.Exec(
|
||
`INSERT INTO votes (id, session_id, round, voter_id, target_id, reason, created_at)
|
||
VALUES (?, ?, 1, ?, ?, ?, ?)`,
|
||
newID(), gameID, voterID, targetID, req.Reason, util.NowStr()); err != nil {
|
||
writeError(w, 500, "服务器错误")
|
||
return
|
||
}
|
||
|
||
broadcast(gameID, map[string]interface{}{
|
||
"type": "vote_cast",
|
||
"data": map[string]interface{}{
|
||
"voter_role_id": req.VoterRoleID,
|
||
"target_role_id": req.TargetRoleID,
|
||
"voter_name": voterName,
|
||
},
|
||
})
|
||
writeJSON(w, 200, map[string]string{"status": "ok"})
|
||
}
|
||
|
||
func EndVote(w http.ResponseWriter, r *http.Request) {
|
||
gameID := chi.URLParam(r, "game_id")
|
||
game, err := getGame(gameID)
|
||
if err != nil {
|
||
writeError(w, 404, "游戏不存在")
|
||
return
|
||
}
|
||
|
||
script := scriptFromConfig(game.Config)
|
||
var allClues map[string]scripts.Clue
|
||
var roleByID map[string]*scripts.Role
|
||
if script != nil {
|
||
allClues = map[string]scripts.Clue{}
|
||
for _, c := range script.Clues {
|
||
allClues[c.ID] = c
|
||
}
|
||
roleByID = map[string]*scripts.Role{}
|
||
for i := range script.Roles {
|
||
roleByID[script.Roles[i].ID] = &script.Roles[i]
|
||
}
|
||
}
|
||
|
||
existingVotes := queryVotes(gameID)
|
||
existingVoterIDs := map[string]bool{}
|
||
for _, v := range existingVotes {
|
||
existingVoterIDs[v.VoterID] = true
|
||
}
|
||
|
||
playersList := queryPlayers(gameID)
|
||
playersByID := map[string]*models.SessionPlayer{}
|
||
for _, p := range playersList {
|
||
playersByID[p.ID] = p
|
||
}
|
||
|
||
releasedClueIDs := map[string]bool{}
|
||
for _, c := range queryClues(gameID) {
|
||
if c.Status == "released" {
|
||
releasedClueIDs[c.ClueID] = true
|
||
}
|
||
}
|
||
knownClues := []string{}
|
||
if allClues != nil {
|
||
for cid := range releasedClueIDs {
|
||
if c, ok := allClues[cid]; ok {
|
||
knownClues = append(knownClues, c.Description)
|
||
}
|
||
}
|
||
}
|
||
|
||
var npcPlayers []*models.SessionPlayer
|
||
for _, p := range playersList {
|
||
if !p.IsHuman && !existingVoterIDs[p.ID] {
|
||
npcPlayers = append(npcPlayers, p)
|
||
}
|
||
}
|
||
|
||
if script != nil {
|
||
for _, npc := range npcPlayers {
|
||
var targets []*models.SessionPlayer
|
||
for _, p := range playersList {
|
||
if p.ID != npc.ID {
|
||
targets = append(targets, p)
|
||
}
|
||
}
|
||
if len(targets) == 0 {
|
||
continue
|
||
}
|
||
|
||
var targetID string
|
||
if npcRole, ok := roleByID[npc.RoleID]; ok {
|
||
agent := agents.NewNPCAgent(npcRole, script.Background, "")
|
||
voteReason := agent.Respond("投票阶段", []map[string]string{}, "请投票选出你最怀疑的人,并说明理由", false, knownClues, "", nil)
|
||
voteContent, _ := voteReason["content"].(string)
|
||
targetID = targets[rand.Intn(len(targets))].ID
|
||
for _, t := range targets {
|
||
if t.RoleName != "" && strings.Contains(voteContent, t.RoleName) {
|
||
targetID = t.ID
|
||
break
|
||
}
|
||
}
|
||
} else {
|
||
targetID = targets[rand.Intn(len(targets))].ID
|
||
}
|
||
|
||
if _, err := database.DB.Exec(
|
||
`INSERT INTO votes (id, session_id, round, voter_id, target_id, reason, created_at)
|
||
VALUES (?, ?, 1, ?, ?, '', ?)`,
|
||
newID(), gameID, npc.ID, targetID, util.NowStr()); err != nil {
|
||
continue
|
||
}
|
||
|
||
targetPlayer := playersByID[targetID]
|
||
if targetPlayer != nil {
|
||
broadcast(gameID, map[string]interface{}{
|
||
"type": "vote_cast",
|
||
"data": map[string]interface{}{
|
||
"voter_role_id": npc.RoleID,
|
||
"voter_name": npc.RoleName,
|
||
"target_role_id": targetPlayer.RoleID,
|
||
"target_name": targetPlayer.RoleName,
|
||
},
|
||
})
|
||
}
|
||
}
|
||
}
|
||
|
||
votes := queryVotes(gameID)
|
||
tally := map[string][]string{}
|
||
for _, v := range votes {
|
||
targetRole := "unknown"
|
||
if p, ok := playersByID[v.TargetID]; ok {
|
||
targetRole = p.RoleID
|
||
}
|
||
voterRole := "unknown"
|
||
if p, ok := playersByID[v.VoterID]; ok {
|
||
voterRole = p.RoleID
|
||
}
|
||
tally[targetRole] = append(tally[targetRole], voterRole)
|
||
}
|
||
|
||
maxVotes := 0
|
||
accusedRoleID := ""
|
||
for roleID, voters := range tally {
|
||
if len(voters) > maxVotes {
|
||
maxVotes = len(voters)
|
||
accusedRoleID = roleID
|
||
}
|
||
}
|
||
|
||
accusedName := accusedRoleID
|
||
if script != nil {
|
||
for _, role := range script.Roles {
|
||
if role.ID == accusedRoleID {
|
||
accusedName = role.Name
|
||
break
|
||
}
|
||
}
|
||
}
|
||
truth := ""
|
||
if script != nil {
|
||
truth = script.Truth
|
||
}
|
||
|
||
if _, err := database.DB.Exec(
|
||
`UPDATE game_sessions SET status = ?, completed_at = ? WHERE id = ?`,
|
||
models.GameStatusCompleted, util.NowStr(), gameID); err != nil {
|
||
writeError(w, 500, "服务器错误")
|
||
return
|
||
}
|
||
|
||
content := fmt.Sprintf("投票结束!最多票指向:%s(%d票)\n\n真相:%s", accusedName, maxVotes, truth)
|
||
msgID := newID()
|
||
now := util.NowStr()
|
||
if _, err := database.DB.Exec(
|
||
`INSERT INTO chat_messages (id, session_id, sender_role_id, sender_name, message_type, content, phase_index, created_at)
|
||
VALUES (?, ?, 'dm', 'DM', ?, ?, ?, ?)`,
|
||
msgID, gameID, models.MessageTypeSystem, content, game.PhaseIndex, now); err != nil {
|
||
writeError(w, 500, "服务器错误")
|
||
return
|
||
}
|
||
|
||
broadcast(gameID, map[string]interface{}{
|
||
"type": "vote_result",
|
||
"data": map[string]interface{}{
|
||
"tally": tally,
|
||
"accused_role_id": accusedRoleID,
|
||
"accused_name": accusedName,
|
||
"truth": truth,
|
||
"game_id": gameID,
|
||
"message": map[string]interface{}{
|
||
"id": msgID,
|
||
"sender_name": "DM",
|
||
"sender_role_id": "dm",
|
||
"message_type": models.MessageTypeSystem,
|
||
"content": content,
|
||
"created_at": now,
|
||
},
|
||
},
|
||
})
|
||
|
||
removeGameConns(gameID)
|
||
|
||
writeJSON(w, 200, map[string]interface{}{
|
||
"status": "completed",
|
||
"accused": accusedName,
|
||
"truth": truth,
|
||
})
|
||
}
|
||
|
||
func GetGameState(w http.ResponseWriter, r *http.Request) {
|
||
gameID := chi.URLParam(r, "game_id")
|
||
game, err := getGame(gameID)
|
||
if err != nil {
|
||
writeError(w, 404, "游戏不存在")
|
||
return
|
||
}
|
||
|
||
players := queryPlayers(gameID)
|
||
clues := queryClues(gameID)
|
||
msgs := queryMessages(gameID)
|
||
script := scriptFromConfig(game.Config)
|
||
|
||
playersOut := []map[string]interface{}{}
|
||
for _, p := range players {
|
||
playersOut = append(playersOut, map[string]interface{}{
|
||
"id": p.ID,
|
||
"role_id": p.RoleID,
|
||
"role_name": p.RoleName,
|
||
"is_human": p.IsHuman,
|
||
"status": p.Status,
|
||
})
|
||
}
|
||
cluesOut := []map[string]interface{}{}
|
||
for _, c := range clues {
|
||
cluesOut = append(cluesOut, map[string]interface{}{"clue_id": c.ClueID, "status": c.Status})
|
||
}
|
||
msgsOut := []map[string]interface{}{}
|
||
for _, m := range msgs {
|
||
msgsOut = append(msgsOut, messageDict(m))
|
||
}
|
||
|
||
var scriptOut interface{}
|
||
if script != nil {
|
||
scriptOut = script
|
||
} else {
|
||
scriptOut = map[string]interface{}{}
|
||
}
|
||
|
||
writeJSON(w, 200, map[string]interface{}{
|
||
"id": game.ID,
|
||
"script_id": game.ScriptID,
|
||
"status": game.Status,
|
||
"phase": game.Phase,
|
||
"phase_index": game.PhaseIndex,
|
||
"script": scriptOut,
|
||
"players": playersOut,
|
||
"clues": cluesOut,
|
||
"messages": msgsOut,
|
||
})
|
||
}
|
||
|
||
func GetReplay(w http.ResponseWriter, r *http.Request) {
|
||
gameID := chi.URLParam(r, "game_id")
|
||
game, err := getGame(gameID)
|
||
if err != nil {
|
||
writeError(w, 404, "游戏不存在")
|
||
return
|
||
}
|
||
|
||
msgs := queryMessages(gameID)
|
||
votes := queryVotes(gameID)
|
||
players := map[string]*models.SessionPlayer{}
|
||
for _, p := range queryPlayers(gameID) {
|
||
players[p.ID] = p
|
||
}
|
||
script := scriptFromConfig(game.Config)
|
||
|
||
scriptTitle := game.ScriptID
|
||
truth := ""
|
||
var rolesOut []map[string]interface{}
|
||
if script != nil {
|
||
scriptTitle = script.Title
|
||
truth = script.Truth
|
||
for _, role := range script.Roles {
|
||
rolesOut = append(rolesOut, map[string]interface{}{
|
||
"id": role.ID,
|
||
"name": role.Name,
|
||
"publicProfile": role.PublicProfile,
|
||
"secretProfile": role.SecretProfile,
|
||
"goal": role.Goal,
|
||
})
|
||
}
|
||
}
|
||
|
||
msgsOut := []map[string]interface{}{}
|
||
for _, m := range msgs {
|
||
msgsOut = append(msgsOut, map[string]interface{}{
|
||
"id": m.ID,
|
||
"sender_name": m.SenderName,
|
||
"content": m.Content,
|
||
"type": m.MessageType,
|
||
"time": util.TimeStr(m.CreatedAt),
|
||
})
|
||
}
|
||
voteDetails := []map[string]interface{}{}
|
||
for _, v := range votes {
|
||
voter := players[v.VoterID]
|
||
target := players[v.TargetID]
|
||
voterName := "unknown"
|
||
if voter != nil {
|
||
voterName = voter.RoleName
|
||
}
|
||
targetName := "unknown"
|
||
if target != nil {
|
||
targetName = target.RoleName
|
||
}
|
||
reason := ""
|
||
if v.Reason != nil {
|
||
reason = *v.Reason
|
||
}
|
||
voteDetails = append(voteDetails, map[string]interface{}{
|
||
"voter_name": voterName,
|
||
"target_name": targetName,
|
||
"reason": reason,
|
||
})
|
||
}
|
||
|
||
writeJSON(w, 200, map[string]interface{}{
|
||
"game_id": gameID,
|
||
"script_title": scriptTitle,
|
||
"truth": truth,
|
||
"roles": rolesOut,
|
||
"messages": msgsOut,
|
||
"votes": voteDetails,
|
||
})
|
||
}
|