refactor: executors and routers

* refactor: executors and routers
This commit is contained in:
Ilia Denisov
2026-02-09 15:53:34 +03:00
committed by GitHub
parent e48a0c8b96
commit d9c8de27e5
38 changed files with 508 additions and 838 deletions
+4 -4
View File
@@ -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))
+5 -5
View File
@@ -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))
+5 -1
View File
@@ -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
+108 -38
View File
@@ -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 {
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
}
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
}
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 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
Cache *Cache
}
type Param struct {
StoragePath string
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
}
type Configurer func(*Param)
func NewController(config Configurer) (*Controller, error) {
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
}
+6 -4
View File
@@ -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
}
+9 -6
View File
@@ -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))
}
+53
View File
@@ -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()
+3
View File
@@ -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:
+5
View File
@@ -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...)
}
-19
View File
@@ -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)
}
-16
View File
@@ -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)
})
}
-19
View File
@@ -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)
}
-58
View File
@@ -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))
})
}
-19
View File
@@ -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)
}
-32
View File
@@ -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)
}
-70
View File
@@ -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)
}
}
})
}
-54
View File
@@ -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)
}
-114
View File
@@ -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))
})
}
-19
View File
@@ -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
}
-31
View File
@@ -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)
}
-59
View File
@@ -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))
}
})
}
-52
View File
@@ -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)
}
-84
View File
@@ -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)
}
+25 -7
View File
@@ -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"`
+16
View File
@@ -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
}
+18 -14
View File
@@ -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"`
}
+12 -2
View File
@@ -1,6 +1,16 @@
package rest
type Status struct {
import "github.com/google/uuid"
type StateResponse struct {
ID uuid.UUID `json:"id"`
Turn uint `json:"turn"`
Players int `json:"players"`
Stage uint `json:"stage"`
Players []PlayerState `json:"player"`
}
type PlayerState struct {
ID uuid.UUID `json:"id"`
Name string `json:"name"`
Extinct bool `json:"extinct"`
}
+19 -25
View File
@@ -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{
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
}
+49 -27
View File
@@ -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
}
+71 -2
View File
@@ -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
+4 -8
View File
@@ -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{
+4 -10
View File
@@ -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)
}
+3 -13
View File
@@ -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
}
+6 -13
View File
@@ -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
}
+3 -8
View File
@@ -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)
}
+28
View File
@@ -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{})
}
+16
View File
@@ -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)
}
+34 -3
View File
@@ -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)
}
}