feat: game lobby service

This commit is contained in:
Ilia Denisov
2026-04-25 23:20:55 +02:00
committed by GitHub
parent 32dc29359a
commit 48b0056b49
336 changed files with 57074 additions and 1418 deletions
@@ -0,0 +1,184 @@
package redisstate_test
import (
"context"
"sort"
"testing"
"galaxy/lobby/internal/adapters/redisstate"
"galaxy/lobby/internal/domain/common"
"galaxy/lobby/internal/ports"
"github.com/alicebob/miniredis/v2"
"github.com/redis/go-redis/v9"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func newGameTurnStatsStore(t *testing.T) (*redisstate.GameTurnStatsStore, *miniredis.Miniredis) {
t.Helper()
server := miniredis.RunT(t)
client := redis.NewClient(&redis.Options{Addr: server.Addr()})
t.Cleanup(func() { _ = client.Close() })
store, err := redisstate.NewGameTurnStatsStore(client)
require.NoError(t, err)
return store, server
}
func TestGameTurnStatsStoreSaveInitialFreezesValues(t *testing.T) {
store, _ := newGameTurnStatsStore(t)
ctx := context.Background()
gameID := common.GameID("game-stats-1")
require.NoError(t, store.SaveInitial(ctx, gameID, []ports.PlayerInitialStats{
{UserID: "user-a", Planets: 3, Population: 100, ShipsBuilt: 7},
}))
require.NoError(t, store.SaveInitial(ctx, gameID, []ports.PlayerInitialStats{
{UserID: "user-a", Planets: 99, Population: 9999, ShipsBuilt: 999},
}))
aggregate, err := store.Load(ctx, gameID)
require.NoError(t, err)
require.Len(t, aggregate.Players, 1)
assert.Equal(t, int64(3), aggregate.Players[0].InitialPlanets)
assert.Equal(t, int64(100), aggregate.Players[0].InitialPopulation)
assert.Equal(t, int64(7), aggregate.Players[0].InitialShipsBuilt)
assert.Equal(t, int64(3), aggregate.Players[0].MaxPlanets)
assert.Equal(t, int64(100), aggregate.Players[0].MaxPopulation)
assert.Equal(t, int64(7), aggregate.Players[0].MaxShipsBuilt)
}
func TestGameTurnStatsStoreUpdateMaxRaisesOnly(t *testing.T) {
store, _ := newGameTurnStatsStore(t)
ctx := context.Background()
gameID := common.GameID("game-stats-2")
require.NoError(t, store.SaveInitial(ctx, gameID, []ports.PlayerInitialStats{
{UserID: "user-a", Planets: 3, Population: 100, ShipsBuilt: 7},
}))
require.NoError(t, store.UpdateMax(ctx, gameID, []ports.PlayerObservedStats{
{UserID: "user-a", Planets: 5, Population: 80, ShipsBuilt: 9},
}))
require.NoError(t, store.UpdateMax(ctx, gameID, []ports.PlayerObservedStats{
{UserID: "user-a", Planets: 4, Population: 60, ShipsBuilt: 8},
}))
aggregate, err := store.Load(ctx, gameID)
require.NoError(t, err)
require.Len(t, aggregate.Players, 1)
assert.Equal(t, int64(3), aggregate.Players[0].InitialPlanets)
assert.Equal(t, int64(100), aggregate.Players[0].InitialPopulation)
assert.Equal(t, int64(7), aggregate.Players[0].InitialShipsBuilt)
assert.Equal(t, int64(5), aggregate.Players[0].MaxPlanets)
assert.Equal(t, int64(100), aggregate.Players[0].MaxPopulation)
assert.Equal(t, int64(9), aggregate.Players[0].MaxShipsBuilt)
}
func TestGameTurnStatsStoreUpdateMaxBeforeSaveInitial(t *testing.T) {
store, _ := newGameTurnStatsStore(t)
ctx := context.Background()
gameID := common.GameID("game-stats-3")
require.NoError(t, store.UpdateMax(ctx, gameID, []ports.PlayerObservedStats{
{UserID: "user-a", Planets: 4, Population: 50, ShipsBuilt: 1},
}))
require.NoError(t, store.SaveInitial(ctx, gameID, []ports.PlayerInitialStats{
{UserID: "user-a", Planets: 99, Population: 99, ShipsBuilt: 99},
}))
aggregate, err := store.Load(ctx, gameID)
require.NoError(t, err)
require.Len(t, aggregate.Players, 1)
assert.Equal(t, int64(4), aggregate.Players[0].InitialPlanets)
assert.Equal(t, int64(50), aggregate.Players[0].InitialPopulation)
assert.Equal(t, int64(1), aggregate.Players[0].InitialShipsBuilt)
assert.Equal(t, int64(4), aggregate.Players[0].MaxPlanets)
assert.Equal(t, int64(50), aggregate.Players[0].MaxPopulation)
assert.Equal(t, int64(1), aggregate.Players[0].MaxShipsBuilt)
}
func TestGameTurnStatsStoreLoadEmpty(t *testing.T) {
store, _ := newGameTurnStatsStore(t)
gameID := common.GameID("game-stats-empty")
aggregate, err := store.Load(context.Background(), gameID)
require.NoError(t, err)
assert.Equal(t, gameID, aggregate.GameID)
assert.Empty(t, aggregate.Players)
}
func TestGameTurnStatsStoreLoadSortsByUserID(t *testing.T) {
store, _ := newGameTurnStatsStore(t)
ctx := context.Background()
gameID := common.GameID("game-stats-sorted")
require.NoError(t, store.SaveInitial(ctx, gameID, []ports.PlayerInitialStats{
{UserID: "user-c", Planets: 1, Population: 1, ShipsBuilt: 1},
{UserID: "user-a", Planets: 2, Population: 2, ShipsBuilt: 2},
{UserID: "user-b", Planets: 3, Population: 3, ShipsBuilt: 3},
}))
aggregate, err := store.Load(ctx, gameID)
require.NoError(t, err)
require.Len(t, aggregate.Players, 3)
got := []string{aggregate.Players[0].UserID, aggregate.Players[1].UserID, aggregate.Players[2].UserID}
expected := []string{"user-a", "user-b", "user-c"}
require.True(t, sort.StringsAreSorted(got))
assert.Equal(t, expected, got)
}
func TestGameTurnStatsStoreDeleteRemovesEverything(t *testing.T) {
store, server := newGameTurnStatsStore(t)
ctx := context.Background()
gameID := common.GameID("game-stats-del")
require.NoError(t, store.SaveInitial(ctx, gameID, []ports.PlayerInitialStats{
{UserID: "user-a", Planets: 1, Population: 1, ShipsBuilt: 1},
{UserID: "user-b", Planets: 2, Population: 2, ShipsBuilt: 2},
}))
require.NoError(t, store.Delete(ctx, gameID))
aggregate, err := store.Load(ctx, gameID)
require.NoError(t, err)
assert.Empty(t, aggregate.Players)
keyspace := redisstate.Keyspace{}
assert.False(t, server.Exists(keyspace.GameTurnStatsByGame(gameID)))
assert.False(t, server.Exists(keyspace.GameTurnStat(gameID, "user-a")))
assert.False(t, server.Exists(keyspace.GameTurnStat(gameID, "user-b")))
}
func TestGameTurnStatsStoreDeleteIsIdempotent(t *testing.T) {
store, _ := newGameTurnStatsStore(t)
ctx := context.Background()
gameID := common.GameID("game-stats-del-noop")
require.NoError(t, store.Delete(ctx, gameID))
require.NoError(t, store.Delete(ctx, gameID))
}
func TestGameTurnStatsStoreRejectsInvalidInputs(t *testing.T) {
store, _ := newGameTurnStatsStore(t)
ctx := context.Background()
gameID := common.GameID("game-stats-bad")
err := store.SaveInitial(ctx, gameID, []ports.PlayerInitialStats{
{UserID: "", Planets: 1, Population: 1, ShipsBuilt: 1},
})
assert.Error(t, err)
err = store.UpdateMax(ctx, gameID, []ports.PlayerObservedStats{
{UserID: "user-a", Planets: -1, Population: 1, ShipsBuilt: 1},
})
assert.Error(t, err)
_, err = store.Load(ctx, common.GameID(""))
assert.Error(t, err)
}