Add pets for tiers 1-3.
This commit is contained in:
+71
-17
@@ -85,6 +85,18 @@ type Game struct {
|
||||
Battle *BattleResult `json:"battle,omitempty"` // most recent battle
|
||||
NextCardID int `json:"nextCardId"`
|
||||
WinnerSeat int `json:"winnerSeat"` // set at gameover; -1 = tie
|
||||
|
||||
// RollDie overrides the rock die (faces 0,0,1,1,2,2) for tests. Nil
|
||||
// (including after loading from storage) means a fair random roll.
|
||||
RollDie func() int `json:"-"`
|
||||
}
|
||||
|
||||
// rollRockDie rolls one rock die: 0, 1, or 2 with equal probability.
|
||||
func (g *Game) rollRockDie() int {
|
||||
if g.RollDie != nil {
|
||||
return g.RollDie()
|
||||
}
|
||||
return randInt(3)
|
||||
}
|
||||
|
||||
var (
|
||||
@@ -247,23 +259,25 @@ func (g *Game) Buy(playerID string, rowIdx int) error {
|
||||
return fmt.Errorf("%w: no card in that shop slot", ErrInvalidAction)
|
||||
}
|
||||
p.Coins--
|
||||
p.Deck = append(p.Deck, g.ShopRow[rowIdx])
|
||||
bought := g.ShopRow[rowIdx]
|
||||
p.Deck = append(p.Deck, bought)
|
||||
g.ShopRow[rowIdx] = g.drawFromTier(g.Round)
|
||||
g.applyShopTrigger(p, bought, TriggerBuy)
|
||||
g.advanceShopTurn()
|
||||
return nil
|
||||
}
|
||||
|
||||
// Discard spends one coin to convert any number (>=1) of the player's cards
|
||||
// into that many apples.
|
||||
func (g *Game) Discard(playerID string, cardIDs []string) error {
|
||||
// Sell spends one coin to sell any number (>=1) of the player's cards: each
|
||||
// becomes an apple, and Sell effects on the sold cards fire.
|
||||
func (g *Game) Sell(playerID string, cardIDs []string) error {
|
||||
p, err := g.requireShopTurn(playerID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(cardIDs) == 0 {
|
||||
return fmt.Errorf("%w: choose at least one card to discard", ErrInvalidAction)
|
||||
return fmt.Errorf("%w: choose at least one card to sell", ErrInvalidAction)
|
||||
}
|
||||
if err := g.convertToApples(p, cardIDs); err != nil {
|
||||
if err := g.sellCards(p, cardIDs); err != nil {
|
||||
return err
|
||||
}
|
||||
p.Coins--
|
||||
@@ -271,9 +285,27 @@ func (g *Game) Discard(playerID string, cardIDs []string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// convertToApples removes the given cards from p's deck and adds one apple
|
||||
// per removed card. It validates before mutating.
|
||||
func (g *Game) convertToApples(p *Player, cardIDs []string) error {
|
||||
// applyShopTrigger fires a shop-time trigger (buy/sell/triple) on one card.
|
||||
func (g *Game) applyShopTrigger(p *Player, c Card, trigger EffectTrigger) {
|
||||
for _, e := range c.Effects {
|
||||
if e.Trigger != trigger || g.Round < e.MinRound {
|
||||
continue
|
||||
}
|
||||
switch e.Action {
|
||||
case ActionGainApple:
|
||||
for range e.count() {
|
||||
p.Deck = append(p.Deck, g.newApple())
|
||||
}
|
||||
case ActionRefreshGold:
|
||||
p.Coins = min(p.Coins+e.count(), CoinsPerRound)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// sellCards removes the given cards from p's deck, adds one apple per
|
||||
// removed card, and fires the sold cards' Sell effects. It validates before
|
||||
// mutating.
|
||||
func (g *Game) sellCards(p *Player, cardIDs []string) error {
|
||||
if hasDuplicates(cardIDs) {
|
||||
return fmt.Errorf("%w: duplicate card", ErrInvalidAction)
|
||||
}
|
||||
@@ -282,11 +314,15 @@ func (g *Game) convertToApples(p *Player, cardIDs []string) error {
|
||||
return fmt.Errorf("%w: card not in your deck", ErrInvalidAction)
|
||||
}
|
||||
}
|
||||
sold := make([]Card, 0, len(cardIDs))
|
||||
for _, id := range cardIDs {
|
||||
p.Deck = slices.Delete(p.Deck, p.cardIndex(id), p.cardIndex(id)+1)
|
||||
idx := p.cardIndex(id)
|
||||
sold = append(sold, p.Deck[idx])
|
||||
p.Deck = slices.Delete(p.Deck, idx, idx+1)
|
||||
}
|
||||
for range cardIDs {
|
||||
for _, c := range sold {
|
||||
p.Deck = append(p.Deck, g.newApple())
|
||||
g.applyShopTrigger(p, c, TriggerSell)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -326,8 +362,11 @@ func (g *Game) TradeStart(playerID string, cardIDs []string) error {
|
||||
return fmt.Errorf("%w: next tier deck is exhausted", ErrInvalidAction)
|
||||
}
|
||||
// Validated; commit.
|
||||
traded := make([]Card, 0, TradeInCount)
|
||||
for _, id := range cardIDs {
|
||||
p.Deck = slices.Delete(p.Deck, p.cardIndex(id), p.cardIndex(id)+1)
|
||||
idx := p.cardIndex(id)
|
||||
traded = append(traded, p.Deck[idx])
|
||||
p.Deck = slices.Delete(p.Deck, idx, idx+1)
|
||||
}
|
||||
p.Coins--
|
||||
g.Pending = &PendingTrade{
|
||||
@@ -335,6 +374,10 @@ func (g *Game) TradeStart(playerID string, cardIDs []string) error {
|
||||
Tier: nextTier,
|
||||
Options: [2]Card{g.drawFromTier(nextTier), g.drawFromTier(nextTier)},
|
||||
}
|
||||
// Triple effects fire on the traded-in cards themselves (e.g. Fish).
|
||||
for _, c := range traded {
|
||||
g.applyShopTrigger(p, c, TriggerTriple)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -353,6 +396,8 @@ func (g *Game) TradeChoose(playerID string, pick int) error {
|
||||
tierIdx := g.Pending.Tier - 1
|
||||
g.ShopDecks[tierIdx] = append(g.ShopDecks[tierIdx], other)
|
||||
g.Pending = nil
|
||||
// Pets obtained via the Triple action trigger their Buy effects.
|
||||
g.applyShopTrigger(p, chosen, TriggerBuy)
|
||||
g.advanceShopTurn()
|
||||
return nil
|
||||
}
|
||||
@@ -398,9 +443,9 @@ func (g *Game) endShop() {
|
||||
g.beginArrange()
|
||||
}
|
||||
|
||||
// CleanupDiscard performs the forced end-of-shop discard: the player must
|
||||
// convert exactly their excess pets into apples.
|
||||
func (g *Game) CleanupDiscard(playerID string, cardIDs []string) error {
|
||||
// CleanupSell performs the forced end-of-shop sale: the player must sell
|
||||
// exactly their excess pets (each becomes an apple; Sell effects fire).
|
||||
func (g *Game) CleanupSell(playerID string, cardIDs []string) error {
|
||||
if g.Phase != PhaseCleanup {
|
||||
return ErrWrongPhase
|
||||
}
|
||||
@@ -413,7 +458,7 @@ func (g *Game) CleanupDiscard(playerID string, cardIDs []string) error {
|
||||
return fmt.Errorf("%w: you are not over the pet limit", ErrInvalidAction)
|
||||
}
|
||||
if len(cardIDs) != excess {
|
||||
return fmt.Errorf("%w: discard exactly %d pets", ErrInvalidAction, excess)
|
||||
return fmt.Errorf("%w: sell exactly %d pets", ErrInvalidAction, excess)
|
||||
}
|
||||
for _, id := range cardIDs {
|
||||
idx := p.cardIndex(id)
|
||||
@@ -421,7 +466,7 @@ func (g *Game) CleanupDiscard(playerID string, cardIDs []string) error {
|
||||
return fmt.Errorf("%w: pick pets from your deck", ErrInvalidAction)
|
||||
}
|
||||
}
|
||||
if err := g.convertToApples(p, cardIDs); err != nil {
|
||||
if err := g.sellCards(p, cardIDs); err != nil {
|
||||
return err
|
||||
}
|
||||
p.Ready = true
|
||||
@@ -444,6 +489,11 @@ func (g *Game) beginArrange() {
|
||||
g.Phase = PhaseArrange
|
||||
for _, p := range g.Players {
|
||||
p.Ready = false
|
||||
// Battle Prep effects fire now, before players order their cards
|
||||
// (e.g. Giraffe hands out apples that can go into the deck order).
|
||||
for _, c := range slices.Clone(p.Deck) {
|
||||
g.applyShopTrigger(p, c, TriggerBattlePrep)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -494,6 +544,10 @@ func (g *Game) AcknowledgeBattle(playerID string) error {
|
||||
if !g.allReady() {
|
||||
return nil
|
||||
}
|
||||
// Temporary cards (apples, bees) expire once their battle has happened.
|
||||
for _, pl := range g.Players {
|
||||
pl.Deck = slices.DeleteFunc(pl.Deck, func(c Card) bool { return c.Temporary })
|
||||
}
|
||||
if g.Round >= MaxRounds {
|
||||
g.finish()
|
||||
return nil
|
||||
|
||||
Reference in New Issue
Block a user