Files
super-auto-pets-board-game/internal/server/bot_e2e_test.go
T

99 lines
2.9 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"}`))
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, "")
// The host opens in the lobby, adds a bot, then starts the game — the
// unified lobby flow a real client drives.
if p := readState(t, ctx, ws).Phase; p != game.PhaseLobby {
t.Fatalf("phase = %s, want lobby on create", p)
}
send(t, ctx, ws, map[string]any{"type": "addBot", "difficulty": "easy"})
send(t, ctx, ws, map[string]any{"type": "start"})
var v *game.View
startDeadline := time.Now().Add(5 * time.Second)
for (v == nil || v.Phase != game.PhaseShop) && time.Now().Before(startDeadline) {
rctx, rcancel := context.WithTimeout(ctx, 3*time.Second)
v = readState(t, rctx, ws)
rcancel()
}
if v.Phase != game.PhaseShop {
t.Fatalf("phase = %s, want shop after host starts", 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)
}