This commit is contained in:
2026-06-18 17:31:10 +08:00
commit 6dd576158c
13 changed files with 2454 additions and 0 deletions
+349
View File
@@ -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
}