tests: produce on planets, unload on routes

This commit is contained in:
Ilia Denisov
2026-01-23 00:28:23 +02:00
parent 9825e05c0e
commit 812e0d4afd
15 changed files with 522 additions and 103 deletions
+2 -2
View File
@@ -71,8 +71,8 @@ func (c *Cache) ProduceBombings() []BombingPlanetReport {
} else { } else {
// Если на планете остались также и колонисты, то они превращаются в население, // Если на планете остались также и колонисты, то они превращаются в население,
// а накопленная промышленность возмещает потери производства. // а накопленная промышленность возмещает потери производства.
p.UnpackCOLtoPOP() p.UnpackColonists()
p.UnpackCAPtoIND() p.UnpackCapital()
} }
} }
return report return report
+10 -2
View File
@@ -67,8 +67,16 @@ func (c *Cache) RaceTechLevel(ri int, t game.Tech, v float64) {
c.raceTechLevel(ri, t, v) c.raceTechLevel(ri, t, v)
} }
func (c *Cache) ListRouteEligibleGroupIds(pn uint) iter.Seq[int] { func (c *Cache) ListRoutedSendGroupIds(pn uint) iter.Seq[int] {
return c.listRouteEligibleGroupIds(pn) return c.listRoutedSendGroupIds(pn)
}
func (c *Cache) ListRoutedUnloadShipGroupIds(pn uint, rt game.RouteType) iter.Seq[int] {
return c.listRoutedUnloadShipGroupIds(pn, rt)
}
func (c *Cache) SelectColUnloadGroup(groups []int) (result iter.Seq[int]) {
return c.selectColUnloadGroup(groups)
} }
func (c *Cache) ListMoveableGroupIds() iter.Seq[int] { func (c *Cache) ListMoveableGroupIds() iter.Seq[int] {
+2
View File
@@ -57,6 +57,7 @@ var (
Race_1_Cruiser_idx = 2 Race_1_Cruiser_idx = 2
Uninhabited_Planet_3_num uint = 3 Uninhabited_Planet_3_num uint = 3
Uninhabited_Planet_4_num uint = 4
ShipType_Cruiser = "Cruiser" ShipType_Cruiser = "Cruiser"
@@ -100,6 +101,7 @@ func newGame() *game.Game {
controller.NewPlanet(R1_Planet_1_num, "Planet_1", Race_1.ID, 2, 2, 100, 0, 0, 0, game.ProductionNone.AsType(uuid.Nil)), controller.NewPlanet(R1_Planet_1_num, "Planet_1", Race_1.ID, 2, 2, 100, 0, 0, 0, game.ProductionNone.AsType(uuid.Nil)),
controller.NewPlanet(R0_Planet_2_num, "Planet_2", Race_0.ID, 3, 3, 100, 0, 0, 0, game.ProductionNone.AsType(uuid.Nil)), controller.NewPlanet(R0_Planet_2_num, "Planet_2", Race_0.ID, 3, 3, 100, 0, 0, 0, game.ProductionNone.AsType(uuid.Nil)),
controller.NewPlanet(Uninhabited_Planet_3_num, "Planet_3", uuid.Nil, 500, 500, 100, 0, 0, 0, game.ProductionNone.AsType(uuid.Nil)), controller.NewPlanet(Uninhabited_Planet_3_num, "Planet_3", uuid.Nil, 500, 500, 100, 0, 0, 0, game.ProductionNone.AsType(uuid.Nil)),
controller.NewPlanet(Uninhabited_Planet_4_num, "Planet_4", uuid.Nil, 10, 10, 500, 0, 0, 10, game.ProductionNone.AsType(uuid.Nil)),
}, },
}, },
} }
-2
View File
@@ -34,11 +34,9 @@ func MakeTurn(c *Controller, r Repo, g *game.Game) error {
// 09. Корабли, где это возможно, объединяются в группы. // 09. Корабли, где это возможно, объединяются в группы.
// 10. На планетах производится промышленность, добывается сырье, разрабатываются новые технологии. // 10. На планетах производится промышленность, добывается сырье, разрабатываются новые технологии.
// 11. Увеличивается население планет. // 11. Увеличивается население планет.
// TODO: tests
c.Cache.TurnPlanetProductions() c.Cache.TurnPlanetProductions()
// 12. Товары выгружаются в конце грузовых маршрутов. // 12. Товары выгружаются в конце грузовых маршрутов.
// TODO: tests
c.Cache.TurnUnloadEnroutedGroups() c.Cache.TurnUnloadEnroutedGroups()
/*** Last steps ***/ /*** Last steps ***/
+10 -17
View File
@@ -211,29 +211,30 @@ func (c *Cache) TurnPlanetProductions() {
} }
case game.ResearchScience: case game.ResearchScience:
sc := c.mustScience(ri, *p.Production.SubjectID) sc := c.mustScience(ri, *p.Production.SubjectID)
IncreaseTech(r, p, sc.Drive, sc.Weapons, sc.Shields, sc.Cargo) ResearchTech(r, p.ProductionCapacity(), sc.Drive, sc.Weapons, sc.Shields, sc.Cargo)
case game.ResearchDrive: case game.ResearchDrive:
IncreaseTech(r, p, 1., 0, 0, 0) ResearchTech(r, p.ProductionCapacity(), 1., 0, 0, 0)
case game.ResearchWeapons: case game.ResearchWeapons:
IncreaseTech(r, p, 0, 1., 0, 0) ResearchTech(r, p.ProductionCapacity(), 0, 1., 0, 0)
case game.ResearchShields: case game.ResearchShields:
IncreaseTech(r, p, 0, 0, 1., 0) ResearchTech(r, p.ProductionCapacity(), 0, 0, 1., 0)
case game.ResearchCargo: case game.ResearchCargo:
IncreaseTech(r, p, 0, 0, 0, 1.) ResearchTech(r, p.ProductionCapacity(), 0, 0, 0, 1.)
case game.ProductionMaterial: case game.ProductionMaterial:
p.IncreaseMaterial() p.ProduceMaterial()
case game.ProductionCapital: case game.ProductionCapital:
p.IncreaseIndustry() p.ProduceIndustry()
default: default:
panic(fmt.Sprintf("unprocessed production type: %v", pt)) panic(fmt.Sprintf("unprocessed production type: %v", pt))
} }
p.IncreasePopulation() // last step: increase population / colonists
p.ProducePopulation()
} }
c.TurnMergeEqualShipGroups() c.TurnMergeEqualShipGroups()
} }
// listProducingPlanets iterates over all inhabited planet numbers with production not set to None. // listProducingPlanets iterates over all inhabited planet numbers with defined production type.
// Planets producing ships guaranteed to be iterated first for correct turn actions order. // Planets producing ships guaranteed to be iterated first for correct turn actions order.
func (c *Cache) listProducingPlanets() iter.Seq[uint] { func (c *Cache) listProducingPlanets() iter.Seq[uint] {
ordered := make([]int, 0) ordered := make([]int, 0)
@@ -261,14 +262,6 @@ func (c *Cache) listProducingPlanets() iter.Seq[uint] {
} }
} }
func IncreaseTech(r *game.Race, p *game.Planet, d, w, s, c float64) {
increment := 5000. / p.ProductionCapacity()
r.Tech.Set(game.TechDrive, r.Tech.Value(game.TechDrive)+increment*d)
r.Tech.Set(game.TechWeapons, r.Tech.Value(game.TechWeapons)+increment*w)
r.Tech.Set(game.TechShields, r.Tech.Value(game.TechShields)+increment*s)
r.Tech.Set(game.TechCargo, r.Tech.Value(game.TechCargo)+increment*c)
}
// Internal funcs // Internal funcs
func (c *Cache) putPopulation(pn uint, v float64) { func (c *Cache) putPopulation(pn uint, v float64) {
+58
View File
@@ -229,3 +229,61 @@ func TestListProducingPlanets(t *testing.T) {
assert.Equal(t, R0_Planet_2_num, c.MustPlanet(planets[0]).Number) assert.Equal(t, R0_Planet_2_num, c.MustPlanet(planets[0]).Number)
assert.Equal(t, R0_Planet_0_num, c.MustPlanet(planets[1]).Number) assert.Equal(t, R0_Planet_0_num, c.MustPlanet(planets[1]).Number)
} }
func TestTurnPlanetProductions(t *testing.T) {
c, g := newCache()
assert.NoError(t, c.CreateShipType(Race_0_idx, "Drone", 1, 0, 0, 0, 0))
assert.NoError(t, g.CreateScience(Race_0.Name, "Equality", 0.25, 0.25, 0.25, 0.25))
c.MustPlanet(R0_Planet_0_num).Resources = 10.
c.MustPlanet(R0_Planet_0_num).Size = 1000.
c.MustPlanet(R0_Planet_0_num).Population = 1000.
c.MustPlanet(R0_Planet_0_num).Industry = 1000.
pn := int(R0_Planet_0_num)
assert.NoError(t, g.PlanetProduction(Race_0.Name, pn, "CAP", ""))
assert.Equal(t, 0.0, c.MustPlanet(R0_Planet_0_num).Capital)
assert.Equal(t, 0.0, c.MustPlanet(R0_Planet_0_num).Colonists)
c.TurnPlanetProductions()
assert.InDelta(t, 196., c.MustPlanet(R0_Planet_0_num).Capital, 0.1)
assert.InDelta(t, 10.0, c.MustPlanet(R0_Planet_0_num).Colonists, 0.000001) // FIXME: should store more exact value
assert.NoError(t, g.PlanetProduction(Race_0.Name, pn, "MAT", ""))
assert.Equal(t, 0.0, c.MustPlanet(R0_Planet_0_num).Material)
c.TurnPlanetProductions()
assert.Equal(t, 10000., c.MustPlanet(R0_Planet_0_num).Material)
assert.InDelta(t, 20.0, c.MustPlanet(R0_Planet_0_num).Colonists, 0.000001)
assert.NoError(t, g.PlanetProduction(Race_0.Name, pn, "DRIVE", ""))
assert.Equal(t, 1.1, c.Race(Race_0_idx).TechLevel(game.TechDrive))
c.TurnPlanetProductions()
assert.Equal(t, 1.3, c.Race(Race_0_idx).TechLevel(game.TechDrive))
assert.NoError(t, g.PlanetProduction(Race_0.Name, pn, "WEAPONS", ""))
assert.Equal(t, 1.2, c.Race(Race_0_idx).TechLevel(game.TechWeapons))
c.TurnPlanetProductions()
assert.Equal(t, 1.4, c.Race(Race_0_idx).TechLevel(game.TechWeapons))
assert.NoError(t, g.PlanetProduction(Race_0.Name, pn, "SHIELDS", ""))
assert.Equal(t, 1.3, c.Race(Race_0_idx).TechLevel(game.TechShields))
c.TurnPlanetProductions()
assert.Equal(t, 1.5, c.Race(Race_0_idx).TechLevel(game.TechShields))
assert.NoError(t, g.PlanetProduction(Race_0.Name, pn, "CARGO", ""))
assert.Equal(t, 1.4, c.Race(Race_0_idx).TechLevel(game.TechCargo))
c.TurnPlanetProductions()
assert.InDelta(t, 1.6, c.Race(Race_0_idx).TechLevel(game.TechCargo), 0.1) // FIXME: 1.5999999999999999 -> 1.6
assert.NoError(t, g.PlanetProduction(Race_0.Name, pn, "SCIENCE", "Equality"))
c.TurnPlanetProductions()
assert.Equal(t, 1.35, c.Race(Race_0_idx).TechLevel(game.TechDrive))
assert.Equal(t, 1.45, c.Race(Race_0_idx).TechLevel(game.TechWeapons))
assert.Equal(t, 1.55, c.Race(Race_0_idx).TechLevel(game.TechShields))
assert.Equal(t, 1.65, c.Race(Race_0_idx).TechLevel(game.TechCargo))
assert.NoError(t, g.PlanetProduction(Race_0.Name, pn, "SHIP", "Drone"))
assert.Len(t, slices.Collect(c.RaceShipGroups(Race_0_idx)), 0)
c.TurnPlanetProductions()
assert.Len(t, slices.Collect(c.RaceShipGroups(Race_0_idx)), 1)
assert.Equal(t, uint(100), c.ShipGroup(0).Number)
}
+63 -39
View File
@@ -41,7 +41,7 @@ func (c *Cache) SetRoute(ri int, rt game.RouteType, origin, destination uint) er
} }
rangeToDestination := util.ShortDistance(c.g.Map.Width, c.g.Map.Height, p1.X, p1.Y, p2.X, p2.Y) rangeToDestination := util.ShortDistance(c.g.Map.Width, c.g.Map.Height, p1.X, p1.Y, p2.X, p2.Y)
if rangeToDestination > c.g.Race[ri].FlightDistance() { if rangeToDestination > c.g.Race[ri].FlightDistance() {
return e.NewSendUnreachableDestinationError("range=%.03f", rangeToDestination) return e.NewSendUnreachableDestinationError("range=%.03f max=%.03f", rangeToDestination, c.g.Race[ri].FlightDistance())
} }
c.SetPlanetRoute(rt, origin, destination) c.SetPlanetRoute(rt, origin, destination)
@@ -97,7 +97,7 @@ func (c *Cache) EnrouteGroups() {
if len(c.g.Map.Planet[pi].Route) == 0 { if len(c.g.Map.Planet[pi].Route) == 0 {
continue continue
} }
groups := slices.Collect(c.listRouteEligibleGroupIds(c.g.Map.Planet[pi].Number)) groups := slices.Collect(c.listRoutedSendGroupIds(c.g.Map.Planet[pi].Number))
if len(groups) == 0 { if len(groups) == 0 {
continue continue
} }
@@ -171,7 +171,7 @@ func (c *Cache) EnrouteGroups() {
} }
} }
func (c *Cache) listRouteEligibleGroupIds(pn uint) iter.Seq[int] { func (c *Cache) listRoutedSendGroupIds(pn uint) iter.Seq[int] {
return func(yield func(int) bool) { return func(yield func(int) bool) {
p := c.MustPlanet(pn) p := c.MustPlanet(pn)
for i := range c.ShipGroupsIndex() { for i := range c.ShipGroupsIndex() {
@@ -194,63 +194,83 @@ func (c *Cache) listRouteEligibleGroupIds(pn uint) iter.Seq[int] {
// Невозможно лишь выгрузить колонистов на чужой планете. // Невозможно лишь выгрузить колонистов на чужой планете.
func (c *Cache) TurnUnloadEnroutedGroups() { func (c *Cache) TurnUnloadEnroutedGroups() {
for pi := range c.g.Map.Planet { for i := range c.g.Map.Planet {
p := &c.g.Map.Planet[pi] p := &c.g.Map.Planet[i]
colGroups := c.listUnloadEligibleShipGroupIds(p.Number, game.RouteColonist) c.doUnload(c.unloadRoutedColonists(p.Number, c.listRoutedUnloadShipGroupIds(p.Number, game.RouteColonist)))
if p.Owner == uuid.Nil {
c.selectColUnloadGroup(colGroups)
} else {
for sgi := range colGroups {
sg := c.ShipGroup(sgi)
if sg.OwnerID != p.Owner {
continue
}
c.unloadCargoUnsafe(sgi, sg.Load)
}
}
for _, rt := range []game.RouteType{game.RouteMaterial, game.RouteCapital} { for _, rt := range []game.RouteType{game.RouteMaterial, game.RouteCapital} {
for sgi := range c.listUnloadEligibleShipGroupIds(p.Number, rt) { c.doUnload(c.listRoutedUnloadShipGroupIds(p.Number, rt))
c.unloadCargoUnsafe(sgi, c.ShipGroup(sgi).Load)
}
} }
} }
} }
func (c *Cache) selectColUnloadGroup(seq iter.Seq[int]) { func (c *Cache) doUnload(groups iter.Seq[int]) {
for sgi := range groups {
c.unsafeUnloadCargo(sgi, c.ShipGroup(sgi).Load)
}
}
func (c *Cache) unloadRoutedColonists(pn uint, groups iter.Seq[int]) iter.Seq[int] {
p := c.MustPlanet(pn)
gr := slices.Collect(groups)
if p.Owner == uuid.Nil {
return c.selectColUnloadGroup(gr)
}
return func(yield func(int) bool) {
for _, sgi := range gr {
sg := c.ShipGroup(sgi)
if p.Owner != sg.OwnerID {
continue
}
if !yield(sgi) {
return
}
}
}
}
func (c *Cache) selectColUnloadGroup(groups []int) (result iter.Seq[int]) {
groupByRace := make(map[int][]int) groupByRace := make(map[int][]int)
loadByRace := make(map[int]float64) loadByRace := make(map[int]float64)
for i := range seq { for _, i := range groups {
sg := c.ShipGroup(i) sg := c.ShipGroup(i)
ri := c.RaceIndex(sg.OwnerID) ri := c.RaceIndex(sg.OwnerID)
groupByRace[ri] = append(groupByRace[ri], i) groupByRace[ri] = append(groupByRace[ri], i)
loadByRace[ri] += sg.Load loadByRace[ri] += sg.Load
} }
if len(loadByRace) < 2 { if len(loadByRace) < 2 {
for _, gr := range groupByRace { // only one race has to unload cargo
for _, sgi := range gr { result = slices.Values(groups)
c.unloadCargoUnsafe(sgi, c.ShipGroup(sgi).Load)
}
}
return return
} }
// select winner to unload // select winner to unload cargo
id := MaxOrRandomLoadId(loadByRace)
result = slices.Values(groupByRace[id])
return
}
func MaxOrRandomLoadId(IDtoLoad map[int]float64) int {
if len(IDtoLoad) < 2 {
panic("IDtoLoad 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]) })
raceIdx := slices.Collect(maps.Keys(loadByRace))
slices.SortFunc(raceIdx, func(ri1, ri2 int) int { return cmp.Compare(loadByRace[ri2], loadByRace[ri1]) })
if loadByRace[raceIdx[0]] == loadByRace[raceIdx[1]] {
// no single winner with highest load // no single winner with highest load
raceIdx = slices.DeleteFunc(raceIdx, func(v int) bool { return loadByRace[v] < loadByRace[raceIdx[0]] }) if IDtoLoad[IDs[0]] == IDtoLoad[IDs[1]] {
rand.Shuffle(len(raceIdx), func(i, j int) { raceIdx[i], raceIdx[j] = raceIdx[j], raceIdx[i] }) // remove IDs which load less than maximum
// now raceIdx[0] has a random race index IDs = slices.DeleteFunc(IDs, func(v int) bool { return IDtoLoad[v] < IDtoLoad[IDs[0]] })
} // IDs[0] will have random index
for _, sgi := range groupByRace[raceIdx[0]] { rand.Shuffle(len(IDs), func(i, j int) { IDs[i], IDs[j] = IDs[j], IDs[i] })
c.unloadCargoUnsafe(sgi, c.ShipGroup(sgi).Load)
} }
return IDs[0]
} }
func (c *Cache) listUnloadEligibleShipGroupIds(pn uint, routeType game.RouteType) iter.Seq[int] { func (c *Cache) listRoutedUnloadShipGroupIds(pn uint, routeType game.RouteType) iter.Seq[int] {
return func(yield func(int) bool) { return func(yield func(int) bool) {
yielded := make(map[int]bool)
for i := range c.g.Map.Planet { for i := range c.g.Map.Planet {
for rt, dest := range c.g.Map.Planet[i].Route { for rt, dest := range c.g.Map.Planet[i].Route {
if dest != pn || rt != routeType { if dest != pn || rt != routeType {
@@ -258,12 +278,16 @@ func (c *Cache) listUnloadEligibleShipGroupIds(pn uint, routeType game.RouteType
} }
for i := range c.ShipGroupsIndex() { for i := range c.ShipGroupsIndex() {
sg := c.ShipGroup(i) sg := c.ShipGroup(i)
if sg.FleetID != nil || sg.State() != game.StateInOrbit || sg.CargoType == nil { if _, ok := yielded[i]; ok || sg.FleetID != nil || sg.CargoType == nil || sg.Load == 0. || sg.State() != game.StateInOrbit || sg.Destination != dest {
continue
}
if v, ok := game.RouteToCargo[rt]; !ok || v != *sg.CargoType {
continue continue
} }
if !yield(i) { if !yield(i) {
return return
} }
yielded[i] = true
} }
} }
} }
+164 -2
View File
@@ -4,6 +4,7 @@ import (
"slices" "slices"
"testing" "testing"
"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/game" "github.com/iliadenisov/galaxy/internal/model/game"
@@ -91,7 +92,7 @@ func TestRemoveRoute(t *testing.T) {
e.GenericErrorText(e.ErrInputEntityNotOwned)) e.GenericErrorText(e.ErrInputEntityNotOwned))
} }
func TestListRouteEligibleGroupIds(t *testing.T) { func TestListRoutedSendGroupIds(t *testing.T) {
c, g := newCache() c, g := newCache()
// 1: idx = 0 / Ready to load // 1: idx = 0 / Ready to load
@@ -120,7 +121,7 @@ func TestListRouteEligibleGroupIds(t *testing.T) {
assert.NoError(t, c.CreateShips(Race_0_idx, Race_0_Freighter, R0_Planet_0_num, 10)) assert.NoError(t, c.CreateShips(Race_0_idx, Race_0_Freighter, R0_Planet_0_num, 10))
assert.NoError(t, g.JoinShipGroupToFleet(Race_0.Name, "Fleet", 5, 0)) assert.NoError(t, g.JoinShipGroupToFleet(Race_0.Name, "Fleet", 5, 0))
planet_0_groups := slices.Collect(c.ListRouteEligibleGroupIds(0)) planet_0_groups := slices.Collect(c.ListRoutedSendGroupIds(0))
assert.Len(t, planet_0_groups, 1) assert.Len(t, planet_0_groups, 1)
for _, i := range planet_0_groups { for _, i := range planet_0_groups {
sg := c.ShipGroup(i) sg := c.ShipGroup(i)
@@ -254,3 +255,164 @@ func TestEnrouteGroups_LaunchOrder(t *testing.T) {
assert.Nil(t, c.ShipGroup(sgi).CargoType) assert.Nil(t, c.ShipGroup(sgi).CargoType)
assert.Equal(t, uint(1), c.ShipGroup(sgi).Number) assert.Equal(t, uint(1), c.ShipGroup(sgi).Number)
} }
func TestListRoutedUnloadShipGroupIds(t *testing.T) {
c, g := newCache()
// 1: idx = 0 / Empty cargo
assert.NoError(t, c.CreateShips(Race_0_idx, Race_0_Freighter, R0_Planet_0_num, 10))
c.ShipGroup(0).CargoType = game.CargoColonist.Ref()
c.ShipGroup(0).Load = 0.
// 2: idx = 1 / Has no cargo bay
assert.NoError(t, c.CreateShips(Race_0_idx, Race_0_Gunship, R0_Planet_0_num, 1))
// 3: idx = 2 / In_Space
assert.NoError(t, c.CreateShips(Race_0_idx, Race_0_Freighter, R0_Planet_0_num, 7))
c.ShipGroup(2).StateInSpace = &game.InSpace{
Origin: 2,
Range: 31.337,
}
// 4: idx = 3 / loaded with COL
assert.NoError(t, c.CreateShips(Race_0_idx, Race_0_Freighter, R0_Planet_0_num, 11))
c.ShipGroup(3).CargoType = game.CargoColonist.Ref()
c.ShipGroup(3).Load = 1.234
// 5: idx = 4 / Part of the Fleet
assert.NoError(t, c.CreateShips(Race_0_idx, Race_0_Freighter, R0_Planet_0_num, 10))
assert.NoError(t, g.JoinShipGroupToFleet(Race_0.Name, "Fleet", 5, 0))
assert.NoError(t, g.SetRoute(Race_0.Name, "COL", R0_Planet_0_num, R0_Planet_2_num))
for _, rt := range game.RouteTypeSet {
groups := slices.Collect(c.ListRoutedUnloadShipGroupIds(R0_Planet_2_num, rt))
assert.Len(t, groups, 0, "route: %v", rt)
groups = slices.Collect(c.ListRoutedUnloadShipGroupIds(R0_Planet_0_num, rt))
assert.Len(t, groups, 0, "route: %v", rt)
}
// double route from different planets - must not double group ids
assert.NoError(t, g.SetRoute(Race_0.Name, "COL", R0_Planet_2_num, R0_Planet_0_num))
assert.NoError(t, g.SetRoute(Race_1.Name, "COL", R1_Planet_1_num, R0_Planet_0_num))
// 6: idx = 5 / loaded with CAP
assert.NoError(t, c.CreateShips(Race_0_idx, Race_0_Freighter, R0_Planet_0_num, 11))
c.ShipGroup(5).CargoType = game.CargoCapital.Ref()
c.ShipGroup(5).Load = 1.234
groups := slices.Collect(c.ListRoutedUnloadShipGroupIds(R0_Planet_0_num, game.RouteColonist))
assert.Len(t, groups, 1)
for _, sgi := range groups {
assert.Greater(t, c.ShipGroup(sgi).Load, 0.)
assert.Equal(t, game.StateInOrbit, c.ShipGroup(sgi).State())
assert.Equal(t, R0_Planet_0_num, c.ShipGroup(sgi).Destination)
}
groups = slices.Collect(c.ListRoutedUnloadShipGroupIds(R0_Planet_0_num, game.RouteMaterial))
assert.Len(t, groups, 0)
groups = slices.Collect(c.ListRoutedUnloadShipGroupIds(R0_Planet_0_num, game.RouteCapital))
assert.Len(t, groups, 0)
}
func TestMaxOrRandomLoadId(t *testing.T) {
IDtoLoad := make(map[int]float64)
assert.Panics(t, func() { controller.MaxOrRandomLoadId(IDtoLoad) })
IDtoLoad[1] = 100.
assert.Panics(t, func() { controller.MaxOrRandomLoadId(IDtoLoad) })
IDtoLoad[5] = 100.001
assert.Equal(t, 5, controller.MaxOrRandomLoadId(IDtoLoad))
IDtoLoad[3] = 100.
assert.NotContains(t, []int{1, 3}, controller.MaxOrRandomLoadId(IDtoLoad))
IDtoLoad[7] = 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)
rndCount[id]++
}
assert.Greater(t, rndCount[5], 10)
assert.Greater(t, rndCount[7], 10)
}
func TestSelectColUnloadGroup(t *testing.T) {
c, g := newCache()
assert.NoError(t, g.SetRoute(Race_0.Name, "COL", R0_Planet_2_num, R0_Planet_0_num))
assert.NoError(t, g.SetRoute(Race_1.Name, "COL", R1_Planet_1_num, R0_Planet_0_num))
// 1: idx = 0 / Loaded COL
assert.NoError(t, c.CreateShips(Race_0_idx, Race_0_Freighter, R0_Planet_0_num, 10))
c.ShipGroup(0).CargoType = game.CargoColonist.Ref()
c.ShipGroup(0).Load = 7.
// 2: idx = 1 / Loaded COL
assert.NoError(t, c.CreateShips(Race_0_idx, Race_0_Freighter, R0_Planet_0_num, 5))
c.ShipGroup(1).CargoType = game.CargoColonist.Ref()
c.ShipGroup(1).Load = 5.
groups := slices.Collect(c.ListRoutedUnloadShipGroupIds(R0_Planet_0_num, game.RouteColonist))
assert.Len(t, groups, 2)
unloadGroups := slices.Collect(c.SelectColUnloadGroup(groups))
assert.ElementsMatch(t, groups, unloadGroups)
// 3: idx = 2 / Loaded COL - another race, winner
assert.NoError(t, c.CreateShips(Race_1_idx, Race_1_Freighter, R1_Planet_1_num, 10))
c.ShipGroup(2).Destination = R0_Planet_0_num
c.ShipGroup(2).CargoType = game.CargoColonist.Ref()
c.ShipGroup(2).Load = 12.1
groups = slices.Collect(c.ListRoutedUnloadShipGroupIds(R0_Planet_0_num, game.RouteColonist))
assert.Len(t, groups, 3)
unloadGroups = slices.Collect(c.SelectColUnloadGroup(groups))
assert.Equal(t, 2, unloadGroups[0])
}
func TestTurnUnloadEnroutedGroups(t *testing.T) {
c, g := newCache()
assert.NoError(t, g.SetRoute(Race_0.Name, "MAT", R0_Planet_2_num, R0_Planet_0_num))
assert.NoError(t, g.SetRoute(Race_0.Name, "CAP", R0_Planet_2_num, R0_Planet_0_num))
assert.NoError(t, g.SetRoute(Race_1.Name, "COL", R1_Planet_1_num, R0_Planet_0_num))
assert.NoError(t, g.SetRoute(Race_1.Name, "COL", R1_Planet_1_num, Uninhabited_Planet_4_num))
// 1: idx = 0 / Loaded MAT
assert.NoError(t, c.CreateShips(Race_0_idx, Race_0_Freighter, R0_Planet_0_num, 10))
c.ShipGroup(0).CargoType = game.CargoMaterial.Ref()
c.ShipGroup(0).Load = 222.
// 2: idx = 1 / Loaded CAP
assert.NoError(t, c.CreateShips(Race_0_idx, Race_0_Freighter, R0_Planet_0_num, 10))
c.ShipGroup(1).CargoType = game.CargoCapital.Ref()
c.ShipGroup(1).Load = 11.
// 3: idx = 2 / Loaded COL - on empty planet
assert.NoError(t, c.CreateShips(Race_1_idx, Race_1_Freighter, R1_Planet_1_num, 10))
c.ShipGroup(2).Destination = Uninhabited_Planet_4_num
c.ShipGroup(2).CargoType = game.CargoColonist.Ref()
c.ShipGroup(2).Load = 12.1
// 4: idx = 3 / Loaded COL - on inhabited planet
assert.NoError(t, c.CreateShips(Race_1_idx, Race_1_Freighter, R1_Planet_1_num, 10))
c.ShipGroup(3).Destination = R0_Planet_0_num
c.ShipGroup(3).CargoType = game.CargoColonist.Ref()
c.ShipGroup(3).Load = 17.3
c.TurnUnloadEnroutedGroups()
assert.Equal(t, 0., c.ShipGroup(0).Load)
assert.Equal(t, 222., c.MustPlanet(R0_Planet_0_num).Material)
assert.Equal(t, 0., c.ShipGroup(1).Load)
assert.Equal(t, 11., c.MustPlanet(R0_Planet_0_num).Capital)
assert.Equal(t, 0., c.ShipGroup(2).Load)
assert.Equal(t, 96.8, c.MustPlanet(Uninhabited_Planet_4_num).Population)
assert.Equal(t, Race_1_ID, c.MustPlanet(Uninhabited_Planet_4_num).Owner)
assert.Equal(t, game.ProductionCapital, c.MustPlanet(Uninhabited_Planet_4_num).Production.Type)
assert.Equal(t, 17.3, c.ShipGroup(3).Load)
}
+16 -1
View File
@@ -81,6 +81,22 @@ func (c *Cache) DeleteScience(ri int, name string) error {
return nil return nil
} }
func ResearchTech(r *game.Race, indCapacity float64, drive, weapons, shields, cargo float64) {
increment := indCapacity / 5000.
if drive > 0 {
r.Tech = r.Tech.Set(game.TechDrive, r.Tech.Value(game.TechDrive)+increment*drive)
}
if weapons > 0 {
r.Tech = r.Tech.Set(game.TechWeapons, r.Tech.Value(game.TechWeapons)+increment*weapons)
}
if shields > 0 {
r.Tech = r.Tech.Set(game.TechShields, r.Tech.Value(game.TechShields)+increment*shields)
}
if cargo > 0 {
r.Tech = r.Tech.Set(game.TechCargo, r.Tech.Value(game.TechCargo)+increment*cargo)
}
}
// Internal func // Internal func
func (c *Cache) raceScience(ri int) []game.Science { func (c *Cache) raceScience(ri int) []game.Science {
@@ -96,5 +112,4 @@ func (c *Cache) mustScience(ri int, id uuid.UUID) *game.Science {
panic(fmt.Sprintf("science not found for race=%q id=%v", r.Name, id)) panic(fmt.Sprintf("science not found for race=%q id=%v", r.Name, id))
} }
return &c.g.Race[ri].Sciences[i] return &c.g.Race[ri].Sciences[i]
} }
+35
View File
@@ -4,7 +4,9 @@ import (
"testing" "testing"
"github.com/google/uuid" "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/game"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
@@ -97,3 +99,36 @@ func TestDeleteScience(t *testing.T) {
g.DeleteScience(Race_0.Name, second), g.DeleteScience(Race_0.Name, second),
e.GenericErrorText(e.ErrDeleteSciencePlanetProduction)) e.GenericErrorText(e.ErrDeleteSciencePlanetProduction))
} }
func TestResearchTech(t *testing.T) {
r := Race_0
rr := &r
assert.Equal(t, 1.1, rr.Tech.Value(game.TechDrive))
assert.Equal(t, 1.2, rr.Tech.Value(game.TechWeapons))
assert.Equal(t, 1.3, rr.Tech.Value(game.TechShields))
assert.Equal(t, 1.4, rr.Tech.Value(game.TechCargo))
controller.ResearchTech(rr, 500, 1.0, 0.0, 0.0, 0.0)
assert.InDelta(t, 1.2, rr.Tech.Value(game.TechDrive), 0.000001)
assert.Equal(t, 1.2, rr.Tech.Value(game.TechWeapons))
assert.Equal(t, 1.3, rr.Tech.Value(game.TechShields))
assert.Equal(t, 1.4, rr.Tech.Value(game.TechCargo))
controller.ResearchTech(rr, 500, 0.0, 0.5, 0.0, 0.5)
assert.InDelta(t, 1.2, rr.Tech.Value(game.TechDrive), 0.000001)
assert.Equal(t, 1.25, rr.Tech.Value(game.TechWeapons))
assert.Equal(t, 1.3, rr.Tech.Value(game.TechShields))
assert.Equal(t, 1.45, rr.Tech.Value(game.TechCargo))
controller.ResearchTech(rr, 500, 0.5, 0.0, 0.5, 0.0)
assert.InDelta(t, 1.25, rr.Tech.Value(game.TechDrive), 0.000001)
assert.Equal(t, 1.25, rr.Tech.Value(game.TechWeapons))
assert.Equal(t, 1.35, rr.Tech.Value(game.TechShields))
assert.Equal(t, 1.45, rr.Tech.Value(game.TechCargo))
controller.ResearchTech(rr, 1000, 0.0, 1.0, 0.0, 0.0)
assert.InDelta(t, 1.25, rr.Tech.Value(game.TechDrive), 0.000001)
assert.Equal(t, 1.45, rr.Tech.Value(game.TechWeapons))
assert.Equal(t, 1.35, rr.Tech.Value(game.TechShields))
assert.Equal(t, 1.45, rr.Tech.Value(game.TechCargo))
}
+9 -5
View File
@@ -383,15 +383,18 @@ func (c *Cache) UnloadCargo(ri int, groupIndex uint, ships uint, quantity float6
return e.NewCargoUnoadNotEnoughError("load: %.03f", c.ShipGroup(sgi).Load) return e.NewCargoUnoadNotEnoughError("load: %.03f", c.ShipGroup(sgi).Load)
} }
c.unloadCargoUnsafe(sgi, toBeUnloaded) c.unsafeUnloadCargo(sgi, toBeUnloaded)
return nil return nil
} }
func (c *Cache) unloadCargoUnsafe(sgi int, q float64) { func (c *Cache) unsafeUnloadCargo(sgi int, q float64) {
if q <= 0 { if q <= 0 {
return return
} }
if st := c.ShipGroup(sgi).State(); st != game.StateInOrbit {
panic(fmt.Sprintf("invalid group state: %v", st))
}
c.validateShipGroupIndex(sgi) c.validateShipGroupIndex(sgi)
p := c.MustPlanet(c.ShipGroup(sgi).Destination) p := c.MustPlanet(c.ShipGroup(sgi).Destination)
ct := *c.ShipGroup(sgi).CargoType ct := *c.ShipGroup(sgi).CargoType
@@ -411,12 +414,13 @@ func (c *Cache) unloadCargoUnsafe(sgi int, q float64) {
} }
*availableOnPlanet += q *availableOnPlanet += q
// FIXME: unpack COL / CAP c.ShipGroup(sgi).Load -= q // TODO: apply rounding for Load property?
c.ShipGroup(sgi).Load -= q
if c.ShipGroup(sgi).Load == 0 { if c.ShipGroup(sgi).Load == 0 {
c.ShipGroup(sgi).CargoType = nil c.ShipGroup(sgi).CargoType = nil
} }
p.UnpackColonists()
p.UnpackCapital()
} }
func (c *Controller) GiveawayGroup(raceName, raceAcceptor string, groupIndex, quantity uint) error { func (c *Controller) GiveawayGroup(raceName, raceAcceptor string, groupIndex, quantity uint) error {
+1 -1
View File
@@ -200,7 +200,7 @@ func newGenericError(code int, arg ...any) error {
e.err = arg[i].(error) e.err = arg[i].(error)
i += 1 i += 1
} }
if len(arg) == i+2 { if len(arg) >= i+2 {
e.subject = fmt.Sprintf(asString(arg[i]), arg[i+1:]...) e.subject = fmt.Sprintf(asString(arg[i]), arg[i+1:]...)
} else if len(arg) == i+1 { } else if len(arg) == i+1 {
e.subject = asString(arg[i]) e.subject = asString(arg[i])
+22 -25
View File
@@ -56,7 +56,7 @@ func PlanetProduction(industry, population float64) float64 {
// [x] ind = (res * prod) / (5 * res + 1) // [x] ind = (res * prod) / (5 * res + 1)
// ind = 10 * 1000 / (5 * 10 + 1) = 10000 / 55 = 181.(81) // ind = 10 * 1000 / (5 * 10 + 1) = 10000 / 55 = 181.(81)
// int = 10 * 500 / 55 = 5000 / 55 // int = 10 * 500 / 55 = 5000 / 55
func (p *Planet) IncreaseIndustry() { func (p *Planet) ProduceIndustry() {
production := p.ProductionCapacity() production := p.ProductionCapacity()
var ind float64 var ind float64
if p.Material > 0 { if p.Material > 0 {
@@ -72,27 +72,13 @@ func (p *Planet) IncreaseIndustry() {
} }
} }
func (p *Planet) UnpackCAPtoIND() {
if p.Capital == 0 {
return
}
cap := p.Population - p.Industry
if cap > p.Capital {
cap = p.Capital
}
p.Capital -= cap
p.Industry += cap
}
// Производство материалов // Производство материалов
// TODO: test on real values func (p *Planet) ProduceMaterial() {
func (p *Planet) IncreaseMaterial() {
p.Material += p.ProductionCapacity() * p.Resources p.Material += p.ProductionCapacity() * p.Resources
} }
// Автоматическое увеличение населения на каждом ходу // Автоматическое увеличение населения на каждом ходу
// TODO: test - whether POP is busy on production or not? func (p *Planet) ProducePopulation() {
func (p *Planet) IncreasePopulation() {
p.Population *= 1.08 p.Population *= 1.08
if p.Population > p.Size { if p.Population > p.Size {
p.Colonists += (p.Population - p.Size) / 8. p.Colonists += (p.Population - p.Size) / 8.
@@ -100,17 +86,28 @@ func (p *Planet) IncreasePopulation() {
} }
} }
func (p *Planet) UnpackCOLtoPOP() { func (p *Planet) UnpackCapital() {
if p.Colonists < 1 { if p.Capital == 0 {
return return
} }
maxCOL := uint((p.Size - p.Population) / 8.) deficit := p.Population - p.Industry
if float64(maxCOL) > p.Colonists { if deficit > p.Capital {
maxCOL = uint(p.Colonists) deficit = p.Capital
} }
maxCOL = uint(float64(maxCOL) - math.Mod(float64(maxCOL), 8.)) p.Capital -= deficit
p.Colonists -= float64(maxCOL) p.Industry += deficit
p.Population += float64(maxCOL) * 8 }
func (p *Planet) UnpackColonists() {
if p.Colonists == 0 {
return
}
deficit := (p.Size - p.Population) / 8
if deficit > p.Colonists {
deficit = p.Colonists
}
p.Colonists -= deficit
p.Population += deficit * 8
} }
func UnloadColonists(p Planet, v float64) Planet { func UnloadColonists(p Planet, v float64) Planet {
+123 -5
View File
@@ -14,7 +14,7 @@ func TestPlanetProduction(t *testing.T) {
assert.Equal(t, 250., game.PlanetProduction(0., 1000.)) assert.Equal(t, 250., game.PlanetProduction(0., 1000.))
} }
func TestIncreaseIndustry(t *testing.T) { func TestProduceIndustry(t *testing.T) {
HW := &game.Planet{ HW := &game.Planet{
PlanetReport: game.PlanetReport{ PlanetReport: game.PlanetReport{
UninhabitedPlanet: game.UninhabitedPlanet{ UninhabitedPlanet: game.UninhabitedPlanet{
@@ -35,23 +35,141 @@ func TestIncreaseIndustry(t *testing.T) {
Industry: 500, Industry: 500,
}, },
} }
HW.IncreaseIndustry() HW.ProduceIndustry()
assert.InDelta(t, 196.078, HW.Capital, 0.0005) assert.InDelta(t, 196.078, HW.Capital, 0.0005)
HW.Capital = 0 HW.Capital = 0
HW.Material = 200 HW.Material = 200
HW.IncreaseIndustry() HW.ProduceIndustry()
assert.Equal(t, 200., HW.Capital) assert.Equal(t, 200., HW.Capital)
assert.Equal(t, 0., HW.Material) assert.Equal(t, 0., HW.Material)
DW.IncreaseIndustry() DW.ProduceIndustry()
assert.InDelta(t, 98.039, DW.Capital, 0.0003) assert.InDelta(t, 98.039, DW.Capital, 0.0003)
DW.Capital = 0 DW.Capital = 0
DW.Material = 100 DW.Material = 100
DW.IncreaseIndustry() DW.ProduceIndustry()
assert.Equal(t, 100., DW.Capital) assert.Equal(t, 100., DW.Capital)
assert.Equal(t, 0., DW.Material) assert.Equal(t, 0., DW.Material)
} }
func TestProduceMaterial(t *testing.T) {
HW := &game.Planet{
PlanetReport: game.PlanetReport{
UninhabitedPlanet: game.UninhabitedPlanet{
Size: 1000,
Resources: 10,
},
Population: 1000,
Industry: 1000,
},
}
assert.Equal(t, 0., HW.Material)
HW.ProduceMaterial()
assert.Equal(t, 10000., HW.Material)
HW.Industry = 500
HW.Population = 500
HW.ProduceMaterial()
assert.Equal(t, 15000., HW.Material)
HW.Population = 1000
HW.ProduceMaterial()
assert.Equal(t, 21250., HW.Material)
}
func TestUnpackCapital(t *testing.T) {
HW := &game.Planet{
PlanetReport: game.PlanetReport{
UninhabitedPlanet: game.UninhabitedPlanet{
Size: 1000,
Resources: 10,
},
Population: 1000,
Industry: 1000,
},
}
assert.Equal(t, 0., HW.Capital)
HW.UnpackCapital()
assert.Equal(t, 1000., HW.Industry)
assert.Equal(t, 0., HW.Capital)
HW.Capital = 123.
HW.UnpackCapital()
assert.Equal(t, 1000., HW.Industry)
assert.Equal(t, 123., HW.Capital)
HW.Industry = 987.
HW.UnpackCapital()
assert.Equal(t, 1000., HW.Industry)
assert.Equal(t, 110., HW.Capital)
HW.Population = 876.
HW.Industry = 800.
HW.UnpackCapital()
assert.Equal(t, 876., HW.Population)
assert.Equal(t, 876., HW.Industry)
assert.Equal(t, 34., HW.Capital)
}
func TestUnpackColonists(t *testing.T) {
HW := &game.Planet{
PlanetReport: game.PlanetReport{
UninhabitedPlanet: game.UninhabitedPlanet{
Size: 1000,
Resources: 10,
},
Population: 1000,
Industry: 1000,
},
}
assert.Equal(t, 0., HW.Colonists)
HW.UnpackColonists()
assert.Equal(t, 1000., HW.Population)
assert.Equal(t, 0., HW.Colonists)
HW.Colonists = 1.05
HW.UnpackColonists()
assert.Equal(t, 1000., HW.Population)
assert.Equal(t, 1.05, HW.Colonists)
HW.Population = 996.0
HW.UnpackColonists()
assert.Equal(t, 1000., HW.Population)
assert.Equal(t, 0.55, HW.Colonists)
HW.Population = 0.0
HW.UnpackColonists()
assert.Equal(t, 4.4, HW.Population)
assert.Equal(t, 0., HW.Colonists)
}
func TestProducePopulation(t *testing.T) {
HW := &game.Planet{
PlanetReport: game.PlanetReport{
UninhabitedPlanet: game.UninhabitedPlanet{
Size: 1000,
Resources: 10,
},
Population: 500,
Industry: 1000,
},
}
assert.Equal(t, 500., HW.Population)
assert.Equal(t, 0., HW.Colonists)
HW.ProducePopulation()
assert.Equal(t, 540., HW.Population)
assert.Equal(t, 0., HW.Colonists)
HW.Population = 1000.
HW.ProducePopulation()
assert.Equal(t, 1000., HW.Population)
assert.Equal(t, 10., HW.Colonists)
}
+5
View File
@@ -16,6 +16,11 @@ var (
RouteColonist.String(): RouteColonist, RouteColonist.String(): RouteColonist,
RouteEmpty.String(): RouteEmpty, RouteEmpty.String(): RouteEmpty,
} }
RouteToCargo map[RouteType]CargoType = map[RouteType]CargoType{
RouteColonist: CargoColonist,
RouteCapital: CargoCapital,
RouteMaterial: CargoMaterial,
}
) )
func (rt RouteType) Ref() *RouteType { func (rt RouteType) Ref() *RouteType {