From 812e0d4afd209e24b0290507b7383dbca3608341 Mon Sep 17 00:00:00 2001 From: Ilia Denisov Date: Fri, 23 Jan 2026 00:28:23 +0200 Subject: [PATCH] tests: produce on planets, unload on routes --- internal/controller/bombing.go | 4 +- internal/controller/controller_export_test.go | 12 +- internal/controller/controller_test.go | 2 + internal/controller/generate_turn.go | 2 - internal/controller/planet.go | 27 ++- internal/controller/planet_test.go | 58 ++++++ internal/controller/route.go | 106 ++++++----- internal/controller/route_test.go | 166 +++++++++++++++++- internal/controller/science.go | 17 +- internal/controller/science_test.go | 35 ++++ internal/controller/ship_group.go | 14 +- internal/error/generic.go | 2 +- internal/model/game/planet.go | 47 +++-- internal/model/game/planet_test.go | 128 +++++++++++++- internal/model/game/route.go | 5 + 15 files changed, 522 insertions(+), 103 deletions(-) diff --git a/internal/controller/bombing.go b/internal/controller/bombing.go index 1d2bd09..b168747 100644 --- a/internal/controller/bombing.go +++ b/internal/controller/bombing.go @@ -71,8 +71,8 @@ func (c *Cache) ProduceBombings() []BombingPlanetReport { } else { // Если на планете остались также и колонисты, то они превращаются в население, // а накопленная промышленность возмещает потери производства. - p.UnpackCOLtoPOP() - p.UnpackCAPtoIND() + p.UnpackColonists() + p.UnpackCapital() } } return report diff --git a/internal/controller/controller_export_test.go b/internal/controller/controller_export_test.go index 7f9e3c9..d40d524 100644 --- a/internal/controller/controller_export_test.go +++ b/internal/controller/controller_export_test.go @@ -67,8 +67,16 @@ func (c *Cache) RaceTechLevel(ri int, t game.Tech, v float64) { c.raceTechLevel(ri, t, v) } -func (c *Cache) ListRouteEligibleGroupIds(pn uint) iter.Seq[int] { - return c.listRouteEligibleGroupIds(pn) +func (c *Cache) ListRoutedSendGroupIds(pn uint) iter.Seq[int] { + 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] { diff --git a/internal/controller/controller_test.go b/internal/controller/controller_test.go index 54cf663..ebb4b0a 100644 --- a/internal/controller/controller_test.go +++ b/internal/controller/controller_test.go @@ -57,6 +57,7 @@ var ( Race_1_Cruiser_idx = 2 Uninhabited_Planet_3_num uint = 3 + Uninhabited_Planet_4_num uint = 4 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(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_4_num, "Planet_4", uuid.Nil, 10, 10, 500, 0, 0, 10, game.ProductionNone.AsType(uuid.Nil)), }, }, } diff --git a/internal/controller/generate_turn.go b/internal/controller/generate_turn.go index 6be61c0..907c79f 100644 --- a/internal/controller/generate_turn.go +++ b/internal/controller/generate_turn.go @@ -34,11 +34,9 @@ func MakeTurn(c *Controller, r Repo, g *game.Game) error { // 09. Корабли, где это возможно, объединяются в группы. // 10. На планетах производится промышленность, добывается сырье, разрабатываются новые технологии. // 11. Увеличивается население планет. - // TODO: tests c.Cache.TurnPlanetProductions() // 12. Товары выгружаются в конце грузовых маршрутов. - // TODO: tests c.Cache.TurnUnloadEnroutedGroups() /*** Last steps ***/ diff --git a/internal/controller/planet.go b/internal/controller/planet.go index 33cbc89..c681807 100644 --- a/internal/controller/planet.go +++ b/internal/controller/planet.go @@ -211,29 +211,30 @@ func (c *Cache) TurnPlanetProductions() { } case game.ResearchScience: 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: - IncreaseTech(r, p, 1., 0, 0, 0) + ResearchTech(r, p.ProductionCapacity(), 1., 0, 0, 0) case game.ResearchWeapons: - IncreaseTech(r, p, 0, 1., 0, 0) + ResearchTech(r, p.ProductionCapacity(), 0, 1., 0, 0) case game.ResearchShields: - IncreaseTech(r, p, 0, 0, 1., 0) + ResearchTech(r, p.ProductionCapacity(), 0, 0, 1., 0) case game.ResearchCargo: - IncreaseTech(r, p, 0, 0, 0, 1.) + ResearchTech(r, p.ProductionCapacity(), 0, 0, 0, 1.) case game.ProductionMaterial: - p.IncreaseMaterial() + p.ProduceMaterial() case game.ProductionCapital: - p.IncreaseIndustry() + p.ProduceIndustry() default: panic(fmt.Sprintf("unprocessed production type: %v", pt)) } - p.IncreasePopulation() + // last step: increase population / colonists + p.ProducePopulation() } 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. func (c *Cache) listProducingPlanets() iter.Seq[uint] { 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 func (c *Cache) putPopulation(pn uint, v float64) { diff --git a/internal/controller/planet_test.go b/internal/controller/planet_test.go index 42ec128..d1d0abc 100644 --- a/internal/controller/planet_test.go +++ b/internal/controller/planet_test.go @@ -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_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) +} diff --git a/internal/controller/route.go b/internal/controller/route.go index 725dce8..9468080 100644 --- a/internal/controller/route.go +++ b/internal/controller/route.go @@ -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) 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) @@ -97,7 +97,7 @@ func (c *Cache) EnrouteGroups() { if len(c.g.Map.Planet[pi].Route) == 0 { 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 { 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) { p := c.MustPlanet(pn) for i := range c.ShipGroupsIndex() { @@ -194,63 +194,83 @@ func (c *Cache) listRouteEligibleGroupIds(pn uint) iter.Seq[int] { // Невозможно лишь выгрузить колонистов на чужой планете. func (c *Cache) TurnUnloadEnroutedGroups() { - for pi := range c.g.Map.Planet { - p := &c.g.Map.Planet[pi] - colGroups := c.listUnloadEligibleShipGroupIds(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 i := range c.g.Map.Planet { + p := &c.g.Map.Planet[i] + c.doUnload(c.unloadRoutedColonists(p.Number, c.listRoutedUnloadShipGroupIds(p.Number, game.RouteColonist))) for _, rt := range []game.RouteType{game.RouteMaterial, game.RouteCapital} { - for sgi := range c.listUnloadEligibleShipGroupIds(p.Number, rt) { - c.unloadCargoUnsafe(sgi, c.ShipGroup(sgi).Load) - } + c.doUnload(c.listRoutedUnloadShipGroupIds(p.Number, rt)) } } } -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) loadByRace := make(map[int]float64) - for i := range seq { + for _, i := range groups { sg := c.ShipGroup(i) ri := c.RaceIndex(sg.OwnerID) groupByRace[ri] = append(groupByRace[ri], i) loadByRace[ri] += sg.Load } if len(loadByRace) < 2 { - for _, gr := range groupByRace { - for _, sgi := range gr { - c.unloadCargoUnsafe(sgi, c.ShipGroup(sgi).Load) - } - } + // only one race has to unload cargo + result = slices.Values(groups) return } - // select winner to unload + // select winner to unload cargo + id := MaxOrRandomLoadId(loadByRace) + result = slices.Values(groupByRace[id]) - 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 - raceIdx = slices.DeleteFunc(raceIdx, func(v int) bool { return loadByRace[v] < loadByRace[raceIdx[0]] }) - rand.Shuffle(len(raceIdx), func(i, j int) { raceIdx[i], raceIdx[j] = raceIdx[j], raceIdx[i] }) - // now raceIdx[0] has a random race index - } - for _, sgi := range groupByRace[raceIdx[0]] { - c.unloadCargoUnsafe(sgi, c.ShipGroup(sgi).Load) - } + return } -func (c *Cache) listUnloadEligibleShipGroupIds(pn uint, routeType game.RouteType) iter.Seq[int] { +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]) }) + + // no single winner with highest load + if IDtoLoad[IDs[0]] == IDtoLoad[IDs[1]] { + // remove IDs which load less than maximum + IDs = slices.DeleteFunc(IDs, func(v int) bool { return IDtoLoad[v] < IDtoLoad[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] +} + +func (c *Cache) listRoutedUnloadShipGroupIds(pn uint, routeType game.RouteType) iter.Seq[int] { return func(yield func(int) bool) { + yielded := make(map[int]bool) for i := range c.g.Map.Planet { for rt, dest := range c.g.Map.Planet[i].Route { if dest != pn || rt != routeType { @@ -258,12 +278,16 @@ func (c *Cache) listUnloadEligibleShipGroupIds(pn uint, routeType game.RouteType } for i := range c.ShipGroupsIndex() { 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 } if !yield(i) { return } + yielded[i] = true } } } diff --git a/internal/controller/route_test.go b/internal/controller/route_test.go index 40fb08a..e1ff61b 100644 --- a/internal/controller/route_test.go +++ b/internal/controller/route_test.go @@ -4,6 +4,7 @@ import ( "slices" "testing" + "github.com/iliadenisov/galaxy/internal/controller" e "github.com/iliadenisov/galaxy/internal/error" "github.com/iliadenisov/galaxy/internal/model/game" @@ -91,7 +92,7 @@ func TestRemoveRoute(t *testing.T) { e.GenericErrorText(e.ErrInputEntityNotOwned)) } -func TestListRouteEligibleGroupIds(t *testing.T) { +func TestListRoutedSendGroupIds(t *testing.T) { c, g := newCache() // 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, 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) for _, i := range planet_0_groups { sg := c.ShipGroup(i) @@ -254,3 +255,164 @@ func TestEnrouteGroups_LaunchOrder(t *testing.T) { assert.Nil(t, c.ShipGroup(sgi).CargoType) 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) +} diff --git a/internal/controller/science.go b/internal/controller/science.go index de41516..b496635 100644 --- a/internal/controller/science.go +++ b/internal/controller/science.go @@ -81,6 +81,22 @@ func (c *Cache) DeleteScience(ri int, name string) error { 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 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)) } return &c.g.Race[ri].Sciences[i] - } diff --git a/internal/controller/science_test.go b/internal/controller/science_test.go index 001895b..8ffcf48 100644 --- a/internal/controller/science_test.go +++ b/internal/controller/science_test.go @@ -4,7 +4,9 @@ import ( "testing" "github.com/google/uuid" + "github.com/iliadenisov/galaxy/internal/controller" e "github.com/iliadenisov/galaxy/internal/error" + "github.com/iliadenisov/galaxy/internal/model/game" "github.com/stretchr/testify/assert" ) @@ -97,3 +99,36 @@ func TestDeleteScience(t *testing.T) { g.DeleteScience(Race_0.Name, second), 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)) +} diff --git a/internal/controller/ship_group.go b/internal/controller/ship_group.go index e6ed391..44446ef 100644 --- a/internal/controller/ship_group.go +++ b/internal/controller/ship_group.go @@ -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) } - c.unloadCargoUnsafe(sgi, toBeUnloaded) + c.unsafeUnloadCargo(sgi, toBeUnloaded) return nil } -func (c *Cache) unloadCargoUnsafe(sgi int, q float64) { +func (c *Cache) unsafeUnloadCargo(sgi int, q float64) { if q <= 0 { return } + if st := c.ShipGroup(sgi).State(); st != game.StateInOrbit { + panic(fmt.Sprintf("invalid group state: %v", st)) + } c.validateShipGroupIndex(sgi) p := c.MustPlanet(c.ShipGroup(sgi).Destination) ct := *c.ShipGroup(sgi).CargoType @@ -411,12 +414,13 @@ func (c *Cache) unloadCargoUnsafe(sgi int, q float64) { } *availableOnPlanet += q - // FIXME: unpack COL / CAP - - c.ShipGroup(sgi).Load -= q + c.ShipGroup(sgi).Load -= q // TODO: apply rounding for Load property? if c.ShipGroup(sgi).Load == 0 { c.ShipGroup(sgi).CargoType = nil } + + p.UnpackColonists() + p.UnpackCapital() } func (c *Controller) GiveawayGroup(raceName, raceAcceptor string, groupIndex, quantity uint) error { diff --git a/internal/error/generic.go b/internal/error/generic.go index ec7bc24..12c4083 100644 --- a/internal/error/generic.go +++ b/internal/error/generic.go @@ -200,7 +200,7 @@ func newGenericError(code int, arg ...any) error { e.err = arg[i].(error) i += 1 } - if len(arg) == i+2 { + if len(arg) >= i+2 { e.subject = fmt.Sprintf(asString(arg[i]), arg[i+1:]...) } else if len(arg) == i+1 { e.subject = asString(arg[i]) diff --git a/internal/model/game/planet.go b/internal/model/game/planet.go index 79052bd..123c02a 100644 --- a/internal/model/game/planet.go +++ b/internal/model/game/planet.go @@ -56,7 +56,7 @@ func PlanetProduction(industry, population float64) float64 { // [x] ind = (res * prod) / (5 * res + 1) // ind = 10 * 1000 / (5 * 10 + 1) = 10000 / 55 = 181.(81) // int = 10 * 500 / 55 = 5000 / 55 -func (p *Planet) IncreaseIndustry() { +func (p *Planet) ProduceIndustry() { production := p.ProductionCapacity() var ind float64 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) IncreaseMaterial() { +func (p *Planet) ProduceMaterial() { p.Material += p.ProductionCapacity() * p.Resources } // Автоматическое увеличение населения на каждом ходу -// TODO: test - whether POP is busy on production or not? -func (p *Planet) IncreasePopulation() { +func (p *Planet) ProducePopulation() { p.Population *= 1.08 if p.Population > p.Size { p.Colonists += (p.Population - p.Size) / 8. @@ -100,17 +86,28 @@ func (p *Planet) IncreasePopulation() { } } -func (p *Planet) UnpackCOLtoPOP() { - if p.Colonists < 1 { +func (p *Planet) UnpackCapital() { + if p.Capital == 0 { return } - maxCOL := uint((p.Size - p.Population) / 8.) - if float64(maxCOL) > p.Colonists { - maxCOL = uint(p.Colonists) + deficit := p.Population - p.Industry + if deficit > p.Capital { + deficit = p.Capital } - maxCOL = uint(float64(maxCOL) - math.Mod(float64(maxCOL), 8.)) - p.Colonists -= float64(maxCOL) - p.Population += float64(maxCOL) * 8 + p.Capital -= deficit + p.Industry += deficit +} + +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 { diff --git a/internal/model/game/planet_test.go b/internal/model/game/planet_test.go index a5c816d..cc66cbf 100644 --- a/internal/model/game/planet_test.go +++ b/internal/model/game/planet_test.go @@ -14,7 +14,7 @@ func TestPlanetProduction(t *testing.T) { assert.Equal(t, 250., game.PlanetProduction(0., 1000.)) } -func TestIncreaseIndustry(t *testing.T) { +func TestProduceIndustry(t *testing.T) { HW := &game.Planet{ PlanetReport: game.PlanetReport{ UninhabitedPlanet: game.UninhabitedPlanet{ @@ -35,23 +35,141 @@ func TestIncreaseIndustry(t *testing.T) { Industry: 500, }, } - HW.IncreaseIndustry() + HW.ProduceIndustry() assert.InDelta(t, 196.078, HW.Capital, 0.0005) HW.Capital = 0 HW.Material = 200 - HW.IncreaseIndustry() + HW.ProduceIndustry() assert.Equal(t, 200., HW.Capital) assert.Equal(t, 0., HW.Material) - DW.IncreaseIndustry() + DW.ProduceIndustry() assert.InDelta(t, 98.039, DW.Capital, 0.0003) DW.Capital = 0 DW.Material = 100 - DW.IncreaseIndustry() + DW.ProduceIndustry() assert.Equal(t, 100., DW.Capital) 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) +} diff --git a/internal/model/game/route.go b/internal/model/game/route.go index 7b8f7ff..e2ed9b9 100644 --- a/internal/model/game/route.go +++ b/internal/model/game/route.go @@ -16,6 +16,11 @@ var ( RouteColonist.String(): RouteColonist, RouteEmpty.String(): RouteEmpty, } + RouteToCargo map[RouteType]CargoType = map[RouteType]CargoType{ + RouteColonist: CargoColonist, + RouteCapital: CargoCapital, + RouteMaterial: CargoMaterial, + } ) func (rt RouteType) Ref() *RouteType {