Files
galaxy-game/game/internal/controller/controller_test.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

151 lines
4.1 KiB
Go

package controller_test
import (
"fmt"
"galaxy/game/internal/controller"
"galaxy/game/internal/model/game"
"github.com/google/uuid"
)
var (
Race_0 = game.Race{
ID: Race_0_ID,
VoteFor: Race_0_ID,
Name: "Race_0",
TTL: 10,
Tech: map[game.Tech]game.Float{
game.TechDrive: 1.1,
game.TechWeapons: 1.2,
game.TechShields: 1.3,
game.TechCargo: 1.4,
},
Relations: []game.RaceRelation{
{RaceID: Race_1_ID, Relation: game.RelationWar},
{RaceID: Race_2_ID, Relation: game.RelationWar},
},
}
Race_1 = game.Race{
ID: Race_1_ID,
VoteFor: Race_1_ID,
Name: "Race_1",
TTL: 10,
Tech: map[game.Tech]game.Float{
game.TechDrive: 2.1,
game.TechWeapons: 2.2,
game.TechShields: 2.3,
game.TechCargo: 2.4,
},
Relations: []game.RaceRelation{
{RaceID: Race_0_ID, Relation: game.RelationPeace},
{RaceID: Race_2_ID, Relation: game.RelationPeace},
},
}
Race_Extinct = game.Race{
ID: Race_2_ID,
VoteFor: Race_2_ID,
Name: "Race_Extinct",
Extinct: true,
TTL: 0,
Tech: map[game.Tech]game.Float{
game.TechDrive: 3.1,
game.TechWeapons: 3.2,
game.TechShields: 3.3,
game.TechCargo: 3.4,
},
Relations: []game.RaceRelation{
{RaceID: Race_0_ID, Relation: game.RelationPeace},
{RaceID: Race_1_ID, Relation: game.RelationWar},
},
}
Race_0_ID = uuid.New()
Race_0_idx = 0
Race_0_Gunship = "R0_Gunship"
Race_0_Freighter = "R0_Freighter"
R0_Planet_0_num uint = 0
R0_Planet_2_num uint = 2
Race_0_Gunship_idx = 0
Race_0_Freighter_idx = 1
Race_0_Cruiser_idx = 2
Race_1_ID = uuid.New()
Race_1_idx = 1
Race_1_Gunship = "R1_Gunship"
Race_1_Freighter = "R1_Freighter"
R1_Planet_1_num uint = 1
Race_1_Gunship_idx = 0
Race_1_Freighter_idx = 1
Race_1_Cruiser_idx = 2
Race_2_ID = uuid.New()
Uninhabited_Planet_3_num uint = 3
Uninhabited_Planet_4_num uint = 4
ShipType_Cruiser = "Cruiser"
Cruiser = game.ShipType{
Name: "Cruiser",
Drive: 15,
Armament: 1,
Weapons: 15,
Shields: 15,
Cargo: 0,
}
BadEntityName = "_Bad_entitty_Name"
UnknownRace = "UnknownRace"
InSpace = game.InSpace{Origin: 2, X: floatRef(1.23), Y: floatRef(1.23)}
)
func assertNoError(err error) {
if err != nil {
panic(fmt.Sprintf("init assertion failed: %v", err))
}
}
func newGame() *game.Game {
g := &game.Game{
Race: []game.Race{
Race_0,
Race_1,
Race_Extinct,
},
Map: game.Map{
Width: 1000,
Height: 1000,
Planet: []game.Planet{
controller.NewPlanet(R0_Planet_0_num, "Planet_0", &Race_0.ID, 1, 1, 100, 100, 100, 0, game.ProductionCapital.AsType(uuid.Nil)),
controller.NewPlanet(R1_Planet_1_num, "Planet_1", &Race_1.ID, 2, 2, 100, 0, 0, 0, game.ProductionCapital.AsType(uuid.Nil)),
controller.NewPlanet(R0_Planet_2_num, "Planet_2", &Race_0.ID, 3, 3, 100, 0, 0, 0, game.ProductionCapital.AsType(uuid.Nil)),
controller.NewPlanet(Uninhabited_Planet_3_num, "Planet_3", &uuid.Nil, 500, 500, 100, 0, 0, 0, game.ProductionNone.AsType(uuid.Nil)),
controller.NewPlanet(Uninhabited_Planet_4_num, "Planet_4", nil, 10, 10, 500, 0, 0, 10, game.ProductionNone.AsType(uuid.Nil)),
},
},
}
return g
}
func newCache() (*controller.Cache, *controller.Controller) {
ctl := &controller.Controller{
Cache: controller.NewCache(newGame()),
}
assertNoError(ctl.Cache.ShipClassCreate(Race_0_idx, Race_0_Gunship, 60, 3, 30, 100, 0))
assertNoError(ctl.Cache.ShipClassCreate(Race_0_idx, Race_0_Freighter, 8, 0, 0, 2, 10))
assertNoError(ctl.Cache.ShipClassCreate(Race_0_idx, ShipType_Cruiser, Cruiser.Drive.F(), int(Cruiser.Armament), Cruiser.Weapons.F(), Cruiser.Shields.F(), Cruiser.Cargo.F()))
assertNoError(ctl.Cache.ShipClassCreate(Race_1_idx, Race_1_Gunship, 60, 3, 30, 100, 0))
assertNoError(ctl.Cache.ShipClassCreate(Race_1_idx, Race_1_Freighter, 8, 0, 0, 2, 10))
assertNoError(ctl.Cache.ShipClassCreate(Race_1_idx, ShipType_Cruiser, 15, 2, 15, 15, 0)) // same name - different type (why.)
return ctl.Cache, ctl
}
func floatRef(v float64) *game.Float {
f := game.Float(v)
return &f
}