feat: cargo unload challenge
This commit is contained in:
@@ -1,23 +1,5 @@
|
||||
package controller
|
||||
|
||||
/*
|
||||
TODO: Препроцессинг и сохранение приказов
|
||||
|
||||
Когда приказ (последовательность команд) поступает на сервер, игрок получает
|
||||
уведомление о том, что его команды приняты к производству. Каждая команда из
|
||||
приказа проверяется на корректность и получает отдельное подтверждение. Игрок
|
||||
может послать любое количество приказов по своему усмотрению, однако, каждый
|
||||
новый приказ отменяет предыдущий. Таким образом, можно исправить неверно
|
||||
составленный приказ, но при этом необходимо повторить те команды, которые
|
||||
были отданы верно. К счастью, программа-клиент помогает игроку не запутаться
|
||||
в этом процессе и берёт на себя контроль за целостностью приказов.
|
||||
|
||||
!!! Убедиться, что раса не покинула игру.
|
||||
|
||||
При производстве хода раса может быть исключена по TTL=0.
|
||||
В этом случае нужно игнорировать некоторые приказы, например, передачу ей кораблей.
|
||||
*/
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
|
||||
@@ -10,7 +10,6 @@ import (
|
||||
)
|
||||
|
||||
func (c *Controller) MakeTurn() error {
|
||||
|
||||
if err := c.applyOrders(c.Cache.g.Turn); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
|
||||
"github.com/google/uuid"
|
||||
e "github.com/iliadenisov/galaxy/internal/error"
|
||||
"github.com/iliadenisov/galaxy/internal/model/game"
|
||||
"github.com/iliadenisov/galaxy/internal/model/order"
|
||||
)
|
||||
|
||||
@@ -108,11 +109,12 @@ func (c *Controller) applyCommand(actor string, cmd order.DecodableCommand) (err
|
||||
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)
|
||||
commandRace := make(map[string]string)
|
||||
challenge := make(map[string]*order.CommandShipGroupUnload)
|
||||
cmdApplied := make(map[string]bool)
|
||||
|
||||
for ri := range c.Cache.listRaceActingIdx() {
|
||||
o, ok, err := c.Repo.LoadOrder(t, c.Cache.g.Race[ri].ID)
|
||||
if err != nil {
|
||||
@@ -123,30 +125,33 @@ func (c *Controller) applyOrders(t uint) error {
|
||||
}
|
||||
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)
|
||||
commandRace[o.Commands[i].CommandID()] = c.Cache.g.Race[ri].Name
|
||||
if v, ok := order.AsCommand[*order.CommandShipGroupUnload](o.Commands[i]); ok {
|
||||
if _, ok := challenge[v.ID]; ok {
|
||||
panic(fmt.Sprintf("unload command %s already cached", v.ID))
|
||||
}
|
||||
if ok, err := c.shouldChallenge(v); err != nil {
|
||||
return err
|
||||
} else if ok {
|
||||
challenge[v.ID] = v
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err := c.Cache.shipGroupUnloadColonistChallenge(unloadGroups, unloadQuantities); err != nil {
|
||||
return err
|
||||
for _, cmdID := range c.challengeUnload(challenge) {
|
||||
if err := c.applyCommand(commandRace[cmdID], challenge[cmdID]); err == nil {
|
||||
cmdApplied[cmdID] = true
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
if v, ok := cmdApplied[cmd.CommandID()]; ok && v {
|
||||
continue
|
||||
}
|
||||
// any command might fail due to challenged planets colonization
|
||||
_ = c.applyCommand(c.Cache.g.Race[ri].Name, cmd)
|
||||
_ = c.applyCommand(commandRace[cmd.CommandID()], cmd)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -158,3 +163,57 @@ func (c *Controller) applyOrders(t uint) error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Controller) shouldChallenge(cmd *order.CommandShipGroupUnload) (resut bool, err error) {
|
||||
sgi, ok := c.Cache.shipGroupIndexByID(uuid.MustParse(cmd.ID))
|
||||
if !ok {
|
||||
err = e.NewGameStateError("challenge group unload: group not found: %v", cmd.ID)
|
||||
return
|
||||
}
|
||||
sg := c.Cache.ShipGroup(sgi)
|
||||
pn, ok := sg.AtPlanet()
|
||||
if !ok || sg.CargoType == nil {
|
||||
return false, nil
|
||||
}
|
||||
p := c.Cache.MustPlanet(pn)
|
||||
if p.Owned() || *sg.CargoType != game.CargoColonist {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (c *Controller) challengeUnload(challenge map[string]*order.CommandShipGroupUnload) []string {
|
||||
if len(challenge) == 0 {
|
||||
return nil
|
||||
}
|
||||
planetRaceQuantity := make(map[uint]map[int]float64, 0)
|
||||
raceCommand := make(map[uint]map[int][]string)
|
||||
for cmdID, cmd := range challenge {
|
||||
sgi, ok := c.Cache.shipGroupIndexByID(uuid.MustParse(cmd.ID))
|
||||
if !ok {
|
||||
panic(fmt.Sprintf("challenge group unload: group not found: %v", cmd.ID))
|
||||
}
|
||||
sg := c.Cache.ShipGroup(sgi)
|
||||
ri := c.Cache.ShipGroupOwnerRaceIndex(sgi)
|
||||
pn, ok := sg.AtPlanet()
|
||||
if _, ok := raceCommand[pn]; !ok {
|
||||
raceCommand[pn] = make(map[int][]string)
|
||||
}
|
||||
raceCommand[pn][ri] = append(raceCommand[pn][ri], cmdID)
|
||||
if _, ok := planetRaceQuantity[pn]; !ok {
|
||||
planetRaceQuantity[pn] = make(map[int]float64)
|
||||
}
|
||||
planetRaceQuantity[pn][ri] = planetRaceQuantity[pn][ri] + UnloadCargoRequest(float64(sg.Load), cmd.Quantity)
|
||||
}
|
||||
|
||||
result := make([]string, 0)
|
||||
for pn := range planetRaceQuantity {
|
||||
if len(planetRaceQuantity[pn]) < 2 {
|
||||
continue
|
||||
}
|
||||
winner := MaxOrRandomLoadId(planetRaceQuantity[pn], func(ri int) float64 { return float64(c.Cache.g.Race[ri].Votes) })
|
||||
result = append(result, raceCommand[pn][winner]...)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -171,9 +171,6 @@ func (c *Cache) listRoutedSendGroupIds(pn uint) iter.Seq[int] {
|
||||
}
|
||||
|
||||
// Невозможно лишь выгрузить колонистов на чужой планете.
|
||||
/*
|
||||
TODO: Очерёдность выгрузки согласно правилам
|
||||
*/
|
||||
func (c *Cache) TurnUnloadEnroutedGroups() {
|
||||
for i := range c.g.Map.Planet {
|
||||
p := &c.g.Map.Planet[i]
|
||||
@@ -245,7 +242,7 @@ func (c *Cache) selectColUnloadGroup(groups []int) (result iter.Seq[int]) {
|
||||
}
|
||||
|
||||
// select winner to unload cargo
|
||||
id := MaxOrRandomLoadId(loadByRace)
|
||||
id := MaxOrRandomLoadId(loadByRace, func(ri int) float64 { return float64(c.g.Race[ri].Votes) })
|
||||
result = slices.Values(groupByRace[id])
|
||||
|
||||
return
|
||||
@@ -277,23 +274,25 @@ func (c *Cache) listRoutedUnloadShipGroupIds(pn uint, routeType game.RouteType)
|
||||
}
|
||||
}
|
||||
|
||||
func MaxOrRandomLoadId(loadByRace map[int]float64) int {
|
||||
if len(loadByRace) < 2 {
|
||||
func MaxOrRandomLoadId(raceLoad map[int]float64, pop func(int) float64) int {
|
||||
if len(raceLoad) < 2 {
|
||||
panic("loadByRace must contain at least 2 keys")
|
||||
}
|
||||
IDs := slices.Collect(maps.Keys(loadByRace))
|
||||
slices.SortFunc(IDs, func(id1, id2 int) int {
|
||||
raceIndex := slices.Collect(maps.Keys(raceLoad))
|
||||
slices.SortFunc(raceIndex, func(ria, rib int) int {
|
||||
return cmp.Or(
|
||||
cmp.Compare(loadByRace[id2], loadByRace[id1]),
|
||||
// maximum quantity of unloading colonists
|
||||
cmp.Compare(raceLoad[rib], raceLoad[ria]),
|
||||
|
||||
// maximum population of the race
|
||||
cmp.Compare(pop(rib), pop(ria)),
|
||||
|
||||
// Random winner
|
||||
cmp.Compare(rand.Float64(), rand.Float64()),
|
||||
|
||||
// in theoty, unreacheable option, but let's randomize again
|
||||
cmp.Compare(rand.Float64(), rand.Float64()),
|
||||
)
|
||||
})
|
||||
|
||||
// no single winner with highest load
|
||||
if loadByRace[IDs[0]] == loadByRace[IDs[1]] {
|
||||
// remove IDs which load less than maximum
|
||||
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] })
|
||||
}
|
||||
return IDs[0]
|
||||
return raceIndex[0]
|
||||
}
|
||||
|
||||
@@ -320,26 +320,45 @@ func TestListRoutedUnloadShipGroupIds(t *testing.T) {
|
||||
func TestMaxOrRandomLoadId(t *testing.T) {
|
||||
IDtoLoad := make(map[int]float64)
|
||||
|
||||
assert.Panics(t, func() { controller.MaxOrRandomLoadId(IDtoLoad) })
|
||||
pop := func(ri int) float64 {
|
||||
switch ri {
|
||||
case 1:
|
||||
return 0
|
||||
case 3:
|
||||
return 0
|
||||
case 5:
|
||||
return 9.99
|
||||
case 7:
|
||||
return 10
|
||||
case 11:
|
||||
return 10
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
assert.Panics(t, func() { controller.MaxOrRandomLoadId(IDtoLoad, pop) })
|
||||
IDtoLoad[1] = 100.
|
||||
assert.Panics(t, func() { controller.MaxOrRandomLoadId(IDtoLoad) })
|
||||
assert.Panics(t, func() { controller.MaxOrRandomLoadId(IDtoLoad, pop) })
|
||||
|
||||
IDtoLoad[5] = 100.001
|
||||
assert.Equal(t, 5, controller.MaxOrRandomLoadId(IDtoLoad))
|
||||
assert.Equal(t, 5, controller.MaxOrRandomLoadId(IDtoLoad, pop))
|
||||
|
||||
IDtoLoad[3] = 100.
|
||||
assert.NotContains(t, []int{1, 3}, controller.MaxOrRandomLoadId(IDtoLoad))
|
||||
assert.NotContains(t, []int{1, 3}, controller.MaxOrRandomLoadId(IDtoLoad, pop))
|
||||
|
||||
IDtoLoad[7] = 100.001
|
||||
assert.Equal(t, 7, controller.MaxOrRandomLoadId(IDtoLoad, pop))
|
||||
|
||||
IDtoLoad[11] = 100.001
|
||||
rndCount := make(map[int]int)
|
||||
for range 100 {
|
||||
id := controller.MaxOrRandomLoadId(IDtoLoad)
|
||||
assert.NotContains(t, []int{1, 3}, id)
|
||||
assert.Contains(t, []int{5, 7}, id)
|
||||
id := controller.MaxOrRandomLoadId(IDtoLoad, pop)
|
||||
assert.NotContains(t, []int{1, 3, 5}, id)
|
||||
assert.Contains(t, []int{7, 11}, id)
|
||||
rndCount[id]++
|
||||
}
|
||||
assert.Greater(t, rndCount[5], 10)
|
||||
assert.Greater(t, rndCount[7], 10)
|
||||
assert.Greater(t, rndCount[11], 10)
|
||||
}
|
||||
|
||||
func TestSelectColUnloadGroup(t *testing.T) {
|
||||
|
||||
@@ -5,7 +5,6 @@ import (
|
||||
"fmt"
|
||||
"iter"
|
||||
"maps"
|
||||
"math/rand/v2"
|
||||
"slices"
|
||||
|
||||
"github.com/google/uuid"
|
||||
@@ -283,97 +282,16 @@ func (c *Cache) shipGroupUnload(ri int, groupID uuid.UUID, quantity float64) err
|
||||
return e.NewEntityNotOwnedError("planet #%d unload %v", p.Number, ct)
|
||||
}
|
||||
|
||||
toBeUnloaded := quantity
|
||||
if quantity == 0 {
|
||||
toBeUnloaded = float64(c.ShipGroup(sgi).Load)
|
||||
}
|
||||
if toBeUnloaded > float64(c.ShipGroup(sgi).Load) {
|
||||
return e.NewCargoUnoadNotEnoughError("load: %.03f", c.ShipGroup(sgi).Load)
|
||||
}
|
||||
|
||||
c.unsafeUnloadCargo(sgi, toBeUnloaded)
|
||||
|
||||
c.unsafeUnloadCargo(sgi, UnloadCargoRequest(float64(c.ShipGroup(sgi).Load), quantity))
|
||||
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))
|
||||
func UnloadCargoRequest(load, quantity float64) float64 {
|
||||
result := quantity
|
||||
if result == 0 || result > load {
|
||||
result = load
|
||||
}
|
||||
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
|
||||
return result
|
||||
}
|
||||
|
||||
func (c *Cache) shipGroupIndexByID(id uuid.UUID) (int, bool) {
|
||||
|
||||
@@ -447,11 +447,6 @@ func TestShipGroupUnload(t *testing.T) {
|
||||
assert.ErrorContains(t,
|
||||
g.ShipGroupUnload(Race_0.Name, c.ShipGroup(4).ID, 0),
|
||||
e.GenericErrorText(e.ErrInputEntityNotOwned))
|
||||
c.ShipGroup(0).CargoType = game.CargoColonist.Ref()
|
||||
c.ShipGroup(0).Load = 100
|
||||
assert.ErrorContains(t,
|
||||
g.ShipGroupUnload(Race_0.Name, c.ShipGroup(0).ID, 101),
|
||||
e.GenericErrorText(e.ErrInputCargoUnoadNotEnough))
|
||||
|
||||
assert.Len(t, slices.Collect(c.RaceShipGroups(Race_0_idx)), 6)
|
||||
|
||||
@@ -468,7 +463,9 @@ func TestShipGroupUnload(t *testing.T) {
|
||||
assert.Nil(t, c.ShipGroup(5).CargoType)
|
||||
|
||||
// unload ALL
|
||||
assert.NoError(t, g.ShipGroupUnload(Race_0.Name, c.ShipGroup(0).ID, 0))
|
||||
c.ShipGroup(0).CargoType = game.CargoColonist.Ref()
|
||||
c.ShipGroup(0).Load = 100
|
||||
assert.NoError(t, g.ShipGroupUnload(Race_0.Name, c.ShipGroup(0).ID, 101))
|
||||
assert.Equal(t, 100.0, number.Fixed3(c.MustPlanet(R0_Planet_0_num).Colonists.F()))
|
||||
assert.Equal(t, 0.0, number.Fixed3(c.ShipGroup(0).Load.F()))
|
||||
assert.Nil(t, c.ShipGroup(0).CargoType)
|
||||
|
||||
Reference in New Issue
Block a user