diff --git a/game/internal/controller/report.go b/game/internal/controller/report.go index 0532f20..56b6a73 100644 --- a/game/internal/controller/report.go +++ b/game/internal/controller/report.go @@ -412,7 +412,6 @@ func (c *Cache) ReportIncomingGroup(ri int, rep *mr.Report) { if sg.OwnerID == r.ID || sg.State() != game.StateInSpace { continue } - p1 := c.MustPlanet(sg.StateInSpace.Origin) p2 := c.MustPlanet(sg.Destination) if !p2.OwnedBy(r.ID) { continue @@ -434,7 +433,9 @@ func (c *Cache) ReportIncomingGroup(ri int, rep *mr.Report) { continue } - distance := calc.ShortDistance(c.g.Map.Width, c.g.Map.Height, p1.X.F(), p1.Y.F(), p2.X.F(), p2.Y.F()) + // Remaining distance is measured from the group's current position in + // hyperspace to the destination, not from its origin planet. + distance := calc.ShortDistance(c.g.Map.Width, c.g.Map.Height, sg.StateInSpace.X.F(), sg.StateInSpace.Y.F(), p2.X.F(), p2.Y.F()) var speed, mass float64 if sg.FleetID != nil { speed, mass = c.FleetSpeedAndMass(c.MustFleetIndex(*sg.FleetID)) @@ -577,12 +578,12 @@ func (c *Cache) ReportShipProduction(ri int, rep *mr.Report) { st := c.MustShipType(ri, *p.Production.SubjectID) sliceIndexValidate(&rep.ShipProduction, i) - rep.ShipProduction[pi].Planet = p.Number - rep.ShipProduction[pi].Class = st.Name - rep.ShipProduction[pi].Cost = mr.F(calc.ShipProductionCost(st.EmptyMass())) - rep.ShipProduction[pi].Free = mr.F(c.PlanetProductionCapacity(p.Number)) - rep.ShipProduction[pi].ProdUsed = mr.F((*p.Production.ProdUsed).F()) - rep.ShipProduction[pi].Percent = mr.F((*p.Production.Progress).F()) + rep.ShipProduction[i].Planet = p.Number + rep.ShipProduction[i].Class = st.Name + rep.ShipProduction[i].Cost = mr.F(calc.ShipProductionCost(st.EmptyMass())) + rep.ShipProduction[i].Free = mr.F(c.PlanetProductionCapacity(p.Number)) + rep.ShipProduction[i].ProdUsed = mr.F((*p.Production.ProdUsed).F()) + rep.ShipProduction[i].Percent = mr.F((*p.Production.Progress).F()) i++ } } diff --git a/game/internal/controller/report_test.go b/game/internal/controller/report_test.go index 8879901..a0c7de2 100644 --- a/game/internal/controller/report_test.go +++ b/game/internal/controller/report_test.go @@ -165,3 +165,34 @@ func TestReportOtherShipClassFromBattle(t *testing.T) { assert.Equal(t, report.F(0.), g.Cargo) assert.Equal(t, report.F(220.), g.Mass) } + +// TestReportShipProductionIndex guards the report index: when the only +// ship-producing planet is not the first planet in the map, its entry must +// land at the compacted report index, not the planet's map index (which would +// write out of the grown slice and panic). +func TestReportShipProductionIndex(t *testing.T) { + c, _ := newCache() + assert.NoError(t, c.PlanetProduce(Race_0_idx, int(R0_Planet_2_num), game.ProductionShip, ShipType_Cruiser)) + + rep := c.InitReport(1) + c.ReportShipProduction(Race_0_idx, rep) + assert.Len(t, rep.ShipProduction, 1) + assert.Equal(t, R0_Planet_2_num, rep.ShipProduction[0].Planet) +} + +// TestReportIncomingGroupRemainingDistance checks the reported distance is the +// remaining distance from the group's current hyperspace position to the +// destination, not the full origin-to-destination route. +func TestReportIncomingGroupRemainingDistance(t *testing.T) { + c, _ := newCache() + gi := c.CreateShipsUnsafe_T(Race_1_idx, c.MustShipClass(Race_1_idx, Race_1_Gunship).ID, R1_Planet_1_num, 1) + c.ShipGroup(gi).Destination = R0_Planet_0_num // Planet_0 at (1,1) + c.ShipGroup(gi).StateInSpace = &game.InSpace{Origin: R1_Planet_1_num, X: floatRef(5), Y: floatRef(5)} + + rep := c.InitReport(1) + c.ReportIncomingGroup(Race_0_idx, rep) + assert.Len(t, rep.IncomingGroup, 1) + // current (5,5) -> dest (1,1) = sqrt(32) ≈ 5.657; the origin (2,2) -> dest + // route would be sqrt(2) ≈ 1.414. + assert.InDelta(t, 5.657, rep.IncomingGroup[0].Distance.F(), 0.01) +} diff --git a/game/internal/controller/science.go b/game/internal/controller/science.go index a33d20e..2b18759 100644 --- a/game/internal/controller/science.go +++ b/game/internal/controller/science.go @@ -2,6 +2,7 @@ package controller import ( "fmt" + "math" "slices" "galaxy/util" @@ -36,7 +37,9 @@ func (c *Cache) ScienceCreate(ri int, name string, drive, weapons, shileds, carg return e.NewCargoValueError(cargo) } sum := drive + weapons + shileds + cargo - if sum != 1 { + // The proportions must add up to one; a small tolerance keeps inputs like + // 0.1+0.2+0.3+0.4 (which is 1 only up to float rounding) from being rejected. + if math.Abs(sum-1) > 1e-9 { return e.NewScienceSumValuesError("D=%f W=%f S=%f C=%f sum=%f", drive, weapons, shileds, cargo, sum) } c.g.Race[ri].Sciences = append(c.g.Race[ri].Sciences, game.Science{ diff --git a/game/internal/controller/science_test.go b/game/internal/controller/science_test.go index 27db5d9..0b5665c 100644 --- a/game/internal/controller/science_test.go +++ b/game/internal/controller/science_test.go @@ -136,3 +136,14 @@ func TestResearchTech(t *testing.T) { assert.Equal(t, 1.35, rr.Tech.Value(game.TechShields)) assert.Equal(t, 1.45, rr.Tech.Value(game.TechCargo)) } + +// TestScienceCreateFloatTolerance checks that proportions which sum to 1 only +// up to float rounding (0.1+0.2+0.3+0.4 == 1.0000000000000002) are accepted, +// while a sum clearly off one is still rejected. +func TestScienceCreateFloatTolerance(t *testing.T) { + _, g := newCache() + assert.NoError(t, g.ScienceCreate(Race_0.Name, "FloatSum", 0.1, 0.2, 0.3, 0.4)) + assert.ErrorContains(t, + g.ScienceCreate(Race_0.Name, "NotOne", 0.1, 0.2, 0.3, 0.3), + e.GenericErrorText(e.ErrInputScienceSumValues)) +} diff --git a/game/internal/controller/ship_group.go b/game/internal/controller/ship_group.go index 52f3770..b99f6c5 100644 --- a/game/internal/controller/ship_group.go +++ b/game/internal/controller/ship_group.go @@ -177,6 +177,12 @@ func (c *Cache) shipGroupDismantle(ri int, groupIndex uuid.UUID) error { case game.CargoColonist: if p.OwnedBy(c.g.Race[ri].ID) { p = game.UnloadColonists(p, load) + } else if !p.Owned() { + // Over a neutral planet the colonists settle it: the planet + // becomes the dismantling race's and the colonists join its + // population. Over a foreign planet they are simply lost. + p.Own(c.g.Race[ri].ID) + p = game.UnloadColonists(p, load) } case game.CargoMaterial: p.Material = p.Material.Add(load) diff --git a/game/internal/controller/ship_group_test.go b/game/internal/controller/ship_group_test.go index 0ef88fe..be5b226 100644 --- a/game/internal/controller/ship_group_test.go +++ b/game/internal/controller/ship_group_test.go @@ -567,3 +567,20 @@ func TestUnsafeDeleteShipGroup(t *testing.T) { assert.Equal(t, uint(3), c.ShipGroup(0).Number) assert.Equal(t, uint(7), c.ShipGroup(1).Number) } + +// TestShipGroupDismantleColonizesNeutralPlanet checks that dismantling a +// colonist-laden group over an uninhabited planet settles it: the planet +// becomes the race's and the colonists join its population. +func TestShipGroupDismantleColonizesNeutralPlanet(t *testing.T) { + c, g := newCache() + gi := c.CreateShipsUnsafe_T(Race_0_idx, c.MustShipClass(Race_0_idx, Race_0_Freighter).ID, Uninhabited_Planet_4_num, 10) + c.ShipGroup(gi).CargoType = game.CargoColonist.Ref() + c.ShipGroup(gi).Load = 10.0 + assert.False(t, c.MustPlanet(Uninhabited_Planet_4_num).Owned()) + + assert.NoError(t, g.ShipGroupDismantle(Race_0.Name, c.ShipGroup(gi).ID)) + + p := c.MustPlanet(Uninhabited_Planet_4_num) + assert.True(t, p.OwnedBy(Race_0_ID)) + assert.Equal(t, 80., p.Population.F()) // 10 colonists * 8 +} diff --git a/game/internal/generator/settings.go b/game/internal/generator/settings.go index b634061..f43c6e9 100644 --- a/game/internal/generator/settings.go +++ b/game/internal/generator/settings.go @@ -46,7 +46,7 @@ type PlanetSetting struct { MinDistanceHW uint32 MinSize float64 MaxSize float64 - MinResource float64 // Rules: [0.1, 20] + MinResource float64 // minimum natural resources for the class MaxResource float64 Ratio float64 // The proportion of the total number of free planets in the galaxy } @@ -71,7 +71,7 @@ func DefaultMapSetting() MapSetting { MinDistanceHW: 20, MinSize: 1500, MaxSize: 2500, - MinResource: 0.1, + MinResource: 0.001, MaxResource: 3, Ratio: 0.06, }, diff --git a/game/rules.txt b/game/rules.txt index 740838f..34712e5 100644 --- a/game/rules.txt +++ b/game/rules.txt @@ -224,7 +224,7 @@ World", HW) и двумя планетам размером по 500 (такие Супер большие планеты 1500-2500 0-3 6% -Просто большие планеты 1000-2000 0.1-10 18% +Просто большие планеты 1000-2000 1-10 18% Обычные планеты 0-1000 0.1-10 50% @@ -349,17 +349,16 @@ World", HW) и двумя планетам размером по 500 (такие технологий, взятых в определяемых Вами пропорциях. Когда Вы переключаете производство на планете на исследования в области определенной Вами науки, производственные единицы расходуются на те технологии, из которых состоит -данная наука, причем в соответствии с заданной Вами пропорции. Общая сумма -частей различных технологий в каждой науке равна 100% или единице в дробном -исчислении. +данная наука, причем в соответствии с заданной Вами пропорцией. Доли +технологий в науке задаются в долях единицы, и их сумма равна единице (100%). -Например, Вы определили науку с именем "First Step", которая состоит из 10 -частей технологии Двигателей, 5 частей технологии Вооружения, 30 частей -технологии Защиты и 0 частей технологии Грузоперевозок. Тогда при изучении -такой науки у Вас 22% доступных производственных единиц планеты будут -израсходованы на разработки в области Двигателей, 11% - на Вооружение и 67% - -на технологию Защиты. Таким образом за один ход на одной планете Вы имеете -возможность повысить сразу несколько технологических уровней. +Например, наука с именем "First Step" задана долями 0.222 для Двигателей, +0.111 для Вооружения, 0.667 для Защиты и 0 для Грузоперевозок (в сумме — +единица; это соответствует соотношению 10 : 5 : 30 : 0). Тогда при изучении +такой науки 22.2% доступных производственных единиц планеты будут +израсходованы на разработки в области Двигателей, 11.1% — на Вооружение и +66.7% — на технологию Защиты. Таким образом за один ход на одной планете Вы +имеете возможность повысить сразу несколько технологических уровней. Сырьё (Материалы) @@ -376,8 +375,10 @@ World", HW) и двумя планетам размером по 500 (такие Как известно, каждая планета имеет неизменную характеристику - Природные Ресурсы, которая показывает, насколько планета богата запасами металлов, угля, нефти и т.п. Планеты с высоким показателем Ресурсов требуют меньших -затрат на производство сырья. Показатель находится в диапазоне от 0.1 до 20, -среднее значение 1.5. Ваши первые планеты имеют показатели ресурсов 10, что +затрат на производство сырья. Показатель зависит от типа планеты (см. таблицу +выше): у обитаемых планет он строго больше нуля и доходит до 25 у редких +богато одарённых планет, и лишь у астероидов равен нулю. Ваши первые планеты +имеют показатели ресурсов 10, что означает, что каждая производственная единица может произвести 10 единиц сырья. Планета с показателем сырья 0.1 может произвести только 0.1 единиц сырья на каждую производственную единицу. Произведённое сырьё складируется @@ -641,9 +642,11 @@ Megafreighter 80 2 2 30 100 Корабли, находящиеся на орбите планеты, могут быть разобраны на составляющие материалы. Запас сырья на планете, где находились корабли, будет увеличен на массу этих кораблей. Если корабли несли какой-либо груз, он сперва будет -выгружен на планету, за исключением колонистов: при демонтаже корабля над -чужой планетой колонисты не смогут быть выгружены и навсегда останутся в -стадии заморозки на просторах Галактики. +выгружен на планету. С колонистами есть особенность: над своей планетой они +выгружаются и пополняют население, над необитаемой планетой — выгружаются и +заселяют её (планета становится Вашей), а над чужой планетой колонисты не +смогут быть выгружены и навсегда останутся в стадии заморозки на просторах +Галактики. Флоты diff --git a/pkg/calc/validator.go b/pkg/calc/validator.go index 7d7a1d2..f4e02b5 100644 --- a/pkg/calc/validator.go +++ b/pkg/calc/validator.go @@ -15,7 +15,7 @@ func ValidateShipTypeValues(d float64, a int, w, s, c float64) error { return e.NewShieldsValueError(s) } if !CheckShipTypeValueDWSC(c) { - return e.NewCargoValueError(s) + return e.NewCargoValueError(c) } if a < 0 { return e.NewShipTypeArmamentValueError(a)