Files
galaxy-game/game/internal/controller/generate_game.go
T
Ilia Denisov 601970b028
Tests · Go / test (push) Successful in 2m27s
Tests · UI / test (push) Waiting to run
Tests · Integration / integration (pull_request) Successful in 1m45s
Tests · Go / test (pull_request) Successful in 3m13s
Tests · UI / test (pull_request) Successful in 3m8s
refactor(game): lock-free storage, remove /command, flatten engine wrapper
Three-stage refactor of the game-engine plumbing (game logic untouched):

Stage 1 — lock-free persistence + admin serialisation. Remove the file
lock from repo/fs (the .lock file, the Read/Write-vs-*Safe duality and the
dead ReadSafe polling) and replace the two-step rename with a single atomic
rename so concurrent reads are torn-free without a lock. Serialise the
state-mutating admin writers (init/turn/banish) with one shared router
LimitMiddleware, rewritten to block on the request context instead of a
racy shared 100ms timer.

Stage 2 — remove the obsolete immediate-command path end to end. Players
submit through PUT /api/v1/order; the legacy PUT /api/v1/command path is
deleted across game (route, handler, 24 command factories, Ctrl), backend
(Commands handler/route, engineclient.ExecuteCommands), gateway (dispatch +
executeUserGamesCommand + routing entry), the FlatBuffers/model contract
(UserGamesCommand[Response]) and transcoder, plus every affected
OpenAPI/README/FUNCTIONAL/ARCHITECTURE doc. The integration proxy test is
converted to the order path.

Stage 3 — flatten the REST->engine wrapper. Replace the executor adapter,
the controller package functions and RepoController with one concrete
controller.Service; drop the single-implementation Repo and Storage
interfaces (repo.Repo / fs.FS are now concrete). Handlers depend on a thin
handler.Engine seam and own the domain->REST projection; storage is
resolved once at startup instead of per request.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-30 13:37:07 +02:00

155 lines
4.1 KiB
Go

package controller
import (
"fmt"
"math/rand/v2"
"slices"
"galaxy/game/internal/generator"
"galaxy/game/internal/model/game"
"galaxy/game/internal/repo"
"github.com/google/uuid"
)
// NewGame initialises a fresh game in storage under the supplied
// gameID. The caller is expected to have validated gameID against
// uuid.Nil and to have ruled out collisions with existing state.
func NewGame(r *repo.Repo, gameID uuid.UUID, races []string) (uuid.UUID, error) {
m, err := generator.Generate(func(ms *generator.MapSetting) {
ms.Players = uint32(len(races))
})
if err != nil {
return uuid.Nil, fmt.Errorf("generate map: %s", err)
}
return newGameOnMap(r, gameID, races, m)
}
func newGameOnMap(r *repo.Repo, gameID uuid.UUID, races []string, m generator.Map) (uuid.UUID, error) {
g, err := buildGameOnMap(gameID, races, m)
if err != nil {
return uuid.Nil, err
}
if err := r.SaveNewTurn(0, g); err != nil {
return uuid.Nil, err
}
c := NewCache(g)
for rep := range c.Report(c.g.Turn, nil, nil) {
if err := r.SaveReport(c.g.Turn, rep); err != nil {
return uuid.Nil, err
}
}
return g.ID, nil
}
func buildGameOnMap(gameID uuid.UUID, races []string, m generator.Map) (*game.Game, error) {
if len(races) != len(m.HomePlanets) {
return nil, fmt.Errorf("generate map: wrong number of home planets: %d, expected: %d ", len(m.HomePlanets), len(races))
}
g := &game.Game{
ID: gameID,
Turn: 0,
Race: make([]game.Race, len(races)),
}
gameMap := &game.Map{
Width: m.Width,
Height: m.Height,
Planet: make([]game.Planet, 0),
}
var planetCount uint = 0
relations := make([]game.RaceRelation, len(races))
for i := range races {
raceID, err := uuid.NewRandom()
if err != nil {
return nil, fmt.Errorf("generate race uuid: %s", err)
}
relations[i] = game.RaceRelation{RaceID: raceID, Relation: game.RelationWar}
g.Race[i] = game.Race{
ID: raceID,
Name: races[i],
VoteFor: raceID,
TTL: 10,
Tech: game.NewTechSet(),
}
gameMap.Planet = append(gameMap.Planet, NewPlanet(
planetCount,
m.HomePlanets[i].HW.RandomName(),
&raceID,
m.HomePlanets[i].HW.Position.X,
m.HomePlanets[i].HW.Position.Y,
m.HomePlanets[i].HW.Size,
m.HomePlanets[i].HW.Size, // HW's pop & ind = size
m.HomePlanets[i].HW.Size,
m.HomePlanets[i].HW.Resources,
game.ResearchDrive.AsType(uuid.Nil),
))
planetCount++
for dw := range m.HomePlanets[i].DW {
gameMap.Planet = append(gameMap.Planet, NewPlanet(
planetCount,
m.HomePlanets[i].DW[dw].RandomName(),
&raceID,
m.HomePlanets[i].DW[dw].Position.X,
m.HomePlanets[i].DW[dw].Position.Y,
m.HomePlanets[i].DW[dw].Size,
m.HomePlanets[i].DW[dw].Size, // DW's pop & ind = size
m.HomePlanets[i].DW[dw].Size,
m.HomePlanets[i].DW[dw].Resources,
game.ResearchDrive.AsType(uuid.Nil),
))
planetCount++
}
}
for i := range g.Race {
rel := slices.Clone(relations)
selfIdx := slices.IndexFunc(rel, func(a game.RaceRelation) bool { return a.RaceID == g.Race[i].ID })
g.Race[i].Relations = append(rel[:selfIdx], rel[selfIdx+1:]...)
}
for i := range m.FreePlanets {
gameMap.Planet = append(gameMap.Planet, NewPlanet(
planetCount,
m.FreePlanets[i].RandomName(),
&uuid.Nil,
m.FreePlanets[i].Position.X,
m.FreePlanets[i].Position.Y,
m.FreePlanets[i].Size,
0,
0,
m.FreePlanets[i].Resources,
game.ProductionNone.AsType(uuid.Nil),
))
planetCount++
}
rand.Shuffle(len(gameMap.Planet), func(i, j int) {
gameMap.Planet[i].Number, gameMap.Planet[j].Number = gameMap.Planet[j].Number, gameMap.Planet[i].Number
})
for i := range gameMap.Planet {
g.Votes = g.Votes.Add(gameMap.Planet[i].Votes())
}
g.Map = *gameMap
return g, nil
}
func NewPlanet(num uint, name string, owner *uuid.UUID, x, y, size, pop, ind, res float64, prod game.Production) game.Planet {
if owner != nil && *owner == uuid.Nil {
owner = nil
}
return game.Planet{
Owner: owner,
X: game.F(x),
Y: game.F(y),
Number: num,
Size: game.F(size),
Name: name,
Resources: game.F(res),
Population: game.F(pop),
Industry: game.F(ind),
Production: prod,
}
}