refactor: rewrite backend in Go, replacing Python FastAPI
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.
This commit is contained in:
@@ -0,0 +1,263 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
|
||||
"mmgame/internal/config"
|
||||
"mmgame/internal/database"
|
||||
"mmgame/internal/models"
|
||||
"mmgame/internal/util"
|
||||
)
|
||||
|
||||
var errUnauthorized = errors.New("unauthorized")
|
||||
|
||||
func createToken(userID string) (string, error) {
|
||||
exp := time.Now().Add(time.Duration(config.Cfg.AccessTokenExpireMinutes) * time.Minute)
|
||||
claims := jwt.MapClaims{
|
||||
"sub": userID,
|
||||
"exp": exp.Unix(),
|
||||
"v": 0,
|
||||
}
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||
return token.SignedString([]byte(config.Cfg.SecretKey))
|
||||
}
|
||||
|
||||
func currentUser(r *http.Request) (*models.User, error) {
|
||||
auth := r.Header.Get("Authorization")
|
||||
if !strings.HasPrefix(auth, "Bearer ") {
|
||||
return nil, errUnauthorized
|
||||
}
|
||||
tokenStr := strings.TrimPrefix(auth, "Bearer ")
|
||||
claims := jwt.MapClaims{}
|
||||
token, err := jwt.ParseWithClaims(tokenStr, claims, func(t *jwt.Token) (interface{}, error) {
|
||||
return []byte(config.Cfg.SecretKey), nil
|
||||
}, jwt.WithValidMethods([]string{config.Cfg.Algorithm}))
|
||||
if err != nil || !token.Valid {
|
||||
return nil, errUnauthorized
|
||||
}
|
||||
userID, _ := claims["sub"].(string)
|
||||
if userID == "" {
|
||||
return nil, errUnauthorized
|
||||
}
|
||||
row := database.DB.QueryRow(
|
||||
`SELECT id, email, nickname, avatar_url, password_hash, role, is_guest, game_count, token_version, created_at
|
||||
FROM users WHERE id = ?`, userID)
|
||||
var u models.User
|
||||
var email, nickname, avatarURL, passwordHash, createdAt sql.NullString
|
||||
if err := row.Scan(&u.ID, &email, &nickname, &avatarURL, &passwordHash, &u.Role,
|
||||
&u.IsGuest, &u.GameCount, &u.TokenVersion, &createdAt); err != nil {
|
||||
return nil, errUnauthorized
|
||||
}
|
||||
if email.Valid {
|
||||
u.Email = &email.String
|
||||
}
|
||||
if nickname.Valid {
|
||||
u.Nickname = &nickname.String
|
||||
}
|
||||
if avatarURL.Valid {
|
||||
u.AvatarURL = &avatarURL.String
|
||||
}
|
||||
if passwordHash.Valid {
|
||||
u.PasswordHash = &passwordHash.String
|
||||
}
|
||||
u.CreatedAt = util.ParseTime(createdAt.String)
|
||||
return &u, nil
|
||||
}
|
||||
|
||||
func Register(w http.ResponseWriter, r *http.Request) {
|
||||
var req struct {
|
||||
Email string `json:"email"`
|
||||
Password string `json:"password"`
|
||||
Nickname string `json:"nickname"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
writeError(w, 422, "请求格式错误")
|
||||
return
|
||||
}
|
||||
var existingID string
|
||||
err := database.DB.QueryRow(`SELECT id FROM users WHERE email = ?`, req.Email).Scan(&existingID)
|
||||
if err == nil {
|
||||
writeError(w, 400, "邮箱已注册")
|
||||
return
|
||||
}
|
||||
if err != sql.ErrNoRows {
|
||||
writeError(w, 500, "服务器错误")
|
||||
return
|
||||
}
|
||||
hash, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
writeError(w, 500, "服务器错误")
|
||||
return
|
||||
}
|
||||
userID := newID()
|
||||
_, err = database.DB.Exec(
|
||||
`INSERT INTO users (id, email, nickname, password_hash, role, is_guest, game_count, token_version, created_at)
|
||||
VALUES (?, ?, ?, ?, ?, 0, 0, 0, ?)`,
|
||||
userID, req.Email, req.Nickname, string(hash), models.RolePlayer, util.NowStr())
|
||||
if err != nil {
|
||||
writeError(w, 500, "服务器错误")
|
||||
return
|
||||
}
|
||||
token, _ := createToken(userID)
|
||||
writeJSON(w, 200, map[string]interface{}{
|
||||
"token": token,
|
||||
"user_id": userID,
|
||||
"nickname": req.Nickname,
|
||||
"is_guest": false,
|
||||
})
|
||||
}
|
||||
|
||||
func Login(w http.ResponseWriter, r *http.Request) {
|
||||
var req struct {
|
||||
Email string `json:"email"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
writeError(w, 422, "请求格式错误")
|
||||
return
|
||||
}
|
||||
var id, email, nickname, passwordHash string
|
||||
err := database.DB.QueryRow(
|
||||
`SELECT id, email, nickname, password_hash FROM users WHERE email = ?`, req.Email).
|
||||
Scan(&id, &email, &nickname, &passwordHash)
|
||||
if err == sql.ErrNoRows || passwordHash == "" {
|
||||
writeError(w, 401, "邮箱或密码错误")
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
writeError(w, 500, "服务器错误")
|
||||
return
|
||||
}
|
||||
if bcrypt.CompareHashAndPassword([]byte(passwordHash), []byte(req.Password)) != nil {
|
||||
writeError(w, 401, "邮箱或密码错误")
|
||||
return
|
||||
}
|
||||
token, _ := createToken(id)
|
||||
writeJSON(w, 200, map[string]interface{}{
|
||||
"token": token,
|
||||
"user_id": id,
|
||||
"nickname": nickname,
|
||||
"is_guest": false,
|
||||
})
|
||||
}
|
||||
|
||||
func GuestLogin(w http.ResponseWriter, r *http.Request) {
|
||||
var req struct {
|
||||
Nickname string `json:"nickname"`
|
||||
}
|
||||
_ = json.NewDecoder(r.Body).Decode(&req)
|
||||
if req.Nickname == "" {
|
||||
req.Nickname = "游客"
|
||||
}
|
||||
userID := newID()
|
||||
_, err := database.DB.Exec(
|
||||
`INSERT INTO users (id, nickname, role, is_guest, game_count, token_version, created_at)
|
||||
VALUES (?, ?, ?, 1, 0, 0, ?)`,
|
||||
userID, req.Nickname, models.RolePlayer, util.NowStr())
|
||||
if err != nil {
|
||||
writeError(w, 500, "服务器错误")
|
||||
return
|
||||
}
|
||||
token, _ := createToken(userID)
|
||||
writeJSON(w, 200, map[string]interface{}{
|
||||
"token": token,
|
||||
"user_id": userID,
|
||||
"nickname": req.Nickname,
|
||||
"is_guest": true,
|
||||
})
|
||||
}
|
||||
|
||||
func Logout(w http.ResponseWriter, r *http.Request) {
|
||||
writeJSON(w, 200, map[string]string{"status": "ok"})
|
||||
}
|
||||
|
||||
func GetProfile(w http.ResponseWriter, r *http.Request) {
|
||||
user, err := currentUser(r)
|
||||
if err != nil {
|
||||
writeError(w, 401, "未登录")
|
||||
return
|
||||
}
|
||||
writeJSON(w, 200, map[string]interface{}{
|
||||
"user_id": user.ID,
|
||||
"email": nullStr(user.Email),
|
||||
"nickname": nullStr(user.Nickname),
|
||||
"avatar_url": nullStr(user.AvatarURL),
|
||||
"is_guest": user.IsGuest,
|
||||
"role": user.Role,
|
||||
"game_count": user.GameCount,
|
||||
"created_at": util.TimeStr(user.CreatedAt),
|
||||
})
|
||||
}
|
||||
|
||||
func UpdateProfile(w http.ResponseWriter, r *http.Request) {
|
||||
user, err := currentUser(r)
|
||||
if err != nil {
|
||||
writeError(w, 401, "未登录")
|
||||
return
|
||||
}
|
||||
var data map[string]interface{}
|
||||
_ = json.NewDecoder(r.Body).Decode(&data)
|
||||
|
||||
var nickname, avatarURL *string
|
||||
if v, ok := data["nickname"].(string); ok {
|
||||
nickname = &v
|
||||
}
|
||||
if v, ok := data["avatar_url"].(string); ok {
|
||||
avatarURL = &v
|
||||
}
|
||||
if nickname != nil {
|
||||
if _, err := database.DB.Exec(`UPDATE users SET nickname = ? WHERE id = ?`, *nickname, user.ID); err != nil {
|
||||
writeError(w, 500, "服务器错误")
|
||||
return
|
||||
}
|
||||
}
|
||||
if avatarURL != nil {
|
||||
if _, err := database.DB.Exec(`UPDATE users SET avatar_url = ? WHERE id = ?`, *avatarURL, user.ID); err != nil {
|
||||
writeError(w, 500, "服务器错误")
|
||||
return
|
||||
}
|
||||
}
|
||||
writeJSON(w, 200, map[string]string{"status": "ok"})
|
||||
}
|
||||
|
||||
func GetHistory(w http.ResponseWriter, r *http.Request) {
|
||||
if _, err := currentUser(r); err != nil {
|
||||
writeError(w, 401, "未登录")
|
||||
return
|
||||
}
|
||||
rows, err := database.DB.Query(
|
||||
`SELECT id, script_id, status, phase, phase_index, started_at, completed_at, created_at
|
||||
FROM game_sessions ORDER BY created_at DESC LIMIT 50`)
|
||||
if err != nil {
|
||||
writeError(w, 500, "服务器错误")
|
||||
return
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []map[string]interface{}
|
||||
for rows.Next() {
|
||||
var id, scriptID, status, phase, startedAt, completedAt, createdAt sql.NullString
|
||||
var phaseIndex int
|
||||
if err := rows.Scan(&id, &scriptID, &status, &phase, &phaseIndex, &startedAt, &completedAt, &createdAt); err != nil {
|
||||
continue
|
||||
}
|
||||
items = append(items, map[string]interface{}{
|
||||
"id": id.String,
|
||||
"script_id": scriptID.String,
|
||||
"status": status.String,
|
||||
"phase": phase.String,
|
||||
"phase_index": phaseIndex,
|
||||
"started_at": startedAt.String,
|
||||
"completed_at": completedAt.String,
|
||||
"created_at": createdAt.String,
|
||||
})
|
||||
}
|
||||
writeJSON(w, 200, items)
|
||||
}
|
||||
@@ -0,0 +1,866 @@
|
||||
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,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,218 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"mmgame/internal/database"
|
||||
"mmgame/internal/models"
|
||||
"mmgame/internal/scripts"
|
||||
"mmgame/internal/util"
|
||||
)
|
||||
|
||||
func newID() string {
|
||||
b := make([]byte, 16)
|
||||
_, _ = rand.Read(b)
|
||||
b[6] = (b[6] & 0x0f) | 0x40
|
||||
b[8] = (b[8] & 0x3f) | 0x80
|
||||
return fmt.Sprintf("%x-%x-%x-%x-%x", b[0:4], b[4:6], b[6:8], b[8:10], b[10:16])
|
||||
}
|
||||
|
||||
func writeJSON(w http.ResponseWriter, status int, v interface{}) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(status)
|
||||
_ = json.NewEncoder(w).Encode(v)
|
||||
}
|
||||
|
||||
func writeError(w http.ResponseWriter, status int, detail string) {
|
||||
writeJSON(w, status, map[string]string{"detail": detail})
|
||||
}
|
||||
|
||||
func nullStr(s *string) string {
|
||||
if s == nil {
|
||||
return ""
|
||||
}
|
||||
return *s
|
||||
}
|
||||
|
||||
func Health(w http.ResponseWriter, r *http.Request) {
|
||||
writeJSON(w, 200, map[string]string{"status": "ok"})
|
||||
}
|
||||
|
||||
func parseConfig(configStr string) map[string]interface{} {
|
||||
if configStr == "" {
|
||||
return map[string]interface{}{}
|
||||
}
|
||||
var m map[string]interface{}
|
||||
if err := json.Unmarshal([]byte(configStr), &m); err != nil {
|
||||
return map[string]interface{}{}
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
func scriptFromConfig(configStr string) *scripts.Script {
|
||||
cfg := parseConfig(configStr)
|
||||
raw, ok := cfg["script"]
|
||||
if !ok || raw == nil {
|
||||
return nil
|
||||
}
|
||||
b, err := json.Marshal(raw)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
var s scripts.Script
|
||||
if err := json.Unmarshal(b, &s); err != nil {
|
||||
return nil
|
||||
}
|
||||
return &s
|
||||
}
|
||||
|
||||
func getGame(gameID string) (*models.GameSession, error) {
|
||||
row := database.DB.QueryRow(
|
||||
`SELECT id, script_id, status, phase, phase_index, started_at, completed_at, config, created_at
|
||||
FROM game_sessions WHERE id = ?`, gameID)
|
||||
var g models.GameSession
|
||||
var startedAt, completedAt, configStr, createdAt sql.NullString
|
||||
err := row.Scan(&g.ID, &g.ScriptID, &g.Status, &g.Phase, &g.PhaseIndex,
|
||||
&startedAt, &completedAt, &configStr, &createdAt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if startedAt.Valid {
|
||||
t := util.ParseTime(startedAt.String)
|
||||
g.StartedAt = &t
|
||||
}
|
||||
if completedAt.Valid {
|
||||
t := util.ParseTime(completedAt.String)
|
||||
g.CompletedAt = &t
|
||||
}
|
||||
g.Config = configStr.String
|
||||
g.CreatedAt = util.ParseTime(createdAt.String)
|
||||
return &g, nil
|
||||
}
|
||||
|
||||
func queryPlayers(gameID string) []*models.SessionPlayer {
|
||||
rows, err := database.DB.Query(
|
||||
`SELECT id, session_id, user_id, role_id, role_name, is_human, is_ready, avatar_url, status, joined_at
|
||||
FROM session_players WHERE session_id = ?`, gameID)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
defer rows.Close()
|
||||
var list []*models.SessionPlayer
|
||||
for rows.Next() {
|
||||
p := &models.SessionPlayer{}
|
||||
var userID, avatarURL, joinedAt sql.NullString
|
||||
if err := rows.Scan(&p.ID, &p.SessionID, &userID, &p.RoleID, &p.RoleName,
|
||||
&p.IsHuman, &p.IsReady, &avatarURL, &p.Status, &joinedAt); err != nil {
|
||||
continue
|
||||
}
|
||||
if userID.Valid {
|
||||
p.UserID = &userID.String
|
||||
}
|
||||
if avatarURL.Valid {
|
||||
p.AvatarURL = &avatarURL.String
|
||||
}
|
||||
p.JoinedAt = util.ParseTime(joinedAt.String)
|
||||
list = append(list, p)
|
||||
}
|
||||
return list
|
||||
}
|
||||
|
||||
func queryMessages(gameID string) []*models.ChatMessage {
|
||||
rows, err := database.DB.Query(
|
||||
`SELECT id, session_id, sender_role_id, sender_name, message_type, content, target_role_id, clue_id, phase_index, created_at
|
||||
FROM chat_messages WHERE session_id = ? ORDER BY created_at`, gameID)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
defer rows.Close()
|
||||
var list []*models.ChatMessage
|
||||
for rows.Next() {
|
||||
m := &models.ChatMessage{}
|
||||
var targetRoleID, clueID, createdAt sql.NullString
|
||||
if err := rows.Scan(&m.ID, &m.SessionID, &m.SenderRoleID, &m.SenderName, &m.MessageType,
|
||||
&m.Content, &targetRoleID, &clueID, &m.PhaseIndex, &createdAt); err != nil {
|
||||
continue
|
||||
}
|
||||
if targetRoleID.Valid {
|
||||
m.TargetRoleID = &targetRoleID.String
|
||||
}
|
||||
if clueID.Valid {
|
||||
m.ClueID = &clueID.String
|
||||
}
|
||||
m.CreatedAt = util.ParseTime(createdAt.String)
|
||||
list = append(list, m)
|
||||
}
|
||||
return list
|
||||
}
|
||||
|
||||
func queryClues(gameID string) []*models.ClueState {
|
||||
rows, err := database.DB.Query(
|
||||
`SELECT id, session_id, clue_id, status, revealed_by, released_at, created_at
|
||||
FROM clues_state WHERE session_id = ?`, gameID)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
defer rows.Close()
|
||||
var list []*models.ClueState
|
||||
for rows.Next() {
|
||||
c := &models.ClueState{}
|
||||
var revealedBy, releasedAt, createdAt sql.NullString
|
||||
if err := rows.Scan(&c.ID, &c.SessionID, &c.ClueID, &c.Status, &revealedBy, &releasedAt, &createdAt); err != nil {
|
||||
continue
|
||||
}
|
||||
if revealedBy.Valid {
|
||||
c.RevealedBy = &revealedBy.String
|
||||
}
|
||||
if releasedAt.Valid {
|
||||
t := util.ParseTime(releasedAt.String)
|
||||
c.ReleasedAt = &t
|
||||
}
|
||||
c.CreatedAt = util.ParseTime(createdAt.String)
|
||||
list = append(list, c)
|
||||
}
|
||||
return list
|
||||
}
|
||||
|
||||
func queryVotes(gameID string) []*models.Vote {
|
||||
rows, err := database.DB.Query(
|
||||
`SELECT id, session_id, round, voter_id, target_id, reason, created_at
|
||||
FROM votes WHERE session_id = ?`, gameID)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
defer rows.Close()
|
||||
var list []*models.Vote
|
||||
for rows.Next() {
|
||||
v := &models.Vote{}
|
||||
var reason, createdAt sql.NullString
|
||||
if err := rows.Scan(&v.ID, &v.SessionID, &v.Round, &v.VoterID, &v.TargetID, &reason, &createdAt); err != nil {
|
||||
continue
|
||||
}
|
||||
if reason.Valid {
|
||||
v.Reason = &reason.String
|
||||
}
|
||||
v.CreatedAt = util.ParseTime(createdAt.String)
|
||||
list = append(list, v)
|
||||
}
|
||||
return list
|
||||
}
|
||||
|
||||
func messageDict(m *models.ChatMessage) map[string]interface{} {
|
||||
d := map[string]interface{}{
|
||||
"id": m.ID,
|
||||
"sender_role_id": m.SenderRoleID,
|
||||
"sender_name": m.SenderName,
|
||||
"message_type": m.MessageType,
|
||||
"content": m.Content,
|
||||
"target_role_id": m.TargetRoleID,
|
||||
"clue_id": m.ClueID,
|
||||
"phase_index": m.PhaseIndex,
|
||||
"created_at": util.TimeStr(m.CreatedAt),
|
||||
}
|
||||
return d
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
|
||||
"mmgame/internal/scripts"
|
||||
)
|
||||
|
||||
func ListScripts(w http.ResponseWriter, r *http.Request) {
|
||||
items := []map[string]interface{}{}
|
||||
for _, s := range scripts.SCRIPTS {
|
||||
items = append(items, map[string]interface{}{
|
||||
"id": s.ID,
|
||||
"title": s.Title,
|
||||
"type": s.Type,
|
||||
"difficulty": s.Difficulty,
|
||||
"min_players": s.PlayerCount.Min,
|
||||
"max_players": s.PlayerCount.Max,
|
||||
"duration": s.Duration,
|
||||
"background": s.Background,
|
||||
})
|
||||
}
|
||||
writeJSON(w, 200, items)
|
||||
}
|
||||
|
||||
func GetScript(w http.ResponseWriter, r *http.Request) {
|
||||
scriptID := chi.URLParam(r, "script_id")
|
||||
s := scripts.GetByID(scriptID)
|
||||
if s == nil {
|
||||
writeError(w, 404, "剧本不存在")
|
||||
return
|
||||
}
|
||||
writeJSON(w, 200, s)
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"sync"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/gorilla/websocket"
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
Conn *websocket.Conn
|
||||
RoleID string
|
||||
}
|
||||
|
||||
var (
|
||||
connsMu sync.RWMutex
|
||||
activeConns = map[string][]*Client{}
|
||||
)
|
||||
|
||||
var upgrader = websocket.Upgrader{
|
||||
CheckOrigin: func(r *http.Request) bool { return true },
|
||||
}
|
||||
|
||||
func removeConn(gameID string, c *Client) {
|
||||
connsMu.Lock()
|
||||
defer connsMu.Unlock()
|
||||
clients := activeConns[gameID]
|
||||
for i, x := range clients {
|
||||
if x == c {
|
||||
activeConns[gameID] = append(clients[:i], clients[i+1:]...)
|
||||
break
|
||||
}
|
||||
}
|
||||
if len(activeConns[gameID]) == 0 {
|
||||
delete(activeConns, gameID)
|
||||
}
|
||||
}
|
||||
|
||||
func removeGameConns(gameID string) {
|
||||
connsMu.Lock()
|
||||
defer connsMu.Unlock()
|
||||
if clients, ok := activeConns[gameID]; ok {
|
||||
for _, c := range clients {
|
||||
_ = c.Conn.Close()
|
||||
}
|
||||
}
|
||||
delete(activeConns, gameID)
|
||||
}
|
||||
|
||||
func broadcast(gameID string, message interface{}) {
|
||||
data, err := json.Marshal(message)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
connsMu.RLock()
|
||||
clients := append([]*Client{}, activeConns[gameID]...)
|
||||
connsMu.RUnlock()
|
||||
for _, c := range clients {
|
||||
if err := c.Conn.WriteMessage(websocket.TextMessage, data); err != nil {
|
||||
removeConn(gameID, c)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func broadcastToRole(gameID, targetRoleID string, message interface{}) {
|
||||
data, err := json.Marshal(message)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
connsMu.RLock()
|
||||
clients := append([]*Client{}, activeConns[gameID]...)
|
||||
connsMu.RUnlock()
|
||||
for _, c := range clients {
|
||||
if c.RoleID != "" && c.RoleID != targetRoleID {
|
||||
continue
|
||||
}
|
||||
if err := c.Conn.WriteMessage(websocket.TextMessage, data); err != nil {
|
||||
removeConn(gameID, c)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func GameWebSocket(w http.ResponseWriter, r *http.Request) {
|
||||
gameID := chi.URLParam(r, "game_id")
|
||||
roleID := r.URL.Query().Get("role_id")
|
||||
|
||||
conn, err := upgrader.Upgrade(w, r, nil)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
client := &Client{Conn: conn, RoleID: roleID}
|
||||
|
||||
connsMu.Lock()
|
||||
activeConns[gameID] = append(activeConns[gameID], client)
|
||||
connsMu.Unlock()
|
||||
|
||||
defer func() {
|
||||
removeConn(gameID, client)
|
||||
_ = conn.Close()
|
||||
}()
|
||||
|
||||
for {
|
||||
_, data, err := conn.ReadMessage()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
var msg struct {
|
||||
Type string `json:"type"`
|
||||
}
|
||||
if err := json.Unmarshal(data, &msg); err != nil {
|
||||
continue
|
||||
}
|
||||
if msg.Type == "ping" {
|
||||
if err := conn.WriteMessage(websocket.TextMessage, []byte(`{"type":"pong"}`)); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user