Add bot to play against.

This commit is contained in:
Greyson Parrelli
2026-07-23 15:58:12 -04:00
parent 3825dbacec
commit db1a6ef290
23 changed files with 1852 additions and 23 deletions
+36 -9
View File
@@ -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)