Add bot to play against.
This commit is contained in:
@@ -874,10 +874,10 @@ func (g *Game) resolveBattle() {
|
||||
// Tagged "result" so the client can hold it back until the replay finishes
|
||||
// (the outcome is known now, but showing it early would spoil the battle).
|
||||
if winner < 0 {
|
||||
g.addLog(LogEntry{Seat: -1, Icon: "⚔️", Kind: "result",
|
||||
g.addLog(LogEntry{Seat: -1, Icon: "⚔️", Kind: LogResult,
|
||||
Text: fmt.Sprintf("Round %d battle ends in a draw.", g.Round)})
|
||||
} else {
|
||||
g.addLog(LogEntry{Seat: winner, Icon: "⚔️", Kind: "result",
|
||||
g.addLog(LogEntry{Seat: winner, Icon: "⚔️", Kind: LogResult,
|
||||
Text: fmt.Sprintf("%s wins the round %d battle (+%d🏆).", pname(winner), g.Round, res.Trophies)})
|
||||
}
|
||||
for _, p := range g.Players {
|
||||
|
||||
+36
-9
@@ -3,6 +3,7 @@ package game
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math/big"
|
||||
@@ -49,6 +50,13 @@ type Player struct {
|
||||
// TripledThisRound records whether the player used the Triple (trade-in)
|
||||
// action during the current round's shop (Bison's Battle Prep).
|
||||
TripledThisRound bool `json:"tripledThisRound"`
|
||||
// IsBot marks a computer-controlled seat. The engine treats bots exactly
|
||||
// like humans; the server drives their actions. BotLevel is the bot's
|
||||
// skill in [0, 1]; BotMemory is the bot's private notebook, opaque to the
|
||||
// engine and persisted with the game so knowledge survives restarts.
|
||||
IsBot bool `json:"isBot,omitempty"`
|
||||
BotLevel float64 `json:"botLevel,omitempty"`
|
||||
BotMemory json.RawMessage `json:"botMemory,omitempty"`
|
||||
}
|
||||
|
||||
// PetCount counts pet cards in the player's deck.
|
||||
@@ -196,6 +204,19 @@ func (g *Game) AddPlayer(name string) (*Player, error) {
|
||||
return p, nil
|
||||
}
|
||||
|
||||
// AddBot seats a computer-controlled player. Bots count as connected from
|
||||
// the start; the server is responsible for driving their actions.
|
||||
func (g *Game) AddBot(name string, level float64) (*Player, error) {
|
||||
p, err := g.AddPlayer(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
p.IsBot = true
|
||||
p.BotLevel = min(max(level, 0), 1)
|
||||
p.Connected = true
|
||||
return p, nil
|
||||
}
|
||||
|
||||
// PlayerByID returns the player, or nil.
|
||||
func (g *Game) PlayerByID(id string) *Player {
|
||||
for _, p := range g.Players {
|
||||
@@ -278,7 +299,8 @@ func (g *Game) Buy(playerID string, rowIdx int) error {
|
||||
p.Coins--
|
||||
bought := g.ShopRow[rowIdx]
|
||||
p.Deck = append(p.Deck, bought)
|
||||
g.logf(p.Seat, "🛒", "%s bought %s %s.", p.Name, article(bought.Name), bought.Name)
|
||||
g.addLog(LogEntry{Seat: p.Seat, Icon: "🛒", Kind: LogBuy, Source: bought.ID, CardName: bought.Name,
|
||||
Text: fmt.Sprintf("%s bought %s %s.", p.Name, article(bought.Name), bought.Name)})
|
||||
g.ShopRow[rowIdx] = g.drawFromTier(g.Round)
|
||||
g.applyShopTrigger(p, bought, TriggerBuy)
|
||||
g.advanceShopTurn()
|
||||
@@ -320,7 +342,7 @@ func (g *Game) applyShopTrigger(p *Player, c Card, trigger EffectTrigger) {
|
||||
for range n {
|
||||
p.Deck = append(p.Deck, g.newApple())
|
||||
}
|
||||
g.addLog(LogEntry{Seat: p.Seat, Icon: "🍎", Source: c.ID, Spawn: "apple",
|
||||
g.addLog(LogEntry{Seat: p.Seat, Icon: "🍎", Source: c.ID, Spawn: "apple", Count: n,
|
||||
Text: fmt.Sprintf("%s adds %d apple%s to %s's deck.", c.Name, n, plural(n), p.Name)})
|
||||
case ActionRefreshGold:
|
||||
p.Coins = min(p.Coins+e.count(), CoinsPerRound)
|
||||
@@ -336,7 +358,7 @@ func (g *Game) applyShopTrigger(p *Player, c Card, trigger EffectTrigger) {
|
||||
p.Deck = append(p.Deck, g.newApple())
|
||||
}
|
||||
if apples > 0 {
|
||||
g.addLog(LogEntry{Seat: p.Seat, Icon: "🍎", Source: c.ID, Spawn: "apple",
|
||||
g.addLog(LogEntry{Seat: p.Seat, Icon: "🍎", Source: c.ID, Spawn: "apple", Count: apples,
|
||||
Text: fmt.Sprintf("%s doubles %s's apples (+%d).", c.Name, p.Name, apples)})
|
||||
}
|
||||
}
|
||||
@@ -363,7 +385,7 @@ func (g *Game) sellCards(p *Player, cardIDs []string) error {
|
||||
}
|
||||
for _, c := range sold {
|
||||
p.Deck = append(p.Deck, g.newApple())
|
||||
g.addLog(LogEntry{Seat: p.Seat, Icon: "🍎", Source: c.ID, Spawn: "apple",
|
||||
g.addLog(LogEntry{Seat: p.Seat, Icon: "🍎", Kind: LogSell, Source: c.ID, CardName: c.Name, Spawn: "apple",
|
||||
Text: fmt.Sprintf("%s sold %s — it becomes an apple.", p.Name, c.Name)})
|
||||
g.applyShopTrigger(p, c, TriggerSell)
|
||||
}
|
||||
@@ -427,11 +449,14 @@ func (g *Game) TradeStart(playerID string, cardIDs []string) error {
|
||||
// The discarded trio is public — everyone sees what was given up — even
|
||||
// though the pet ultimately chosen stays secret (see TradeChoose).
|
||||
names := make([]string, len(traded))
|
||||
ids := make([]string, len(traded))
|
||||
for i, c := range traded {
|
||||
names[i] = c.Name
|
||||
ids[i] = c.ID
|
||||
}
|
||||
g.logf(p.Seat, "🔄", "%s traded in %s (%s) for a tier %d pick.",
|
||||
p.Name, strings.Join(names, ", "), suit, nextTier)
|
||||
g.addLog(LogEntry{Seat: p.Seat, Icon: "🔄", Kind: LogTrade, Cards: ids,
|
||||
Text: fmt.Sprintf("%s traded in %s (%s) for a tier %d pick.",
|
||||
p.Name, strings.Join(names, ", "), suit, nextTier)})
|
||||
g.Pending = &PendingTrade{
|
||||
PlayerID: playerID,
|
||||
Tier: nextTier,
|
||||
@@ -463,10 +488,12 @@ func (g *Game) TradeChoose(playerID string, pick int) error {
|
||||
// can't see the deck. But a pet with a Buy ability performs it publicly, so
|
||||
// we have to reveal that pet (its effect log names it anyway).
|
||||
if hasBuyEffect(chosen) {
|
||||
g.logf(p.Seat, "🔄", "%s's trade pick is %s %s — its buy ability triggers.",
|
||||
p.Name, article(chosen.Name), chosen.Name)
|
||||
g.addLog(LogEntry{Seat: p.Seat, Icon: "🔄", Kind: LogTradePick, CardName: chosen.Name,
|
||||
Text: fmt.Sprintf("%s's trade pick is %s %s — its buy ability triggers.",
|
||||
p.Name, article(chosen.Name), chosen.Name)})
|
||||
} else {
|
||||
g.logf(p.Seat, "🔄", "%s keeps their trade pick hidden.", p.Name)
|
||||
g.addLog(LogEntry{Seat: p.Seat, Icon: "🔄", Kind: LogTradePick,
|
||||
Text: fmt.Sprintf("%s keeps their trade pick hidden.", p.Name)})
|
||||
}
|
||||
// Pets obtained via the Triple action trigger their Buy effects.
|
||||
g.applyShopTrigger(p, chosen, TriggerBuy)
|
||||
|
||||
+17
-1
@@ -14,12 +14,28 @@ type LogEntry struct {
|
||||
Phase Phase `json:"phase"`
|
||||
Seat int `json:"seat"` // acting seat, or -1 when none
|
||||
Icon string `json:"icon,omitempty"` // leading emoji
|
||||
Kind string `json:"kind,omitempty"` // e.g. "result" (battle outcome)
|
||||
Kind string `json:"kind,omitempty"` // structured tag; see constants below
|
||||
Text string `json:"text"` // the sentence itself
|
||||
Source string `json:"source,omitempty"` // card id that caused a spawn
|
||||
Spawn string `json:"spawn,omitempty"` // "apple" | "bee" for spawn entries
|
||||
// The fields below add machine-readable copies of facts the Text already
|
||||
// states publicly, so observers (the AI player included) don't have to
|
||||
// parse English. They must never carry information the text doesn't.
|
||||
Count int `json:"count,omitempty"` // e.g. apples gained
|
||||
CardName string `json:"cardName,omitempty"` // named card, when public
|
||||
Cards []string `json:"cards,omitempty"` // card ids involved, when public
|
||||
}
|
||||
|
||||
// Structured LogEntry.Kind tags. Only "result" affects the client; the rest
|
||||
// exist so observers can follow the public action stream structurally.
|
||||
const (
|
||||
LogResult = "result" // battle outcome (client holds it until the replay ends)
|
||||
LogBuy = "buy" // Seat bought Source/CardName from the shop row
|
||||
LogSell = "sell" // Seat sold Source/CardName (it became an apple)
|
||||
LogTrade = "trade" // Seat traded in Cards for a next-tier pick
|
||||
LogTradePick = "tradePick" // Seat took their pick; CardName set when revealed
|
||||
)
|
||||
|
||||
// addLog appends an entry, stamping it with the next sequence number and the
|
||||
// current round/phase. Callers set Seat/Icon/Text (and Source/Spawn when the
|
||||
// entry represents something spawning off a card).
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
package game
|
||||
|
||||
// SimulateBattle resolves a hypothetical two-player battle between the given
|
||||
// arranged decks (top of deck first) and returns the result. It runs on a
|
||||
// scratch game, so it never touches real state — callers (notably the AI
|
||||
// player) can roll out as many what-if battles as they like. Dice rolls are
|
||||
// random unless rollDie is non-nil.
|
||||
func SimulateBattle(round, prioritySeat int, deckA, deckB []Card, rollDie func() int) *BattleResult {
|
||||
g := &Game{
|
||||
Round: round,
|
||||
PrioritySeat: prioritySeat,
|
||||
RollDie: rollDie,
|
||||
// Cards minted during the simulation (apples, bees) get IDs far away
|
||||
// from real ones, purely to avoid confusion when reading results.
|
||||
NextCardID: 1_000_000,
|
||||
Players: []*Player{
|
||||
{Name: "A", Seat: 0, Deck: append([]Card(nil), deckA...)},
|
||||
{Name: "B", Seat: 1, Deck: append([]Card(nil), deckB...)},
|
||||
},
|
||||
}
|
||||
g.resolveBattle()
|
||||
return g.Battle
|
||||
}
|
||||
|
||||
// TierContents returns the full printed contents of a tier's shop deck —
|
||||
// public information from the box. Cards carry placeholder IDs; they are
|
||||
// reference data, not live instances.
|
||||
func TierContents(tier int) []Card {
|
||||
scratch := &Game{}
|
||||
scratch.buildShopDecks()
|
||||
if tier < 1 || tier > len(scratch.ShopDecks) {
|
||||
return nil
|
||||
}
|
||||
return scratch.ShopDecks[tier-1]
|
||||
}
|
||||
@@ -10,6 +10,7 @@ type PlayerView struct {
|
||||
Trophies int `json:"trophies"`
|
||||
Ready bool `json:"ready"`
|
||||
Connected bool `json:"connected"`
|
||||
IsBot bool `json:"isBot,omitempty"`
|
||||
DeckSize int `json:"deckSize"`
|
||||
PetCount int `json:"petCount"`
|
||||
Deck []Card `json:"deck,omitempty"` // self only
|
||||
@@ -70,6 +71,7 @@ func (g *Game) ViewFor(playerID string) View {
|
||||
Trophies: p.Trophies,
|
||||
Ready: p.Ready,
|
||||
Connected: p.Connected,
|
||||
IsBot: p.IsBot,
|
||||
DeckSize: len(p.Deck),
|
||||
PetCount: p.PetCount(),
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user