Fix shop action costs and pass semantics.
Only buying costs gold; selling and trading (Triple) are free. Pass is now a final action, legal only at or under the pet limit: it forfeits remaining gold and ends that player's shopping for the round, with the shop closing once everyone has passed. That makes the separate cleanup phase unreachable (you sell down in-shop before passing), so it is removed. The client asks for confirmation before passing, and the bot knows buys are the only coin sink, when passing is legal, and that it must sell down before it can pass.
This commit is contained in:
+47
-23
@@ -98,8 +98,8 @@ func TestSellConvertsToApples(t *testing.T) {
|
||||
if err := g.Sell(p.ID, []string{plain.ID}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if p.Coins != CoinsPerRound-1 {
|
||||
t.Fatalf("sell should cost 1 coin, coins=%d", p.Coins)
|
||||
if p.Coins != CoinsPerRound {
|
||||
t.Fatalf("selling should be free, coins=%d", p.Coins)
|
||||
}
|
||||
if p.PetCount() != 0 || len(p.Deck) != 1 || p.Deck[0].Food != FoodApple {
|
||||
t.Fatalf("sold pet should become an apple: %+v", p.Deck)
|
||||
@@ -187,8 +187,8 @@ func TestTradeInThreeMatchingSuits(t *testing.T) {
|
||||
if deck2[len(deck2)-1].ID != rejected.ID {
|
||||
t.Fatal("rejected card should go to the bottom of the tier deck")
|
||||
}
|
||||
if p.Coins != CoinsPerRound-1 {
|
||||
t.Fatalf("trade should cost 1 coin, coins=%d", p.Coins)
|
||||
if p.Coins != CoinsPerRound {
|
||||
t.Fatalf("trading should be free, coins=%d", p.Coins)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -307,6 +307,7 @@ func TestSwanTripleRefreshesGold(t *testing.T) {
|
||||
g, _, _ := testGame(t)
|
||||
g.Round = tc.round
|
||||
p := current(g)
|
||||
p.Coins-- // a spent coin, so the refresh has something to restore
|
||||
var ids []string
|
||||
for range 3 {
|
||||
s := g.realPet(t, "Swan")
|
||||
@@ -327,7 +328,7 @@ func TestSwanTripleRefreshesGold(t *testing.T) {
|
||||
func TestGiraffeBattlePrep(t *testing.T) {
|
||||
g, p1, _ := testGame(t)
|
||||
p1.Deck = append(p1.Deck, g.realPet(t, "Giraffe"))
|
||||
spendAllCoins(t, g)
|
||||
passShop(t, g)
|
||||
if g.Phase != PhaseArrange {
|
||||
t.Fatalf("expected arrange, got %s", g.Phase)
|
||||
}
|
||||
@@ -370,8 +371,8 @@ func TestTradeBlockedOnFinalRound(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// spendAllCoins has both players pass until the shop ends.
|
||||
func spendAllCoins(t *testing.T, g *Game) {
|
||||
// passShop has both players pass until the shop ends.
|
||||
func passShop(t *testing.T, g *Game) {
|
||||
t.Helper()
|
||||
for g.Phase == PhaseShop {
|
||||
if err := g.Pass(current(g).ID); err != nil {
|
||||
@@ -382,30 +383,52 @@ func spendAllCoins(t *testing.T, g *Game) {
|
||||
|
||||
func TestShopEndsIntoArrange(t *testing.T) {
|
||||
g, _, _ := testGame(t)
|
||||
spendAllCoins(t, g)
|
||||
passShop(t, g)
|
||||
if g.Phase != PhaseArrange {
|
||||
t.Fatalf("shop should end into arrange when no one is over the pet limit, got %s", g.Phase)
|
||||
t.Fatalf("shop should end into arrange once both players pass, got %s", g.Phase)
|
||||
}
|
||||
}
|
||||
|
||||
func TestForcedDiscardOverPetLimit(t *testing.T) {
|
||||
// Passing is final: a passed player's turn never comes back, and their coins
|
||||
// are forfeit, while the other player keeps shopping.
|
||||
func TestPassEndsShoppingForTheRound(t *testing.T) {
|
||||
g, _, _ := testGame(t)
|
||||
p, other := current(g), g.Players[(g.Turn+1)%2]
|
||||
if err := g.Pass(p.ID); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if p.Coins != 0 {
|
||||
t.Fatalf("passing should forfeit remaining coins, got %d", p.Coins)
|
||||
}
|
||||
if g.Phase != PhaseShop || current(g).ID != other.ID {
|
||||
t.Fatalf("shop should continue with the other player, phase=%s turn=%d", g.Phase, g.Turn)
|
||||
}
|
||||
// A free action by the remaining player must not hand the turn back.
|
||||
junk := g.pet("Junk", 1)
|
||||
other.Deck = append(other.Deck, junk)
|
||||
if err := g.Sell(other.ID, []string{junk.ID}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if current(g).ID != other.ID {
|
||||
t.Fatal("turn must stay with the only player still shopping")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPassBlockedOverPetLimit(t *testing.T) {
|
||||
g, p1, _ := testGame(t)
|
||||
g.Turn = p1.Seat
|
||||
for range MaxPets + 2 {
|
||||
p1.Deck = append(p1.Deck, g.pet("Extra", 1))
|
||||
}
|
||||
spendAllCoins(t, g)
|
||||
if g.Phase != PhaseCleanup {
|
||||
t.Fatalf("player with %d pets must be forced to discard, got phase %s", MaxPets+2, g.Phase)
|
||||
if err := g.Pass(p1.ID); err == nil {
|
||||
t.Fatalf("passing with %d pets must be rejected", MaxPets+2)
|
||||
}
|
||||
// Wrong count rejected.
|
||||
if err := g.CleanupSell(p1.ID, deckIDs(p1, Card.IsPet)[:1]); err == nil {
|
||||
t.Fatal("must sell exactly the excess")
|
||||
}
|
||||
if err := g.CleanupSell(p1.ID, deckIDs(p1, Card.IsPet)[:2]); err != nil {
|
||||
// Selling down (free) unblocks the pass; the discards become apples.
|
||||
if err := g.Sell(p1.ID, deckIDs(p1, Card.IsPet)[:2]); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if p1.PetCount() != MaxPets {
|
||||
t.Fatalf("expected %d pets after cleanup, got %d", MaxPets, p1.PetCount())
|
||||
t.Fatalf("expected %d pets after selling down, got %d", MaxPets, p1.PetCount())
|
||||
}
|
||||
apples := 0
|
||||
for _, c := range p1.Deck {
|
||||
@@ -416,15 +439,16 @@ func TestForcedDiscardOverPetLimit(t *testing.T) {
|
||||
if apples != 2 {
|
||||
t.Fatalf("discarded pets should become apples, got %d", apples)
|
||||
}
|
||||
passShop(t, g)
|
||||
if g.Phase != PhaseArrange {
|
||||
t.Fatalf("cleanup should flow into arrange, got %s", g.Phase)
|
||||
t.Fatalf("shop should flow into arrange, got %s", g.Phase)
|
||||
}
|
||||
}
|
||||
|
||||
func TestArrangeRejectsBadPermutation(t *testing.T) {
|
||||
g, p1, _ := testGame(t)
|
||||
p1.Deck = append(p1.Deck, g.pet("A", 1), g.pet("B", 2))
|
||||
spendAllCoins(t, g)
|
||||
passShop(t, g)
|
||||
if err := g.SubmitOrder(p1.ID, []string{p1.Deck[0].ID}); err == nil {
|
||||
t.Fatal("partial order should be rejected")
|
||||
}
|
||||
@@ -462,7 +486,7 @@ func TestFullGameFlow(t *testing.T) {
|
||||
t.Fatalf("round %d shop dealt tier %d card", round, c.Tier)
|
||||
}
|
||||
}
|
||||
spendAllCoins(t, g)
|
||||
passShop(t, g)
|
||||
for _, p := range g.Players {
|
||||
if err := g.SubmitOrder(p.ID, deckIDs(p, nil)); err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -558,7 +582,7 @@ func TestDebugGrant(t *testing.T) {
|
||||
if !Catalog()[0].IsPet() { // sanity on the shared catalog helper
|
||||
t.Fatal("catalog should start with a pet")
|
||||
}
|
||||
// Not allowed outside shop/cleanup.
|
||||
// Not allowed outside the shop.
|
||||
g.Phase = PhaseBattle
|
||||
if err := g.DebugGrant(p1.ID, "Ant"); err == nil {
|
||||
t.Fatal("grant should be rejected outside the shop")
|
||||
|
||||
Reference in New Issue
Block a user