Files
galaxy-game/game/internal/controller/report.go
T
Ilia Denisov 6ec1098f15
Tests · Go / test (push) Successful in 1m55s
Tests · Integration / integration (pull_request) Successful in 1m49s
Tests · Go / test (pull_request) Successful in 2m6s
fix(game): gate group visibility by visibility range, report battle classes
Bring the report's foreign-group and foreign-class visibility in line
with the rules (game/rules.txt "Движение" and the report sections):

- incoming groups (heading to one of the recipient's planets) are shown
  only within the recipient's visibility range (driveTech*30); beyond it
  a group is hidden even though it is inbound;
- the unidentified-group list now uses the visibility range (it used the
  flight range, driveTech*40), excludes groups heading to the recipient's
  planets (those belong to the incoming list), and reports each group
  once (it previously emitted an entry per in-range owned planet);
- ship classes met in a battle the recipient took part in or witnessed
  now appear in OtherShipClass, with the design looked up from the owner
  race's ship types (the battle report carries only the class name).

The rules already describe this behaviour and the report wire shape is
unchanged, so no documentation change. Tests added for all three.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 08:36:33 +02:00

829 lines
22 KiB
Go

package controller
import (
"cmp"
"fmt"
"iter"
"slices"
"galaxy/calc"
mr "galaxy/model/report"
"galaxy/game/internal/model/game"
"github.com/google/uuid"
)
func (c *Cache) Report(t uint, battles []*mr.BattleReport, bombings []*mr.Bombing) iter.Seq[*mr.Report] {
report := c.InitReport(t)
return func(yield func(*mr.Report) bool) {
for i := range c.listRaceActingIdx() {
c.ReportRace(i, report, battles, bombings)
if !yield(report) {
break
}
}
}
}
func (c *Cache) InitReport(t uint) *mr.Report {
report := &mr.Report{
Turn: t,
Width: c.g.Map.Width,
Height: c.g.Map.Height,
PlanetCount: uint32(len(c.g.Map.Planet)),
Player: make([]mr.Player, len(c.g.Race)),
LocalScience: make([]mr.Science, 0, 10),
OtherScience: make([]mr.OtherScience, 0, 10),
LocalShipClass: make([]mr.ShipClass, 0, 20),
OtherShipClass: make([]mr.OthersShipClass, 0, 50),
Battle: make([]mr.BattleSummary, 0, 10),
Bombing: make([]*mr.Bombing, 0, 10),
IncomingGroup: make([]mr.IncomingGroup, 0, 10),
OnPlanetGroupCache: make(map[uint][]int),
InSpaceGroupRangeCache: make(map[int]map[uint]float64),
}
sumVote, sumPop, sumInd := make(map[int]float64), make(map[int]float64), make(map[int]float64)
planets := make(map[int]uint16)
for pi := range c.g.Map.Planet {
p := &c.g.Map.Planet[pi]
if !p.Owned() {
continue
}
ri := c.RaceIndex(*p.Owner)
sumPop[ri] += p.Population.F()
sumInd[ri] += p.Industry.F()
planets[ri] = planets[ri] + 1
}
for ri := range c.listRaceIdx() {
r := &c.g.Race[ri]
rr := &report.Player[ri]
rr.ID = r.ID
rr.Name = r.Name
rr.Extinct = r.Extinct
rr.Drive = mr.F(r.TechLevel(game.TechDrive))
rr.Weapons = mr.F(r.TechLevel(game.TechWeapons))
rr.Shields = mr.F(r.TechLevel(game.TechShields))
rr.Cargo = mr.F(r.TechLevel(game.TechCargo))
rr.Planets = planets[ri]
rr.Population = mr.F(sumPop[ri])
rr.Industry = mr.F(sumInd[ri])
// give voices by race index
if vi := slices.IndexFunc(c.g.Race, func(v game.Race) bool { return r.VoteFor == v.ID }); vi < 0 {
panic(fmt.Sprintf("voting for unknown race, id=%v", r.VoteFor))
} else {
sumVote[vi] += r.Votes.F()
dest := &report.Player[vi]
dest.Votes = mr.F(sumVote[vi])
}
}
slices.SortFunc(report.Player, func(a, b mr.Player) int { return cmp.Compare(a.Name, b.Name) })
for sgi := range c.g.ShipGroups {
sg := &c.g.ShipGroups[sgi]
if sg.State() == game.StateInSpace {
// pre-calculate distances from in_space ship groups to every planet
if _, ok := report.InSpaceGroupRangeCache[sgi]; !ok {
report.InSpaceGroupRangeCache[sgi] = make(map[uint]float64)
}
for pi := range c.g.Map.Planet {
p2 := &c.g.Map.Planet[pi]
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())
report.InSpaceGroupRangeCache[sgi][p2.Number] = distance
}
} else {
// collect all orbiting ship groups by planet
report.OnPlanetGroupCache[sg.Destination] = append(report.OnPlanetGroupCache[sg.Destination], sgi)
}
}
return report
}
func (c *Cache) ReportRace(ri int, rep *mr.Report, battles []*mr.BattleReport, bombings []*mr.Bombing) {
c.validateRaceIndex(ri)
r := &c.g.Race[ri]
rep.Race = r.Name
rep.RaceID = r.ID
// votes based on population
rep.Votes = mr.F(r.Votes.F())
// relations
for i := range r.Relations {
rii := slices.IndexFunc(rep.Player, func(v mr.Player) bool { return v.ID == r.Relations[i].RaceID })
if rii < 0 {
panic(fmt.Sprintf("opponent race for relation not found, id=%v", r.Relations[i].RaceID))
}
rep.Player[rii].Relation = r.Relations[i].Relation.String()
}
// self-relation is undefined
if i := slices.IndexFunc(rep.Player, func(v mr.Player) bool { return v.ID == r.ID }); i < 0 {
panic(fmt.Sprintf("race not found in report, id=%v", r.ID))
} else {
rep.Player[i].Relation = "-"
}
// sciences
c.ReportLocalScience(ri, rep)
c.ReportOtherScience(ri, rep)
// ship classes
c.ReportLocalShipClass(ri, rep)
c.ReportOtherShipClass(ri, rep, battles)
// battles
c.ReportBattle(ri, rep, battles)
// bombings
c.ReportBombing(ri, rep, bombings)
// incoming groups
c.ReportIncomingGroup(ri, rep)
// player's planets
c.ReportLocalPlanet(ri, rep)
// ships in production
c.ReportShipProduction(ri, rep)
// cargo routes
c.ReportRoute(ri, rep)
// others' planets
c.ReportOtherPlanet(ri, rep)
// uninhabited planets
c.ReportUninhabitedPlanet(ri, rep)
// unidentified planets
c.ReportUnidentifiedPlanet(ri, rep)
// fleets
c.ReportLocalFleet(ri, rep)
// player's groups
c.ReportLocalGroup(ri, rep)
// others' groups
c.ReportOtherGroup(ri, rep)
// unidentified groups
c.ReportUnidentifiedGroup(ri, rep)
}
func (c *Cache) ReportLocalScience(ri int, rep *mr.Report) {
c.validateRaceIndex(ri)
r := &c.g.Race[ri]
clear(rep.LocalScience)
for i := range r.Sciences {
sliceIndexValidate(&rep.LocalScience, i)
rep.LocalScience[i].Name = r.Sciences[i].Name
rep.LocalScience[i].Drive = mr.F(r.Sciences[i].Drive.F())
rep.LocalScience[i].Weapons = mr.F(r.Sciences[i].Weapons.F())
rep.LocalScience[i].Shields = mr.F(r.Sciences[i].Shields.F())
rep.LocalScience[i].Cargo = mr.F(r.Sciences[i].Cargo.F())
}
slices.SortFunc(rep.LocalScience, func(a, b mr.Science) int { return cmp.Compare(a.Name, b.Name) })
}
func (c *Cache) ReportOtherScience(ri int, rep *mr.Report) {
c.validateRaceIndex(ri)
r := &c.g.Race[ri]
clear(rep.OtherScience)
i := 0
for sg := range c.listShipGroups(ri) {
if sg.State() != game.StateInOrbit {
continue
}
p := c.MustPlanet(sg.Destination)
if !p.Owned() || p.OwnedBy(r.ID) || p.Production.Type != game.ResearchScience {
continue
}
ownerIdx := c.RaceIndex(*p.Owner)
owner := &c.g.Race[ownerIdx]
sc := c.mustScience(ownerIdx, *p.Production.SubjectID)
sliceIndexValidate(&rep.OtherScience, i)
rep.OtherScience[i].Name = owner.Name
rep.OtherScience[i].Drive = mr.F(sc.Drive.F())
rep.OtherScience[i].Weapons = mr.F(sc.Weapons.F())
rep.OtherScience[i].Shields = mr.F(sc.Shields.F())
rep.OtherScience[i].Cargo = mr.F(sc.Cargo.F())
i++
}
slices.SortFunc(rep.OtherScience, func(a, b mr.OtherScience) int {
return cmp.Or(cmp.Compare(a.Race, b.Race), cmp.Compare(a.Name, b.Name))
})
}
func (c *Cache) ReportLocalShipClass(ri int, report *mr.Report) {
c.validateRaceIndex(ri)
clear(report.LocalShipClass)
i := 0
for st := range c.ListShipTypes(ri) {
sliceIndexValidate(&report.LocalShipClass, i)
report.LocalShipClass[i].Name = st.Name
report.LocalShipClass[i].Drive = mr.F(st.Drive.F())
report.LocalShipClass[i].Armament = st.Armament
report.LocalShipClass[i].Weapons = mr.F(st.Weapons.F())
report.LocalShipClass[i].Shields = mr.F(st.Shields.F())
report.LocalShipClass[i].Cargo = mr.F(st.Cargo.F())
report.LocalShipClass[i].Mass = mr.F(st.EmptyMass())
i++
}
slices.SortFunc(report.LocalShipClass, func(a, b mr.ShipClass) int { return cmp.Compare(a.Name, b.Name) })
}
func (c *Cache) ReportOtherShipClass(ri int, rep *mr.Report, battles []*mr.BattleReport) {
c.validateRaceIndex(ri)
r := &c.g.Race[ri]
clear(rep.OtherShipClass)
i := 0
used := make(map[uuid.UUID]map[string]bool)
skip := func(ownerID uuid.UUID, className string) bool {
if ownerID == r.ID {
return true
}
if _, ok := used[ownerID]; ok {
if _, ok := used[ownerID][className]; ok {
return true
}
} else {
used[ownerID] = make(map[string]bool)
}
used[ownerID][className] = true
return false
}
// Ship classes seen in battles the recipient took part in or witnessed.
// The battle report carries the class name and owner race; the class
// design is looked up from that race's ship types, which stay present in
// the state even though the groups themselves are deleted before reports
// are generated.
for bi := range battles {
br := battles[bi]
visible := false
for k := range br.Races {
if br.Races[k] == r.ID {
visible = true
break
}
}
if !visible {
continue
}
for si := range br.Ships {
bg := br.Ships[si]
ownerIdx, err := c.raceIndex(bg.Race)
if err != nil {
continue
}
ownerID := c.g.Race[ownerIdx].ID
st, _, ok := c.ShipClass(ownerIdx, bg.ClassName)
if !ok || skip(ownerID, st.Name) {
continue
}
sliceIndexValidate(&rep.OtherShipClass, i)
rep.OtherShipClass[i].Race = bg.Race
rep.OtherShipClass[i].Name = st.Name
rep.OtherShipClass[i].Drive = mr.F(st.Drive.F())
rep.OtherShipClass[i].Armament = st.Armament
rep.OtherShipClass[i].Weapons = mr.F(st.Weapons.F())
rep.OtherShipClass[i].Shields = mr.F(st.Shields.F())
rep.OtherShipClass[i].Cargo = mr.F(st.Cargo.F())
rep.OtherShipClass[i].Mass = mr.F(st.EmptyMass())
i++
}
}
// add visible ships from owned and observed planets
for pn := range rep.OnPlanetGroupCache {
p := c.MustPlanet(pn)
if p.OwnedBy(r.ID) ||
slices.IndexFunc(rep.OnPlanetGroupCache[pn], func(sgi int) bool { return c.ShipGroup(sgi).OwnerID == r.ID }) >= 0 {
for _, sgi := range rep.OnPlanetGroupCache[pn] {
sg := c.ShipGroup(sgi)
st := c.ShipGroupShipClass(sgi)
if skip(sg.OwnerID, st.Name) {
continue
}
sliceIndexValidate(&rep.OtherShipClass, i)
rep.OtherShipClass[i].Race = c.g.Race[c.RaceIndex(sg.OwnerID)].Name
rep.OtherShipClass[i].Name = st.Name
rep.OtherShipClass[i].Drive = mr.F(st.Drive.F())
rep.OtherShipClass[i].Armament = st.Armament
rep.OtherShipClass[i].Weapons = mr.F(st.Weapons.F())
rep.OtherShipClass[i].Shields = mr.F(st.Shields.F())
rep.OtherShipClass[i].Cargo = mr.F(st.Cargo.F())
rep.OtherShipClass[i].Mass = mr.F(st.EmptyMass())
i++
}
}
}
slices.SortFunc(rep.OtherShipClass, func(a, b mr.OthersShipClass) int {
return cmp.Or(cmp.Compare(a.Race, b.Race), cmp.Compare(a.Name, b.Name))
})
}
func (c *Cache) ReportBattle(ri int, rep *mr.Report, br []*mr.BattleReport) {
c.validateRaceIndex(ri)
r := &c.g.Race[ri]
clear(rep.Battle)
i := 0
for bi := range br {
visible := false
for k := range br[bi].Races {
visible = visible || br[bi].Races[k] == r.ID
}
if !visible {
continue
}
sliceIndexValidate(&rep.Battle, i)
rep.Battle[i] = mr.BattleSummary{
ID: br[bi].ID,
Planet: br[bi].Planet,
Shots: uint(len(br[bi].Protocol)),
}
i++
}
}
func (c *Cache) ReportBombing(ri int, rep *mr.Report, bombing []*mr.Bombing) {
c.validateRaceIndex(ri)
r := &c.g.Race[ri]
clear(rep.Bombing)
i := 0
for bi := range bombing {
pn := bombing[bi].Number
visible := bombing[bi].PlanetOwnedID == r.ID // planet may be bombed and wiped
for _, sgi := range rep.OnPlanetGroupCache[pn] {
sg := c.ShipGroup(sgi)
visible = visible || (sg.OwnerID == r.ID && sg.Destination == pn)
}
if !visible {
continue
}
sliceIndexValidate(&rep.Bombing, i)
rep.Bombing[i] = bombing[bi]
i++
}
slices.SortFunc(rep.Bombing, func(a, b *mr.Bombing) int {
return cmp.Or(cmp.Compare(a.Number, b.Number), boolCompare(a.Wiped, b.Wiped))
})
}
func (c *Cache) ReportIncomingGroup(ri int, rep *mr.Report) {
c.validateRaceIndex(ri)
r := &c.g.Race[ri]
clear(rep.IncomingGroup)
i := 0
for sgi := range c.ShipGroupsIndex() {
sg := c.ShipGroup(sgi)
st := c.ShipGroupShipClass(sgi)
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
}
// Beyond the visibility range (driveTech*30) of every owned planet the
// group is not shown at all, even though it heads to one of them.
visible := false
for pi := range c.g.Map.Planet {
op := &c.g.Map.Planet[pi]
if !op.OwnedBy(r.ID) {
continue
}
if d, ok := rep.InSpaceGroupRangeCache[sgi][op.Number]; ok && d <= r.VisibilityDistance() {
visible = true
break
}
}
if !visible {
continue
}
distance := calc.ShortDistance(c.g.Map.Width, c.g.Map.Height, p1.X.F(), p1.Y.F(), p2.X.F(), p2.Y.F())
var speed, mass float64
if sg.FleetID != nil {
speed, mass = c.FleetSpeedAndMass(c.MustFleetIndex(*sg.FleetID))
} else {
speed, mass = sg.Speed(st), sg.FullMass(st)
}
sliceIndexValidate(&rep.IncomingGroup, i)
rep.IncomingGroup[i].Origin = sg.StateInSpace.Origin
rep.IncomingGroup[i].Destination = sg.Destination
rep.IncomingGroup[i].Distance = mr.F(distance)
rep.IncomingGroup[i].Speed = mr.F(speed)
rep.IncomingGroup[i].Mass = mr.F(mass)
i++
}
}
func (c *Cache) ReportLocalPlanet(ri int, rep *mr.Report) {
c.validateRaceIndex(ri)
r := &c.g.Race[ri]
clear(rep.LocalPlanet)
i := 0
for pi := range c.g.Map.Planet {
p := &c.g.Map.Planet[pi]
if !p.OwnedBy(r.ID) {
continue
}
sliceIndexValidate(&rep.LocalPlanet, i)
c.localPlanet(&rep.LocalPlanet[i], p)
// rep.LocalPlanet[i].UnidentifiedPlanet.Number = p.Number
// rep.LocalPlanet[i].UnidentifiedPlanet.X = mr.F(p.X.F())
// rep.LocalPlanet[i].UnidentifiedPlanet.Y = mr.F(p.Y.F())
// rep.LocalPlanet[i].UninhabitedPlanet.Size = mr.F(p.Size.F())
// rep.LocalPlanet[i].UninhabitedPlanet.Name = p.Name
// rep.LocalPlanet[i].UninhabitedPlanet.Resources = mr.F(p.Resources.F())
// rep.LocalPlanet[i].UninhabitedPlanet.Capital = mr.F(p.Capital.F())
// rep.LocalPlanet[i].UninhabitedPlanet.Material = mr.F(p.Material.F())
// rep.LocalPlanet[i].Industry = mr.F(p.Industry.F())
// rep.LocalPlanet[i].Population = mr.F(p.Population.F())
// rep.LocalPlanet[i].Colonists = mr.F(p.Colonists.F())
// rep.LocalPlanet[i].Production = c.PlanetProductionDisplayName(p.Number)
// rep.LocalPlanet[i].FreeIndustry = mr.F(p.ProductionCapacity())
// for _, sgi := range rep.PlanetGroupsCache[p.Number] {
// sg := c.ShipGroup(sgi)
// if sg.StateUpgrade == nil {
// break
// }
// // between-turn report: ships upgrading on the planet decreases free indistrial potential
// rep.LocalPlanet[i].FreeIndustry -= mr.F(sg.StateUpgrade.Cost())
// }
i++
}
}
func (c *Cache) ReportOtherPlanet(ri int, rep *mr.Report) {
c.validateRaceIndex(ri)
r := &c.g.Race[ri]
clear(rep.OtherPlanet)
i := 0
for sg := range c.listShipGroups(ri) {
if sg.State() != game.StateInOrbit {
continue
}
p := c.MustPlanet(sg.Destination)
if !p.Owned() || p.OwnedBy(r.ID) {
continue
}
sliceIndexValidate(&rep.OtherPlanet, i)
c.localPlanet(&rep.OtherPlanet[i].LocalPlanet, p)
rep.OtherPlanet[i].Owner = c.g.Race[c.RaceIndex(*p.Owner)].Name
i++
}
}
func (c *Cache) ReportUninhabitedPlanet(ri int, rep *mr.Report) {
c.validateRaceIndex(ri)
clear(rep.UninhabitedPlanet)
i := 0
for sg := range c.listShipGroups(ri) {
if sg.State() != game.StateInOrbit {
continue
}
p := c.MustPlanet(sg.Destination)
if p.Owned() {
continue
}
sliceIndexValidate(&rep.UninhabitedPlanet, i)
uninhabitedPlanet(&rep.UninhabitedPlanet[i], p)
i++
}
}
func (c *Cache) ReportUnidentifiedPlanet(ri int, rep *mr.Report) {
c.validateRaceIndex(ri)
r := &c.g.Race[ri]
clear(rep.UnidentifiedPlanet)
i := 0
for pi := range c.g.Map.Planet {
p := &c.g.Map.Planet[pi]
// skip player's owned planets
if p.OwnedBy(r.ID) {
continue
}
// skip planets where player's group are orbiting
if slices.IndexFunc(rep.OnPlanetGroupCache[p.Number], func(sgi int) bool { return c.ShipGroup(sgi).OwnerID == r.ID }) >= 0 {
continue
}
sliceIndexValidate(&rep.UnidentifiedPlanet, i)
unidentifiedPlanet(&rep.UnidentifiedPlanet[i], p)
i++
}
}
func (c *Cache) ReportShipProduction(ri int, rep *mr.Report) {
c.validateRaceIndex(ri)
r := &c.g.Race[ri]
clear(rep.ShipProduction)
i := 0
for pi := range c.g.Map.Planet {
p := &c.g.Map.Planet[pi]
if !p.OwnedBy(r.ID) || p.Production.Type != game.ProductionShip {
continue
}
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())
i++
}
}
func (c *Cache) ReportRoute(ri int, rep *mr.Report) {
c.validateRaceIndex(ri)
r := &c.g.Race[ri]
clear(rep.Route)
i := 0
for pi := range c.g.Map.Planet {
p := &c.g.Map.Planet[pi]
if !p.OwnedBy(r.ID) || len(p.Route) == 0 {
continue
}
sliceIndexValidate(&rep.Route, i)
rep.Route[i].Planet = p.Number
// rep.Route[i].Route = make(map[uint]string)
for rt, dest := range p.Route {
rep.Route[i].Route[dest] = rt.String()
}
i++
}
}
func (c *Cache) ReportLocalFleet(ri int, rep *mr.Report) {
c.validateRaceIndex(ri)
clear(rep.LocalFleet)
i := 0
for fl := range c.listFleets(ri) {
fi := c.MustFleetIndex(fl.ID)
gid := slices.Collect(c.fleetGroupIds(ri, fi))
if len(gid) == 0 {
continue
}
speed, _ := c.FleetSpeedAndMass(fi)
fleetState := c.FleetState(fl.ID)
sliceIndexValidate(&rep.LocalFleet, i)
rep.LocalFleet[i].Name = fl.Name
rep.LocalFleet[i].Groups = uint(len(gid))
rep.LocalFleet[i].Speed = mr.F(speed)
rep.LocalFleet[i].State = fleetState.State.String()
rep.LocalFleet[i].Destination = fleetState.Destination
if inSpace, ok := fleetState.InSpace(); ok {
rep.LocalFleet[i].Origin = &inSpace.Origin
p2 := c.MustPlanet(rep.LocalFleet[i].Destination)
rangeToDestination := mr.F(calc.ShortDistance(c.g.Map.Width, c.g.Map.Height, inSpace.X.F(), inSpace.Y.F(), p2.X.F(), p2.Y.F()))
rep.LocalFleet[i].Range = &rangeToDestination
}
i++
}
}
func (c *Cache) ReportLocalGroup(ri int, rep *mr.Report) {
c.validateRaceIndex(ri)
clear(rep.LocalGroup)
i := 0
for sg := range c.listShipGroups(ri) {
sliceIndexValidate(&rep.LocalGroup, i)
st := c.MustShipType(ri, sg.TypeID)
c.otherGroup(&rep.LocalGroup[i].OtherGroup, sg, st)
rep.LocalGroup[i].ID = sg.ID
rep.LocalGroup[i].State = sg.State().String()
if sg.FleetID != nil {
rep.LocalGroup[i].Fleet = &c.g.Fleets[c.MustFleetIndex(*sg.FleetID)].Name
}
// rep.LocalGroup[i].Number = sg.Number
// rep.LocalGroup[i].Class = st.Name
// // rep.LocalGroup[i].Tech = make(map[string]mr.Float)
// for t, v := range sg.Tech {
// rep.LocalGroup[i].Tech[t.String()] = mr.F(v)
// }
// rep.LocalGroup[i].Cargo = sg.CargoString()
// rep.LocalGroup[i].Load = mr.F(sg.Load.F())
// rep.LocalGroup[i].Destination = sg.Destination
// if sg.State() == game.StateInSpace {
// rep.LocalGroup[i].Origin = &sg.StateInSpace.Origin
// p2 := c.MustPlanet(rep.LocalGroup[i].Destination)
// rangeToDestination := mr.F(util.ShortDistance(c.g.Map.Width, c.g.Map.Height, sg.StateInSpace.X.F(), sg.StateInSpace.Y.F(), p2.X.F(), p2.Y.F()))
// rep.LocalGroup[i].Range = &rangeToDestination
// }
// rep.LocalGroup[i].Speed = mr.F(sg.Speed(st))
// rep.LocalGroup[i].Mass = mr.F(st.EmptyMass())
i++
}
}
func (c *Cache) ReportOtherGroup(ri int, rep *mr.Report) {
c.validateRaceIndex(ri)
r := &c.g.Race[ri]
clear(rep.OtherGroup)
used := make(map[int]bool)
skip := func(sgi int) bool {
if c.ShipGroup(sgi).OwnerID == r.ID {
return true
}
if _, ok := used[sgi]; ok {
return true
}
used[sgi] = true
return false
}
i := 0
// visible groups from owned and observed planets
for pn := range rep.OnPlanetGroupCache {
p := c.MustPlanet(pn)
if p.OwnedBy(r.ID) ||
slices.IndexFunc(rep.OnPlanetGroupCache[pn], func(sgi int) bool { return c.ShipGroup(sgi).OwnerID == r.ID }) >= 0 {
for _, sgi := range rep.OnPlanetGroupCache[pn] {
sg := c.ShipGroup(sgi)
st := c.ShipGroupShipClass(sgi)
if skip(sgi) {
continue
}
sliceIndexValidate(&rep.OtherGroup, i)
c.otherGroup(&rep.OtherGroup[i], sg, st)
i++
}
}
}
}
func (c *Cache) ReportUnidentifiedGroup(ri int, rep *mr.Report) {
c.validateRaceIndex(ri)
r := &c.g.Race[ri]
visibility := r.VisibilityDistance()
clear(rep.UnidentifiedGroup)
i := 0
for sgi := range rep.InSpaceGroupRangeCache {
sg := c.ShipGroup(sgi)
if sg.OwnerID == r.ID {
continue
}
if sg.StateInSpace == nil {
panic(fmt.Sprintf("pre-calculated distance group not in space: i=%d", sgi))
}
// Groups heading to one of the recipient's planets are listed in full
// under "incoming groups"; the unidentified list is for the rest.
if c.MustPlanet(sg.Destination).OwnedBy(r.ID) {
continue
}
// Shown once, and only within the visibility range (driveTech*30) of at
// least one of the recipient's planets.
visible := false
for pi := range c.g.Map.Planet {
p := &c.g.Map.Planet[pi]
if !p.OwnedBy(r.ID) {
continue
}
if v, ok := rep.InSpaceGroupRangeCache[sgi][p.Number]; ok && v <= visibility {
visible = true
break
}
}
if !visible {
continue
}
sliceIndexValidate(&rep.UnidentifiedGroup, i)
rep.UnidentifiedGroup[i].X = mr.F(sg.StateInSpace.X.F())
rep.UnidentifiedGroup[i].Y = mr.F(sg.StateInSpace.Y.F())
i++
}
}
func (c *Cache) otherGroup(v *mr.OtherGroup, sg *game.ShipGroup, st *game.ShipType) {
v.Number = sg.Number
v.Class = st.Name
// rep.LocalGroup[i].Tech = make(map[string]mr.Float)
for t, val := range sg.Tech {
v.Tech[t.String()] = mr.F(val.F())
}
v.Cargo = sg.CargoString()
v.Load = mr.F(sg.Load.F())
v.Destination = sg.Destination
if sg.State() == game.StateInSpace {
v.Origin = &sg.StateInSpace.Origin
p2 := c.MustPlanet(v.Destination)
rangeToDestination := mr.F(calc.ShortDistance(c.g.Map.Width, c.g.Map.Height, sg.StateInSpace.X.F(), sg.StateInSpace.Y.F(), p2.X.F(), p2.Y.F()))
v.Range = &rangeToDestination
}
v.Speed = mr.F(sg.Speed(st))
v.Mass = mr.F(st.EmptyMass())
v.Race = c.g.Race[c.RaceIndex(sg.OwnerID)].Name
}
func (c *Cache) localPlanet(v *mr.LocalPlanet, p *game.Planet) {
uninhabitedPlanet(&v.UninhabitedPlanet, p)
v.Industry = mr.F(p.Industry.F())
v.Population = mr.F(p.Population.F())
v.Colonists = mr.F(p.Colonists.F())
v.Production = c.PlanetProductionDisplayName(p.Number)
// between-turn report: ships upgrading on the planet decreases free indistrial potential
v.FreeIndustry = mr.F(c.PlanetProductionCapacity(p.Number))
}
func uninhabitedPlanet(v *mr.UninhabitedPlanet, p *game.Planet) {
unidentifiedPlanet(&v.UnidentifiedPlanet, p)
v.Size = mr.F(p.Size.F())
v.Name = p.Name
v.Resources = mr.F(p.Resources.F())
v.Capital = mr.F(p.Capital.F())
v.Material = mr.F(p.Material.F())
}
func unidentifiedPlanet(v *mr.UnidentifiedPlanet, p *game.Planet) {
v.Number = p.Number
v.X = mr.F(p.X.F())
v.Y = mr.F(p.Y.F())
}
func sliceIndexValidate[S ~[]E, E any](s *S, i int) {
if cap(*s) < i+1 {
*s = slices.Grow(*s, 10)
}
if len(*s) < i+1 {
*s = (*s)[:i+1]
}
}
func boolCompare(a, b bool) int {
if a == b {
return 0
}
if a == false {
return -1
}
return 1
}