diff --git a/internal/controller/battle_test.go b/internal/controller/battle_test.go index 74167f4..a204a4f 100644 --- a/internal/controller/battle_test.go +++ b/internal/controller/battle_test.go @@ -134,11 +134,11 @@ func TestProduceBattles(t *testing.T) { race_C_idx, _ := c.AddRace(race_C_name) race_D_idx, _ := c.AddRace(race_D_name) - assert.NoError(t, g.UpdateRelation(Race_0.Name, Race_1.Name, game.RelationWar)) - assert.NoError(t, g.UpdateRelation(Race_1.Name, Race_0.Name, game.RelationWar)) + assert.NoError(t, g.UpdateRelation(Race_0.Name, Race_1.Name, game.RelationWar.String())) + assert.NoError(t, g.UpdateRelation(Race_1.Name, Race_0.Name, game.RelationWar.String())) - assert.NoError(t, g.UpdateRelation(race_C_name, race_D_name, game.RelationWar)) - assert.NoError(t, g.UpdateRelation(race_D_name, race_C_name, game.RelationWar)) + assert.NoError(t, g.UpdateRelation(race_C_name, race_D_name, game.RelationWar.String())) + assert.NoError(t, g.UpdateRelation(race_D_name, race_C_name, game.RelationWar.String())) assert.Equal(t, game.RelationPeace, c.Relation(Race_0_idx, race_C_idx)) assert.Equal(t, game.RelationPeace, c.Relation(Race_1_idx, race_C_idx)) diff --git a/internal/controller/bombing_test.go b/internal/controller/bombing_test.go index 8450fdb..b24a078 100644 --- a/internal/controller/bombing_test.go +++ b/internal/controller/bombing_test.go @@ -37,8 +37,8 @@ func TestBombPlanet(t *testing.T) { func TestCollectBombingGroups(t *testing.T) { c, g := newCache() - assert.NoError(t, g.UpdateRelation(Race_0.Name, Race_1.Name, game.RelationWar)) - assert.NoError(t, g.UpdateRelation(Race_1.Name, Race_0.Name, game.RelationWar)) + assert.NoError(t, g.UpdateRelation(Race_0.Name, Race_1.Name, game.RelationWar.String())) + assert.NoError(t, g.UpdateRelation(Race_1.Name, Race_0.Name, game.RelationWar.String())) // 1: idx = 0 / Ready to bomb: Race_1/Planet_1 assert.NoError(t, c.CreateShips(Race_0_idx, Race_0_Gunship, R0_Planet_0_num, 2)) // bombs @@ -82,7 +82,7 @@ func TestCollectBombingGroups(t *testing.T) { assert.Equal(t, 1, bg[R0_Planet_2_num][Race_1_idx][0]) // remove bombings from Race_1 - assert.NoError(t, g.UpdateRelation(Race_1.Name, Race_0.Name, game.RelationPeace)) + assert.NoError(t, g.UpdateRelation(Race_1.Name, Race_0.Name, game.RelationPeace.String())) bg = c.CollectBombingGroups() assert.Len(t, bg, 1) assert.Contains(t, bg, R1_Planet_1_num) @@ -95,8 +95,8 @@ func TestCollectBombingGroups(t *testing.T) { func TestProduceBombings(t *testing.T) { c, g := newCache() - assert.NoError(t, g.UpdateRelation(Race_0.Name, Race_1.Name, game.RelationWar)) - assert.NoError(t, g.UpdateRelation(Race_1.Name, Race_0.Name, game.RelationWar)) + assert.NoError(t, g.UpdateRelation(Race_0.Name, Race_1.Name, game.RelationWar.String())) + assert.NoError(t, g.UpdateRelation(Race_1.Name, Race_0.Name, game.RelationWar.String())) // 1: idx = 0 / Bombs on: Race_1/Planet_1 assert.NoError(t, c.CreateShips(Race_0_idx, Race_0_Gunship, R0_Planet_0_num, 3)) diff --git a/internal/controller/command.go b/internal/controller/command.go index d4a48d2..9b20b0a 100644 --- a/internal/controller/command.go +++ b/internal/controller/command.go @@ -38,7 +38,11 @@ func (c Controller) GiveVotes(actor, acceptor string) error { return nil } -func (c Controller) UpdateRelation(actor, acceptor string, rel game.Relation) error { +func (c Controller) UpdateRelation(actor, acceptor string, v string) error { + rel, ok := game.ParseRelation(v) + if !ok { + return e.NewUnknownRelationError(v) + } ri, err := c.Cache.validActor(actor) if err != nil { return err diff --git a/internal/controller/controller.go b/internal/controller/controller.go index 2ee96d0..2092de8 100644 --- a/internal/controller/controller.go +++ b/internal/controller/controller.go @@ -2,7 +2,6 @@ package controller import ( "errors" - "fmt" "github.com/google/uuid" "github.com/iliadenisov/galaxy/internal/model/game" @@ -10,6 +9,8 @@ import ( "github.com/iliadenisov/galaxy/internal/repo" ) +type Configurer func(*Param) + type Repo interface { // Lock must be called before any repository operations Lock() error @@ -42,18 +43,109 @@ type Repo interface { LoadReport(uint, uuid.UUID) (*report.Report, error) } -type Controller struct { - Repo Repo - Cache *Cache +type Ctrl interface { + RaceID(actor string) (uuid.UUID, error) + QuitGame(actor string) error + GiveVotes(actor, acceptor string) error + UpdateRelation(actor, acceptor string, rel string) error + JoinShipGroupToFleet(actor, fleetName string, group, count uint) error + JoinFleets(actor, fleetSourceName, fleetTargetName string) error + SendFleet(actor, fleetName string, planetNumber uint) error + RenamePlanet(actor string, planetNumber int, typeName string) error + PlanetProduction(actor string, planetNumber int, prodType, subject string) error + SetRoute(actor, loadType string, origin, destination uint) error + RemoveRoute(actor, loadType string, origin uint) error + CreateScience(actor, typeName string, drive, weapons, shields, cargo float64) error + DeleteScience(actor, typeName string) error + CreateShipType(actor, typeName string, drive float64, ammo int, weapons, shileds, cargo float64) error + MergeShipType(actor, name, targetName string) error + DeleteShipType(actor, typeName string) error + SendGroup(actor string, groupIndex, planetNumber, quantity uint) error + UpgradeGroup(actor string, groupIndex uint, techInput string, limitShips uint, limitLevel float64) error + JoinEqualGroups(actor string) error + BreakGroup(actor string, groupIndex, quantity uint) error + DisassembleGroup(actor string, groupIndex, quantity uint) error + LoadCargo(actor string, groupIndex uint, cargoType string, ships uint, quantity float64) error + UnloadCargo(actor string, groupIndex uint, ships uint, quantity float64) error + TransferGroup(actor, acceptor string, groupIndex, quantity uint) error } -type Param struct { - StoragePath string +func GenerateGame(configure func(*Param), races []string) (ID uuid.UUID, err error) { + ec, err := NewRepoController(configure) + if err != nil { + return uuid.Nil, err + } + if err = ec.Repo.Lock(); err != nil { + return + } + defer func() { + err = errors.Join(err, ec.Repo.Release()) + }() + + ID, err = NewGame(ec.Repo, races) + return } -type Configurer func(*Param) +func ExecuteCommand(configure func(*Param), consumer func(c Ctrl) error) (err error) { + ec, err := NewRepoController(configure) + if err != nil { + return err + } + return ec.ExecuteCommand(func(c *Controller) error { return consumer(c) }) +} -func NewController(config Configurer) (*Controller, error) { +func GameState(configure func(*Param)) (s game.State, err error) { + ec, err := NewRepoController(configure) + + g, err := ec.Repo.LoadStateSafe() + if err != nil { + return game.State{}, err + } + + result := &game.State{ + ID: g.ID, + Turn: g.Turn, + Stage: g.Stage, + Players: make([]game.PlayerState, len(g.Race)), + } + + for i := range g.Race { + r := &g.Race[i] + result.Players[i].ID = r.ID + result.Players[i].Name = r.Name + result.Players[i].Extinct = r.Extinct + } + + return *result, nil +} + +type RepoController struct { + Repo Repo +} + +func (ec *RepoController) ExecuteCommand(consumer func(c *Controller) error) (err error) { + if err := ec.Repo.Lock(); err != nil { + return err + } + defer func() { + err = errors.Join(err, ec.Repo.Release()) + }() + + g, err := ec.Repo.LoadState() + if err != nil { + return err + } + + err = consumer(NewGameController(g)) + + if err == nil { + g.Stage += 1 + ec.Repo.SaveLastState(g) + } + return +} + +func NewRepoController(config Configurer) (*RepoController, error) { c := &Param{ StoragePath: ".", } @@ -64,44 +156,22 @@ func NewController(config Configurer) (*Controller, error) { if err != nil { return nil, err } - return &Controller{ + return &RepoController{ Repo: r, }, nil } -func NewRepoController(r Repo) *Controller { +func NewGameController(g *game.Game) *Controller { return &Controller{ - Repo: r, + Cache: NewCache(g), } } -func (c *Controller) ExecuteState(consumer func(Repo) error) (err error) { - if err := c.Repo.Lock(); err != nil { - return fmt.Errorf("execute: lock failed: %s", err) - } - defer func() { - err = errors.Join(err, c.Repo.Release()) - }() - err = consumer(c.Repo) - return +type Controller struct { + Repo Repo + Cache *Cache } -func (c *Controller) ExecuteCommand(consumer func(Repo, *game.Game) error) (err error) { - if err := c.Repo.Lock(); err != nil { - return fmt.Errorf("execute: lock failed: %s", err) - } - g, err := c.Repo.LoadState() - if err != nil { - return err - } - defer func() { - err = errors.Join(err, c.Repo.Release()) - }() - c.Cache = NewCache(g) - err = consumer(c.Repo, g) - if err == nil { - g.Stage += 1 - c.Repo.SaveLastState(g) - } - return +type Param struct { + StoragePath string } diff --git a/internal/controller/controller_test.go b/internal/controller/controller_test.go index a9ea0bb..6c45edf 100644 --- a/internal/controller/controller_test.go +++ b/internal/controller/controller_test.go @@ -129,8 +129,10 @@ func newGame() *game.Game { } func newCache() (*controller.Cache, *controller.Controller) { - g := newGame() - c := controller.NewCache(g) + ctl := controller.NewGameController(newGame()) + // g := newGame() + // c := controller.NewCache(g) + c := ctl.Cache assertNoError(c.CreateShipType(Race_0_idx, Race_0_Gunship, 60, 3, 30, 100, 0)) assertNoError(c.CreateShipType(Race_0_idx, Race_0_Freighter, 8, 0, 0, 2, 10)) assertNoError(c.CreateShipType(Race_0_idx, ShipType_Cruiser, Cruiser.Drive.F(), int(Cruiser.Armament), Cruiser.Weapons.F(), Cruiser.Shields.F(), Cruiser.Cargo.F())) @@ -139,8 +141,8 @@ func newCache() (*controller.Cache, *controller.Controller) { assertNoError(c.CreateShipType(Race_1_idx, Race_1_Freighter, 8, 0, 0, 2, 10)) assertNoError(c.CreateShipType(Race_1_idx, ShipType_Cruiser, 15, 2, 15, 15, 0)) // same name - different type (why.) - ctl := controller.NewRepoController(nil) - ctl.Cache = c + // ctl := controller.NewRepoController(nil) + // ctl.Cache = c return c, ctl } diff --git a/internal/controller/race_test.go b/internal/controller/race_test.go index 2f21dd9..30ed673 100644 --- a/internal/controller/race_test.go +++ b/internal/controller/race_test.go @@ -35,23 +35,26 @@ func TestGiveVotes(t *testing.T) { func TestRelation(t *testing.T) { c, g := newCache() - assert.NoError(t, g.UpdateRelation(Race_0.Name, Race_1.Name, game.RelationWar)) - assert.NoError(t, g.UpdateRelation(Race_1.Name, Race_0.Name, game.RelationPeace)) + assert.NoError(t, g.UpdateRelation(Race_0.Name, Race_1.Name, "war")) + assert.NoError(t, g.UpdateRelation(Race_1.Name, Race_0.Name, "PEACE")) assert.Equal(t, game.RelationWar, c.Relation(Race_0_idx, Race_1_idx)) assert.Equal(t, game.RelationPeace, c.Relation(Race_1_idx, Race_0_idx)) assert.ErrorContains(t, - g.UpdateRelation(Race_0.Name, UnknownRace, game.RelationWar), + g.UpdateRelation(Race_0.Name, Race_1.Name, "Wojna"), + e.GenericErrorText(e.ErrInputUnknownRelation)) + assert.ErrorContains(t, + g.UpdateRelation(Race_0.Name, UnknownRace, "War"), e.GenericErrorText(e.ErrInputUnknownRace)) assert.ErrorContains(t, - g.UpdateRelation(UnknownRace, Race_0.Name, game.RelationWar), + g.UpdateRelation(UnknownRace, Race_0.Name, "Peace"), e.GenericErrorText(e.ErrInputUnknownRace)) assert.ErrorContains(t, - g.UpdateRelation(Race_0.Name, Race_Extinct.Name, game.RelationWar), + g.UpdateRelation(Race_0.Name, Race_Extinct.Name, "War"), e.GenericErrorText(e.ErrRaceExinct)) assert.ErrorContains(t, - g.UpdateRelation(Race_Extinct.Name, Race_0.Name, game.RelationWar), + g.UpdateRelation(Race_Extinct.Name, Race_0.Name, "War"), e.GenericErrorText(e.ErrRaceExinct)) } diff --git a/internal/controller/ship_class_test.go b/internal/controller/ship_class_test.go index 78670b4..b4b5564 100644 --- a/internal/controller/ship_class_test.go +++ b/internal/controller/ship_class_test.go @@ -2,6 +2,7 @@ package controller_test import ( "slices" + "strconv" "testing" e "github.com/iliadenisov/galaxy/internal/error" @@ -35,6 +36,58 @@ func TestCreateShipClass(t *testing.T) { e.GenericErrorText(e.ErrInputEntityTypeNameInvalid)) } +func TestCreateShipTypeValidation(t *testing.T) { + race := Race_0.Name + typeName := "Drone" + type tc struct { + name string + d, w, s, c float64 + a int + err string + } + table := []tc{ + // correct values + {typeName, 1, 0, 0, 0, 0, ""}, + {typeName, 1.1, 0, 0, 0, 0, ""}, + {typeName, 1, 1.2, 0, 0, 1, ""}, + {typeName, 1, 1.2, 2.5, 0, 1, ""}, + {typeName, 1, 0, 2.5, 7.7, 0, ""}, + // incorrect values... + {"", 1, 0, 0, 0, 0, e.GenericErrorText(e.ErrInputEntityTypeNameInvalid)}, + {" ", 1, 0, 0, 0, 0, e.GenericErrorText(e.ErrInputEntityTypeNameInvalid)}, + {typeName, 0, 0, 0, 0, 0, e.GenericErrorText(e.ErrInputShipTypeZeroValues)}, + // drive + {typeName, -1, 0, 0, 0, 0, e.GenericErrorText(e.ErrInputDriveValue)}, + {typeName, 0.5, 0, 0, 0, 0, e.GenericErrorText(e.ErrInputDriveValue)}, + // weapons + {typeName, 0, -1, 0, 0, 0, e.GenericErrorText(e.ErrInputWeaponsValue)}, + {typeName, 0, 0.5, 0, 0, 0, e.GenericErrorText(e.ErrInputWeaponsValue)}, + // shields + {typeName, 0, 0, -1, 0, 0, e.GenericErrorText(e.ErrInputShieldsValue)}, + {typeName, 0, 0, 0.5, 0, 0, e.GenericErrorText(e.ErrInputShieldsValue)}, + // cargo + {typeName, 0, 0, 0, -1, 0, e.GenericErrorText(e.ErrInputCargoValue)}, + {typeName, 0, 0, 0, 0.5, 0, e.GenericErrorText(e.ErrInputCargoValue)}, + // armament (and weapons) + {typeName, 0, 0, 0, 0, -1, e.GenericErrorText(e.ErrInputShipTypeArmamentValue)}, + {typeName, 0, 1, 0, 0, 0, e.GenericErrorText(e.ErrInputShipTypeWeaponsAndArmamentValue)}, + {typeName, 0, 0, 0, 0, 1, e.GenericErrorText(e.ErrInputShipTypeWeaponsAndArmamentValue)}, + } + for i, tc := range table { + _, g := newCache() + + if tc.err == "" { + err := g.CreateShipType(race, tc.name+strconv.Itoa(i), tc.d, tc.a, tc.w, tc.s, tc.c) + assert.NoError(t, err) + err = g.CreateShipType(race, tc.name+strconv.Itoa(i), tc.d, tc.a, tc.w, tc.s, tc.c) + assert.ErrorContains(t, err, e.GenericErrorText(e.ErrInputEntityTypeNameDuplicate)) + } else { + err := g.CreateShipType(race, tc.name, tc.d, tc.a, tc.w, tc.s, tc.c) + assert.ErrorContains(t, err, tc.err) + } + } +} + func TestMergeShipClass(t *testing.T) { c, g := newCache() diff --git a/internal/error/generic.go b/internal/error/generic.go index fd0ae62..1bd34eb 100644 --- a/internal/error/generic.go +++ b/internal/error/generic.go @@ -33,6 +33,7 @@ const ( const ( ErrInputUnknownRace int = 3000 + iota + ErrInputUnknownRelation ErrInputSameRace ErrInputEntityTypeNameInvalid ErrInputEntityTypeNameDuplicate @@ -78,6 +79,8 @@ func GenericErrorText(code int) string { return "Invalid game state" case ErrInputUnknownRace: return "Race name is unknown to this game" + case ErrInputUnknownRelation: + return "Unknown relation" case ErrInputSameRace: return "Race name must be different from your own" case ErrInputEntityTypeNameInvalid: diff --git a/internal/error/input.go b/internal/error/input.go index 21d65e9..a2125df 100644 --- a/internal/error/input.go +++ b/internal/error/input.go @@ -3,6 +3,11 @@ package error func NewRaceUnknownError(arg ...any) error { return newGenericError(ErrInputUnknownRace, arg...) } + +func NewUnknownRelationError(arg ...any) error { + return newGenericError(ErrInputUnknownRelation, arg...) +} + func NewSameRaceError(arg ...any) error { return newGenericError(ErrInputSameRace, arg...) } diff --git a/internal/game/cmd_group.go b/internal/game/cmd_group.go deleted file mode 100644 index e9aadf7..0000000 --- a/internal/game/cmd_group.go +++ /dev/null @@ -1,19 +0,0 @@ -package game - -import ( - "github.com/iliadenisov/galaxy/internal/controller" - "github.com/iliadenisov/galaxy/internal/model/game" -) - -func JoinEqualGroups(configure func(*controller.Param), race string) (err error) { - err = control(configure, func(c *controller.Controller) error { - return c.ExecuteCommand(func(r controller.Repo, g *game.Game) error { - return joinEqualGroups(c, race) - }) - }) - return -} - -func joinEqualGroups(c *controller.Controller, race string) error { - return c.JoinEqualGroups(race) -} diff --git a/internal/game/cmd_group_test.go b/internal/game/cmd_group_test.go deleted file mode 100644 index d33e158..0000000 --- a/internal/game/cmd_group_test.go +++ /dev/null @@ -1,16 +0,0 @@ -package game_test - -import ( - "testing" - - "github.com/iliadenisov/galaxy/internal/controller" - "github.com/iliadenisov/galaxy/internal/game" - "github.com/stretchr/testify/assert" -) - -func TestJoinEqualGroups(t *testing.T) { - c(t, func(p func(*controller.Param), g func() *controller.Controller) { - err := game.JoinEqualGroups(p, "race_01") - assert.NoError(t, err) - }) -} diff --git a/internal/game/cmd_planet.go b/internal/game/cmd_planet.go deleted file mode 100644 index 659e6a2..0000000 --- a/internal/game/cmd_planet.go +++ /dev/null @@ -1,19 +0,0 @@ -package game - -import ( - "github.com/iliadenisov/galaxy/internal/controller" - "github.com/iliadenisov/galaxy/internal/model/game" -) - -func RenamePlanet(configure func(*controller.Param), race string, number int, name string) (err error) { - err = control(configure, func(c *controller.Controller) error { - return c.ExecuteCommand(func(r controller.Repo, g *game.Game) error { - return renamePlanet(c, race, number, name) - }) - }) - return -} - -func renamePlanet(c *controller.Controller, race string, number int, name string) error { - return c.RenamePlanet(race, number, name) -} diff --git a/internal/game/cmd_planet_test.go b/internal/game/cmd_planet_test.go deleted file mode 100644 index 18e302f..0000000 --- a/internal/game/cmd_planet_test.go +++ /dev/null @@ -1,58 +0,0 @@ -package game_test - -import ( - "slices" - "testing" - - "github.com/iliadenisov/galaxy/internal/controller" - e "github.com/iliadenisov/galaxy/internal/error" - - "github.com/google/uuid" - "github.com/iliadenisov/galaxy/internal/game" - mg "github.com/iliadenisov/galaxy/internal/model/game" - "github.com/stretchr/testify/assert" -) - -func TestRenamePlanet(t *testing.T) { - g(t, func(p func(*controller.Param), g func() *mg.Game) { - cg := g() - var number int - var owner uuid.UUID - for pl := range cg.Map.Planet { - if cg.Map.Planet[pl].Owned() { - number = int(cg.Map.Planet[pl].Number) - owner = *cg.Map.Planet[pl].Owner - break - } - } - var race string - for r := range cg.Race { - if cg.Race[r].ID == owner { - race = cg.Race[r].Name - break - } - } - newName := "Some-New-Name" - err := game.RenamePlanet(p, race, number, newName) - assert.NoError(t, err) - cg = g() - pi := slices.IndexFunc(cg.Map.Planet, func(pl mg.Planet) bool { return pl.OwnedBy(owner) && pl.Number == uint(number) }) - assert.GreaterOrEqual(t, pi, 0) - assert.Equal(t, "Some-New-Name", cg.Map.Planet[pi].Name) - - ri := slices.IndexFunc(cg.Race, func(r mg.Race) bool { return r.Name != race }) - assert.GreaterOrEqual(t, ri, 0) - otherRace := cg.Race[ri].Name - - err = game.RenamePlanet(p, unknownRaceName, number, newName) - assert.ErrorContains(t, err, e.GenericErrorText(e.ErrInputUnknownRace)) - err = game.RenamePlanet(p, race, number, "") - assert.ErrorContains(t, err, e.GenericErrorText(e.ErrInputEntityTypeNameInvalid)) - err = game.RenamePlanet(p, race, -1, newName) - assert.ErrorContains(t, err, e.GenericErrorText(e.ErrInputPlanetNumber)) - err = game.RenamePlanet(p, race, 100500, newName) - assert.ErrorContains(t, err, e.GenericErrorText(e.ErrInputEntityNotExists)) - err = game.RenamePlanet(p, otherRace, number, newName) - assert.ErrorContains(t, err, e.GenericErrorText(e.ErrInputEntityNotOwned)) - }) -} diff --git a/internal/game/cmd_production.go b/internal/game/cmd_production.go deleted file mode 100644 index 49871d2..0000000 --- a/internal/game/cmd_production.go +++ /dev/null @@ -1,19 +0,0 @@ -package game - -import ( - "github.com/iliadenisov/galaxy/internal/controller" - "github.com/iliadenisov/galaxy/internal/model/game" -) - -func PlanetProduction(configure func(*controller.Param), race string, planetNumber int, prodType, subject string) (err error) { - err = control(configure, func(c *controller.Controller) error { - return c.ExecuteCommand(func(r controller.Repo, g *game.Game) error { - return planetProduction(c, race, planetNumber, prodType, subject) - }) - }) - return -} - -func planetProduction(c *controller.Controller, race string, planetNumber int, prodType, subject string) error { - return c.PlanetProduction(race, planetNumber, prodType, subject) -} diff --git a/internal/game/cmd_science.go b/internal/game/cmd_science.go deleted file mode 100644 index 5c3fbd5..0000000 --- a/internal/game/cmd_science.go +++ /dev/null @@ -1,32 +0,0 @@ -package game - -import ( - "github.com/iliadenisov/galaxy/internal/controller" - "github.com/iliadenisov/galaxy/internal/model/game" -) - -func CreateScience(configure func(*controller.Param), race, typeName string, drive, weapons, shields, cargo float64) (err error) { - err = control(configure, func(c *controller.Controller) error { - return c.ExecuteCommand(func(r controller.Repo, g *game.Game) error { - return createScience(c, race, typeName, drive, weapons, shields, cargo) - }) - }) - return -} - -func createScience(c *controller.Controller, race, typeName string, drive, weapons, shields, cargo float64) error { - return c.CreateScience(race, typeName, drive, weapons, shields, cargo) -} - -func DeleteScience(configure func(*controller.Param), race, typeName string) (err error) { - err = control(configure, func(c *controller.Controller) error { - return c.ExecuteCommand(func(r controller.Repo, g *game.Game) error { - return deleteScience(c, race, typeName) - }) - }) - return -} - -func deleteScience(c *controller.Controller, race, typeName string) error { - return c.DeleteScience(race, typeName) -} diff --git a/internal/game/cmd_science_test.go b/internal/game/cmd_science_test.go deleted file mode 100644 index bd74414..0000000 --- a/internal/game/cmd_science_test.go +++ /dev/null @@ -1,70 +0,0 @@ -package game_test - -import ( - "strconv" - "testing" - - "github.com/iliadenisov/galaxy/internal/controller" - "github.com/iliadenisov/galaxy/internal/game" - - e "github.com/iliadenisov/galaxy/internal/error" - "github.com/stretchr/testify/assert" -) - -func TestCreateScience(t *testing.T) { - typeName := "First Step" - c(t, func(p func(*controller.Param), g func() *controller.Controller) { - err := g().CreateScience(unknownRaceName, " "+typeName+" ", 1, 0, 0, 0) - assert.ErrorContains(t, err, e.GenericErrorText(e.ErrInputUnknownRace)) - err = g().DeleteScience(unknownRaceName, typeName) - assert.ErrorContains(t, err, e.GenericErrorText(e.ErrInputUnknownRace)) - }) -} - -func TestCreateScienceValidation(t *testing.T) { - race := "race_01" - typeName := "First_Step" - type tc struct { - name string - d, w, s, c float64 - err string - } - table := []tc{ - // correct values - {typeName, 1, 0, 0, 0, ""}, - {typeName, 0.5, 0.5, 0, 0, ""}, - {typeName, 0.25, 0.25, 0.25, 0.25, ""}, - {typeName, 0.33, 0.33, 0.34, 0, ""}, - {typeName, 0, 0, 0.99, 0.01, ""}, - // incorrect values... - {"", 1, 0, 0, 0, e.GenericErrorText(e.ErrInputEntityTypeNameInvalid)}, - {" ", 1, 0, 0, 0, e.GenericErrorText(e.ErrInputEntityTypeNameInvalid)}, - {typeName, 0, 0, 0, 0, e.GenericErrorText(e.ErrInputScienceSumValues)}, - // drive - {typeName, -1, 0, 0, 0, e.GenericErrorText(e.ErrInputDriveValue)}, - {typeName, -1, 2, 0, 0, e.GenericErrorText(e.ErrInputDriveValue)}, - // weapons - {typeName, 0, -1, 0, 0, e.GenericErrorText(e.ErrInputWeaponsValue)}, - {typeName, 2, -1, 0, 0, e.GenericErrorText(e.ErrInputWeaponsValue)}, - // shields - {typeName, 0, 0, -1, 0, e.GenericErrorText(e.ErrInputShieldsValue)}, - {typeName, 0.5, 0.5, -1, 0.5, e.GenericErrorText(e.ErrInputShieldsValue)}, - // cargo - {typeName, 0, 0, 0, -1, e.GenericErrorText(e.ErrInputCargoValue)}, - {typeName, 0, 1, 1, -1, e.GenericErrorText(e.ErrInputCargoValue)}, - } - c(t, func(p func(*controller.Param), ctrl func() *controller.Controller) { - for i, tc := range table { - if tc.err == "" { - n := tc.name + strconv.Itoa(i) - err := game.CreateScience(p, race, n, tc.d, tc.w, tc.s, tc.c) - assert.NoError(t, err, "for name=%q", n) - err = game.CreateScience(p, race, n, tc.d, tc.w, tc.s, tc.c) - assert.ErrorContains(t, err, e.GenericErrorText(e.ErrInputEntityTypeNameDuplicate), "for name=%q", n) - } else { - err := game.CreateScience(p, race, tc.name, tc.d, tc.w, tc.s, tc.c) - assert.ErrorContains(t, err, tc.err) - } - } - }) -} diff --git a/internal/game/cmd_ship_type.go b/internal/game/cmd_ship_type.go deleted file mode 100644 index aef6791..0000000 --- a/internal/game/cmd_ship_type.go +++ /dev/null @@ -1,54 +0,0 @@ -package game - -import ( - "github.com/iliadenisov/galaxy/internal/controller" - "github.com/iliadenisov/galaxy/internal/model/game" -) - -func CreateShipType(configure func(*controller.Param), race, typeName string, drive float64, ammo int, weapons, shields, cargo float64) (err error) { - err = control(configure, func(c *controller.Controller) error { - return c.ExecuteCommand(func(r controller.Repo, g *game.Game) error { - return createShipType(c, race, typeName, drive, ammo, weapons, shields, cargo) - }) - }) - return -} - -func createShipType(c *controller.Controller, race, typeName string, drive float64, ammo int, weapons, shields, cargo float64) error { - if err := c.CreateShipType(race, typeName, drive, ammo, weapons, shields, cargo); err != nil { - return err - } - return nil // r.SaveLastState(g) -} - -func MergeShipType(configure func(*controller.Param), race, source, target string) (err error) { - err = control(configure, func(c *controller.Controller) error { - return c.ExecuteCommand(func(r controller.Repo, g *game.Game) error { - return mergeShipType(c, race, source, target) - }) - }) - return -} - -func mergeShipType(c *controller.Controller, race, source, target string) error { - if err := c.MergeShipType(race, source, target); err != nil { - return err - } - return nil // r.SaveLastState(g) -} - -func DeleteShipType(configure func(*controller.Param), race, typeName string) (err error) { - err = control(configure, func(c *controller.Controller) error { - return c.ExecuteCommand(func(r controller.Repo, g *game.Game) error { - return deleteShipType(c, race, typeName) - }) - }) - return -} - -func deleteShipType(c *controller.Controller, race, typeName string) error { - if err := c.DeleteShipType(race, typeName); err != nil { - return err - } - return nil // r.SaveLastState(g) -} diff --git a/internal/game/cmd_ship_type_test.go b/internal/game/cmd_ship_type_test.go deleted file mode 100644 index a8c7e2c..0000000 --- a/internal/game/cmd_ship_type_test.go +++ /dev/null @@ -1,114 +0,0 @@ -package game_test - -import ( - "strconv" - "testing" - - "github.com/iliadenisov/galaxy/internal/controller" - - e "github.com/iliadenisov/galaxy/internal/error" - "github.com/iliadenisov/galaxy/internal/game" - mg "github.com/iliadenisov/galaxy/internal/model/game" - "github.com/stretchr/testify/assert" -) - -func TestCreateShipType(t *testing.T) { - race := "race_01" - typeName := "Drone" - c(t, func(p func(*controller.Param), ctrl func() *controller.Controller) { - err := game.DeleteShipType(p, race, typeName) - assert.ErrorContains(t, err, e.GenericErrorText(e.ErrInputEntityNotExists)) - err = game.CreateShipType(p, unknownRaceName, " "+typeName+" ", 1, 0, 0, 0, 0) - assert.ErrorContains(t, err, e.GenericErrorText(e.ErrInputUnknownRace)) - err = game.CreateShipType(p, race, " "+typeName+" ", 1, 0, 0, 0, 0) - assert.NoError(t, err) - st := ctrl().Cache.ShipTypes(1) - assert.Len(t, st, 1) - assert.Equal(t, st[0].Name, typeName) - assert.Equal(t, st[0].Drive.F(), 1.) - assert.Equal(t, st[0].Weapons.F(), 0.) - assert.Equal(t, st[0].Shields.F(), 0.) - assert.Equal(t, st[0].Cargo.F(), 0.) - assert.Equal(t, st[0].Armament, uint(0)) - err = game.DeleteShipType(p, unknownRaceName, typeName) - assert.ErrorContains(t, err, e.GenericErrorText(e.ErrInputUnknownRace)) - err = game.DeleteShipType(p, race, typeName) - assert.NoError(t, err) - st = ctrl().Cache.ShipTypes(1) - assert.Len(t, st, 0) - }) -} - -func TestCreateShipTypeValidation(t *testing.T) { - race := "race_01" - typeName := "Drone" - type tc struct { - name string - d, w, s, c float64 - a int - err string - } - table := []tc{ - // correct values - {typeName, 1, 0, 0, 0, 0, ""}, - {typeName, 1.1, 0, 0, 0, 0, ""}, - {typeName, 1, 1.2, 0, 0, 1, ""}, - {typeName, 1, 1.2, 2.5, 0, 1, ""}, - {typeName, 1, 0, 2.5, 7.7, 0, ""}, - // incorrect values... - {"", 1, 0, 0, 0, 0, e.GenericErrorText(e.ErrInputEntityTypeNameInvalid)}, - {" ", 1, 0, 0, 0, 0, e.GenericErrorText(e.ErrInputEntityTypeNameInvalid)}, - {typeName, 0, 0, 0, 0, 0, e.GenericErrorText(e.ErrInputShipTypeZeroValues)}, - // drive - {typeName, -1, 0, 0, 0, 0, e.GenericErrorText(e.ErrInputDriveValue)}, - {typeName, 0.5, 0, 0, 0, 0, e.GenericErrorText(e.ErrInputDriveValue)}, - // weapons - {typeName, 0, -1, 0, 0, 0, e.GenericErrorText(e.ErrInputWeaponsValue)}, - {typeName, 0, 0.5, 0, 0, 0, e.GenericErrorText(e.ErrInputWeaponsValue)}, - // shields - {typeName, 0, 0, -1, 0, 0, e.GenericErrorText(e.ErrInputShieldsValue)}, - {typeName, 0, 0, 0.5, 0, 0, e.GenericErrorText(e.ErrInputShieldsValue)}, - // cargo - {typeName, 0, 0, 0, -1, 0, e.GenericErrorText(e.ErrInputCargoValue)}, - {typeName, 0, 0, 0, 0.5, 0, e.GenericErrorText(e.ErrInputCargoValue)}, - // armament (and weapons) - {typeName, 0, 0, 0, 0, -1, e.GenericErrorText(e.ErrInputShipTypeArmamentValue)}, - {typeName, 0, 1, 0, 0, 0, e.GenericErrorText(e.ErrInputShipTypeWeaponsAndArmamentValue)}, - {typeName, 0, 0, 0, 0, 1, e.GenericErrorText(e.ErrInputShipTypeWeaponsAndArmamentValue)}, - } - g(t, func(p func(*controller.Param), g func() *mg.Game) { - for i, tc := range table { - if tc.err == "" { - err := game.CreateShipType(p, race, tc.name+strconv.Itoa(i), tc.d, tc.a, tc.w, tc.s, tc.c) - assert.NoError(t, err) - err = game.CreateShipType(p, race, tc.name+strconv.Itoa(i), tc.d, tc.a, tc.w, tc.s, tc.c) - assert.ErrorContains(t, err, e.GenericErrorText(e.ErrInputEntityTypeNameDuplicate)) - } else { - err := game.CreateShipType(p, race, tc.name, tc.d, tc.a, tc.w, tc.s, tc.c) - assert.ErrorContains(t, err, tc.err) - } - } - }) -} - -func TestMergeShipType(t *testing.T) { - race := "race_01" - c(t, func(p func(*controller.Param), ctrl func() *controller.Controller) { - err := game.CreateShipType(p, race, "Drone", 1, 0, 0, 0, 0) - assert.NoError(t, err) - err = game.CreateShipType(p, race, "Spy", 1, 0, 0, 0, 0) - assert.NoError(t, err) - err = game.CreateShipType(p, race, "Cruiser", 15, 15, 15, 0, 1) - assert.NoError(t, err) - err = game.MergeShipType(p, race, "Sky", "Drone") - assert.ErrorContains(t, err, e.GenericErrorText(e.ErrInputEntityNotExists)) - err = game.MergeShipType(p, race, "Spy", "Freighter") - assert.ErrorContains(t, err, e.GenericErrorText(e.ErrInputEntityNotExists)) - err = game.MergeShipType(p, race, "Spy", "Drone") - assert.NoError(t, err) - st := ctrl().Cache.ShipTypes(1) - assert.Len(t, st, 2) - err = game.MergeShipType(p, race, "Drone", "Cruiser") - assert.ErrorContains(t, err, e.GenericErrorText(e.ErrMergeShipTypeNotEqual)) - }) -} diff --git a/internal/game/cmd_turn.go b/internal/game/cmd_turn.go deleted file mode 100644 index 420518d..0000000 --- a/internal/game/cmd_turn.go +++ /dev/null @@ -1,19 +0,0 @@ -package game - -import ( - "github.com/iliadenisov/galaxy/internal/controller" -) - -func GenerateTurn(configure func(*controller.Param)) (err error) { - err = control(configure, func(c *controller.Controller) error { - return c.ExecuteState(func(r controller.Repo) error { - g, err := r.LoadState() - if err != nil { - return err - } - c.Cache = controller.NewCache(g) - return controller.MakeTurn(c, r) - }) - }) - return -} diff --git a/internal/game/cmd_war_peace.go b/internal/game/cmd_war_peace.go deleted file mode 100644 index 8da79a3..0000000 --- a/internal/game/cmd_war_peace.go +++ /dev/null @@ -1,31 +0,0 @@ -package game - -import ( - "github.com/iliadenisov/galaxy/internal/controller" - "github.com/iliadenisov/galaxy/internal/model/game" -) - -func DeclareWar(configure func(*controller.Param), from, to string) (err error) { - err = control(configure, func(c *controller.Controller) error { - return c.ExecuteCommand(func(r controller.Repo, g *game.Game) error { - return updateRelation(c, from, to, game.RelationWar) - }) - }) - return -} - -func DeclarePeace(configure func(*controller.Param), from, to string) (err error) { - err = control(configure, func(c *controller.Controller) error { - return c.ExecuteCommand(func(r controller.Repo, g *game.Game) error { - return updateRelation(c, from, to, game.RelationPeace) - }) - }) - return -} - -func updateRelation(c *controller.Controller, hostRace, opponentRace string, rel game.Relation) error { - if err := c.UpdateRelation(hostRace, opponentRace, rel); err != nil { - return err - } - return nil // r.SaveLastState(g) -} diff --git a/internal/game/cmd_war_peace_test.go b/internal/game/cmd_war_peace_test.go deleted file mode 100644 index bc58cff..0000000 --- a/internal/game/cmd_war_peace_test.go +++ /dev/null @@ -1,59 +0,0 @@ -package game_test - -import ( - "testing" - - "github.com/iliadenisov/galaxy/internal/controller" - - "github.com/iliadenisov/galaxy/internal/game" - mg "github.com/iliadenisov/galaxy/internal/model/game" - "github.com/stretchr/testify/assert" -) - -func TestDeclarePeaceAndWarSingle(t *testing.T) { - c(t, func(f func(*controller.Param), ctrl func() *controller.Controller) { - hostRace := 5 - opponentRace := 1 - - assert.Equal(t, mg.RelationWar, ctrl().Cache.Relation(5, 1)) - - assert.NoError(t, game.DeclarePeace(f, raceNum(hostRace), raceNum(opponentRace))) - assert.Equal(t, mg.RelationPeace, ctrl().Cache.Relation(hostRace, opponentRace)) - - assert.NoError(t, game.DeclareWar(f, raceNum(hostRace), raceNum(opponentRace))) - assert.Equal(t, mg.RelationWar, ctrl().Cache.Relation(hostRace, opponentRace)) - }) -} - -func TestDeclarePeaceAndWarAll(t *testing.T) { - c(t, func(f func(*controller.Param), ctrl func() *controller.Controller) { - hostRace := 7 - - for i := range testRaceCount { - if i == hostRace { - continue - } - assert.Equal(t, mg.RelationWar, ctrl().Cache.Relation(hostRace, i)) - } - - assert.NoError(t, game.DeclarePeace(f, raceNum(hostRace), raceNum(hostRace))) - assert.Equal(t, 1, int(ctrl().Cache.Stage())) - - for i := range testRaceCount { - if i == hostRace { - continue - } - assert.Equal(t, mg.RelationPeace, ctrl().Cache.Relation(hostRace, i)) - } - - assert.NoError(t, game.DeclareWar(f, raceNum(hostRace), raceNum(hostRace))) - assert.Equal(t, 2, int(ctrl().Cache.Stage())) - - for i := range testRaceCount { - if i == hostRace { - continue - } - assert.Equal(t, mg.RelationWar, ctrl().Cache.Relation(hostRace, i)) - } - }) -} diff --git a/internal/game/controller.go b/internal/game/controller.go deleted file mode 100644 index 697863e..0000000 --- a/internal/game/controller.go +++ /dev/null @@ -1,52 +0,0 @@ -package game - -import ( - "github.com/google/uuid" - "github.com/iliadenisov/galaxy/internal/controller" - "github.com/iliadenisov/galaxy/internal/model/game" - "github.com/iliadenisov/galaxy/internal/model/report" -) - -func GenerateGame(configure func(*controller.Param), races []string) (gameID uuid.UUID, err error) { - err = control(configure, func(c *controller.Controller) error { - return c.ExecuteState(func(r controller.Repo) error { - gameID, err = controller.NewGame(r, races) - return err - }) - }) - return -} - -// LoadState used for lock-safe loading game state and may be called concurrently. -func LoadState(configure func(*controller.Param)) (g *game.Game, err error) { - err = control(configure, func(c *controller.Controller) error { - g, err = c.Repo.LoadStateSafe() - return err - }) - return -} - -func LoadReport(configure func(*controller.Param), t uint, actor string) (g *report.Report, err error) { - err = control(configure, func(c *controller.Controller) error { - game, err := c.Repo.LoadStateSafe() - if err != nil { - return err - } - c.Cache = controller.NewCache(game) - id, err := c.RaceID(actor) - if err != nil { - return err - } - g, err = c.Repo.LoadReport(t, id) - return err - }) - return -} - -func control(configure func(*controller.Param), consumer func(*controller.Controller) error) (err error) { - c, err := controller.NewController(configure) - if err != nil { - return err - } - return consumer(c) -} diff --git a/internal/game/controller_test.go b/internal/game/controller_test.go deleted file mode 100644 index dd3e70e..0000000 --- a/internal/game/controller_test.go +++ /dev/null @@ -1,84 +0,0 @@ -package game_test - -import ( - "fmt" - "testing" - - "github.com/iliadenisov/galaxy/internal/controller" - - "github.com/iliadenisov/galaxy/internal/game" - mg "github.com/iliadenisov/galaxy/internal/model/game" - "github.com/iliadenisov/galaxy/internal/util" - "github.com/stretchr/testify/assert" -) - -const ( - testRaceCount = 20 - unknownRaceName = "Race_RIP" -) - -func raceNum(i int) string { - return fmt.Sprintf("race_%02d", i) -} - -func TestComposeGame(t *testing.T) { - g(t, func(p func(*controller.Param), g func() *mg.Game) { - _, err := game.GenerateGame(p, []string{"r1", "r2"}) - assert.Error(t, err) - assert.ErrorContains(t, err, "turn 0 already saved at 0000/state.json") - }) -} - -func g(t *testing.T, f func(func(*controller.Param), func() *mg.Game)) { - root, cleanup := util.CreateWorkDir(t) - defer cleanup() - races := make([]string, testRaceCount) - for i := range testRaceCount { - races[i] = raceNum(i) - } - p := func(p *controller.Param) { p.StoragePath = root } - _, err := game.GenerateGame(p, races) - if err != nil { - assert.FailNow(t, "g: ComposeGame", err) - return - } - g := func() *mg.Game { - g, err := game.LoadState(p) - if err != nil { - assert.FailNow(t, "g: LoadState", err) - return nil - } - return g - } - f(p, g) -} - -func c(t *testing.T, f func(func(*controller.Param), func() *controller.Controller)) { - root, cleanup := util.CreateWorkDir(t) - defer cleanup() - races := make([]string, testRaceCount) - for i := range testRaceCount { - races[i] = raceNum(i) - } - p := func(p *controller.Param) { p.StoragePath = root } - _, err := game.GenerateGame(p, races) - if err != nil { - assert.FailNow(t, "c: GenerateGame", err) - return - } - ctrl := func() *controller.Controller { - c, err := controller.NewController(p) - if err != nil { - assert.FailNow(t, "c: NewController", err) - return nil - } - g, err := game.LoadState(p) - if err != nil { - assert.FailNow(t, "c: LoadState", err) - return nil - } - c.Cache = controller.NewCache(g) - return c - } - f(p, ctrl) -} diff --git a/internal/model/game/race.go b/internal/model/game/race.go index bef879a..dafe409 100644 --- a/internal/model/game/race.go +++ b/internal/model/game/race.go @@ -1,6 +1,24 @@ package game -import "github.com/google/uuid" +import ( + "strings" + + "github.com/google/uuid" +) + +type Relation string + +const ( + RelationWar Relation = "War" + RelationPeace Relation = "Peace" +) + +var ( + relationSet = map[string]Relation{ + strings.ToLower(RelationWar.String()): RelationWar, + strings.ToLower(RelationPeace.String()): RelationPeace, + } +) type Race struct { ID uuid.UUID `json:"id"` @@ -15,17 +33,17 @@ type Race struct { ShipTypes []ShipType `json:"shipType,omitempty"` } -type Relation string +func ParseRelation(v string) (Relation, bool) { + if v, ok := relationSet[strings.ToLower(v)]; ok { + return v, ok + } + return Relation(""), false +} func (r Relation) String() string { return string(r) } -const ( - RelationWar Relation = "War" - RelationPeace Relation = "Peace" -) - type RaceRelation struct { RaceID uuid.UUID `json:"raceId"` Relation Relation `json:"relation"` diff --git a/internal/model/game/state.go b/internal/model/game/state.go new file mode 100644 index 0000000..4976b37 --- /dev/null +++ b/internal/model/game/state.go @@ -0,0 +1,16 @@ +package game + +import "github.com/google/uuid" + +type State struct { + ID uuid.UUID + Turn uint + Stage uint + Players []PlayerState +} + +type PlayerState struct { + ID uuid.UUID + Name string + Extinct bool +} diff --git a/internal/model/rest/command.go b/internal/model/rest/command.go index 4ad7d2b..2ba4d69 100644 --- a/internal/model/rest/command.go +++ b/internal/model/rest/command.go @@ -1,26 +1,30 @@ package rest -/* -Full list of requirements must be updated when adding new command: +import "encoding/json" -required_without_all=Vote DeclarePeace DeclareWar -| excluded_with=Vote DeclarePeace DeclareWar -*/ type Command struct { - Race string `json:"race" binding:"required,notblank"` - Vote *CommandVote `json:"vote" binding:"required_without_all=DeclarePeace DeclareWar,excluded_with=DeclarePeace DeclareWar"` - DeclarePeace *CommandDeclarePeace `json:"declarePeace" binding:"required_without_all=Vote DeclareWar,excluded_with=Vote DeclareWar"` - DeclareWar *CommandDeclareWar `json:"declareWar" binding:"required_without_all=Vote DeclarePeace,excluded_with=Vote DeclarePeace"` + Actor string `json:"actor" binding:"required,notblank"` + Commands []json.RawMessage `json:"cmd" binding:"min=1"` +} + +type CommandType string + +const ( + CommandTypeVote CommandType = "vote" + CommandTypeRelation CommandType = "declarePeace" +) + +type CommandMeta struct { + Type CommandType `json:"@type"` } type CommandVote struct { + CommandMeta Recipient string `json:"recipient" binding:"required,notblank"` } -type CommandDeclarePeace struct { - Opponent string `json:"recipient" binding:"required,notblank"` -} - -type CommandDeclareWar struct { +type CommandUpdateRelation struct { + CommandMeta Opponent string `json:"recipient" binding:"required,notblank"` + Relation string `json:"relation" binding:"required,notblank"` } diff --git a/internal/model/rest/status.go b/internal/model/rest/status.go index 4d24b22..c7f9f50 100644 --- a/internal/model/rest/status.go +++ b/internal/model/rest/status.go @@ -1,6 +1,16 @@ package rest -type Status struct { - Turn uint `json:"turn"` - Players int `json:"players"` +import "github.com/google/uuid" + +type StateResponse struct { + ID uuid.UUID `json:"id"` + Turn uint `json:"turn"` + Stage uint `json:"stage"` + Players []PlayerState `json:"player"` +} + +type PlayerState struct { + ID uuid.UUID `json:"id"` + Name string `json:"name"` + Extinct bool `json:"extinct"` } diff --git a/internal/router/command_test.go b/internal/router/command_test.go index e19e9c3..ab3a293 100644 --- a/internal/router/command_test.go +++ b/internal/router/command_test.go @@ -1,22 +1,25 @@ package router_test import ( + "encoding/json" "net/http" "net/http/httptest" "testing" "github.com/iliadenisov/galaxy/internal/model/rest" - "github.com/iliadenisov/galaxy/internal/router" "github.com/stretchr/testify/assert" ) func TestCommand(t *testing.T) { - r := router.SetupRouter() + r := setupRouter() payload := rest.Command{ - Race: "SomeRace", - Vote: &rest.CommandVote{ - Recipient: "AnotherRace", + Actor: "SomeRace", + Commands: []json.RawMessage{ + encodeCommand(&rest.CommandVote{ + CommandMeta: rest.CommandMeta{Type: rest.CommandTypeVote}, + Recipient: "AnotherRace", + }), }, } @@ -24,17 +27,17 @@ func TestCommand(t *testing.T) { req, _ := http.NewRequest("PUT", "/api/v1/command", asBody(payload)) r.ServeHTTP(w, req) - assert.Equal(t, http.StatusOK, w.Code, w.Body) + assert.Equal(t, http.StatusNoContent, w.Code, w.Body) // error: notblank validator - payload.Race = "" + payload.Actor = "" w = httptest.NewRecorder() req, _ = http.NewRequest("PUT", "/api/v1/command", asBody(payload)) r.ServeHTTP(w, req) assert.Equal(t, http.StatusBadRequest, w.Code, w.Body) - payload.Race = " " + payload.Actor = " " w = httptest.NewRecorder() req, _ = http.NewRequest("PUT", "/api/v1/command", asBody(payload)) r.ServeHTTP(w, req) @@ -43,24 +46,7 @@ func TestCommand(t *testing.T) { // error: no commands payload = rest.Command{ - Race: "SomeRace", - } - - w = httptest.NewRecorder() - req, _ = http.NewRequest("PUT", "/api/v1/command", asBody(payload)) - r.ServeHTTP(w, req) - - assert.Equal(t, http.StatusBadRequest, w.Code, w.Body) - - // error: more than one command - payload = rest.Command{ - Race: "SomeRace", - Vote: &rest.CommandVote{ - Recipient: "AnotherRace", - }, - DeclarePeace: &rest.CommandDeclarePeace{ - Opponent: "OpponentRace", - }, + Actor: "SomeRace", } w = httptest.NewRecorder() @@ -69,3 +55,11 @@ func TestCommand(t *testing.T) { assert.Equal(t, http.StatusBadRequest, w.Code, w.Body) } + +func encodeCommand(cmd any) json.RawMessage { + v, err := json.Marshal(cmd) + if err != nil { + panic(err) + } + return v +} diff --git a/internal/router/handler/command.go b/internal/router/handler/command.go index 365dac9..8b1b0db 100644 --- a/internal/router/handler/command.go +++ b/internal/router/handler/command.go @@ -1,47 +1,69 @@ package handler import ( - "errors" + "encoding/json" + "fmt" "net/http" "github.com/iliadenisov/galaxy/internal/controller" - "github.com/iliadenisov/galaxy/internal/game" "github.com/gin-gonic/gin" "github.com/iliadenisov/galaxy/internal/model/rest" ) -type CommandExecutor func(controller.Configurer, rest.Command) error - -var ( - ErrCommandNotProcessed = errors.New("command was not processed by executor") -) - -func CommandHandler(c *gin.Context, configurer controller.Configurer, executor CommandExecutor) { +func CommandHandler(c *gin.Context, executor CommandExecutor) { var cmd rest.Command - if err := c.ShouldBindJSON(&cmd); err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + if errorResponded(c, c.ShouldBindJSON(&cmd)) { return } - err := executor(configurer, cmd) - switch { - case err == nil: - c.Status(http.StatusOK) - case errors.Is(err, ErrCommandNotProcessed): - c.Status(http.StatusInternalServerError) // TODO: add error text? + + commands := make([]Command, 0) + for i := range cmd.Commands { + command, err := parseCommand(cmd.Actor, cmd.Commands[i]) + if errorResponded(c, err) { + return + } + commands = append(commands, command) + } + if len(commands) == 0 { + c.JSON(http.StatusBadRequest, gin.H{"error": "no commands given"}) + return + } + + if errorResponded(c, executor.Execute(commands...)) { + return + } + + c.Status(http.StatusNoContent) +} + +func parseCommand(actor string, c json.RawMessage) (Command, error) { + meta := new(rest.CommandMeta) + if err := json.Unmarshal(c, meta); err != nil { + return nil, err + } + switch t := meta.Type; t { + case rest.CommandTypeVote: + return giveVotes(actor, c) + case rest.CommandTypeRelation: + return updateRelation(actor, c) default: - // TODO: separate invalid input and game state errors - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return nil, fmt.Errorf("unknown comman type: %s", t) } } -func ExecuteCommand(config controller.Configurer, cmd rest.Command) error { - switch { - case cmd.DeclareWar != nil: - return game.DeclareWar(config, cmd.Race, cmd.DeclareWar.Opponent) - case cmd.DeclarePeace != nil: - return game.DeclarePeace(config, cmd.Race, cmd.DeclareWar.Opponent) - default: - return ErrCommandNotProcessed +func giveVotes(actor string, c json.RawMessage) (Command, error) { + var v rest.CommandVote + if err := json.Unmarshal(c, &v); err != nil { + return nil, err } + return func(c controller.Ctrl) error { return c.GiveVotes(actor, v.Recipient) }, nil +} + +func updateRelation(actor string, c json.RawMessage) (Command, error) { + var v rest.CommandUpdateRelation + if err := json.Unmarshal(c, &v); err != nil { + return nil, err + } + return func(c controller.Ctrl) error { return c.UpdateRelation(actor, v.Opponent, v.Relation) }, nil } diff --git a/internal/router/handler/handler.go b/internal/router/handler/handler.go index b6e69a5..e8fd477 100644 --- a/internal/router/handler/handler.go +++ b/internal/router/handler/handler.go @@ -3,18 +3,88 @@ package handler import ( "errors" "net/http" + "os" "github.com/gin-gonic/gin" + "github.com/go-playground/validator/v10" + "github.com/google/uuid" + "github.com/iliadenisov/galaxy/internal/controller" e "github.com/iliadenisov/galaxy/internal/error" + "github.com/iliadenisov/galaxy/internal/model/rest" ) -func transformError(c *gin.Context, err error) bool { +type CommandExecutor interface { + GenerateGame([]string) (uuid.UUID, error) + Execute(cmd ...Command) error + GameState() (rest.StateResponse, error) +} + +type Command func(controller.Ctrl) error + +type executor struct { + cfg controller.Configurer +} + +func initConfig() controller.Configurer { + return func(p *controller.Param) { + p.StoragePath = os.Getenv("STORAGE_PATH") + } +} + +func NewDefaultExecutor() CommandExecutor { + return NewDefaultConfigExecutor(initConfig()) +} + +func NewDefaultConfigExecutor(configurer controller.Configurer) CommandExecutor { + return &executor{cfg: configurer} +} + +func (e *executor) Execute(cmd ...Command) error { + return controller.ExecuteCommand(e.cfg, func(c controller.Ctrl) error { + for i := range cmd { + if err := cmd[i](c); err != nil { + return err + } + } + return nil + }) +} + +func (e *executor) GenerateGame(races []string) (uuid.UUID, error) { + return controller.GenerateGame(e.cfg, races) +} + +func (e *executor) GameState() (rest.StateResponse, error) { + s, err := controller.GameState(e.cfg) + if err != nil { + return rest.StateResponse{}, err + } + result := &rest.StateResponse{ + ID: s.ID, + Turn: s.Turn, + Stage: s.Stage, + Players: make([]rest.PlayerState, len(s.Players)), + } + for i := range s.Players { + result.Players[i].ID = s.Players[i].ID + result.Players[i].Name = s.Players[i].Name + result.Players[i].Extinct = s.Players[i].Extinct + } + return *result, nil +} + +func errorResponded(c *gin.Context, err error) bool { if err == nil { return false } var ge = new(e.GenericError) + if v, ok := err.(validator.ValidationErrors); ok { + c.JSON(http.StatusBadRequest, gin.H{"error": v.Error()}) + return true + } + if errors.As(err, ge) { switch ge.Code { case e.ErrGameNotInitialized: @@ -24,7 +94,6 @@ func transformError(c *gin.Context, err error) bool { } } else { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) - } return true diff --git a/internal/router/handler/init.go b/internal/router/handler/init.go index 4f0c97e..1b46633 100644 --- a/internal/router/handler/init.go +++ b/internal/router/handler/init.go @@ -4,15 +4,12 @@ import ( "net/http" "github.com/gin-gonic/gin" - "github.com/iliadenisov/galaxy/internal/controller" - "github.com/iliadenisov/galaxy/internal/game" "github.com/iliadenisov/galaxy/internal/model/rest" ) -func InitHandler(c *gin.Context, config controller.Configurer) { +func InitHandler(c *gin.Context, executor CommandExecutor) { var init rest.Init - if err := c.ShouldBindJSON(&init); err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + if errorResponded(c, c.ShouldBindJSON(&init)) { return } @@ -21,10 +18,9 @@ func InitHandler(c *gin.Context, config controller.Configurer) { races[i] = init.Races[i].Name } - uuid, err := game.GenerateGame(config, races) - if transformError(c, err) { + uuid, err := executor.GenerateGame(races) + if errorResponded(c, err) { return - } c.JSON(http.StatusCreated, rest.InitResponse{ diff --git a/internal/router/handler/status.go b/internal/router/handler/status.go index fab5ddb..7e05a93 100644 --- a/internal/router/handler/status.go +++ b/internal/router/handler/status.go @@ -4,20 +4,14 @@ import ( "net/http" "github.com/gin-gonic/gin" - "github.com/iliadenisov/galaxy/internal/controller" - "github.com/iliadenisov/galaxy/internal/game" - "github.com/iliadenisov/galaxy/internal/model/rest" ) -func StatusHandler(c *gin.Context, config controller.Configurer) { - g, err := game.LoadState(config) +func StatusHandler(c *gin.Context, executor CommandExecutor) { + state, err := executor.GameState() - if transformError(c, err) { + if errorResponded(c, err) { return } - c.JSON(http.StatusOK, rest.Status{ - Turn: g.Turn, - Players: len(g.Race), - }) + c.JSON(http.StatusOK, state) } diff --git a/internal/router/init_test.go b/internal/router/init_test.go index adb210c..f37a18f 100644 --- a/internal/router/init_test.go +++ b/internal/router/init_test.go @@ -2,7 +2,6 @@ package router_test import ( "encoding/json" - "fmt" "net/http" "net/http/httptest" "testing" @@ -11,6 +10,7 @@ import ( "github.com/iliadenisov/galaxy/internal/controller" "github.com/iliadenisov/galaxy/internal/model/rest" "github.com/iliadenisov/galaxy/internal/router" + "github.com/iliadenisov/galaxy/internal/router/handler" "github.com/iliadenisov/galaxy/internal/util" "github.com/stretchr/testify/assert" ) @@ -19,7 +19,7 @@ func TestInit(t *testing.T) { root, cleanup := util.CreateWorkDir(t) defer cleanup() - r := router.SetupRouterConfig(func(p *controller.Param) { p.StoragePath = root }) + r := router.SetupRouter(handler.NewDefaultConfigExecutor(func(p *controller.Param) { p.StoragePath = root })) payload := generateInitRequest(10) @@ -34,7 +34,7 @@ func TestInit(t *testing.T) { } func TestInitValidators(t *testing.T) { - r := router.SetupRouter() + r := setupRouter() payload := generateInitRequest(9) w := httptest.NewRecorder() @@ -43,13 +43,3 @@ func TestInitValidators(t *testing.T) { assert.Equal(t, http.StatusBadRequest, w.Code, w.Body) } - -func generateInitRequest(races int) rest.Init { - request := rest.Init{ - Races: make([]rest.Race, races), - } - for i := range request.Races { - request.Races[i] = rest.Race{Name: fmt.Sprintf("Race_%02d", i)} - } - return request -} diff --git a/internal/router/router.go b/internal/router/router.go index 4a7e9b7..d56cbaf 100644 --- a/internal/router/router.go +++ b/internal/router/router.go @@ -8,7 +8,6 @@ import ( "github.com/gin-gonic/gin" "github.com/gin-gonic/gin/binding" "github.com/go-playground/validator/v10" - "github.com/iliadenisov/galaxy/internal/controller" "github.com/iliadenisov/galaxy/internal/router/handler" ) @@ -16,12 +15,6 @@ const ( ISO8601 = "2006-01-02 15:04:05.0 -07:00" ) -func initConfig() func(*controller.Param) { - return func(p *controller.Param) { - p.StoragePath = os.Getenv("STORAGE_PATH") - } -} - type Router struct { r *gin.Engine executor handler.CommandExecutor @@ -32,14 +25,14 @@ func (r Router) Run() error { } func NewRouter() Router { - return NewRouterExecutor(handler.ExecuteCommand) + return NewRouterExecutor(handler.NewDefaultExecutor()) } func NewRouterExecutor(executor handler.CommandExecutor) Router { - return Router{r: setupRouter(initConfig(), executor)} + return Router{r: setupRouter(executor)} } -func setupRouter(config controller.Configurer, executor handler.CommandExecutor) *gin.Engine { +func setupRouter(executor handler.CommandExecutor) *gin.Engine { gin.SetMode(gin.ReleaseMode) r := gin.New() @@ -55,9 +48,9 @@ func setupRouter(config controller.Configurer, executor handler.CommandExecutor) groupV1 := r.Group("/api/v1") - groupV1.GET("/status", func(ctx *gin.Context) { handler.StatusHandler(ctx, config) }) - groupV1.POST("/init", func(ctx *gin.Context) { handler.InitHandler(ctx, config) }) - groupV1.PUT("/command", LimitMiddleware(1), func(ctx *gin.Context) { handler.CommandHandler(ctx, config, executor) }) + groupV1.GET("/status", func(ctx *gin.Context) { handler.StatusHandler(ctx, executor) }) + groupV1.POST("/init", func(ctx *gin.Context) { handler.InitHandler(ctx, executor) }) + groupV1.PUT("/command", LimitMiddleware(1), func(ctx *gin.Context) { handler.CommandHandler(ctx, executor) }) return r } diff --git a/internal/router/router_export_test.go b/internal/router/router_export_test.go index 460cd30..bc4ede7 100644 --- a/internal/router/router_export_test.go +++ b/internal/router/router_export_test.go @@ -2,14 +2,9 @@ package router import ( "github.com/gin-gonic/gin" - "github.com/iliadenisov/galaxy/internal/controller" - "github.com/iliadenisov/galaxy/internal/model/rest" + "github.com/iliadenisov/galaxy/internal/router/handler" ) -func SetupRouter() *gin.Engine { - return SetupRouterConfig(nil) -} - -func SetupRouterConfig(config controller.Configurer) *gin.Engine { - return setupRouter(config, func(controller.Configurer, rest.Command) error { return nil }) +func SetupRouter(e handler.CommandExecutor) *gin.Engine { + return setupRouter(e) } diff --git a/internal/router/router_helper_test.go b/internal/router/router_helper_test.go new file mode 100644 index 0000000..3566a55 --- /dev/null +++ b/internal/router/router_helper_test.go @@ -0,0 +1,28 @@ +package router_test + +import ( + "github.com/gin-gonic/gin" + "github.com/google/uuid" + "github.com/iliadenisov/galaxy/internal/model/rest" + "github.com/iliadenisov/galaxy/internal/router" + "github.com/iliadenisov/galaxy/internal/router/handler" +) + +type dummyExecutor struct { +} + +func (e *dummyExecutor) Execute(cmd ...handler.Command) error { + return nil +} + +func (e *dummyExecutor) GenerateGame(races []string) (uuid.UUID, error) { + return uuid.New(), nil +} + +func (e *dummyExecutor) GameState() (rest.StateResponse, error) { + return rest.StateResponse{}, nil +} + +func setupRouter() *gin.Engine { + return router.SetupRouter(&dummyExecutor{}) +} diff --git a/internal/router/router_test.go b/internal/router/router_test.go index 4846f8f..5f47b07 100644 --- a/internal/router/router_test.go +++ b/internal/router/router_test.go @@ -2,6 +2,7 @@ package router_test import ( "encoding/json" + "fmt" "net/http" "net/http/httptest" "strings" @@ -10,6 +11,7 @@ import ( "testing" "github.com/gin-gonic/gin" + "github.com/iliadenisov/galaxy/internal/model/rest" "github.com/iliadenisov/galaxy/internal/router" "github.com/stretchr/testify/assert" ) @@ -63,3 +65,17 @@ func limitTestingRouter() *gin.Engine { }) return r } + +func generateInitRequest(races int) rest.Init { + request := rest.Init{ + Races: make([]rest.Race, races), + } + for i := range request.Races { + request.Races[i] = rest.Race{Name: raceName(i)} + } + return request +} + +func raceName(i int) string { + return fmt.Sprintf("Race_%02d", i) +} diff --git a/internal/router/status_test.go b/internal/router/status_test.go index 0179b6c..3104143 100644 --- a/internal/router/status_test.go +++ b/internal/router/status_test.go @@ -1,20 +1,51 @@ package router_test import ( + "encoding/json" "net/http" "net/http/httptest" "testing" + "github.com/google/uuid" + "github.com/iliadenisov/galaxy/internal/controller" + "github.com/iliadenisov/galaxy/internal/model/rest" "github.com/iliadenisov/galaxy/internal/router" + "github.com/iliadenisov/galaxy/internal/router/handler" + "github.com/iliadenisov/galaxy/internal/util" "github.com/stretchr/testify/assert" ) func TestGetStatus(t *testing.T) { - r := router.SetupRouter() + root, cleanup := util.CreateWorkDir(t) + defer cleanup() + + r := router.SetupRouter(handler.NewDefaultConfigExecutor(func(p *controller.Param) { p.StoragePath = root })) + + payload := generateInitRequest(10) w := httptest.NewRecorder() - req, _ := http.NewRequest("GET", "/api/v1/status", nil) + req, _ := http.NewRequest("POST", "/api/v1/init", asBody(payload)) r.ServeHTTP(w, req) - assert.Equal(t, http.StatusNotImplemented, w.Code, w.Body) + assert.Equal(t, http.StatusCreated, w.Code, w.Body) + var initResponse rest.InitResponse + assert.NoError(t, json.Unmarshal(w.Body.Bytes(), &initResponse)) + assert.NoError(t, uuid.Validate(initResponse.UUID.String())) + + w = httptest.NewRecorder() + req, _ = http.NewRequest("GET", "/api/v1/status", nil) + r.ServeHTTP(w, req) + + assert.Equal(t, http.StatusOK, w.Code, w.Body) + var stateResponse rest.StateResponse + assert.NoError(t, json.Unmarshal(w.Body.Bytes(), &stateResponse)) + assert.NoError(t, uuid.Validate(stateResponse.ID.String())) + assert.Equal(t, uint(0), stateResponse.Turn) + assert.Equal(t, uint(0), stateResponse.Stage) + assert.Len(t, stateResponse.Players, 10) + for i := range stateResponse.Players { + assert.NoError(t, uuid.Validate(stateResponse.Players[i].ID.String())) + assert.Equal(t, raceName(i), stateResponse.Players[i].Name) + assert.False(t, stateResponse.Players[i].Extinct) + } }