15d35f6f1f
Engine no longer mints its own game UUID. The orchestrator (backend)
generates the game UUID at game-create time and passes it in the
admin/init request body as the required `gameId` field, so the value
that names the engine container and host bind-mount directory also
ends up inside the engine's state.json.
The engine rejects the zero UUID with 400 and any init that conflicts
with an existing state.json with 409 (a second init on the same gameId
is also a conflict; full idempotency is not part of the contract).
Updates rest.InitRequest, openapi.yaml (schema + 409 response),
controller.GenerateGame/NewGame/buildGameOnMap signatures, the engine
HTTP handler/executor, the backend runtime worker, and the relevant
unit and contract tests. Documentation in game/README.md,
docs/ARCHITECTURE.md, backend/README.md, and backend/docs/{runtime,flows}.md
is updated in the same patch.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
154 lines
4.1 KiB
Go
154 lines
4.1 KiB
Go
package controller
|
|
|
|
import (
|
|
"fmt"
|
|
"math/rand/v2"
|
|
"slices"
|
|
|
|
"galaxy/game/internal/generator"
|
|
"galaxy/game/internal/model/game"
|
|
|
|
"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, 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, 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,
|
|
}
|
|
}
|