feat: order processing
feat: order commands result save/load
This commit is contained in:
@@ -26,6 +26,7 @@ import (
|
||||
"github.com/iliadenisov/galaxy/internal/model/game"
|
||||
)
|
||||
|
||||
// RaceID returns ID of race with given actor's name or error when race not found or extinct
|
||||
func (c Controller) RaceID(actor string) (uuid.UUID, error) {
|
||||
ri, err := c.Cache.validRace(actor)
|
||||
if err != nil {
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/iliadenisov/galaxy/internal/model/game"
|
||||
"github.com/iliadenisov/galaxy/internal/model/order"
|
||||
"github.com/iliadenisov/galaxy/internal/model/report"
|
||||
"github.com/iliadenisov/galaxy/internal/repo"
|
||||
)
|
||||
@@ -41,9 +42,17 @@ type Repo interface {
|
||||
|
||||
// LoadReport loads report for specific turn and player id
|
||||
LoadReport(uint, uuid.UUID) (*report.Report, error)
|
||||
|
||||
// SaveOrder stores order for given turn
|
||||
SaveOrder(uint, uuid.UUID, *order.Order) error
|
||||
|
||||
// LoadOrder loads order for specific turn and player id
|
||||
LoadOrder(uint, uuid.UUID) (*order.Order, bool, error)
|
||||
}
|
||||
|
||||
type Ctrl interface {
|
||||
ValidateOrder(actor string, cmd ...order.DecodableCommand) error
|
||||
// remove below funcs if /command api will be deleted
|
||||
RaceID(actor string) (uuid.UUID, error)
|
||||
RaceQuit(actor string) error
|
||||
RaceVote(actor, acceptor string) error
|
||||
@@ -94,7 +103,7 @@ func GenerateTurn(configure func(*Param)) (err error) {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = ec.ExecuteLocked(func(c *Controller) error { return c.MakeTurn() })
|
||||
err = ec.executeLocked(func(c *Controller) error { return c.MakeTurn() })
|
||||
return
|
||||
}
|
||||
|
||||
@@ -103,7 +112,15 @@ func ExecuteCommand(configure func(*Param), consumer func(c Ctrl) error) (err er
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return ec.ExecuteCommand(func(c *Controller) error { return consumer(c) })
|
||||
return ec.executeCommand(func(c *Controller) error { return consumer(c) })
|
||||
}
|
||||
|
||||
func ValidateOrder(configure func(*Param), actor string, cmd ...order.DecodableCommand) (err error) {
|
||||
ec, err := NewRepoController(configure)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return ec.validateOrder(actor, cmd...)
|
||||
}
|
||||
|
||||
func GameState(configure func(*Param)) (s game.State, err error) {
|
||||
@@ -138,34 +155,6 @@ type RepoController struct {
|
||||
Repo Repo
|
||||
}
|
||||
|
||||
func (ec *RepoController) ExecuteLocked(consumer func(*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(ec.NewGameController(g))
|
||||
return
|
||||
}
|
||||
|
||||
func (ec *RepoController) ExecuteCommand(consumer func(*Controller) error) (err error) {
|
||||
return ec.ExecuteLocked(func(c *Controller) error {
|
||||
err = consumer(c)
|
||||
if err == nil {
|
||||
c.Cache.StageCommand()
|
||||
err = c.saveState()
|
||||
}
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
func NewRepoController(config Configurer) (*RepoController, error) {
|
||||
c := &Param{
|
||||
StoragePath: ".",
|
||||
@@ -189,6 +178,60 @@ func (ec *RepoController) NewGameController(g *game.Game) *Controller {
|
||||
}
|
||||
}
|
||||
|
||||
func (ec *RepoController) validateOrder(actor string, cmd ...order.DecodableCommand) (err error) {
|
||||
return ec.executeSafe(func(t uint, c *Controller) error {
|
||||
id, err := c.RaceID(actor)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = c.ValidateOrder(actor, cmd...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
o := &order.Order{Commands: make([]order.DecodableCommand, len(cmd))}
|
||||
copy(o.Commands, cmd)
|
||||
return ec.Repo.SaveOrder(t, id, o)
|
||||
})
|
||||
}
|
||||
|
||||
func (ec *RepoController) executeCommand(consumer func(*Controller) error) (err error) {
|
||||
return ec.executeLocked(func(c *Controller) error {
|
||||
err = consumer(c)
|
||||
if err == nil {
|
||||
c.Cache.StageCommand()
|
||||
err = c.saveState()
|
||||
}
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
func (ec *RepoController) executeSafe(consumer func(uint, *Controller) error) (err error) {
|
||||
g, err := ec.Repo.LoadStateSafe()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = consumer(g.Turn, ec.NewGameController(g))
|
||||
return
|
||||
}
|
||||
|
||||
func (ec *RepoController) executeLocked(consumer func(*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(ec.NewGameController(g))
|
||||
return
|
||||
}
|
||||
|
||||
func (c *Controller) saveState() error {
|
||||
return c.Repo.SaveLastState(c.Cache.g)
|
||||
}
|
||||
|
||||
@@ -10,12 +10,15 @@ import (
|
||||
)
|
||||
|
||||
func (c *Controller) MakeTurn() error {
|
||||
|
||||
if err := c.applyOrders(c.Cache.g.Turn); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Next turn
|
||||
c.Cache.g.Turn += 1
|
||||
c.Cache.g.Stage = 0
|
||||
|
||||
// TODO: Выполнение приказов
|
||||
|
||||
// 01. Вышедшие расы удаляются из списка участвующих рас перед началом просчета очередного хода
|
||||
c.Cache.TurnWipeExtinctRaces()
|
||||
|
||||
|
||||
@@ -0,0 +1,160 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/google/uuid"
|
||||
e "github.com/iliadenisov/galaxy/internal/error"
|
||||
"github.com/iliadenisov/galaxy/internal/model/order"
|
||||
)
|
||||
|
||||
func (c *Controller) ValidateOrder(actor string, commands ...order.DecodableCommand) (err error) {
|
||||
for i := range commands {
|
||||
if _, ok := commands[i].(order.CommandRaceQuit); ok && i != len(commands)-1 {
|
||||
err = e.NewQuitCommandFollowedByCommandError()
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = errors.Join(err, c.applyCommand(actor, commands[i]))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (c *Controller) applyCommand(actor string, cmd order.DecodableCommand) (err error) {
|
||||
var m *order.CommandMeta
|
||||
if v, ok := order.AsCommand[*order.CommandRaceQuit](cmd); ok {
|
||||
m = &v.CommandMeta
|
||||
err = c.RaceQuit(actor)
|
||||
} else if v, ok := order.AsCommand[*order.CommandRaceVote](cmd); ok {
|
||||
m = &v.CommandMeta
|
||||
err = c.RaceVote(actor, v.Acceptor)
|
||||
} else if v, ok := order.AsCommand[*order.CommandRaceRelation](cmd); ok {
|
||||
m = &v.CommandMeta
|
||||
err = c.RaceRelation(actor, v.Acceptor, v.Relation)
|
||||
} else if v, ok := order.AsCommand[*order.CommandShipClassCreate](cmd); ok {
|
||||
m = &v.CommandMeta
|
||||
err = c.ShipClassCreate(actor, v.Name, v.Drive, int(v.Armament), v.Weapons, v.Shields, v.Cargo)
|
||||
} else if v, ok := order.AsCommand[*order.CommandShipClassMerge](cmd); ok {
|
||||
m = &v.CommandMeta
|
||||
err = c.ShipClassMerge(actor, v.Name, v.Target)
|
||||
} else if v, ok := order.AsCommand[*order.CommandShipClassRemove](cmd); ok {
|
||||
m = &v.CommandMeta
|
||||
err = c.ShipClassRemove(actor, v.Name)
|
||||
} else if v, ok := order.AsCommand[*order.CommandShipGroupLoad](cmd); ok {
|
||||
m = &v.CommandMeta
|
||||
err = c.ShipGroupLoad(actor, uuid.MustParse(v.ID), v.Cargo, v.Quantity)
|
||||
} else if v, ok := order.AsCommand[*order.CommandShipGroupUnload](cmd); ok {
|
||||
m = &v.CommandMeta
|
||||
err = c.ShipGroupUnload(actor, uuid.MustParse(v.ID), v.Quantity)
|
||||
} else if v, ok := order.AsCommand[*order.CommandShipGroupSend](cmd); ok {
|
||||
m = &v.CommandMeta
|
||||
err = c.ShipGroupSend(actor, uuid.MustParse(v.ID), uint(v.Destination))
|
||||
} else if v, ok := order.AsCommand[*order.CommandShipGroupUpgrade](cmd); ok {
|
||||
m = &v.CommandMeta
|
||||
err = c.ShipGroupUpgrade(actor, uuid.MustParse(v.ID), v.Tech, v.Level)
|
||||
} else if v, ok := order.AsCommand[*order.CommandShipGroupMerge](cmd); ok {
|
||||
m = &v.CommandMeta
|
||||
err = c.ShipGroupMerge(actor)
|
||||
} else if v, ok := order.AsCommand[*order.CommandShipGroupBreak](cmd); ok {
|
||||
m = &v.CommandMeta
|
||||
err = c.ShipGroupBreak(actor, uuid.MustParse(v.ID), uuid.MustParse(v.NewID), uint(v.Quantity))
|
||||
} else if v, ok := order.AsCommand[*order.CommandShipGroupDismantle](cmd); ok {
|
||||
m = &v.CommandMeta
|
||||
err = c.ShipGroupDismantle(actor, uuid.MustParse(v.ID))
|
||||
} else if v, ok := order.AsCommand[*order.CommandShipGroupTransfer](cmd); ok {
|
||||
m = &v.CommandMeta
|
||||
err = c.ShipGroupTransfer(actor, v.Acceptor, uuid.MustParse(v.ID))
|
||||
} else if v, ok := order.AsCommand[*order.CommandShipGroupJoinFleet](cmd); ok {
|
||||
m = &v.CommandMeta
|
||||
err = c.ShipGroupJoinFleet(actor, v.Name, uuid.MustParse(v.ID))
|
||||
} else if v, ok := order.AsCommand[*order.CommandFleetMerge](cmd); ok {
|
||||
m = &v.CommandMeta
|
||||
err = c.FleetMerge(actor, v.Name, v.Target)
|
||||
} else if v, ok := order.AsCommand[*order.CommandFleetSend](cmd); ok {
|
||||
m = &v.CommandMeta
|
||||
err = c.FleetSend(actor, v.Name, uint(v.Destination))
|
||||
} else if v, ok := order.AsCommand[*order.CommandScienceCreate](cmd); ok {
|
||||
m = &v.CommandMeta
|
||||
err = c.ScienceCreate(actor, v.Name, v.Drive, v.Weapons, v.Shields, v.Cargo)
|
||||
} else if v, ok := order.AsCommand[*order.CommandScienceRemove](cmd); ok {
|
||||
m = &v.CommandMeta
|
||||
err = c.ScienceRemove(actor, v.Name)
|
||||
} else if v, ok := order.AsCommand[*order.CommandPlanetRename](cmd); ok {
|
||||
m = &v.CommandMeta
|
||||
err = c.PlanetRename(actor, v.Number, v.Name)
|
||||
} else if v, ok := order.AsCommand[*order.CommandPlanetProduce](cmd); ok {
|
||||
m = &v.CommandMeta
|
||||
err = c.PlanetProduce(actor, v.Number, v.Production, v.Subject)
|
||||
} else if v, ok := order.AsCommand[*order.CommandPlanetRouteSet](cmd); ok {
|
||||
m = &v.CommandMeta
|
||||
err = c.PlanetRouteSet(actor, v.LoadType, uint(v.Origin), uint(v.Destination))
|
||||
} else if v, ok := order.AsCommand[*order.CommandPlanetRouteRemove](cmd); ok {
|
||||
m = &v.CommandMeta
|
||||
err = c.PlanetRouteRemove(actor, v.LoadType, uint(v.Origin))
|
||||
} else {
|
||||
return e.NewUnrecognizedCommandError(cmd.CommandType().String())
|
||||
}
|
||||
|
||||
if ge, ok := errors.AsType[*e.GenericError](err); ok {
|
||||
m.Result(ge.Code)
|
||||
} else if err != nil {
|
||||
panic(fmt.Errorf("error applying command has unknown origin: %w", err))
|
||||
} else {
|
||||
m.Result(0)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: test commands ordering
|
||||
func (c *Controller) applyOrders(t uint) error {
|
||||
raceOrder := make(map[int][]order.DecodableCommand)
|
||||
unloadGroups := make([]uuid.UUID, 0)
|
||||
unloadQuantities := make([]float64, 0)
|
||||
for ri := range c.Cache.listRaceActingIdx() {
|
||||
o, ok, err := c.Repo.LoadOrder(t, c.Cache.g.Race[ri].ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
raceOrder[ri] = o.Commands
|
||||
for i := range o.Commands {
|
||||
if o.Commands[i].CommandType() == order.CommandTypeShipGroupUnload {
|
||||
unloadCommand := o.Commands[i].(order.CommandShipGroupUnload)
|
||||
unloadGroups = append(unloadGroups, uuid.MustParse(unloadCommand.ID))
|
||||
unloadQuantities = append(unloadQuantities, unloadCommand.Quantity)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err := c.Cache.shipGroupUnloadColonistChallenge(unloadGroups, unloadQuantities); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for ri := range raceOrder {
|
||||
for _, cmd := range raceOrder[ri] {
|
||||
if cmd.CommandType() == order.CommandTypeShipGroupUnload {
|
||||
// group unload commands execution should be failed at this point,
|
||||
// otherwise produce no-errors
|
||||
if v, ok := order.AsCommand[*order.CommandShipGroupUnload](cmd); ok {
|
||||
v.Result(0)
|
||||
}
|
||||
continue
|
||||
}
|
||||
// any command might fail due to challenged planets colonization
|
||||
_ = c.applyCommand(c.Cache.g.Race[ri].Name, cmd)
|
||||
}
|
||||
}
|
||||
|
||||
for ri := range c.Cache.listRaceActingIdx() {
|
||||
if err := c.Repo.SaveOrder(t, c.Cache.g.Race[ri].ID, &order.Order{Commands: raceOrder[ri]}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -91,6 +91,7 @@ func (c *Cache) validActor(name string) (int, error) {
|
||||
return i, nil
|
||||
}
|
||||
|
||||
// validRace returns index of race with given name or error when race not found or extinct
|
||||
func (c *Cache) validRace(name string) (int, error) {
|
||||
i, err := c.raceIndex(name)
|
||||
if err != nil {
|
||||
|
||||
@@ -277,17 +277,21 @@ func (c *Cache) listRoutedUnloadShipGroupIds(pn uint, routeType game.RouteType)
|
||||
}
|
||||
}
|
||||
|
||||
func MaxOrRandomLoadId(IDtoLoad map[int]float64) int {
|
||||
if len(IDtoLoad) < 2 {
|
||||
panic("IDtoLoad must contain at least 2 keys")
|
||||
func MaxOrRandomLoadId(loadByRace map[int]float64) int {
|
||||
if len(loadByRace) < 2 {
|
||||
panic("loadByRace must contain at least 2 keys")
|
||||
}
|
||||
IDs := slices.Collect(maps.Keys(IDtoLoad))
|
||||
slices.SortFunc(IDs, func(id1, id2 int) int { return cmp.Compare(IDtoLoad[id2], IDtoLoad[id1]) })
|
||||
IDs := slices.Collect(maps.Keys(loadByRace))
|
||||
slices.SortFunc(IDs, func(id1, id2 int) int {
|
||||
return cmp.Or(
|
||||
cmp.Compare(loadByRace[id2], loadByRace[id1]),
|
||||
)
|
||||
})
|
||||
|
||||
// no single winner with highest load
|
||||
if IDtoLoad[IDs[0]] == IDtoLoad[IDs[1]] {
|
||||
if loadByRace[IDs[0]] == loadByRace[IDs[1]] {
|
||||
// remove IDs which load less than maximum
|
||||
IDs = slices.DeleteFunc(IDs, func(v int) bool { return IDtoLoad[v] < IDtoLoad[IDs[0]] })
|
||||
IDs = slices.DeleteFunc(IDs, func(v int) bool { return loadByRace[v] < loadByRace[IDs[0]] })
|
||||
// IDs[0] will have random index
|
||||
rand.Shuffle(len(IDs), func(i, j int) { IDs[i], IDs[j] = IDs[j], IDs[i] })
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"fmt"
|
||||
"iter"
|
||||
"maps"
|
||||
"math/rand/v2"
|
||||
"slices"
|
||||
|
||||
"github.com/google/uuid"
|
||||
@@ -295,6 +296,95 @@ func (c *Cache) shipGroupUnload(ri int, groupID uuid.UUID, quantity float64) err
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Cache) shipGroupUnloadColonistChallenge(groupIDs []uuid.UUID, quantites []float64) error {
|
||||
if len(groupIDs) != len(quantites) {
|
||||
e.NewGameStateError("challenge group unload: groups=%d quantities=%d", len(groupIDs), len(quantites))
|
||||
}
|
||||
if len(groupIDs) == 0 {
|
||||
return nil
|
||||
}
|
||||
type challenger struct {
|
||||
ri int
|
||||
sgi int
|
||||
quantity float64
|
||||
}
|
||||
challenge := make(map[uint]map[int][]challenger, 0)
|
||||
for i, gid := range groupIDs {
|
||||
sgi, ok := c.shipGroupIndexByID(gid)
|
||||
if !ok {
|
||||
return e.NewGameStateError("challenge group unload: group not found: %v", gid)
|
||||
}
|
||||
sg := c.ShipGroup(sgi)
|
||||
pn, ok := sg.AtPlanet()
|
||||
if !ok || sg.CargoType == nil {
|
||||
continue
|
||||
}
|
||||
p := c.MustPlanet(pn)
|
||||
ri := c.ShipGroupOwnerRaceIndex(sgi)
|
||||
if p.Owned() || *sg.CargoType != game.CargoColonist {
|
||||
if err := c.shipGroupUnload(ri, gid, quantites[i]); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if _, ok := challenge[pn]; !ok {
|
||||
challenge[pn] = make(map[int][]challenger)
|
||||
}
|
||||
challenge[pn][ri] = append(challenge[pn][ri], challenger{ri: ri, sgi: sgi, quantity: quantites[i]})
|
||||
}
|
||||
for pn := range challenge {
|
||||
if len(challenge[pn]) < 2 {
|
||||
for _, v := range challenge[pn] {
|
||||
for _, ch := range v {
|
||||
if err := c.shipGroupUnload(ch.ri, c.ShipGroup(ch.sgi).ID, ch.quantity); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
sum := make(map[int]float64)
|
||||
races := slices.Collect(maps.Keys(challenge[pn]))
|
||||
for ri, groups := range challenge[pn] {
|
||||
for i := range groups {
|
||||
sum[ri] = sum[ri] + groups[i].quantity
|
||||
}
|
||||
c.listProducingPlanets()
|
||||
}
|
||||
|
||||
slices.SortFunc(races, func(ria, rib int) int {
|
||||
return cmp.Or(
|
||||
// Наибольшее количество выгружаемых колонистов
|
||||
cmp.Compare(sum[rib], sum[ria]),
|
||||
|
||||
// Наибольшее количество населения расы (они же голоса)
|
||||
cmp.Compare(c.g.Race[rib].Votes, c.g.Race[ria].Votes),
|
||||
|
||||
// Случайный выбор претендента
|
||||
cmp.Compare(rand.Float64(), rand.Float64()),
|
||||
|
||||
// in theoty, unreacheable option, but let's randomize again
|
||||
cmp.Compare(rand.Float64(), rand.Float64()),
|
||||
)
|
||||
})
|
||||
|
||||
for _, ch := range challenge[pn][races[0]] {
|
||||
if err := c.shipGroupUnload(ch.ri, c.ShipGroup(ch.sgi).ID, ch.quantity); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Cache) shipGroupIndexByID(id uuid.UUID) (int, bool) {
|
||||
for sgi := range c.g.ShipGroups {
|
||||
if c.g.ShipGroups[sgi].ID == id {
|
||||
return sgi, true
|
||||
}
|
||||
}
|
||||
return -1, false
|
||||
}
|
||||
|
||||
func (c *Cache) unsafeUnloadCargo(sgi int, q float64) {
|
||||
if q <= 0 {
|
||||
return
|
||||
|
||||
Reference in New Issue
Block a user