feat(lobby): drop left honest-AI games from the finished list
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 9s
CI / integration (pull_request) Successful in 13s
CI / ui (pull_request) Has been skipped
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m4s
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 9s
CI / integration (pull_request) Successful in 13s
CI / ui (pull_request) Has been skipped
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m4s
A finished honest-AI (vs_ai) game the player left — by resigning or by abandoning it to the 7-day inactivity timeout (end_reason 'resign'/'timeout') — no longer appears in that player's own lobby finished list. The new game.Service.ListForLobby filters ListForAccount for the lobby endpoint only; the admin console and the account-merge count keep the full set. The filter keys on the game's end reason, not on which seat left, so it extends to any player should the robot ever resign.
This commit is contained in:
@@ -0,0 +1,134 @@
|
||||
//go:build integration
|
||||
|
||||
package inttest
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
|
||||
"scrabble/backend/internal/engine"
|
||||
"scrabble/backend/internal/game"
|
||||
)
|
||||
|
||||
// TestListForLobbyDropsLeftAIGames covers the lobby filter (game.Service.ListForLobby): a
|
||||
// finished honest-AI game the player left — by resigning or by abandoning to the inactivity
|
||||
// timeout — drops out of the lobby list, while it stays in ListForAccount so the admin console
|
||||
// and the account-merge count keep the full set. A normally finished AI game, an active AI game
|
||||
// and a left human game all stay in the lobby. The drop is a game property, not a per-resigner
|
||||
// one, so it leaves every seat's lobby.
|
||||
func TestListForLobbyDropsLeftAIGames(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
svc := newGameService()
|
||||
|
||||
createAI := func(t *testing.T) (uuid.UUID, []uuid.UUID) {
|
||||
t.Helper()
|
||||
seats := []uuid.UUID{provisionAccount(t), provisionAccount(t)}
|
||||
g, err := svc.Create(ctx, game.CreateParams{
|
||||
Variant: engine.VariantEnglish, Seats: seats, TurnTimeout: time.Hour, Seed: 1, VsAI: true,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("create AI game: %v", err)
|
||||
}
|
||||
return g.ID, seats
|
||||
}
|
||||
|
||||
t.Run("resign dropped from lobby, kept in account list", func(t *testing.T) {
|
||||
gid, seats := createAI(t)
|
||||
me := seats[0]
|
||||
res, err := svc.Resign(ctx, gid, me)
|
||||
if err != nil {
|
||||
t.Fatalf("resign: %v", err)
|
||||
}
|
||||
if res.Game.EndReason != "resign" {
|
||||
t.Fatalf("end reason = %q, want resign", res.Game.EndReason)
|
||||
}
|
||||
if lobbyHas(t, svc, me, gid) {
|
||||
t.Error("resigned AI game still in the lobby list")
|
||||
}
|
||||
if !containsGame(t, svc, me, gid) {
|
||||
t.Error("resigned AI game missing from the full account list (the row must stay)")
|
||||
}
|
||||
if lobbyHas(t, svc, seats[1], gid) {
|
||||
t.Error("resigned AI game still in the opponent's lobby list (drop is per-game)")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("timeout dropped from lobby", func(t *testing.T) {
|
||||
gid, seats := createAI(t)
|
||||
me := seats[0] // seat 0 is to move and will be timed out
|
||||
// Disable the away window so the sweep is deterministic regardless of wall clock.
|
||||
setAway(t, me, "UTC", "00:00", "00:00")
|
||||
backdate(t, gid, time.Now().UTC().Add(-(game.AIInactivityTimeout + 2*time.Hour)))
|
||||
if n, err := svc.SweepTimeouts(ctx, time.Now().UTC()); err != nil || n < 1 {
|
||||
t.Fatalf("sweep swept %d (err %v), want >= 1", n, err)
|
||||
}
|
||||
if status, reason := gameStatus(t, svc, gid); status != game.StatusFinished || reason != "timeout" {
|
||||
t.Fatalf("game not timed out: status %q reason %q", status, reason)
|
||||
}
|
||||
if lobbyHas(t, svc, me, gid) {
|
||||
t.Error("abandoned (timed-out) AI game still in the lobby list")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("active AI game stays in lobby", func(t *testing.T) {
|
||||
gid, seats := createAI(t)
|
||||
if !lobbyHas(t, svc, seats[0], gid) {
|
||||
t.Error("active AI game missing from the lobby list")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("normally finished AI game stays in lobby", func(t *testing.T) {
|
||||
gid, seats := createAI(t)
|
||||
var fin game.MoveResult
|
||||
for i := 0; i < 8; i++ { // six consecutive passes end the game scoreless
|
||||
r, err := svc.Pass(ctx, gid, seats[i%2])
|
||||
if err != nil {
|
||||
t.Fatalf("pass %d: %v", i, err)
|
||||
}
|
||||
fin = r
|
||||
if r.Game.Status == game.StatusFinished {
|
||||
break
|
||||
}
|
||||
}
|
||||
if fin.Game.Status != game.StatusFinished || fin.Game.EndReason != "scoreless" {
|
||||
t.Fatalf("AI game not scoreless-finished: status %q reason %q", fin.Game.Status, fin.Game.EndReason)
|
||||
}
|
||||
if !lobbyHas(t, svc, seats[0], gid) {
|
||||
t.Error("normally finished AI game must stay in the lobby list")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("left human game stays in lobby", func(t *testing.T) {
|
||||
seats := []uuid.UUID{provisionAccount(t), provisionAccount(t)}
|
||||
g, err := svc.Create(ctx, game.CreateParams{
|
||||
Variant: engine.VariantEnglish, Seats: seats, TurnTimeout: 24 * time.Hour, Seed: 1,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("create human game: %v", err)
|
||||
}
|
||||
if _, err := svc.Resign(ctx, g.ID, seats[0]); err != nil {
|
||||
t.Fatalf("resign: %v", err)
|
||||
}
|
||||
if !lobbyHas(t, svc, seats[0], g.ID) {
|
||||
t.Error("resigned human game must stay in the lobby list (only AI games are dropped)")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// lobbyHas reports whether the account's own lobby list (ListForLobby) includes gameID.
|
||||
func lobbyHas(t *testing.T, svc *game.Service, accountID, gameID uuid.UUID) bool {
|
||||
t.Helper()
|
||||
games, err := svc.ListForLobby(context.Background(), accountID)
|
||||
if err != nil {
|
||||
t.Fatalf("list for lobby: %v", err)
|
||||
}
|
||||
for _, g := range games {
|
||||
if g.ID == gameID {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
Reference in New Issue
Block a user