first
This commit is contained in:
+25
@@ -0,0 +1,25 @@
|
|||||||
|
# Python-generated files
|
||||||
|
__pycache__/
|
||||||
|
*.py[oc]
|
||||||
|
build/
|
||||||
|
dist/
|
||||||
|
wheels/
|
||||||
|
*.egg-info
|
||||||
|
|
||||||
|
# from ide
|
||||||
|
.vscode
|
||||||
|
.idea
|
||||||
|
|
||||||
|
# Virtual environments
|
||||||
|
.venv
|
||||||
|
venv
|
||||||
|
|
||||||
|
# config
|
||||||
|
settings.json
|
||||||
|
|
||||||
|
# logs
|
||||||
|
log/*
|
||||||
|
|
||||||
|
cmake-build-debug
|
||||||
|
|
||||||
|
|
||||||
+104
@@ -0,0 +1,104 @@
|
|||||||
|
package game
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"math/rand"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type CardDef struct {
|
||||||
|
TypeID string
|
||||||
|
Name string
|
||||||
|
MP int
|
||||||
|
Priority int
|
||||||
|
Count int
|
||||||
|
Unusable bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type Card struct {
|
||||||
|
UID string
|
||||||
|
TypeID string
|
||||||
|
Name string
|
||||||
|
MP int
|
||||||
|
}
|
||||||
|
|
||||||
|
var AllCardDefs = []CardDef{
|
||||||
|
{"student_council_president", "学生会长", 3, 4, 1, false},
|
||||||
|
{"class_monitor", "班长", 2, 4, 2, false},
|
||||||
|
{"honor_student", "优等生", 2, 4, 3, false},
|
||||||
|
{"discipline_committee_member", "风纪委员", 1, 4, 2, false},
|
||||||
|
{"library_committee_member", "图书委员", 1, 4, 3, false},
|
||||||
|
{"health_committee_member", "保健委员", 1, 4, 2, false},
|
||||||
|
{"young_lady", "大小姐", 1, 4, 3, false},
|
||||||
|
{"newspaper_club", "新闻部", 1, 4, 3, false},
|
||||||
|
{"go_home_club", "归宅部", 0, 5, 3, false},
|
||||||
|
{"infected_person", "感染者", 0, 2, 1, false},
|
||||||
|
{"accomplice", "共犯", 0, 3, 1, false},
|
||||||
|
{"culprit", "犯人", 0, 3, 1, true},
|
||||||
|
{"alien", "外星人", -1, 1, 1, false},
|
||||||
|
}
|
||||||
|
|
||||||
|
type RemoveRule struct {
|
||||||
|
TypeID string
|
||||||
|
Count int
|
||||||
|
}
|
||||||
|
|
||||||
|
type GameSetting struct {
|
||||||
|
PlayerCount int
|
||||||
|
HarmonyTarget int
|
||||||
|
RemoveCards []RemoveRule
|
||||||
|
TotalCards int
|
||||||
|
HandSize int
|
||||||
|
}
|
||||||
|
|
||||||
|
var GameSettings = map[int]GameSetting{
|
||||||
|
3: {3, 9, []RemoveRule{
|
||||||
|
{"library_committee_member", 1}, {"accomplice", 1}, {"honor_student", 1},
|
||||||
|
{"discipline_committee_member", 1}, {"young_lady", 1}, {"newspaper_club", 1}, {"go_home_club", 1},
|
||||||
|
}, 18, 6},
|
||||||
|
4: {4, 8, []RemoveRule{{"library_committee_member", 1}}, 24, 6},
|
||||||
|
5: {5, 7, nil, 25, 5},
|
||||||
|
6: {6, 6, []RemoveRule{{"library_committee_member", 1}}, 24, 4},
|
||||||
|
}
|
||||||
|
|
||||||
|
func BuildDeck(playerCount int) ([]*Card, GameSetting) {
|
||||||
|
setting := GameSettings[playerCount]
|
||||||
|
rng := rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||||
|
|
||||||
|
removeCount := make(map[string]int)
|
||||||
|
for _, r := range setting.RemoveCards {
|
||||||
|
removeCount[r.TypeID] += r.Count
|
||||||
|
}
|
||||||
|
|
||||||
|
var deck []*Card
|
||||||
|
uid := 0
|
||||||
|
for _, def := range AllCardDefs {
|
||||||
|
toRemove := removeCount[def.TypeID]
|
||||||
|
toAdd := def.Count - toRemove
|
||||||
|
for i := 0; i < toAdd; i++ {
|
||||||
|
uid++
|
||||||
|
deck = append(deck, &Card{
|
||||||
|
UID: fmt.Sprintf("c_%03d", uid),
|
||||||
|
TypeID: def.TypeID,
|
||||||
|
Name: def.Name,
|
||||||
|
MP: def.MP,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rng.Shuffle(len(deck), func(i, j int) { deck[i], deck[j] = deck[j], deck[i] })
|
||||||
|
return deck, setting
|
||||||
|
}
|
||||||
|
|
||||||
|
func IsUnusable(typeID string) bool {
|
||||||
|
return typeID == "culprit"
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetCardDef(typeID string) *CardDef {
|
||||||
|
for i := range AllCardDefs {
|
||||||
|
if AllCardDefs[i].TypeID == typeID {
|
||||||
|
return &AllCardDefs[i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
+719
@@ -0,0 +1,719 @@
|
|||||||
|
package game
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
type EffectState struct {
|
||||||
|
Active bool
|
||||||
|
CardTypeID string
|
||||||
|
CasterID string
|
||||||
|
Step int
|
||||||
|
WaitingFor string
|
||||||
|
ChoiceType string
|
||||||
|
ChoiceData map[string]interface{}
|
||||||
|
TempData map[string]interface{}
|
||||||
|
PendingResponses map[string]interface{}
|
||||||
|
AllCollected bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Engine) triggerEffect(caster *PlayerState, card *Card) bool {
|
||||||
|
switch card.TypeID {
|
||||||
|
case "student_council_president", "culprit", "alien":
|
||||||
|
return false
|
||||||
|
case "library_committee_member":
|
||||||
|
return e.effectLibrary(caster)
|
||||||
|
case "discipline_committee_member":
|
||||||
|
return e.effectDiscipline(caster)
|
||||||
|
case "health_committee_member":
|
||||||
|
return e.effectHealth(caster)
|
||||||
|
case "go_home_club":
|
||||||
|
return e.effectGoHome(caster)
|
||||||
|
case "accomplice":
|
||||||
|
return e.effectAccomplice(caster)
|
||||||
|
case "class_monitor":
|
||||||
|
return e.effectClassMonitor(caster)
|
||||||
|
case "young_lady":
|
||||||
|
return e.effectYoungLady(caster)
|
||||||
|
case "newspaper_club":
|
||||||
|
return e.effectNewspaper(caster)
|
||||||
|
case "honor_student":
|
||||||
|
return e.effectHonorStudent(caster)
|
||||||
|
case "infected_person":
|
||||||
|
return e.effectInfected(caster)
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Engine) effectLibrary(caster *PlayerState) bool {
|
||||||
|
if len(e.HarmonyZone) == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
var cards []map[string]interface{}
|
||||||
|
for _, c := range e.HarmonyZone {
|
||||||
|
cards = append(cards, map[string]interface{}{
|
||||||
|
"uid": c.UID, "type_id": c.TypeID, "name": c.Name, "mp": c.MP,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
e.Effect = &EffectState{
|
||||||
|
Active: true, CardTypeID: "library_committee_member", CasterID: caster.ID,
|
||||||
|
WaitingFor: caster.ID, ChoiceType: "view_cards", Step: 0,
|
||||||
|
ChoiceData: map[string]interface{}{"cards": cards, "message": "调和区的卡牌"},
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Engine) effectDiscipline(caster *PlayerState) bool {
|
||||||
|
var targets []map[string]interface{}
|
||||||
|
for _, p := range e.Players {
|
||||||
|
if p.ID != caster.ID && !p.IsExited && len(p.Hand) > 0 {
|
||||||
|
targets = append(targets, map[string]interface{}{"player_id": p.ID, "nickname": p.Nickname})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(targets) == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
e.Effect = &EffectState{
|
||||||
|
Active: true, CardTypeID: "discipline_committee_member", CasterID: caster.ID,
|
||||||
|
WaitingFor: caster.ID, ChoiceType: "select_player", Step: 0,
|
||||||
|
ChoiceData: map[string]interface{}{"targets": targets, "message": "选择要查看手牌的玩家"},
|
||||||
|
TempData: make(map[string]interface{}),
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Engine) effectHealth(caster *PlayerState) bool {
|
||||||
|
var cards []map[string]interface{}
|
||||||
|
for _, p := range e.Players {
|
||||||
|
for _, c := range p.SkillZone {
|
||||||
|
if c.TypeID != "health_committee_member" {
|
||||||
|
cards = append(cards, map[string]interface{}{
|
||||||
|
"uid": c.UID, "type_id": c.TypeID, "name": c.Name, "owner_id": p.ID,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(cards) == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
e.Effect = &EffectState{
|
||||||
|
Active: true, CardTypeID: "health_committee_member", CasterID: caster.ID,
|
||||||
|
WaitingFor: caster.ID, ChoiceType: "select_skill_card", Step: 0,
|
||||||
|
ChoiceData: map[string]interface{}{"cards": cards, "message": "选择一张已使用的牌加入手牌"},
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Engine) effectGoHome(caster *PlayerState) bool {
|
||||||
|
if len(e.HarmonyZone) == 0 || len(caster.Hand) == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
var handCards []map[string]interface{}
|
||||||
|
for _, c := range caster.Hand {
|
||||||
|
handCards = append(handCards, map[string]interface{}{"uid": c.UID, "type_id": c.TypeID, "name": c.Name})
|
||||||
|
}
|
||||||
|
e.Effect = &EffectState{
|
||||||
|
Active: true, CardTypeID: "go_home_club", CasterID: caster.ID,
|
||||||
|
WaitingFor: caster.ID, ChoiceType: "select_card_from_hand", Step: 0,
|
||||||
|
ChoiceData: map[string]interface{}{"cards": handCards, "message": "选择一张手牌与调和区交换"},
|
||||||
|
TempData: make(map[string]interface{}),
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Engine) effectAccomplice(caster *PlayerState) bool {
|
||||||
|
var cards []map[string]interface{}
|
||||||
|
for _, p := range e.Players {
|
||||||
|
for _, c := range p.ChallengeZone {
|
||||||
|
cards = append(cards, map[string]interface{}{
|
||||||
|
"uid": c.UID, "owner_id": p.ID,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(cards) == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
e.Effect = &EffectState{
|
||||||
|
Active: true, CardTypeID: "accomplice", CasterID: caster.ID,
|
||||||
|
WaitingFor: caster.ID, ChoiceType: "select_challenge_card", Step: 0,
|
||||||
|
ChoiceData: map[string]interface{}{"cards": cards, "message": "选择一张质疑区的牌"},
|
||||||
|
TempData: make(map[string]interface{}),
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Engine) effectClassMonitor(caster *PlayerState) bool {
|
||||||
|
var targets []map[string]interface{}
|
||||||
|
for _, p := range e.Players {
|
||||||
|
if !p.IsExited && len(p.Hand) > 0 {
|
||||||
|
targets = append(targets, map[string]interface{}{"player_id": p.ID, "nickname": p.Nickname})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(targets) < 2 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
e.Effect = &EffectState{
|
||||||
|
Active: true, CardTypeID: "class_monitor", CasterID: caster.ID,
|
||||||
|
WaitingFor: caster.ID, ChoiceType: "select_player", Step: 0,
|
||||||
|
ChoiceData: map[string]interface{}{"targets": targets, "message": "选择换牌的第一名玩家(可以是自己)"},
|
||||||
|
TempData: make(map[string]interface{}),
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Engine) effectYoungLady(caster *PlayerState) bool {
|
||||||
|
var targets []map[string]interface{}
|
||||||
|
for _, p := range e.Players {
|
||||||
|
if p.ID != caster.ID && !p.IsExited && len(p.Hand) > 0 {
|
||||||
|
targets = append(targets, map[string]interface{}{"player_id": p.ID, "nickname": p.Nickname})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(targets) == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
e.Effect = &EffectState{
|
||||||
|
Active: true, CardTypeID: "young_lady", CasterID: caster.ID,
|
||||||
|
WaitingFor: caster.ID, ChoiceType: "select_player", Step: 0,
|
||||||
|
ChoiceData: map[string]interface{}{"targets": targets, "message": "选择一名玩家抽取手牌"},
|
||||||
|
TempData: make(map[string]interface{}),
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Engine) effectNewspaper(caster *PlayerState) bool {
|
||||||
|
e.Effect = &EffectState{
|
||||||
|
Active: true, CardTypeID: "newspaper_club", CasterID: caster.ID,
|
||||||
|
ChoiceType: "all_select_card", Step: 0,
|
||||||
|
ChoiceData: map[string]interface{}{"message": "选择一张手牌传给左手边的玩家"},
|
||||||
|
PendingResponses: make(map[string]interface{}),
|
||||||
|
}
|
||||||
|
for _, p := range e.Players {
|
||||||
|
if !p.IsExited && len(p.Hand) > 0 {
|
||||||
|
e.Effect.WaitingFor = p.ID
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Engine) effectHonorStudent(caster *PlayerState) bool {
|
||||||
|
var culpritHolder, alienHolder string
|
||||||
|
for _, p := range e.Players {
|
||||||
|
if p.ID == caster.ID {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for _, c := range p.Hand {
|
||||||
|
if c.TypeID == "culprit" {
|
||||||
|
culpritHolder = p.ID
|
||||||
|
}
|
||||||
|
if c.TypeID == "alien" {
|
||||||
|
alienHolder = p.ID
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
e.Effect = &EffectState{
|
||||||
|
Active: true, CardTypeID: "honor_student", CasterID: caster.ID,
|
||||||
|
TempData: map[string]interface{}{
|
||||||
|
"culprit_holder": culpritHolder,
|
||||||
|
"alien_holder": alienHolder,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if culpritHolder != "" {
|
||||||
|
cp := e.GetPlayer(culpritHolder)
|
||||||
|
if cp != nil && !cp.IsExited {
|
||||||
|
e.Effect.Step = 0
|
||||||
|
e.Effect.WaitingFor = culpritHolder
|
||||||
|
e.Effect.ChoiceType = "select_option"
|
||||||
|
e.Effect.ChoiceData = map[string]interface{}{
|
||||||
|
"message": "有人使用了优等生!你是犯人,必须吐出舌头。",
|
||||||
|
"options": []map[string]interface{}{
|
||||||
|
{"value": "long", "label": "吐长舌头"},
|
||||||
|
{"value": "short", "label": "吐短舌头"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if alienHolder != "" {
|
||||||
|
ap := e.GetPlayer(alienHolder)
|
||||||
|
if ap != nil && !ap.IsExited {
|
||||||
|
e.Effect.Step = 1
|
||||||
|
e.Effect.WaitingFor = alienHolder
|
||||||
|
e.Effect.ChoiceType = "select_option"
|
||||||
|
e.Effect.ChoiceData = map[string]interface{}{
|
||||||
|
"message": "有人使用了优等生!你是外星人,是否要装作犯人吐舌头?",
|
||||||
|
"options": []map[string]interface{}{
|
||||||
|
{"value": "long", "label": "吐长舌头"},
|
||||||
|
{"value": "short", "label": "吐短舌头"},
|
||||||
|
{"value": "none", "label": "不吐舌头"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
e.Effect.Step = 99
|
||||||
|
e.Effect.WaitingFor = caster.ID
|
||||||
|
e.Effect.ChoiceType = "view_cards"
|
||||||
|
e.Effect.ChoiceData = map[string]interface{}{"message": "优等生效果:没有人吐舌头"}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Engine) effectInfected(caster *PlayerState) bool {
|
||||||
|
if len(caster.Hand) <= 1 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
caster.InfectedActive = true
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// HandleEffectResponse processes a player's response during effect resolution
|
||||||
|
func (e *Engine) HandleEffectResponse(playerID string, response map[string]interface{}) error {
|
||||||
|
ef := e.Effect
|
||||||
|
if ef == nil || !ef.Active {
|
||||||
|
return fmt.Errorf("no active effect")
|
||||||
|
}
|
||||||
|
|
||||||
|
switch ef.CardTypeID {
|
||||||
|
case "library_committee_member", "student_council_president":
|
||||||
|
return e.finishEffect()
|
||||||
|
case "discipline_committee_member":
|
||||||
|
return e.respondDiscipline(playerID, response)
|
||||||
|
case "health_committee_member":
|
||||||
|
return e.respondHealth(playerID, response)
|
||||||
|
case "go_home_club":
|
||||||
|
return e.respondGoHome(playerID, response)
|
||||||
|
case "accomplice":
|
||||||
|
return e.respondAccomplice(playerID, response)
|
||||||
|
case "class_monitor":
|
||||||
|
return e.respondClassMonitor(playerID, response)
|
||||||
|
case "young_lady":
|
||||||
|
return e.respondYoungLady(playerID, response)
|
||||||
|
case "newspaper_club":
|
||||||
|
return e.respondNewspaper(playerID, response)
|
||||||
|
case "honor_student":
|
||||||
|
return e.respondHonorStudent(playerID, response)
|
||||||
|
case "infected_person":
|
||||||
|
return e.respondInfected(playerID, response)
|
||||||
|
}
|
||||||
|
return e.finishEffect()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Engine) respondDiscipline(pid string, resp map[string]interface{}) error {
|
||||||
|
ef := e.Effect
|
||||||
|
if ef.Step == 0 {
|
||||||
|
tid, _ := resp["player_id"].(string)
|
||||||
|
target := e.GetPlayer(tid)
|
||||||
|
if target == nil {
|
||||||
|
return fmt.Errorf("invalid target")
|
||||||
|
}
|
||||||
|
var cards []map[string]interface{}
|
||||||
|
for _, c := range target.Hand {
|
||||||
|
cards = append(cards, map[string]interface{}{"uid": c.UID, "type_id": c.TypeID, "name": c.Name, "mp": c.MP})
|
||||||
|
}
|
||||||
|
ef.Step = 1
|
||||||
|
ef.ChoiceType = "view_cards"
|
||||||
|
ef.ChoiceData = map[string]interface{}{"cards": cards, "message": target.Nickname + " 的手牌"}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return e.finishEffect()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Engine) respondHealth(pid string, resp map[string]interface{}) error {
|
||||||
|
uid, _ := resp["card_uid"].(string)
|
||||||
|
for _, p := range e.Players {
|
||||||
|
for i, c := range p.SkillZone {
|
||||||
|
if c.UID == uid && c.TypeID != "health_committee_member" {
|
||||||
|
p.SkillZone = append(p.SkillZone[:i], p.SkillZone[i+1:]...)
|
||||||
|
caster := e.GetPlayer(e.Effect.CasterID)
|
||||||
|
caster.Hand = append(caster.Hand, c)
|
||||||
|
return e.finishEffect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fmt.Errorf("card not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Engine) respondGoHome(pid string, resp map[string]interface{}) error {
|
||||||
|
ef := e.Effect
|
||||||
|
if ef.Step == 0 {
|
||||||
|
uid, _ := resp["card_uid"].(string)
|
||||||
|
ef.TempData["hand_card_uid"] = uid
|
||||||
|
ef.Step = 1
|
||||||
|
var hCards []map[string]interface{}
|
||||||
|
for i, c := range e.HarmonyZone {
|
||||||
|
hCards = append(hCards, map[string]interface{}{"uid": c.UID, "index": i})
|
||||||
|
}
|
||||||
|
ef.ChoiceType = "select_harmony_card"
|
||||||
|
ef.ChoiceData = map[string]interface{}{"cards": hCards, "message": "选择调和区的一张牌"}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
hUID, _ := resp["card_uid"].(string)
|
||||||
|
handUID, _ := ef.TempData["hand_card_uid"].(string)
|
||||||
|
caster := e.GetPlayer(ef.CasterID)
|
||||||
|
|
||||||
|
var handCard *Card
|
||||||
|
for i, c := range caster.Hand {
|
||||||
|
if c.UID == handUID {
|
||||||
|
handCard = c
|
||||||
|
caster.Hand = append(caster.Hand[:i], caster.Hand[i+1:]...)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var harmonyCard *Card
|
||||||
|
for i, c := range e.HarmonyZone {
|
||||||
|
if c.UID == hUID {
|
||||||
|
harmonyCard = c
|
||||||
|
e.HarmonyZone = append(e.HarmonyZone[:i], e.HarmonyZone[i+1:]...)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if handCard != nil {
|
||||||
|
e.HarmonyZone = append(e.HarmonyZone, handCard)
|
||||||
|
}
|
||||||
|
if harmonyCard != nil {
|
||||||
|
caster.Hand = append(caster.Hand, harmonyCard)
|
||||||
|
}
|
||||||
|
return e.finishEffect()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Engine) respondAccomplice(pid string, resp map[string]interface{}) error {
|
||||||
|
ef := e.Effect
|
||||||
|
if ef.Step == 0 {
|
||||||
|
uid, _ := resp["card_uid"].(string)
|
||||||
|
ef.TempData["challenge_uid"] = uid
|
||||||
|
owner, _ := resp["owner_id"].(string)
|
||||||
|
ef.TempData["challenge_owner"] = owner
|
||||||
|
ef.Step = 1
|
||||||
|
var targets []map[string]interface{}
|
||||||
|
for _, p := range e.Players {
|
||||||
|
if p.ID != owner {
|
||||||
|
targets = append(targets, map[string]interface{}{"player_id": p.ID, "nickname": p.Nickname})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ef.ChoiceType = "select_player"
|
||||||
|
ef.ChoiceData = map[string]interface{}{"targets": targets, "message": "选择转移到哪个玩家面前"}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
targetID, _ := resp["player_id"].(string)
|
||||||
|
cUID, _ := ef.TempData["challenge_uid"].(string)
|
||||||
|
ownerID, _ := ef.TempData["challenge_owner"].(string)
|
||||||
|
|
||||||
|
owner := e.GetPlayer(ownerID)
|
||||||
|
target := e.GetPlayer(targetID)
|
||||||
|
if owner == nil || target == nil {
|
||||||
|
return e.finishEffect()
|
||||||
|
}
|
||||||
|
for i, c := range owner.ChallengeZone {
|
||||||
|
if c.UID == cUID {
|
||||||
|
owner.ChallengeZone = append(owner.ChallengeZone[:i], owner.ChallengeZone[i+1:]...)
|
||||||
|
target.ChallengeZone = append(target.ChallengeZone, c)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return e.finishEffect()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Engine) respondClassMonitor(pid string, resp map[string]interface{}) error {
|
||||||
|
ef := e.Effect
|
||||||
|
if ef.Step == 0 {
|
||||||
|
aid, _ := resp["player_id"].(string)
|
||||||
|
ef.TempData["player_a_id"] = aid
|
||||||
|
ef.Step = 1
|
||||||
|
var targets []map[string]interface{}
|
||||||
|
for _, p := range e.Players {
|
||||||
|
if p.ID != aid && !p.IsExited && len(p.Hand) > 0 {
|
||||||
|
targets = append(targets, map[string]interface{}{"player_id": p.ID, "nickname": p.Nickname})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ef.WaitingFor = ef.CasterID
|
||||||
|
ef.ChoiceType = "select_player"
|
||||||
|
ef.ChoiceData = map[string]interface{}{"targets": targets, "message": "选择换牌的第二名玩家"}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if ef.Step == 1 {
|
||||||
|
bid, _ := resp["player_id"].(string)
|
||||||
|
ef.TempData["player_b_id"] = bid
|
||||||
|
ef.Step = 2
|
||||||
|
aid, _ := ef.TempData["player_a_id"].(string)
|
||||||
|
playerA := e.GetPlayer(aid)
|
||||||
|
ef.WaitingFor = aid
|
||||||
|
var cards []map[string]interface{}
|
||||||
|
for _, c := range playerA.Hand {
|
||||||
|
cards = append(cards, map[string]interface{}{"uid": c.UID, "type_id": c.TypeID, "name": c.Name})
|
||||||
|
}
|
||||||
|
ef.ChoiceType = "select_card_from_hand"
|
||||||
|
ef.ChoiceData = map[string]interface{}{"cards": cards, "message": playerA.Nickname + ":选择一张手牌交给对方"}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if ef.Step == 2 {
|
||||||
|
uid, _ := resp["card_uid"].(string)
|
||||||
|
ef.TempData["card_a_uid"] = uid
|
||||||
|
ef.Step = 3
|
||||||
|
bid, _ := ef.TempData["player_b_id"].(string)
|
||||||
|
playerB := e.GetPlayer(bid)
|
||||||
|
ef.WaitingFor = bid
|
||||||
|
var cards []map[string]interface{}
|
||||||
|
for _, c := range playerB.Hand {
|
||||||
|
cards = append(cards, map[string]interface{}{"uid": c.UID, "type_id": c.TypeID, "name": c.Name})
|
||||||
|
}
|
||||||
|
ef.ChoiceType = "select_card_from_hand"
|
||||||
|
ef.ChoiceData = map[string]interface{}{"cards": cards, "message": playerB.Nickname + ":选择一张手牌交给对方"}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
bUID, _ := resp["card_uid"].(string)
|
||||||
|
aUID, _ := ef.TempData["card_a_uid"].(string)
|
||||||
|
aid, _ := ef.TempData["player_a_id"].(string)
|
||||||
|
bid, _ := ef.TempData["player_b_id"].(string)
|
||||||
|
pA := e.GetPlayer(aid)
|
||||||
|
pB := e.GetPlayer(bid)
|
||||||
|
var cA, cB *Card
|
||||||
|
for i, c := range pA.Hand {
|
||||||
|
if c.UID == aUID {
|
||||||
|
cA = c
|
||||||
|
pA.Hand = append(pA.Hand[:i], pA.Hand[i+1:]...)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for i, c := range pB.Hand {
|
||||||
|
if c.UID == bUID {
|
||||||
|
cB = c
|
||||||
|
pB.Hand = append(pB.Hand[:i], pB.Hand[i+1:]...)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if cA != nil {
|
||||||
|
pB.Hand = append(pB.Hand, cA)
|
||||||
|
}
|
||||||
|
if cB != nil {
|
||||||
|
pA.Hand = append(pA.Hand, cB)
|
||||||
|
}
|
||||||
|
return e.finishEffect()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Engine) respondYoungLady(pid string, resp map[string]interface{}) error {
|
||||||
|
ef := e.Effect
|
||||||
|
caster := e.GetPlayer(ef.CasterID)
|
||||||
|
if ef.Step == 0 {
|
||||||
|
tid, _ := resp["player_id"].(string)
|
||||||
|
target := e.GetPlayer(tid)
|
||||||
|
if target == nil || len(target.Hand) == 0 {
|
||||||
|
return e.finishEffect()
|
||||||
|
}
|
||||||
|
drawn := target.Hand[e.rng.Intn(len(target.Hand))]
|
||||||
|
e.removeFromHand(target, drawn.UID)
|
||||||
|
caster.Hand = append(caster.Hand, drawn)
|
||||||
|
|
||||||
|
ef.TempData["target_id"] = tid
|
||||||
|
ef.Step = 1
|
||||||
|
ef.WaitingFor = ef.CasterID
|
||||||
|
var cards []map[string]interface{}
|
||||||
|
for _, c := range caster.Hand {
|
||||||
|
cards = append(cards, map[string]interface{}{"uid": c.UID, "type_id": c.TypeID, "name": c.Name})
|
||||||
|
}
|
||||||
|
ef.ChoiceType = "select_card_from_hand"
|
||||||
|
ef.ChoiceData = map[string]interface{}{
|
||||||
|
"cards": cards,
|
||||||
|
"message": "从 " + target.Nickname + " 抽到了「" + drawn.Name + "」,选择一张手牌反面返还",
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
uid, _ := resp["card_uid"].(string)
|
||||||
|
tid, _ := ef.TempData["target_id"].(string)
|
||||||
|
target := e.GetPlayer(tid)
|
||||||
|
for i, c := range caster.Hand {
|
||||||
|
if c.UID == uid {
|
||||||
|
caster.Hand = append(caster.Hand[:i], caster.Hand[i+1:]...)
|
||||||
|
if target != nil {
|
||||||
|
target.Hand = append(target.Hand, c)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return e.finishEffect()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Engine) respondNewspaper(pid string, resp map[string]interface{}) error {
|
||||||
|
ef := e.Effect
|
||||||
|
uid, _ := resp["card_uid"].(string)
|
||||||
|
ef.PendingResponses[pid] = uid
|
||||||
|
|
||||||
|
allDone := true
|
||||||
|
for _, p := range e.Players {
|
||||||
|
if !p.IsExited && len(p.Hand) > 0 {
|
||||||
|
if _, ok := ef.PendingResponses[p.ID]; !ok {
|
||||||
|
allDone = false
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !allDone {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type passInfo struct {
|
||||||
|
from *PlayerState
|
||||||
|
card *Card
|
||||||
|
}
|
||||||
|
var passes []passInfo
|
||||||
|
activePlayers := e.getActivePlayers()
|
||||||
|
for _, p := range activePlayers {
|
||||||
|
uidStr, ok := ef.PendingResponses[p.ID].(string)
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for _, c := range p.Hand {
|
||||||
|
if c.UID == uidStr {
|
||||||
|
passes = append(passes, passInfo{from: p, card: c})
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, pi := range passes {
|
||||||
|
e.removeFromHand(pi.from, pi.card.UID)
|
||||||
|
}
|
||||||
|
for i, pi := range passes {
|
||||||
|
nextIdx := (i + 1) % len(activePlayers)
|
||||||
|
activePlayers[nextIdx].Hand = append(activePlayers[nextIdx].Hand, pi.card)
|
||||||
|
}
|
||||||
|
|
||||||
|
return e.finishEffect()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Engine) respondHonorStudent(pid string, resp map[string]interface{}) error {
|
||||||
|
ef := e.Effect
|
||||||
|
|
||||||
|
if ef.Step == 0 {
|
||||||
|
tongue, _ := resp["value"].(string)
|
||||||
|
culpritID, _ := ef.TempData["culprit_holder"].(string)
|
||||||
|
cp := e.GetPlayer(culpritID)
|
||||||
|
name := culpritID
|
||||||
|
if cp != nil {
|
||||||
|
name = cp.Nickname
|
||||||
|
}
|
||||||
|
tongues := []interface{}{map[string]interface{}{"nickname": name, "tongue": tongue}}
|
||||||
|
ef.TempData["tongues"] = tongues
|
||||||
|
|
||||||
|
alienHolder, _ := ef.TempData["alien_holder"].(string)
|
||||||
|
if alienHolder != "" {
|
||||||
|
ap := e.GetPlayer(alienHolder)
|
||||||
|
if ap != nil && !ap.IsExited {
|
||||||
|
ef.Step = 1
|
||||||
|
ef.WaitingFor = alienHolder
|
||||||
|
ef.ChoiceType = "select_option"
|
||||||
|
ef.ChoiceData = map[string]interface{}{
|
||||||
|
"message": "有人使用了优等生!你是外星人,是否要装作犯人吐舌头?",
|
||||||
|
"options": []interface{}{
|
||||||
|
map[string]interface{}{"value": "long", "label": "吐长舌头"},
|
||||||
|
map[string]interface{}{"value": "short", "label": "吐短舌头"},
|
||||||
|
map[string]interface{}{"value": "none", "label": "不吐舌头"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return e.showHonorResult()
|
||||||
|
}
|
||||||
|
|
||||||
|
if ef.Step == 1 {
|
||||||
|
tongue, _ := resp["value"].(string)
|
||||||
|
if tongue != "none" {
|
||||||
|
alienHolder, _ := ef.TempData["alien_holder"].(string)
|
||||||
|
ap := e.GetPlayer(alienHolder)
|
||||||
|
name := alienHolder
|
||||||
|
if ap != nil {
|
||||||
|
name = ap.Nickname
|
||||||
|
}
|
||||||
|
tongues, _ := ef.TempData["tongues"].([]interface{})
|
||||||
|
tongues = append(tongues, map[string]interface{}{"nickname": name, "tongue": tongue})
|
||||||
|
ef.TempData["tongues"] = tongues
|
||||||
|
}
|
||||||
|
return e.showHonorResult()
|
||||||
|
}
|
||||||
|
|
||||||
|
return e.finishEffect()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Engine) showHonorResult() error {
|
||||||
|
ef := e.Effect
|
||||||
|
tongues, _ := ef.TempData["tongues"].([]interface{})
|
||||||
|
msg := fmt.Sprintf("优等生效果:%d人吐了舌头\n", len(tongues))
|
||||||
|
for _, t := range tongues {
|
||||||
|
tm, _ := t.(map[string]interface{})
|
||||||
|
nick, _ := tm["nickname"].(string)
|
||||||
|
tongue, _ := tm["tongue"].(string)
|
||||||
|
label := "长舌头"
|
||||||
|
if tongue == "short" {
|
||||||
|
label = "短舌头"
|
||||||
|
}
|
||||||
|
msg += fmt.Sprintf(" • %s — %s\n", nick, label)
|
||||||
|
}
|
||||||
|
ef.Step = 99
|
||||||
|
ef.WaitingFor = ef.CasterID
|
||||||
|
ef.ChoiceType = "view_cards"
|
||||||
|
ef.ChoiceData = map[string]interface{}{"message": msg, "tongues": tongues}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Engine) finishEffect() error {
|
||||||
|
e.Effect = nil
|
||||||
|
caster := e.GetPlayer(e.Players[e.CurrentTurn].ID)
|
||||||
|
e.finishTurn(caster)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Engine) getActivePlayers() []*PlayerState {
|
||||||
|
var active []*PlayerState
|
||||||
|
for _, p := range e.Players {
|
||||||
|
if !p.IsExited && len(p.Hand) > 0 {
|
||||||
|
active = append(active, p)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return active
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Engine) CheckInfectedEffect() bool {
|
||||||
|
p := e.CurrentPlayer()
|
||||||
|
if p == nil || !p.InfectedActive {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
still := false
|
||||||
|
for _, c := range p.SkillZone {
|
||||||
|
if c.TypeID == "infected_person" {
|
||||||
|
still = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !still || len(e.HarmonyZone) == 0 {
|
||||||
|
p.InfectedActive = false
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
p.InfectedActive = false
|
||||||
|
var hCards []map[string]interface{}
|
||||||
|
for _, c := range e.HarmonyZone {
|
||||||
|
hCards = append(hCards, map[string]interface{}{"uid": c.UID})
|
||||||
|
}
|
||||||
|
e.Effect = &EffectState{
|
||||||
|
Active: true, CardTypeID: "infected_person", CasterID: p.ID,
|
||||||
|
WaitingFor: p.ID, ChoiceType: "select_harmony_card", Step: 0,
|
||||||
|
ChoiceData: map[string]interface{}{"cards": hCards, "message": "感染者效果:选择调和区的一张牌加入手牌"},
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Engine) respondInfected(pid string, resp map[string]interface{}) error {
|
||||||
|
uid, _ := resp["card_uid"].(string)
|
||||||
|
caster := e.GetPlayer(e.Effect.CasterID)
|
||||||
|
for i, c := range e.HarmonyZone {
|
||||||
|
if c.UID == uid {
|
||||||
|
e.HarmonyZone = append(e.HarmonyZone[:i], e.HarmonyZone[i+1:]...)
|
||||||
|
caster.Hand = append(caster.Hand, c)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
e.Effect = nil
|
||||||
|
return nil
|
||||||
|
}
|
||||||
+349
@@ -0,0 +1,349 @@
|
|||||||
|
package game
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"math/rand"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type PlayerState struct {
|
||||||
|
ID string
|
||||||
|
Nickname string
|
||||||
|
IsBot bool
|
||||||
|
Hand []*Card
|
||||||
|
SkillZone []*Card
|
||||||
|
ChallengeZone []*Card
|
||||||
|
IsExited bool
|
||||||
|
InfectedActive bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type Engine struct {
|
||||||
|
Players []*PlayerState
|
||||||
|
HarmonyZone []*Card
|
||||||
|
CurrentTurn int
|
||||||
|
Phase string
|
||||||
|
TurnPhase string
|
||||||
|
HarmonyTarget int
|
||||||
|
Setting GameSetting
|
||||||
|
SelectedCard *Card
|
||||||
|
SelectedBy string
|
||||||
|
Effect *EffectState
|
||||||
|
rng *rand.Rand
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewEngine(playerIDs []string, nicknames []string, botFlags []bool, playerCount int) *Engine {
|
||||||
|
e := &Engine{
|
||||||
|
Phase: "dealing",
|
||||||
|
rng: rand.New(rand.NewSource(time.Now().UnixNano())),
|
||||||
|
}
|
||||||
|
for i, pid := range playerIDs {
|
||||||
|
e.Players = append(e.Players, &PlayerState{
|
||||||
|
ID: pid, Nickname: nicknames[i], IsBot: botFlags[i],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Engine) Deal() {
|
||||||
|
deck, setting := BuildDeck(len(e.Players))
|
||||||
|
e.Setting = setting
|
||||||
|
e.HarmonyTarget = setting.HarmonyTarget
|
||||||
|
idx := 0
|
||||||
|
for idx < len(deck) {
|
||||||
|
for _, p := range e.Players {
|
||||||
|
if idx >= len(deck) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
p.Hand = append(p.Hand, deck[idx])
|
||||||
|
idx++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
e.CurrentTurn = e.findStartingPlayer()
|
||||||
|
e.Phase = "playing"
|
||||||
|
e.TurnPhase = "select_card"
|
||||||
|
e.startTurn()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Engine) findStartingPlayer() int {
|
||||||
|
for i, p := range e.Players {
|
||||||
|
for _, c := range p.Hand {
|
||||||
|
if c.TypeID == "student_council_president" {
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return e.rng.Intn(len(e.Players))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Engine) startTurn() {
|
||||||
|
if e.CheckInfectedEffect() {
|
||||||
|
e.TurnPhase = "effect_resolving"
|
||||||
|
} else {
|
||||||
|
e.TurnPhase = "select_card"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Engine) CurrentPlayer() *PlayerState {
|
||||||
|
if e.CurrentTurn < 0 || e.CurrentTurn >= len(e.Players) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return e.Players[e.CurrentTurn]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Engine) GetPlayer(id string) *PlayerState {
|
||||||
|
for _, p := range e.Players {
|
||||||
|
if p.ID == id {
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Engine) SelectCard(playerID, cardUID string) error {
|
||||||
|
if e.Phase != "playing" || e.TurnPhase != "select_card" {
|
||||||
|
return errors.New("cannot select card now")
|
||||||
|
}
|
||||||
|
cp := e.CurrentPlayer()
|
||||||
|
if cp == nil || cp.ID != playerID {
|
||||||
|
return errors.New("not your turn")
|
||||||
|
}
|
||||||
|
for _, c := range cp.Hand {
|
||||||
|
if c.UID == cardUID {
|
||||||
|
if IsUnusable(c.TypeID) {
|
||||||
|
return fmt.Errorf("「%s」无法被使用", c.Name)
|
||||||
|
}
|
||||||
|
e.SelectedCard = c
|
||||||
|
e.SelectedBy = playerID
|
||||||
|
e.TurnPhase = "select_action"
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return errors.New("card not in hand")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Engine) ConfirmAction(playerID, action string, targetPlayerID string) error {
|
||||||
|
if e.Phase != "playing" {
|
||||||
|
return errors.New("not playing")
|
||||||
|
}
|
||||||
|
cp := e.CurrentPlayer()
|
||||||
|
if cp == nil || cp.ID != playerID {
|
||||||
|
return errors.New("not your turn")
|
||||||
|
}
|
||||||
|
if e.SelectedCard == nil {
|
||||||
|
return errors.New("no card selected")
|
||||||
|
}
|
||||||
|
switch action {
|
||||||
|
case "skill":
|
||||||
|
return e.doSkill(cp)
|
||||||
|
case "harmony":
|
||||||
|
return e.doHarmony(cp)
|
||||||
|
case "challenge":
|
||||||
|
if e.TurnPhase == "select_action" && targetPlayerID == "" {
|
||||||
|
e.TurnPhase = "select_target"
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return e.doChallenge(cp, targetPlayerID)
|
||||||
|
}
|
||||||
|
return errors.New("unknown action")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Engine) doSkill(p *PlayerState) error {
|
||||||
|
card := e.SelectedCard
|
||||||
|
e.removeFromHand(p, card.UID)
|
||||||
|
p.SkillZone = append(p.SkillZone, card)
|
||||||
|
e.SelectedCard = nil
|
||||||
|
e.SelectedBy = ""
|
||||||
|
|
||||||
|
if e.triggerEffect(p, card) {
|
||||||
|
e.TurnPhase = "effect_resolving"
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
e.finishTurn(p)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Engine) doHarmony(p *PlayerState) error {
|
||||||
|
card := e.SelectedCard
|
||||||
|
e.removeFromHand(p, card.UID)
|
||||||
|
e.HarmonyZone = append(e.HarmonyZone, card)
|
||||||
|
e.SelectedCard = nil
|
||||||
|
e.SelectedBy = ""
|
||||||
|
e.finishTurn(p)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Engine) doChallenge(p *PlayerState, targetID string) error {
|
||||||
|
if targetID == "" || targetID == p.ID {
|
||||||
|
return errors.New("invalid challenge target")
|
||||||
|
}
|
||||||
|
target := e.GetPlayer(targetID)
|
||||||
|
if target == nil {
|
||||||
|
return errors.New("target not found")
|
||||||
|
}
|
||||||
|
card := e.SelectedCard
|
||||||
|
e.removeFromHand(p, card.UID)
|
||||||
|
target.ChallengeZone = append(target.ChallengeZone, card)
|
||||||
|
e.SelectedCard = nil
|
||||||
|
e.SelectedBy = ""
|
||||||
|
e.finishTurn(p)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Engine) finishTurn(p *PlayerState) {
|
||||||
|
if len(p.Hand) <= 1 {
|
||||||
|
p.IsExited = true
|
||||||
|
}
|
||||||
|
if e.allExited() {
|
||||||
|
e.Phase = "all_exited"
|
||||||
|
e.TurnPhase = ""
|
||||||
|
return
|
||||||
|
}
|
||||||
|
e.advanceTurn()
|
||||||
|
e.startTurn()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Engine) advanceTurn() {
|
||||||
|
n := len(e.Players)
|
||||||
|
next := (e.CurrentTurn + 1) % n
|
||||||
|
for i := 0; i < n; i++ {
|
||||||
|
if !e.Players[next].IsExited {
|
||||||
|
e.CurrentTurn = next
|
||||||
|
return
|
||||||
|
}
|
||||||
|
next = (next + 1) % n
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Engine) allExited() bool {
|
||||||
|
for _, p := range e.Players {
|
||||||
|
if !p.IsExited {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Engine) removeFromHand(p *PlayerState, uid string) {
|
||||||
|
for i, c := range p.Hand {
|
||||||
|
if c.UID == uid {
|
||||||
|
p.Hand = append(p.Hand[:i], p.Hand[i+1:]...)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Engine) IsEffectResolving() bool {
|
||||||
|
return e.Effect != nil && e.Effect.Active
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Engine) EffectNeedsResponseFrom(playerID string) bool {
|
||||||
|
if !e.IsEffectResolving() {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
ef := e.Effect
|
||||||
|
if ef.ChoiceType == "all_select_card" {
|
||||||
|
_, done := ef.PendingResponses[playerID]
|
||||||
|
p := e.GetPlayer(playerID)
|
||||||
|
return !done && p != nil && !p.IsExited && len(p.Hand) > 0
|
||||||
|
}
|
||||||
|
return ef.WaitingFor == playerID
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Engine) BotAutoPlay(playerID string) (string, string) {
|
||||||
|
p := e.GetPlayer(playerID)
|
||||||
|
if p == nil || !p.IsBot || p.IsExited {
|
||||||
|
return "", ""
|
||||||
|
}
|
||||||
|
var playable []*Card
|
||||||
|
for _, c := range p.Hand {
|
||||||
|
if !IsUnusable(c.TypeID) {
|
||||||
|
playable = append(playable, c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(playable) == 0 {
|
||||||
|
return "", ""
|
||||||
|
}
|
||||||
|
card := playable[e.rng.Intn(len(playable))]
|
||||||
|
e.SelectedCard = card
|
||||||
|
e.SelectedBy = playerID
|
||||||
|
|
||||||
|
roll := e.rng.Intn(10)
|
||||||
|
if roll < 4 {
|
||||||
|
return "harmony", ""
|
||||||
|
} else if roll < 7 {
|
||||||
|
return "skill", ""
|
||||||
|
} else {
|
||||||
|
var targets []string
|
||||||
|
for _, o := range e.Players {
|
||||||
|
if o.ID != playerID && !o.IsExited {
|
||||||
|
targets = append(targets, o.ID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(targets) == 0 {
|
||||||
|
return "harmony", ""
|
||||||
|
}
|
||||||
|
return "challenge", targets[e.rng.Intn(len(targets))]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Engine) BotAutoEffectResponse(playerID string) map[string]interface{} {
|
||||||
|
ef := e.Effect
|
||||||
|
if ef == nil || !ef.Active {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
p := e.GetPlayer(playerID)
|
||||||
|
if p == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
resp := make(map[string]interface{})
|
||||||
|
|
||||||
|
switch ef.ChoiceType {
|
||||||
|
case "view_cards":
|
||||||
|
resp["ok"] = true
|
||||||
|
case "select_player":
|
||||||
|
if targets, ok := ef.ChoiceData["targets"].([]map[string]interface{}); ok && len(targets) > 0 {
|
||||||
|
resp["player_id"] = targets[e.rng.Intn(len(targets))]["player_id"]
|
||||||
|
}
|
||||||
|
case "select_card_from_hand", "all_select_card":
|
||||||
|
if len(p.Hand) > 0 {
|
||||||
|
playable := p.Hand
|
||||||
|
var filtered []*Card
|
||||||
|
for _, c := range playable {
|
||||||
|
if !IsUnusable(c.TypeID) {
|
||||||
|
filtered = append(filtered, c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(filtered) > 0 {
|
||||||
|
playable = filtered
|
||||||
|
}
|
||||||
|
resp["card_uid"] = playable[e.rng.Intn(len(playable))].UID
|
||||||
|
}
|
||||||
|
case "select_skill_card":
|
||||||
|
if cards, ok := ef.ChoiceData["cards"].([]map[string]interface{}); ok && len(cards) > 0 {
|
||||||
|
resp["card_uid"] = cards[e.rng.Intn(len(cards))]["uid"]
|
||||||
|
}
|
||||||
|
case "select_challenge_card":
|
||||||
|
if cards, ok := ef.ChoiceData["cards"].([]map[string]interface{}); ok && len(cards) > 0 {
|
||||||
|
c := cards[e.rng.Intn(len(cards))]
|
||||||
|
resp["card_uid"] = c["uid"]
|
||||||
|
resp["owner_id"] = c["owner_id"]
|
||||||
|
}
|
||||||
|
case "select_harmony_card":
|
||||||
|
if cards, ok := ef.ChoiceData["cards"].([]map[string]interface{}); ok && len(cards) > 0 {
|
||||||
|
resp["card_uid"] = cards[e.rng.Intn(len(cards))]["uid"]
|
||||||
|
}
|
||||||
|
case "yes_no":
|
||||||
|
resp["yes"] = e.rng.Intn(2) == 0
|
||||||
|
case "select_option":
|
||||||
|
if opts, ok := ef.ChoiceData["options"].([]map[string]interface{}); ok && len(opts) > 0 {
|
||||||
|
resp["value"] = opts[e.rng.Intn(len(opts))]["value"]
|
||||||
|
} else if opts2, ok := ef.ChoiceData["options"].([]interface{}); ok && len(opts2) > 0 {
|
||||||
|
if m, ok := opts2[e.rng.Intn(len(opts2))].(map[string]interface{}); ok {
|
||||||
|
resp["value"] = m["value"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return resp
|
||||||
|
}
|
||||||
@@ -0,0 +1,189 @@
|
|||||||
|
package game
|
||||||
|
|
||||||
|
type victoryCheck struct {
|
||||||
|
ID string
|
||||||
|
Nickname string
|
||||||
|
CardType string
|
||||||
|
CardName string
|
||||||
|
Priority int
|
||||||
|
Met bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Engine) RunSettlement() map[string]interface{} {
|
||||||
|
harmony := e.settleHarmony()
|
||||||
|
challenge := e.settleChallenge()
|
||||||
|
harmonyOK := harmony["success"].(bool)
|
||||||
|
imprisoned := challenge["imprisoned"].([]string)
|
||||||
|
victory := e.settleVictory(harmonyOK, imprisoned)
|
||||||
|
return map[string]interface{}{
|
||||||
|
"harmony": harmony, "challenge": challenge, "victory": victory,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Engine) settleHarmony() map[string]interface{} {
|
||||||
|
var cards []map[string]interface{}
|
||||||
|
total := 0
|
||||||
|
for _, c := range e.HarmonyZone {
|
||||||
|
cards = append(cards, map[string]interface{}{
|
||||||
|
"uid": c.UID, "type_id": c.TypeID, "name": c.Name, "mp": c.MP,
|
||||||
|
})
|
||||||
|
total += c.MP
|
||||||
|
}
|
||||||
|
return map[string]interface{}{
|
||||||
|
"cards": cards, "total_mp": total, "target": e.HarmonyTarget,
|
||||||
|
"success": total >= e.HarmonyTarget,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Engine) settleChallenge() map[string]interface{} {
|
||||||
|
var perPlayer []map[string]interface{}
|
||||||
|
maxMP := 0
|
||||||
|
for _, p := range e.Players {
|
||||||
|
var cards []map[string]interface{}
|
||||||
|
total := 0
|
||||||
|
for _, c := range p.ChallengeZone {
|
||||||
|
eff := c.MP
|
||||||
|
if eff < 0 {
|
||||||
|
eff = 0
|
||||||
|
}
|
||||||
|
total += eff
|
||||||
|
cards = append(cards, map[string]interface{}{
|
||||||
|
"uid": c.UID, "type_id": c.TypeID, "name": c.Name, "mp": c.MP, "effective_mp": eff,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
perPlayer = append(perPlayer, map[string]interface{}{
|
||||||
|
"player_id": p.ID, "nickname": p.Nickname, "cards": cards, "total_mp": total,
|
||||||
|
})
|
||||||
|
if total > maxMP {
|
||||||
|
maxMP = total
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
allSame := true
|
||||||
|
for _, pp := range perPlayer {
|
||||||
|
if pp["total_mp"].(int) != perPlayer[0]["total_mp"].(int) {
|
||||||
|
allSame = false
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var imprisoned []string
|
||||||
|
if !allSame && maxMP > 0 {
|
||||||
|
for _, pp := range perPlayer {
|
||||||
|
if pp["total_mp"].(int) == maxMP {
|
||||||
|
imprisoned = append(imprisoned, pp["player_id"].(string))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return map[string]interface{}{
|
||||||
|
"per_player": perPlayer, "max_mp": maxMP, "imprisoned": imprisoned,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Engine) settleVictory(harmonyOK bool, imprisoned []string) map[string]interface{} {
|
||||||
|
isJailed := func(pid string) bool {
|
||||||
|
for _, id := range imprisoned {
|
||||||
|
if id == pid {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
var all []victoryCheck
|
||||||
|
for _, p := range e.Players {
|
||||||
|
if len(p.Hand) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
last := p.Hand[0]
|
||||||
|
def := GetCardDef(last.TypeID)
|
||||||
|
if def == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
all = append(all, victoryCheck{
|
||||||
|
ID: p.ID, Nickname: p.Nickname,
|
||||||
|
CardType: last.TypeID, CardName: last.Name, Priority: def.Priority,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
culpritWins := false
|
||||||
|
|
||||||
|
for pri := 1; pri <= 5; pri++ {
|
||||||
|
var group []int
|
||||||
|
for i, vc := range all {
|
||||||
|
if vc.Priority == pri {
|
||||||
|
group = append(group, i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(group) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, i := range group {
|
||||||
|
switch all[i].CardType {
|
||||||
|
case "culprit":
|
||||||
|
all[i].Met = !isJailed(all[i].ID)
|
||||||
|
if all[i].Met {
|
||||||
|
culpritWins = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, i := range group {
|
||||||
|
switch all[i].CardType {
|
||||||
|
case "alien":
|
||||||
|
all[i].Met = isJailed(all[i].ID)
|
||||||
|
case "infected_person":
|
||||||
|
all[i].Met = !harmonyOK
|
||||||
|
case "accomplice":
|
||||||
|
all[i].Met = culpritWins
|
||||||
|
case "go_home_club":
|
||||||
|
all[i].Met = true
|
||||||
|
case "culprit":
|
||||||
|
// already set above
|
||||||
|
default:
|
||||||
|
all[i].Met = harmonyOK
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
anyWin := false
|
||||||
|
for _, i := range group {
|
||||||
|
if all[i].Met {
|
||||||
|
anyWin = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !anyWin {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
var winners []map[string]interface{}
|
||||||
|
for _, i := range group {
|
||||||
|
if all[i].Met {
|
||||||
|
winners = append(winners, vcToMap(all[i]))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return map[string]interface{}{
|
||||||
|
"checks": allToMaps(all), "winners": winners,
|
||||||
|
"winning_priority": pri, "end_type": "normal",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return map[string]interface{}{
|
||||||
|
"checks": allToMaps(all), "winners": []map[string]interface{}{}, "end_type": "all_dead",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func vcToMap(vc victoryCheck) map[string]interface{} {
|
||||||
|
return map[string]interface{}{
|
||||||
|
"player_id": vc.ID, "nickname": vc.Nickname,
|
||||||
|
"card_type": vc.CardType, "card_name": vc.CardName,
|
||||||
|
"priority": vc.Priority, "met": vc.Met,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func allToMaps(all []victoryCheck) []map[string]interface{} {
|
||||||
|
var r []map[string]interface{}
|
||||||
|
for _, vc := range all {
|
||||||
|
r = append(r, vcToMap(vc))
|
||||||
|
}
|
||||||
|
return r
|
||||||
|
}
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
package game
|
||||||
|
|
||||||
|
func (e *Engine) Snapshot(forPlayerID string) map[string]interface{} {
|
||||||
|
cp := e.CurrentPlayer()
|
||||||
|
currentTurnID := ""
|
||||||
|
if cp != nil {
|
||||||
|
currentTurnID = cp.ID
|
||||||
|
}
|
||||||
|
|
||||||
|
snap := map[string]interface{}{
|
||||||
|
"phase": e.Phase,
|
||||||
|
"turn_phase": e.TurnPhase,
|
||||||
|
"current_turn": currentTurnID,
|
||||||
|
"your_player_id": forPlayerID,
|
||||||
|
"harmony_target": e.HarmonyTarget,
|
||||||
|
}
|
||||||
|
|
||||||
|
var players []map[string]interface{}
|
||||||
|
for _, p := range e.Players {
|
||||||
|
pi := map[string]interface{}{
|
||||||
|
"player_id": p.ID,
|
||||||
|
"nickname": p.Nickname,
|
||||||
|
"is_bot": p.IsBot,
|
||||||
|
"is_exited": p.IsExited,
|
||||||
|
"hand_card_count": len(p.Hand),
|
||||||
|
"challenge_zone_count": len(p.ChallengeZone),
|
||||||
|
}
|
||||||
|
var skill []map[string]interface{}
|
||||||
|
for _, c := range p.SkillZone {
|
||||||
|
skill = append(skill, map[string]interface{}{
|
||||||
|
"uid": c.UID, "type_id": c.TypeID, "name": c.Name, "mp": c.MP,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
pi["skill_zone"] = skill
|
||||||
|
players = append(players, pi)
|
||||||
|
}
|
||||||
|
snap["players"] = players
|
||||||
|
|
||||||
|
me := e.GetPlayer(forPlayerID)
|
||||||
|
var hand []map[string]interface{}
|
||||||
|
if me != nil {
|
||||||
|
for _, c := range me.Hand {
|
||||||
|
hand = append(hand, map[string]interface{}{
|
||||||
|
"uid": c.UID, "type_id": c.TypeID, "name": c.Name, "mp": c.MP,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
snap["hand_cards"] = hand
|
||||||
|
|
||||||
|
var harmony []map[string]interface{}
|
||||||
|
for _, c := range e.HarmonyZone {
|
||||||
|
harmony = append(harmony, map[string]interface{}{
|
||||||
|
"uid": c.UID, "face_up": false,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
snap["harmony_zone"] = harmony
|
||||||
|
|
||||||
|
if e.Effect != nil && e.Effect.Active {
|
||||||
|
ef := e.Effect
|
||||||
|
effectInfo := map[string]interface{}{
|
||||||
|
"active": true,
|
||||||
|
"card_type": ef.CardTypeID,
|
||||||
|
"caster_id": ef.CasterID,
|
||||||
|
}
|
||||||
|
if e.EffectNeedsResponseFrom(forPlayerID) {
|
||||||
|
effectInfo["needs_response"] = true
|
||||||
|
effectInfo["choice_type"] = ef.ChoiceType
|
||||||
|
effectInfo["choice_data"] = ef.ChoiceData
|
||||||
|
} else {
|
||||||
|
effectInfo["needs_response"] = false
|
||||||
|
effectInfo["waiting_message"] = "等待效果结算中..."
|
||||||
|
}
|
||||||
|
snap["effect"] = effectInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
return snap
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
module Embalming_Girl_Server
|
||||||
|
|
||||||
|
go 1.22
|
||||||
|
|
||||||
|
require github.com/gorilla/websocket v1.5.3
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||||
|
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||||
+626
@@ -0,0 +1,626 @@
|
|||||||
|
package handler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"Embalming_Girl_Server/game"
|
||||||
|
"Embalming_Girl_Server/room"
|
||||||
|
"encoding/json"
|
||||||
|
"log"
|
||||||
|
"math/rand"
|
||||||
|
"net/http"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gorilla/websocket"
|
||||||
|
)
|
||||||
|
|
||||||
|
var upgrader = websocket.Upgrader{
|
||||||
|
CheckOrigin: func(r *http.Request) bool { return true },
|
||||||
|
}
|
||||||
|
|
||||||
|
type Client struct {
|
||||||
|
ID string
|
||||||
|
RoomID string
|
||||||
|
Conn *websocket.Conn
|
||||||
|
Send chan []byte
|
||||||
|
mu sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
type Hub struct {
|
||||||
|
RoomMgr *room.Manager
|
||||||
|
clients map[string]*Client
|
||||||
|
engines map[string]*game.Engine
|
||||||
|
mu sync.RWMutex
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewHub() *Hub {
|
||||||
|
return &Hub{
|
||||||
|
RoomMgr: room.NewManager(),
|
||||||
|
clients: make(map[string]*Client),
|
||||||
|
engines: make(map[string]*game.Engine),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Hub) InitTestRooms() {
|
||||||
|
for _, n := range []int{3, 4, 5, 6} {
|
||||||
|
rid := "TEST" + string(rune('0'+n))
|
||||||
|
h.RoomMgr.CreateTestRoom(rid, n)
|
||||||
|
log.Printf("Test room: %s (%d players, %d bots)", rid, n, n-1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Hub) ServeWS(w http.ResponseWriter, r *http.Request) {
|
||||||
|
conn, err := upgrader.Upgrade(w, r, nil)
|
||||||
|
if err != nil {
|
||||||
|
log.Println("Upgrade:", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
clientID := generateClientID()
|
||||||
|
client := &Client{ID: clientID, Conn: conn, Send: make(chan []byte, 256)}
|
||||||
|
h.mu.Lock()
|
||||||
|
h.clients[clientID] = client
|
||||||
|
h.mu.Unlock()
|
||||||
|
log.Printf("Connected: %s", clientID)
|
||||||
|
go h.writePump(client)
|
||||||
|
go h.readPump(client)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Hub) readPump(c *Client) {
|
||||||
|
defer func() { h.removeClient(c); c.Conn.Close() }()
|
||||||
|
for {
|
||||||
|
msgType, msg, err := c.Conn.ReadMessage()
|
||||||
|
if err != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if msgType == websocket.BinaryMessage {
|
||||||
|
h.relayAudio(c, msg)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
var m map[string]interface{}
|
||||||
|
if json.Unmarshal(msg, &m) == nil {
|
||||||
|
h.handleMessage(c, m)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Hub) writePump(c *Client) {
|
||||||
|
defer c.Conn.Close()
|
||||||
|
for data := range c.Send {
|
||||||
|
c.mu.Lock()
|
||||||
|
err := c.Conn.WriteMessage(websocket.TextMessage, data)
|
||||||
|
c.mu.Unlock()
|
||||||
|
if err != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Hub) removeClient(c *Client) {
|
||||||
|
h.mu.Lock()
|
||||||
|
delete(h.clients, c.ID)
|
||||||
|
h.mu.Unlock()
|
||||||
|
if c.RoomID != "" {
|
||||||
|
h.RoomMgr.LeaveRoom(c.RoomID, c.ID)
|
||||||
|
rm := h.RoomMgr.GetRoom(c.RoomID)
|
||||||
|
if rm != nil {
|
||||||
|
h.broadcastRoomState(rm)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
close(c.Send)
|
||||||
|
log.Printf("Disconnected: %s", c.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Hub) handleMessage(c *Client, msg map[string]interface{}) {
|
||||||
|
t, _ := msg["type"].(string)
|
||||||
|
p, _ := msg["payload"].(map[string]interface{})
|
||||||
|
switch t {
|
||||||
|
case "create_room":
|
||||||
|
h.handleCreateRoom(c, p)
|
||||||
|
case "join_room":
|
||||||
|
h.handleJoinRoom(c, p)
|
||||||
|
case "leave_room":
|
||||||
|
h.handleLeaveRoom(c)
|
||||||
|
case "set_ready":
|
||||||
|
h.handleSetReady(c, p)
|
||||||
|
case "start_game":
|
||||||
|
h.handleStartGame(c)
|
||||||
|
case "list_rooms":
|
||||||
|
h.handleListRooms(c)
|
||||||
|
case "select_card":
|
||||||
|
h.handleSelectCard(c, p)
|
||||||
|
case "confirm_action":
|
||||||
|
h.handleConfirmAction(c, p)
|
||||||
|
case "effect_response":
|
||||||
|
h.handleEffectResponse(c, p)
|
||||||
|
case "send_chat":
|
||||||
|
h.handleChat(c, p)
|
||||||
|
case "heartbeat":
|
||||||
|
h.sendTo(c, "heartbeat_ack", nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Room handlers ----
|
||||||
|
|
||||||
|
func (h *Hub) handleListRooms(c *Client) {
|
||||||
|
h.sendTo(c, "room_list", map[string]interface{}{"rooms": h.RoomMgr.ListRooms()})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Hub) handleCreateRoom(c *Client, p map[string]interface{}) {
|
||||||
|
nick, _ := p["nickname"].(string)
|
||||||
|
mp := 4
|
||||||
|
if v, ok := p["max_players"].(float64); ok {
|
||||||
|
mp = int(v)
|
||||||
|
}
|
||||||
|
if nick == "" {
|
||||||
|
nick = "Player"
|
||||||
|
}
|
||||||
|
rm := h.RoomMgr.CreateRoom(c.ID, nick, mp)
|
||||||
|
c.RoomID = rm.ID
|
||||||
|
rm.Players[c.ID].Send = c.Send
|
||||||
|
h.sendTo(c, "room_created", map[string]interface{}{"room_id": rm.ID})
|
||||||
|
h.broadcastRoomState(rm)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Hub) handleJoinRoom(c *Client, p map[string]interface{}) {
|
||||||
|
rid, _ := p["room_id"].(string)
|
||||||
|
nick, _ := p["nickname"].(string)
|
||||||
|
if nick == "" {
|
||||||
|
nick = "Player"
|
||||||
|
}
|
||||||
|
rm, ok := h.RoomMgr.JoinRoom(rid, c.ID, nick)
|
||||||
|
if !ok {
|
||||||
|
h.sendTo(c, "error", map[string]interface{}{"message": "Cannot join room"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.RoomID = rm.ID
|
||||||
|
rm.Players[c.ID].Send = c.Send
|
||||||
|
h.sendTo(c, "room_joined", map[string]interface{}{"room_id": rm.ID})
|
||||||
|
h.broadcastRoomState(rm)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Hub) handleLeaveRoom(c *Client) {
|
||||||
|
if c.RoomID == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
rid := c.RoomID
|
||||||
|
h.RoomMgr.LeaveRoom(rid, c.ID)
|
||||||
|
c.RoomID = ""
|
||||||
|
h.sendTo(c, "room_left", nil)
|
||||||
|
if rm := h.RoomMgr.GetRoom(rid); rm != nil {
|
||||||
|
h.broadcastRoomState(rm)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Hub) handleChat(c *Client, p map[string]interface{}) {
|
||||||
|
if c.RoomID == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
rm := h.RoomMgr.GetRoom(c.RoomID)
|
||||||
|
if rm == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
text, _ := p["text"].(string)
|
||||||
|
if text == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
nick := ""
|
||||||
|
if pl, ok := rm.Players[c.ID]; ok {
|
||||||
|
nick = pl.Nickname
|
||||||
|
}
|
||||||
|
msg := map[string]interface{}{
|
||||||
|
"sender_id": c.ID, "nickname": nick, "text": text,
|
||||||
|
}
|
||||||
|
for _, pid := range rm.PlayerOrder {
|
||||||
|
pl := rm.Players[pid]
|
||||||
|
if pl.IsBot || pl.Send == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
h.sendToChannel(pl.Send, "chat", msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Hub) handleSetReady(c *Client, p map[string]interface{}) {
|
||||||
|
ready, _ := p["ready"].(bool)
|
||||||
|
if c.RoomID == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
h.RoomMgr.SetReady(c.RoomID, c.ID, ready)
|
||||||
|
if rm := h.RoomMgr.GetRoom(c.RoomID); rm != nil {
|
||||||
|
h.broadcastRoomState(rm)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Game start ----
|
||||||
|
|
||||||
|
func (h *Hub) handleStartGame(c *Client) {
|
||||||
|
if c.RoomID == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
rm := h.RoomMgr.GetRoom(c.RoomID)
|
||||||
|
if rm == nil || !rm.Players[c.ID].IsHost {
|
||||||
|
h.sendTo(c, "error", map[string]interface{}{"message": "Only host can start"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if len(rm.Players) < 3 {
|
||||||
|
h.sendTo(c, "error", map[string]interface{}{"message": "Need 3+ players"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var ids, nicks []string
|
||||||
|
var bots []bool
|
||||||
|
for _, pid := range rm.PlayerOrder {
|
||||||
|
p := rm.Players[pid]
|
||||||
|
ids = append(ids, p.ID)
|
||||||
|
nicks = append(nicks, p.Nickname)
|
||||||
|
bots = append(bots, p.IsBot)
|
||||||
|
}
|
||||||
|
|
||||||
|
eng := game.NewEngine(ids, nicks, bots, len(ids))
|
||||||
|
eng.Deal()
|
||||||
|
|
||||||
|
h.mu.Lock()
|
||||||
|
h.engines[rm.ID] = eng
|
||||||
|
h.mu.Unlock()
|
||||||
|
|
||||||
|
rm.State = "playing"
|
||||||
|
rm.HarmonyTarget = eng.HarmonyTarget
|
||||||
|
|
||||||
|
for _, pid := range rm.PlayerOrder {
|
||||||
|
p := rm.Players[pid]
|
||||||
|
if p.IsBot || p.Send == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
h.sendToChannel(p.Send, "game_start", map[string]interface{}{
|
||||||
|
"your_player_id": pid,
|
||||||
|
"harmony_target": eng.HarmonyTarget,
|
||||||
|
"player_count": len(ids),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("Game started in %s, %d players, target=%d, first=%s",
|
||||||
|
rm.ID, len(ids), eng.HarmonyTarget, eng.CurrentPlayer().ID)
|
||||||
|
|
||||||
|
h.broadcastSnapshots(rm.ID)
|
||||||
|
h.processBotTurns(rm.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Game action handlers ----
|
||||||
|
|
||||||
|
func (h *Hub) handleSelectCard(c *Client, p map[string]interface{}) {
|
||||||
|
h.mu.RLock()
|
||||||
|
eng := h.engines[c.RoomID]
|
||||||
|
h.mu.RUnlock()
|
||||||
|
if eng == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
uid, _ := p["card_uid"].(string)
|
||||||
|
if err := eng.SelectCard(c.ID, uid); err != nil {
|
||||||
|
h.sendTo(c, "error", map[string]interface{}{"message": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
h.broadcastSnapshots(c.RoomID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Hub) handleConfirmAction(c *Client, p map[string]interface{}) {
|
||||||
|
h.mu.RLock()
|
||||||
|
eng := h.engines[c.RoomID]
|
||||||
|
h.mu.RUnlock()
|
||||||
|
if eng == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
action, _ := p["action"].(string)
|
||||||
|
target, _ := p["target_player_id"].(string)
|
||||||
|
|
||||||
|
if err := eng.ConfirmAction(c.ID, action, target); err != nil {
|
||||||
|
h.sendTo(c, "error", map[string]interface{}{"message": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
h.broadcastSnapshots(c.RoomID)
|
||||||
|
|
||||||
|
if eng.IsEffectResolving() {
|
||||||
|
h.processBotEffectResponses(c.RoomID)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if eng.Phase == "all_exited" {
|
||||||
|
h.handleAllExited(c.RoomID)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
h.processBotTurns(c.RoomID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Hub) handleEffectResponse(c *Client, p map[string]interface{}) {
|
||||||
|
h.mu.RLock()
|
||||||
|
eng := h.engines[c.RoomID]
|
||||||
|
h.mu.RUnlock()
|
||||||
|
if eng == nil || !eng.IsEffectResolving() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := eng.HandleEffectResponse(c.ID, p); err != nil {
|
||||||
|
h.sendTo(c, "error", map[string]interface{}{"message": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
h.broadcastSnapshots(c.RoomID)
|
||||||
|
|
||||||
|
if eng.IsEffectResolving() {
|
||||||
|
h.processBotEffectResponses(c.RoomID)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if eng.Phase == "all_exited" {
|
||||||
|
h.handleAllExited(c.RoomID)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
h.processBotTurns(c.RoomID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Hub) processBotEffectResponses(roomID string) {
|
||||||
|
h.mu.RLock()
|
||||||
|
eng := h.engines[roomID]
|
||||||
|
h.mu.RUnlock()
|
||||||
|
if eng == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for eng.IsEffectResolving() {
|
||||||
|
ef := eng.Effect
|
||||||
|
if ef == nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if ef.ChoiceType == "all_select_card" {
|
||||||
|
anyBotLeft := false
|
||||||
|
for _, p := range eng.Players {
|
||||||
|
if p.IsBot && eng.EffectNeedsResponseFrom(p.ID) {
|
||||||
|
time.Sleep(200 * time.Millisecond)
|
||||||
|
resp := eng.BotAutoEffectResponse(p.ID)
|
||||||
|
if resp != nil {
|
||||||
|
eng.HandleEffectResponse(p.ID, resp)
|
||||||
|
}
|
||||||
|
anyBotLeft = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !anyBotLeft {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
h.broadcastSnapshots(roomID)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
waitFor := ef.WaitingFor
|
||||||
|
bot := eng.GetPlayer(waitFor)
|
||||||
|
if bot == nil || !bot.IsBot {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
time.Sleep(200 * time.Millisecond)
|
||||||
|
resp := eng.BotAutoEffectResponse(waitFor)
|
||||||
|
if resp != nil {
|
||||||
|
eng.HandleEffectResponse(waitFor, resp)
|
||||||
|
}
|
||||||
|
h.broadcastSnapshots(roomID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Hub) processBotTurns(roomID string) {
|
||||||
|
h.mu.RLock()
|
||||||
|
eng := h.engines[roomID]
|
||||||
|
h.mu.RUnlock()
|
||||||
|
if eng == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for eng.Phase == "playing" && !eng.IsEffectResolving() {
|
||||||
|
cp := eng.CurrentPlayer()
|
||||||
|
if cp == nil || !cp.IsBot {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
time.Sleep(300 * time.Millisecond)
|
||||||
|
|
||||||
|
action, target := eng.BotAutoPlay(cp.ID)
|
||||||
|
if action == "" {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
eng.ConfirmAction(cp.ID, action, target)
|
||||||
|
h.broadcastSnapshots(roomID)
|
||||||
|
|
||||||
|
if eng.IsEffectResolving() {
|
||||||
|
h.processBotEffectResponses(roomID)
|
||||||
|
if eng.IsEffectResolving() {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
h.broadcastSnapshots(roomID)
|
||||||
|
}
|
||||||
|
|
||||||
|
if eng.Phase == "all_exited" {
|
||||||
|
h.handleAllExited(roomID)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Hub) handleAllExited(roomID string) {
|
||||||
|
rm := h.RoomMgr.GetRoom(roomID)
|
||||||
|
if rm == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
h.mu.RLock()
|
||||||
|
eng := h.engines[roomID]
|
||||||
|
h.mu.RUnlock()
|
||||||
|
if eng == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
result := eng.RunSettlement()
|
||||||
|
log.Printf("Settlement in room %s: %s", roomID, result["victory"].(map[string]interface{})["end_type"])
|
||||||
|
|
||||||
|
for _, pid := range rm.PlayerOrder {
|
||||||
|
p := rm.Players[pid]
|
||||||
|
if p.IsBot || p.Send == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
h.sendToChannel(p.Send, "settlement_result", result)
|
||||||
|
}
|
||||||
|
|
||||||
|
endType, _ := result["victory"].(map[string]interface{})["end_type"].(string)
|
||||||
|
winners, _ := result["victory"].(map[string]interface{})["winners"].([]map[string]interface{})
|
||||||
|
var winnerNames []string
|
||||||
|
for _, w := range winners {
|
||||||
|
winnerNames = append(winnerNames, w["nickname"].(string))
|
||||||
|
}
|
||||||
|
|
||||||
|
endMsg := map[string]interface{}{"end_type": endType}
|
||||||
|
if endType == "all_dead" {
|
||||||
|
endMsg["message"] = "全灭结局 — 没有任何人获胜"
|
||||||
|
} else {
|
||||||
|
msg := ""
|
||||||
|
for i, n := range winnerNames {
|
||||||
|
if i > 0 {
|
||||||
|
msg += ", "
|
||||||
|
}
|
||||||
|
msg += n
|
||||||
|
}
|
||||||
|
endMsg["message"] = "获胜者: " + msg
|
||||||
|
endMsg["winners"] = winners
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, pid := range rm.PlayerOrder {
|
||||||
|
p := rm.Players[pid]
|
||||||
|
if p.IsBot || p.Send == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
h.sendToChannel(p.Send, "game_end", endMsg)
|
||||||
|
}
|
||||||
|
|
||||||
|
rm.State = "waiting"
|
||||||
|
for _, p := range rm.Players {
|
||||||
|
p.IsReady = false
|
||||||
|
if p.IsBot {
|
||||||
|
p.IsReady = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
h.mu.Lock()
|
||||||
|
delete(h.engines, roomID)
|
||||||
|
h.mu.Unlock()
|
||||||
|
h.broadcastRoomState(rm)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Audio relay ----
|
||||||
|
|
||||||
|
func (h *Hub) relayAudio(sender *Client, pcm []byte) {
|
||||||
|
if sender.RoomID == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
rm := h.RoomMgr.GetRoom(sender.RoomID)
|
||||||
|
if rm == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
idBytes := []byte(sender.ID)
|
||||||
|
header := make([]byte, 2+len(idBytes))
|
||||||
|
header[0] = byte(len(idBytes) >> 8)
|
||||||
|
header[1] = byte(len(idBytes))
|
||||||
|
copy(header[2:], idBytes)
|
||||||
|
frame := append(header, pcm...)
|
||||||
|
|
||||||
|
h.mu.RLock()
|
||||||
|
defer h.mu.RUnlock()
|
||||||
|
for _, pid := range rm.PlayerOrder {
|
||||||
|
p := rm.Players[pid]
|
||||||
|
if p.IsBot || pid == sender.ID {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if cl, ok := h.clients[pid]; ok {
|
||||||
|
cl.mu.Lock()
|
||||||
|
cl.Conn.WriteMessage(websocket.BinaryMessage, frame)
|
||||||
|
cl.mu.Unlock()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Broadcast helpers ----
|
||||||
|
|
||||||
|
func (h *Hub) broadcastSnapshots(roomID string) {
|
||||||
|
rm := h.RoomMgr.GetRoom(roomID)
|
||||||
|
if rm == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
h.mu.RLock()
|
||||||
|
eng := h.engines[roomID]
|
||||||
|
h.mu.RUnlock()
|
||||||
|
if eng == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, pid := range rm.PlayerOrder {
|
||||||
|
p := rm.Players[pid]
|
||||||
|
if p.IsBot || p.Send == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
snap := eng.Snapshot(pid)
|
||||||
|
data := buildMessage("state_snapshot", snap)
|
||||||
|
select {
|
||||||
|
case p.Send <- data:
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Hub) broadcastRoomState(rm *room.Room) {
|
||||||
|
players := rm.GetPlayerInfos()
|
||||||
|
for _, pid := range rm.PlayerOrder {
|
||||||
|
p, ok := rm.Players[pid]
|
||||||
|
if !ok || p.IsBot || p.Send == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
st := map[string]interface{}{
|
||||||
|
"room_id": rm.ID, "state": rm.State,
|
||||||
|
"harmony_target": rm.HarmonyTarget,
|
||||||
|
"players": players, "your_player_id": pid,
|
||||||
|
}
|
||||||
|
select {
|
||||||
|
case p.Send <- buildMessage("room_state", st):
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Hub) sendTo(c *Client, t string, p map[string]interface{}) {
|
||||||
|
select {
|
||||||
|
case c.Send <- buildMessage(t, p):
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Hub) sendToChannel(ch chan []byte, t string, p map[string]interface{}) {
|
||||||
|
select {
|
||||||
|
case ch <- buildMessage(t, p):
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildMessage(t string, p map[string]interface{}) []byte {
|
||||||
|
m := map[string]interface{}{"type": t}
|
||||||
|
if p != nil {
|
||||||
|
m["payload"] = p
|
||||||
|
}
|
||||||
|
d, _ := json.Marshal(m)
|
||||||
|
return d
|
||||||
|
}
|
||||||
|
|
||||||
|
func generateClientID() string {
|
||||||
|
const cs = "abcdefghijklmnopqrstuvwxyz0123456789"
|
||||||
|
b := make([]byte, 12)
|
||||||
|
for i := range b {
|
||||||
|
b[i] = cs[rand.Intn(len(cs))]
|
||||||
|
}
|
||||||
|
return string(b)
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"Embalming_Girl_Server/handler"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
hub := handler.NewHub()
|
||||||
|
hub.InitTestRooms()
|
||||||
|
|
||||||
|
http.HandleFunc("/ws", hub.ServeWS)
|
||||||
|
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
||||||
|
w.Write([]byte("Embalming Girl Server v0.1.0"))
|
||||||
|
})
|
||||||
|
|
||||||
|
addr := ":8080"
|
||||||
|
log.Printf("Server started on %s", addr)
|
||||||
|
log.Printf("Test rooms: TEST3, TEST4, TEST5, TEST6")
|
||||||
|
if err := http.ListenAndServe(addr, nil); err != nil {
|
||||||
|
log.Fatal("ListenAndServe:", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
type Message struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
Seq int `json:"seq,omitempty"`
|
||||||
|
Timestamp int64 `json:"timestamp,omitempty"`
|
||||||
|
Payload map[string]interface{} `json:"payload,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type PlayerInfo struct {
|
||||||
|
PlayerID string `json:"player_id"`
|
||||||
|
Nickname string `json:"nickname"`
|
||||||
|
IsReady bool `json:"is_ready"`
|
||||||
|
IsHost bool `json:"is_host"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type RoomState struct {
|
||||||
|
RoomID string `json:"room_id"`
|
||||||
|
State string `json:"state"`
|
||||||
|
HarmonyTarget int `json:"harmony_target"`
|
||||||
|
Players []PlayerInfo `json:"players"`
|
||||||
|
YourPlayerID string `json:"your_player_id"`
|
||||||
|
}
|
||||||
+310
@@ -0,0 +1,310 @@
|
|||||||
|
package room
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"math/rand"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Player struct {
|
||||||
|
ID string
|
||||||
|
Nickname string
|
||||||
|
IsReady bool
|
||||||
|
IsHost bool
|
||||||
|
IsBot bool
|
||||||
|
Send chan []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
type Room struct {
|
||||||
|
ID string
|
||||||
|
State string
|
||||||
|
MaxPlayers int
|
||||||
|
HarmonyTarget int
|
||||||
|
ChaosMode bool
|
||||||
|
Players map[string]*Player
|
||||||
|
PlayerOrder []string
|
||||||
|
mu sync.RWMutex
|
||||||
|
}
|
||||||
|
|
||||||
|
type Manager struct {
|
||||||
|
rooms map[string]*Room
|
||||||
|
mu sync.RWMutex
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewManager() *Manager {
|
||||||
|
return &Manager{
|
||||||
|
rooms: make(map[string]*Room),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) CreateTestRoom(roomID string, playerCount int) *Room {
|
||||||
|
m.mu.Lock()
|
||||||
|
defer m.mu.Unlock()
|
||||||
|
|
||||||
|
rm := &Room{
|
||||||
|
ID: roomID,
|
||||||
|
State: "waiting",
|
||||||
|
MaxPlayers: playerCount,
|
||||||
|
Players: make(map[string]*Player),
|
||||||
|
}
|
||||||
|
|
||||||
|
botNames := []string{"Bot-A", "Bot-B", "Bot-C", "Bot-D", "Bot-E"}
|
||||||
|
for i := 0; i < playerCount-1; i++ {
|
||||||
|
botID := fmt.Sprintf("bot_%s_%d", roomID, i)
|
||||||
|
bot := &Player{
|
||||||
|
ID: botID,
|
||||||
|
Nickname: botNames[i%len(botNames)],
|
||||||
|
IsReady: true,
|
||||||
|
IsBot: true,
|
||||||
|
Send: nil,
|
||||||
|
}
|
||||||
|
rm.Players[botID] = bot
|
||||||
|
rm.PlayerOrder = append(rm.PlayerOrder, botID)
|
||||||
|
}
|
||||||
|
|
||||||
|
m.rooms[roomID] = rm
|
||||||
|
return rm
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) CreateRoom(hostID, nickname string, maxPlayers int) *Room {
|
||||||
|
m.mu.Lock()
|
||||||
|
defer m.mu.Unlock()
|
||||||
|
|
||||||
|
roomID := generateRoomID()
|
||||||
|
room := &Room{
|
||||||
|
ID: roomID,
|
||||||
|
State: "waiting",
|
||||||
|
MaxPlayers: maxPlayers,
|
||||||
|
Players: make(map[string]*Player),
|
||||||
|
}
|
||||||
|
|
||||||
|
host := &Player{
|
||||||
|
ID: hostID,
|
||||||
|
Nickname: nickname,
|
||||||
|
IsHost: true,
|
||||||
|
Send: make(chan []byte, 256),
|
||||||
|
}
|
||||||
|
room.Players[hostID] = host
|
||||||
|
room.PlayerOrder = append(room.PlayerOrder, hostID)
|
||||||
|
|
||||||
|
m.rooms[roomID] = room
|
||||||
|
return room
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) GetRoom(roomID string) *Room {
|
||||||
|
m.mu.RLock()
|
||||||
|
defer m.mu.RUnlock()
|
||||||
|
return m.rooms[roomID]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) ListRooms() []map[string]interface{} {
|
||||||
|
m.mu.RLock()
|
||||||
|
defer m.mu.RUnlock()
|
||||||
|
|
||||||
|
var list []map[string]interface{}
|
||||||
|
for _, rm := range m.rooms {
|
||||||
|
rm.mu.RLock()
|
||||||
|
realCount := 0
|
||||||
|
totalCount := len(rm.Players)
|
||||||
|
for _, p := range rm.Players {
|
||||||
|
if !p.IsBot {
|
||||||
|
realCount++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rm.mu.RUnlock()
|
||||||
|
list = append(list, map[string]interface{}{
|
||||||
|
"room_id": rm.ID,
|
||||||
|
"state": rm.State,
|
||||||
|
"player_count": totalCount,
|
||||||
|
"real_players": realCount,
|
||||||
|
"max_players": rm.MaxPlayers,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return list
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) JoinRoom(roomID, playerID, nickname string) (*Room, bool) {
|
||||||
|
m.mu.RLock()
|
||||||
|
room, exists := m.rooms[roomID]
|
||||||
|
m.mu.RUnlock()
|
||||||
|
|
||||||
|
if !exists {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
room.mu.Lock()
|
||||||
|
defer room.mu.Unlock()
|
||||||
|
|
||||||
|
if len(room.Players) >= room.MaxPlayers {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
if room.State != "waiting" {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
player := &Player{
|
||||||
|
ID: playerID,
|
||||||
|
Nickname: nickname,
|
||||||
|
Send: make(chan []byte, 256),
|
||||||
|
}
|
||||||
|
room.Players[playerID] = player
|
||||||
|
room.PlayerOrder = append(room.PlayerOrder, playerID)
|
||||||
|
|
||||||
|
hasRealHost := false
|
||||||
|
for _, p := range room.Players {
|
||||||
|
if p.IsHost && !p.IsBot {
|
||||||
|
hasRealHost = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !hasRealHost {
|
||||||
|
for _, p := range room.Players {
|
||||||
|
p.IsHost = false
|
||||||
|
}
|
||||||
|
player.IsHost = true
|
||||||
|
}
|
||||||
|
|
||||||
|
return room, true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) LeaveRoom(roomID, playerID string) {
|
||||||
|
m.mu.Lock()
|
||||||
|
defer m.mu.Unlock()
|
||||||
|
|
||||||
|
room, exists := m.rooms[roomID]
|
||||||
|
if !exists {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
room.mu.Lock()
|
||||||
|
defer room.mu.Unlock()
|
||||||
|
|
||||||
|
delete(room.Players, playerID)
|
||||||
|
|
||||||
|
newOrder := make([]string, 0)
|
||||||
|
for _, id := range room.PlayerOrder {
|
||||||
|
if id != playerID {
|
||||||
|
newOrder = append(newOrder, id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
room.PlayerOrder = newOrder
|
||||||
|
|
||||||
|
realCount := 0
|
||||||
|
for _, p := range room.Players {
|
||||||
|
if !p.IsBot {
|
||||||
|
realCount++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if realCount == 0 && !isTestRoom(roomID) {
|
||||||
|
delete(m.rooms, roomID)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if realCount == 0 && isTestRoom(roomID) {
|
||||||
|
for _, p := range room.Players {
|
||||||
|
p.IsHost = false
|
||||||
|
}
|
||||||
|
room.State = "waiting"
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
hasHost := false
|
||||||
|
for _, p := range room.Players {
|
||||||
|
if p.IsHost && !p.IsBot {
|
||||||
|
hasHost = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !hasHost {
|
||||||
|
for _, p := range room.Players {
|
||||||
|
p.IsHost = false
|
||||||
|
}
|
||||||
|
for _, id := range room.PlayerOrder {
|
||||||
|
p := room.Players[id]
|
||||||
|
if !p.IsBot {
|
||||||
|
p.IsHost = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func isTestRoom(roomID string) bool {
|
||||||
|
return roomID == "TEST3" || roomID == "TEST4" || roomID == "TEST5" || roomID == "TEST6"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) SetReady(roomID, playerID string, ready bool) bool {
|
||||||
|
room := m.GetRoom(roomID)
|
||||||
|
if room == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
room.mu.Lock()
|
||||||
|
defer room.mu.Unlock()
|
||||||
|
p, exists := room.Players[playerID]
|
||||||
|
if !exists {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
p.IsReady = ready
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Room) GetPlayerInfos() []map[string]interface{} {
|
||||||
|
r.mu.RLock()
|
||||||
|
defer r.mu.RUnlock()
|
||||||
|
|
||||||
|
var infos []map[string]interface{}
|
||||||
|
for _, id := range r.PlayerOrder {
|
||||||
|
p, ok := r.Players[id]
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
infos = append(infos, map[string]interface{}{
|
||||||
|
"player_id": p.ID,
|
||||||
|
"nickname": p.Nickname,
|
||||||
|
"is_ready": p.IsReady,
|
||||||
|
"is_host": p.IsHost,
|
||||||
|
"is_bot": p.IsBot,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return infos
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Room) BroadcastToReal(data []byte) {
|
||||||
|
r.mu.RLock()
|
||||||
|
defer r.mu.RUnlock()
|
||||||
|
|
||||||
|
for _, p := range r.Players {
|
||||||
|
if p.IsBot || p.Send == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
select {
|
||||||
|
case p.Send <- data:
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Room) SendToPlayer(playerID string, data []byte) {
|
||||||
|
r.mu.RLock()
|
||||||
|
defer r.mu.RUnlock()
|
||||||
|
p, ok := r.Players[playerID]
|
||||||
|
if !ok || p.IsBot || p.Send == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
select {
|
||||||
|
case p.Send <- data:
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func generateRoomID() string {
|
||||||
|
src := rand.NewSource(time.Now().UnixNano())
|
||||||
|
r := rand.New(src)
|
||||||
|
const charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
||||||
|
b := make([]byte, 6)
|
||||||
|
for i := range b {
|
||||||
|
b[i] = charset[r.Intn(len(charset))]
|
||||||
|
}
|
||||||
|
return string(b)
|
||||||
|
}
|
||||||
BIN
Binary file not shown.
Reference in New Issue
Block a user