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_C_idx, _ := c.AddRace(race_C_name)
race_D_idx, _ := c.AddRace(race_D_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_0.Name, Race_1.Name, game.RelationWar.String()))
assert.NoError(t, g.UpdateRelation(Race_1.Name, Race_0.Name, game.RelationWar)) 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_C_name, race_D_name, game.RelationWar.String()))
assert.NoError(t, g.UpdateRelation(race_D_name, race_C_name, game.RelationWar)) 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_0_idx, race_C_idx))
assert.Equal(t, game.RelationPeace, c.Relation(Race_1_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) { func TestCollectBombingGroups(t *testing.T) {
c, g := newCache() c, g := newCache()
assert.NoError(t, g.UpdateRelation(Race_0.Name, Race_1.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)) assert.NoError(t, g.UpdateRelation(Race_1.Name, Race_0.Name, game.RelationWar.String()))
// 1: idx = 0 / Ready to bomb: Race_1/Planet_1 // 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 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]) assert.Equal(t, 1, bg[R0_Planet_2_num][Race_1_idx][0])
// remove bombings from Race_1 // 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() bg = c.CollectBombingGroups()
assert.Len(t, bg, 1) assert.Len(t, bg, 1)
assert.Contains(t, bg, R1_Planet_1_num) assert.Contains(t, bg, R1_Planet_1_num)
@@ -95,8 +95,8 @@ func TestCollectBombingGroups(t *testing.T) {
func TestProduceBombings(t *testing.T) { func TestProduceBombings(t *testing.T) {
c, g := newCache() c, g := newCache()
assert.NoError(t, g.UpdateRelation(Race_0.Name, Race_1.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)) assert.NoError(t, g.UpdateRelation(Race_1.Name, Race_0.Name, game.RelationWar.String()))
// 1: idx = 0 / Bombs on: Race_1/Planet_1 // 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)) 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 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) ri, err := c.Cache.validActor(actor)
if err != nil { if err != nil {
return err return err
+108 -38
View File
@@ -2,7 +2,6 @@ package controller
import ( import (
"errors" "errors"
"fmt"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/iliadenisov/galaxy/internal/model/game" "github.com/iliadenisov/galaxy/internal/model/game"
@@ -10,6 +9,8 @@ import (
"github.com/iliadenisov/galaxy/internal/repo" "github.com/iliadenisov/galaxy/internal/repo"
) )
type Configurer func(*Param)
type Repo interface { type Repo interface {
// Lock must be called before any repository operations // Lock must be called before any repository operations
Lock() error Lock() error
@@ -42,18 +43,109 @@ type Repo interface {
LoadReport(uint, uuid.UUID) (*report.Report, error) 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 Repo Repo
Cache *Cache
} }
type Param struct { func (ec *RepoController) ExecuteCommand(consumer func(c *Controller) error) (err error) {
StoragePath string 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 NewRepoController(config Configurer) (*RepoController, error) {
func NewController(config Configurer) (*Controller, error) {
c := &Param{ c := &Param{
StoragePath: ".", StoragePath: ".",
} }
@@ -64,44 +156,22 @@ func NewController(config Configurer) (*Controller, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
return &Controller{ return &RepoController{
Repo: r, Repo: r,
}, nil }, nil
} }
func NewRepoController(r Repo) *Controller { func NewGameController(g *game.Game) *Controller {
return &Controller{ return &Controller{
Repo: r, Cache: NewCache(g),
} }
} }
func (c *Controller) ExecuteState(consumer func(Repo) error) (err error) { type Controller struct {
if err := c.Repo.Lock(); err != nil { Repo Repo
return fmt.Errorf("execute: lock failed: %s", err) Cache *Cache
}
defer func() {
err = errors.Join(err, c.Repo.Release())
}()
err = consumer(c.Repo)
return
} }
func (c *Controller) ExecuteCommand(consumer func(Repo, *game.Game) error) (err error) { type Param struct {
if err := c.Repo.Lock(); err != nil { StoragePath string
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
} }
+6 -4
View File
@@ -129,8 +129,10 @@ func newGame() *game.Game {
} }
func newCache() (*controller.Cache, *controller.Controller) { func newCache() (*controller.Cache, *controller.Controller) {
g := newGame() ctl := controller.NewGameController(newGame())
c := controller.NewCache(g) // 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_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, 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())) 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, 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.) assertNoError(c.CreateShipType(Race_1_idx, ShipType_Cruiser, 15, 2, 15, 15, 0)) // same name - different type (why.)
ctl := controller.NewRepoController(nil) // ctl := controller.NewRepoController(nil)
ctl.Cache = c // ctl.Cache = c
return c, ctl return c, ctl
} }
+9 -6
View File
@@ -35,23 +35,26 @@ func TestGiveVotes(t *testing.T) {
func TestRelation(t *testing.T) { func TestRelation(t *testing.T) {
c, g := newCache() c, g := newCache()
assert.NoError(t, g.UpdateRelation(Race_0.Name, Race_1.Name, game.RelationWar)) assert.NoError(t, g.UpdateRelation(Race_0.Name, Race_1.Name, "war"))
assert.NoError(t, g.UpdateRelation(Race_1.Name, Race_0.Name, game.RelationPeace)) 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.RelationWar, c.Relation(Race_0_idx, Race_1_idx))
assert.Equal(t, game.RelationPeace, c.Relation(Race_1_idx, Race_0_idx)) assert.Equal(t, game.RelationPeace, c.Relation(Race_1_idx, Race_0_idx))
assert.ErrorContains(t, 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)) e.GenericErrorText(e.ErrInputUnknownRace))
assert.ErrorContains(t, assert.ErrorContains(t,
g.UpdateRelation(UnknownRace, Race_0.Name, game.RelationWar), g.UpdateRelation(UnknownRace, Race_0.Name, "Peace"),
e.GenericErrorText(e.ErrInputUnknownRace)) e.GenericErrorText(e.ErrInputUnknownRace))
assert.ErrorContains(t, 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)) e.GenericErrorText(e.ErrRaceExinct))
assert.ErrorContains(t, 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)) e.GenericErrorText(e.ErrRaceExinct))
} }
+53
View File
@@ -2,6 +2,7 @@ package controller_test
import ( import (
"slices" "slices"
"strconv"
"testing" "testing"
e "github.com/iliadenisov/galaxy/internal/error" e "github.com/iliadenisov/galaxy/internal/error"
@@ -35,6 +36,58 @@ func TestCreateShipClass(t *testing.T) {
e.GenericErrorText(e.ErrInputEntityTypeNameInvalid)) 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) { func TestMergeShipClass(t *testing.T) {
c, g := newCache() c, g := newCache()
+3
View File
@@ -33,6 +33,7 @@ const (
const ( const (
ErrInputUnknownRace int = 3000 + iota ErrInputUnknownRace int = 3000 + iota
ErrInputUnknownRelation
ErrInputSameRace ErrInputSameRace
ErrInputEntityTypeNameInvalid ErrInputEntityTypeNameInvalid
ErrInputEntityTypeNameDuplicate ErrInputEntityTypeNameDuplicate
@@ -78,6 +79,8 @@ func GenericErrorText(code int) string {
return "Invalid game state" return "Invalid game state"
case ErrInputUnknownRace: case ErrInputUnknownRace:
return "Race name is unknown to this game" return "Race name is unknown to this game"
case ErrInputUnknownRelation:
return "Unknown relation"
case ErrInputSameRace: case ErrInputSameRace:
return "Race name must be different from your own" return "Race name must be different from your own"
case ErrInputEntityTypeNameInvalid: case ErrInputEntityTypeNameInvalid:
+5
View File
@@ -3,6 +3,11 @@ package error
func NewRaceUnknownError(arg ...any) error { func NewRaceUnknownError(arg ...any) error {
return newGenericError(ErrInputUnknownRace, arg...) return newGenericError(ErrInputUnknownRace, arg...)
} }
func NewUnknownRelationError(arg ...any) error {
return newGenericError(ErrInputUnknownRelation, arg...)
}
func NewSameRaceError(arg ...any) error { func NewSameRaceError(arg ...any) error {
return newGenericError(ErrInputSameRace, arg...) 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 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 { type Race struct {
ID uuid.UUID `json:"id"` ID uuid.UUID `json:"id"`
@@ -15,17 +33,17 @@ type Race struct {
ShipTypes []ShipType `json:"shipType,omitempty"` 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 { func (r Relation) String() string {
return string(r) return string(r)
} }
const (
RelationWar Relation = "War"
RelationPeace Relation = "Peace"
)
type RaceRelation struct { type RaceRelation struct {
RaceID uuid.UUID `json:"raceId"` RaceID uuid.UUID `json:"raceId"`
Relation Relation `json:"relation"` 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 package rest
/* import "encoding/json"
Full list of requirements must be updated when adding new command:
required_without_all=Vote DeclarePeace DeclareWar
| excluded_with=Vote DeclarePeace DeclareWar
*/
type Command struct { type Command struct {
Race string `json:"race" binding:"required,notblank"` Actor string `json:"actor" binding:"required,notblank"`
Vote *CommandVote `json:"vote" binding:"required_without_all=DeclarePeace DeclareWar,excluded_with=DeclarePeace DeclareWar"` Commands []json.RawMessage `json:"cmd" binding:"min=1"`
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"`
type CommandType string
const (
CommandTypeVote CommandType = "vote"
CommandTypeRelation CommandType = "declarePeace"
)
type CommandMeta struct {
Type CommandType `json:"@type"`
} }
type CommandVote struct { type CommandVote struct {
CommandMeta
Recipient string `json:"recipient" binding:"required,notblank"` Recipient string `json:"recipient" binding:"required,notblank"`
} }
type CommandDeclarePeace struct { type CommandUpdateRelation struct {
Opponent string `json:"recipient" binding:"required,notblank"` CommandMeta
}
type CommandDeclareWar struct {
Opponent string `json:"recipient" binding:"required,notblank"` Opponent string `json:"recipient" binding:"required,notblank"`
Relation string `json:"relation" binding:"required,notblank"`
} }
+12 -2
View File
@@ -1,6 +1,16 @@
package rest package rest
type Status struct { import "github.com/google/uuid"
type StateResponse struct {
ID uuid.UUID `json:"id"`
Turn uint `json:"turn"` 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 package router_test
import ( import (
"encoding/json"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"testing" "testing"
"github.com/iliadenisov/galaxy/internal/model/rest" "github.com/iliadenisov/galaxy/internal/model/rest"
"github.com/iliadenisov/galaxy/internal/router"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
func TestCommand(t *testing.T) { func TestCommand(t *testing.T) {
r := router.SetupRouter() r := setupRouter()
payload := rest.Command{ payload := rest.Command{
Race: "SomeRace", Actor: "SomeRace",
Vote: &rest.CommandVote{ Commands: []json.RawMessage{
encodeCommand(&rest.CommandVote{
CommandMeta: rest.CommandMeta{Type: rest.CommandTypeVote},
Recipient: "AnotherRace", Recipient: "AnotherRace",
}),
}, },
} }
@@ -24,17 +27,17 @@ func TestCommand(t *testing.T) {
req, _ := http.NewRequest("PUT", "/api/v1/command", asBody(payload)) req, _ := http.NewRequest("PUT", "/api/v1/command", asBody(payload))
r.ServeHTTP(w, req) 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 // error: notblank validator
payload.Race = "" payload.Actor = ""
w = httptest.NewRecorder() w = httptest.NewRecorder()
req, _ = http.NewRequest("PUT", "/api/v1/command", asBody(payload)) req, _ = http.NewRequest("PUT", "/api/v1/command", asBody(payload))
r.ServeHTTP(w, req) r.ServeHTTP(w, req)
assert.Equal(t, http.StatusBadRequest, w.Code, w.Body) assert.Equal(t, http.StatusBadRequest, w.Code, w.Body)
payload.Race = " " payload.Actor = " "
w = httptest.NewRecorder() w = httptest.NewRecorder()
req, _ = http.NewRequest("PUT", "/api/v1/command", asBody(payload)) req, _ = http.NewRequest("PUT", "/api/v1/command", asBody(payload))
r.ServeHTTP(w, req) r.ServeHTTP(w, req)
@@ -43,24 +46,7 @@ func TestCommand(t *testing.T) {
// error: no commands // error: no commands
payload = rest.Command{ payload = rest.Command{
Race: "SomeRace", Actor: "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",
},
} }
w = httptest.NewRecorder() w = httptest.NewRecorder()
@@ -69,3 +55,11 @@ func TestCommand(t *testing.T) {
assert.Equal(t, http.StatusBadRequest, w.Code, w.Body) 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 package handler
import ( import (
"errors" "encoding/json"
"fmt"
"net/http" "net/http"
"github.com/iliadenisov/galaxy/internal/controller" "github.com/iliadenisov/galaxy/internal/controller"
"github.com/iliadenisov/galaxy/internal/game"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/iliadenisov/galaxy/internal/model/rest" "github.com/iliadenisov/galaxy/internal/model/rest"
) )
type CommandExecutor func(controller.Configurer, rest.Command) error func CommandHandler(c *gin.Context, executor CommandExecutor) {
var (
ErrCommandNotProcessed = errors.New("command was not processed by executor")
)
func CommandHandler(c *gin.Context, configurer controller.Configurer, executor CommandExecutor) {
var cmd rest.Command var cmd rest.Command
if err := c.ShouldBindJSON(&cmd); err != nil { if errorResponded(c, c.ShouldBindJSON(&cmd)) {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return return
} }
err := executor(configurer, cmd)
switch { commands := make([]Command, 0)
case err == nil: for i := range cmd.Commands {
c.Status(http.StatusOK) command, err := parseCommand(cmd.Actor, cmd.Commands[i])
case errors.Is(err, ErrCommandNotProcessed): if errorResponded(c, err) {
c.Status(http.StatusInternalServerError) // TODO: add error text? 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: default:
// TODO: separate invalid input and game state errors return nil, fmt.Errorf("unknown comman type: %s", t)
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
} }
} }
func ExecuteCommand(config controller.Configurer, cmd rest.Command) error { func giveVotes(actor string, c json.RawMessage) (Command, error) {
switch { var v rest.CommandVote
case cmd.DeclareWar != nil: if err := json.Unmarshal(c, &v); err != nil {
return game.DeclareWar(config, cmd.Race, cmd.DeclareWar.Opponent) return nil, err
case cmd.DeclarePeace != nil:
return game.DeclarePeace(config, cmd.Race, cmd.DeclareWar.Opponent)
default:
return ErrCommandNotProcessed
} }
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 ( import (
"errors" "errors"
"net/http" "net/http"
"os"
"github.com/gin-gonic/gin" "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" 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 { if err == nil {
return false return false
} }
var ge = new(e.GenericError) 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) { if errors.As(err, ge) {
switch ge.Code { switch ge.Code {
case e.ErrGameNotInitialized: case e.ErrGameNotInitialized:
@@ -24,7 +94,6 @@ func transformError(c *gin.Context, err error) bool {
} }
} else { } else {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
} }
return true return true
+4 -8
View File
@@ -4,15 +4,12 @@ import (
"net/http" "net/http"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/iliadenisov/galaxy/internal/controller"
"github.com/iliadenisov/galaxy/internal/game"
"github.com/iliadenisov/galaxy/internal/model/rest" "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 var init rest.Init
if err := c.ShouldBindJSON(&init); err != nil { if errorResponded(c, c.ShouldBindJSON(&init)) {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return return
} }
@@ -21,10 +18,9 @@ func InitHandler(c *gin.Context, config controller.Configurer) {
races[i] = init.Races[i].Name races[i] = init.Races[i].Name
} }
uuid, err := game.GenerateGame(config, races) uuid, err := executor.GenerateGame(races)
if transformError(c, err) { if errorResponded(c, err) {
return return
} }
c.JSON(http.StatusCreated, rest.InitResponse{ c.JSON(http.StatusCreated, rest.InitResponse{
+4 -10
View File
@@ -4,20 +4,14 @@ import (
"net/http" "net/http"
"github.com/gin-gonic/gin" "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) { func StatusHandler(c *gin.Context, executor CommandExecutor) {
g, err := game.LoadState(config) state, err := executor.GameState()
if transformError(c, err) { if errorResponded(c, err) {
return return
} }
c.JSON(http.StatusOK, rest.Status{ c.JSON(http.StatusOK, state)
Turn: g.Turn,
Players: len(g.Race),
})
} }
+3 -13
View File
@@ -2,7 +2,6 @@ package router_test
import ( import (
"encoding/json" "encoding/json"
"fmt"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"testing" "testing"
@@ -11,6 +10,7 @@ import (
"github.com/iliadenisov/galaxy/internal/controller" "github.com/iliadenisov/galaxy/internal/controller"
"github.com/iliadenisov/galaxy/internal/model/rest" "github.com/iliadenisov/galaxy/internal/model/rest"
"github.com/iliadenisov/galaxy/internal/router" "github.com/iliadenisov/galaxy/internal/router"
"github.com/iliadenisov/galaxy/internal/router/handler"
"github.com/iliadenisov/galaxy/internal/util" "github.com/iliadenisov/galaxy/internal/util"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
@@ -19,7 +19,7 @@ func TestInit(t *testing.T) {
root, cleanup := util.CreateWorkDir(t) root, cleanup := util.CreateWorkDir(t)
defer cleanup() 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) payload := generateInitRequest(10)
@@ -34,7 +34,7 @@ func TestInit(t *testing.T) {
} }
func TestInitValidators(t *testing.T) { func TestInitValidators(t *testing.T) {
r := router.SetupRouter() r := setupRouter()
payload := generateInitRequest(9) payload := generateInitRequest(9)
w := httptest.NewRecorder() w := httptest.NewRecorder()
@@ -43,13 +43,3 @@ func TestInitValidators(t *testing.T) {
assert.Equal(t, http.StatusBadRequest, w.Code, w.Body) 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"
"github.com/gin-gonic/gin/binding" "github.com/gin-gonic/gin/binding"
"github.com/go-playground/validator/v10" "github.com/go-playground/validator/v10"
"github.com/iliadenisov/galaxy/internal/controller"
"github.com/iliadenisov/galaxy/internal/router/handler" "github.com/iliadenisov/galaxy/internal/router/handler"
) )
@@ -16,12 +15,6 @@ const (
ISO8601 = "2006-01-02 15:04:05.0 -07:00" 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 { type Router struct {
r *gin.Engine r *gin.Engine
executor handler.CommandExecutor executor handler.CommandExecutor
@@ -32,14 +25,14 @@ func (r Router) Run() error {
} }
func NewRouter() Router { func NewRouter() Router {
return NewRouterExecutor(handler.ExecuteCommand) return NewRouterExecutor(handler.NewDefaultExecutor())
} }
func NewRouterExecutor(executor handler.CommandExecutor) Router { 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) gin.SetMode(gin.ReleaseMode)
r := gin.New() r := gin.New()
@@ -55,9 +48,9 @@ func setupRouter(config controller.Configurer, executor handler.CommandExecutor)
groupV1 := r.Group("/api/v1") groupV1 := r.Group("/api/v1")
groupV1.GET("/status", func(ctx *gin.Context) { handler.StatusHandler(ctx, config) }) groupV1.GET("/status", func(ctx *gin.Context) { handler.StatusHandler(ctx, executor) })
groupV1.POST("/init", func(ctx *gin.Context) { handler.InitHandler(ctx, config) }) groupV1.POST("/init", func(ctx *gin.Context) { handler.InitHandler(ctx, executor) })
groupV1.PUT("/command", LimitMiddleware(1), func(ctx *gin.Context) { handler.CommandHandler(ctx, config, executor) }) groupV1.PUT("/command", LimitMiddleware(1), func(ctx *gin.Context) { handler.CommandHandler(ctx, executor) })
return r return r
} }
+3 -8
View File
@@ -2,14 +2,9 @@ package router
import ( import (
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/iliadenisov/galaxy/internal/controller" "github.com/iliadenisov/galaxy/internal/router/handler"
"github.com/iliadenisov/galaxy/internal/model/rest"
) )
func SetupRouter() *gin.Engine { func SetupRouter(e handler.CommandExecutor) *gin.Engine {
return SetupRouterConfig(nil) return setupRouter(e)
}
func SetupRouterConfig(config controller.Configurer) *gin.Engine {
return setupRouter(config, func(controller.Configurer, rest.Command) error { return nil })
} }
+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 ( import (
"encoding/json" "encoding/json"
"fmt"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"strings" "strings"
@@ -10,6 +11,7 @@ import (
"testing" "testing"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/iliadenisov/galaxy/internal/model/rest"
"github.com/iliadenisov/galaxy/internal/router" "github.com/iliadenisov/galaxy/internal/router"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
@@ -63,3 +65,17 @@ func limitTestingRouter() *gin.Engine {
}) })
return r 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 package router_test
import ( import (
"encoding/json"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"testing" "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"
"github.com/iliadenisov/galaxy/internal/router/handler"
"github.com/iliadenisov/galaxy/internal/util"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
func TestGetStatus(t *testing.T) { 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() w := httptest.NewRecorder()
req, _ := http.NewRequest("GET", "/api/v1/status", nil) req, _ := http.NewRequest("POST", "/api/v1/init", asBody(payload))
r.ServeHTTP(w, req) 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)
}
} }