408 lines
9.2 KiB
Go
408 lines
9.2 KiB
Go
package game
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"math/rand"
|
|
"time"
|
|
)
|
|
|
|
type PlayerState struct {
|
|
ID string
|
|
Nickname string
|
|
IsBot bool
|
|
BotControlled 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
|
|
ChaosMode bool
|
|
ActionLogs []string
|
|
TurnID int
|
|
RoundNum int
|
|
rng *rand.Rand
|
|
}
|
|
|
|
func (e *Engine) log(msg string) { e.ActionLogs = append(e.ActionLogs, msg) }
|
|
func (e *Engine) FlushLogs() []string { l := e.ActionLogs; e.ActionLogs = nil; return l }
|
|
|
|
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.RoundNum = 1
|
|
e.TurnID = 0
|
|
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() {
|
|
e.TurnID++
|
|
cp := e.CurrentPlayer()
|
|
if cp != nil && !cp.IsBot && !cp.BotControlled {
|
|
e.log(fmt.Sprintf("轮到 %s 的回合 (第%d轮)", cp.Nickname, e.RoundNum))
|
|
}
|
|
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.log(p.Nickname + " 使用了特技「" + card.Name + "」")
|
|
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.log(p.Nickname + " 将一张牌放入了调和区")
|
|
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)
|
|
card.PlacedBy = p.ID
|
|
card.PlacedByNick = p.Nickname
|
|
target.ChallengeZone = append(target.ChallengeZone, card)
|
|
e.log(p.Nickname + " 质疑了 " + target.Nickname)
|
|
e.SelectedCard = nil
|
|
e.SelectedBy = ""
|
|
e.finishTurn(p)
|
|
return nil
|
|
}
|
|
|
|
func (e *Engine) finishTurn(p *PlayerState) {
|
|
if len(p.Hand) <= 1 {
|
|
p.IsExited = true
|
|
e.log(p.Nickname + " 暂时退出了游戏")
|
|
}
|
|
if e.allExited() {
|
|
e.Phase = "all_exited"
|
|
e.TurnPhase = ""
|
|
return
|
|
}
|
|
e.advanceTurn()
|
|
e.startTurn()
|
|
}
|
|
|
|
func (e *Engine) advanceTurn() {
|
|
n := len(e.Players)
|
|
old := e.CurrentTurn
|
|
next := (e.CurrentTurn + 1) % n
|
|
for i := 0; i < n; i++ {
|
|
if !e.Players[next].IsExited {
|
|
e.CurrentTurn = next
|
|
if next <= old {
|
|
e.RoundNum++
|
|
}
|
|
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) IsHandTargetable(p *PlayerState) bool {
|
|
if len(p.Hand) == 0 {
|
|
return false
|
|
}
|
|
if e.ChaosMode {
|
|
return true
|
|
}
|
|
return !p.IsExited
|
|
}
|
|
|
|
func (e *Engine) UpdatePlayerID(oldID, newID string) {
|
|
for _, p := range e.Players {
|
|
if p.ID == oldID {
|
|
p.ID = newID
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
func (e *Engine) SetBotControlled(playerID string, v bool) {
|
|
for _, p := range e.Players {
|
|
if p.ID == playerID {
|
|
p.BotControlled = v
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
func (e *Engine) IsPlayerBotLike(playerID string) bool {
|
|
p := e.GetPlayer(playerID)
|
|
return p != nil && (p.IsBot || p.BotControlled)
|
|
}
|
|
|
|
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.BotControlled) || 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
|
|
}
|