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.
85 lines
2.3 KiB
Go
85 lines
2.3 KiB
Go
package server
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/coder/websocket"
|
|
|
|
"github.com/greyson/super-auto-pets-board-game/internal/game"
|
|
"github.com/greyson/super-auto-pets-board-game/internal/store"
|
|
)
|
|
|
|
// TestE2EBotGame creates a vs-computer game over the API, plays the human's
|
|
// shop turns over the wire, and verifies the scheduled bot actually takes
|
|
// its own turns (spends coins) without any second client connected.
|
|
func TestE2EBotGame(t *testing.T) {
|
|
st, err := store.Open(t.TempDir())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer st.Close()
|
|
srv := New(st, "", false)
|
|
ts := httptest.NewServer(srv.Handler())
|
|
defer ts.Close()
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
|
defer cancel()
|
|
|
|
resp, err := http.Post(ts.URL+"/api/games", "application/json",
|
|
bytes.NewBufferString(`{"name":"Human","bot":"easy"}`))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var join joinResponse
|
|
if err := json.NewDecoder(resp.Body).Decode(&join); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
resp.Body.Close()
|
|
|
|
wsBase := "ws" + strings.TrimPrefix(ts.URL, "http")
|
|
ws, _, err := websocket.Dial(ctx,
|
|
wsBase+"/api/ws?game="+join.GameID+"&player="+join.PlayerID+"&token="+join.Token, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer ws.Close(websocket.StatusNormalClosure, "")
|
|
|
|
v := readState(t, ctx, ws)
|
|
if v.Phase != game.PhaseShop {
|
|
t.Fatalf("phase = %s, want shop (bot fills the lobby instantly)", v.Phase)
|
|
}
|
|
var bot *game.PlayerView
|
|
for i := range v.Players {
|
|
if v.Players[i].IsBot {
|
|
bot = &v.Players[i]
|
|
}
|
|
}
|
|
if bot == nil {
|
|
t.Fatal("no bot seat in the game")
|
|
}
|
|
|
|
// Play the human side: pass on our first turn (passing is final, so one
|
|
// is all we get). The game can only reach the arrange phase if the bot
|
|
// shops and passes on its own too.
|
|
deadline := time.Now().Add(25 * time.Second)
|
|
for v.Phase == game.PhaseShop && time.Now().Before(deadline) {
|
|
if v.Turn == v.YouSeat && !v.Players[v.YouSeat].Ready && v.Pending == nil {
|
|
send(t, ctx, ws, map[string]any{"type": "pass"})
|
|
}
|
|
rctx, rcancel := context.WithTimeout(ctx, 10*time.Second)
|
|
v = readState(t, rctx, ws)
|
|
rcancel()
|
|
}
|
|
if v.Phase == game.PhaseShop {
|
|
t.Fatalf("shop never ended; bot coins=%d", v.Players[bot.Seat].Coins)
|
|
}
|
|
t.Logf("reached phase %s; bot played its shop turns", v.Phase)
|
|
}
|