diff --git a/client/client.go b/client/client.go index 44e02b5..8d0ea9f 100644 --- a/client/client.go +++ b/client/client.go @@ -198,7 +198,7 @@ func (e *client) loadWorld(w *world.World) { if w == nil { return } - w.SetCircleRadiusScaleFp(world.SCALE / 4) + w.SetCircleRadiusScaleFp(world.SCALE / 1000) e.world = w // TODO: store camera position in user settings e.wp.CameraXWorldFp = w.W / 2 diff --git a/client/world/world.go b/client/world/world.go index da61e92..c51be40 100644 --- a/client/world/world.go +++ b/client/world/world.go @@ -87,7 +87,7 @@ func NewWorld(width, height int) *World { themeDefaultCircleStyleID: StyleIDDefaultCircle, themeDefaultPointStyleID: StyleIDDefaultPoint, - circleRadiusScaleFp: 1, + circleRadiusScaleFp: SCALE, derivedCache: make(map[derivedStyleKey]StyleID, 128), diff --git a/game/internal/generator/generator.go b/game/internal/generator/generator.go index 4cdabc9..4f4c25d 100644 --- a/game/internal/generator/generator.go +++ b/game/internal/generator/generator.go @@ -2,10 +2,16 @@ package generator import ( "fmt" + "galaxy/util" "math" "math/rand" ) +const ( + fullSectorWithFactor = int(360. / defaultFactor) + deadZoneDWGrad = 15. +) + func Generate(cfg ...func(*MapSetting)) (Map, error) { ms := DefaultMapSetting() for i := range cfg { @@ -43,12 +49,35 @@ func Generate(cfg ...func(*MapSetting)) (Map, error) { } hwPlanet := NewPlanet(PlanetClassHW, hwCoord, ms.HWSize, ms.HWResources) m.HomePlanets[player] = PlanetarySystem{HW: hwPlanet, DW: make([]Planet, ms.DWCount)} + grads := make(map[uint16]bool, fullSectorWithFactor) + for i := range fullSectorWithFactor { + grads[uint16(i)] = true + } for dw := 0; dw < int(ms.DWCount); dw++ { p := rand.Float64()*(float64(ms.DWMaxDistance)-float64(ms.DWMinDistance)) + float64(ms.DWMinDistance) - phi := rand.Float64() * 360 - x := p * math.Cos(phi) - y := p * math.Sin(phi) - dwPlanet := NewPlanet(PlanetClassDW, Coordinate{hwCoord.X + x, hwCoord.Y + y}, ms.DWSize, ms.DWResources) + free := make([]uint16, 0) + for g := range grads { + if v, ok := grads[g]; ok && v { + free = append(free, g) + } + } + randGrad := free[rand.Intn(len(free))] + phi := float64(randGrad) * defaultFactor + for i := range int(deadZoneDWGrad / defaultFactor) { + lx := randGrad - uint16(i) + if uint16(i) > randGrad { + lx = uint16(fullSectorWithFactor) - (uint16(i) - randGrad) + } + grads[lx] = false + rx := randGrad + uint16(i) + if rx > uint16(fullSectorWithFactor) { + rx = rx - uint16(fullSectorWithFactor) + } + grads[rx] = false + } + x := util.WrapF(hwCoord.X+p*math.Cos(phi), int(size)) + y := util.WrapF(hwCoord.Y+p*math.Sin(phi), int(size)) + dwPlanet := NewPlanet(PlanetClassDW, Coordinate{x, y}, ms.DWSize, ms.DWResources) m.HomePlanets[player].DW[dw] = dwPlanet } } diff --git a/game/internal/generator/generator_test.go b/game/internal/generator/generator_test.go index 56641f9..c0ec1c7 100644 --- a/game/internal/generator/generator_test.go +++ b/game/internal/generator/generator_test.go @@ -24,10 +24,12 @@ func TestGenerator(t *testing.T) { } assert.Equal(t, players, len(m.HomePlanets), "hw-s count") for hw := range m.HomePlanets { + testPlanetPositionOnMap(t, generator.PlanetClassHW, m.Height, m.Width, m.HomePlanets[hw].HW.Position.X, m.HomePlanets[hw].HW.Position.Y) assert.Equal(t, s.HWSize, m.HomePlanets[hw].HW.Size, "hw #%d: size", hw) assert.Equal(t, s.HWResources, m.HomePlanets[hw].HW.Resources, "hw #%d: resources", hw) assert.Equal(t, int(s.DWCount), len(m.HomePlanets[hw].DW), "hw #%d: dw-s count", hw) for dw := range m.HomePlanets[hw].DW { + testPlanetPositionOnMap(t, generator.PlanetClassDW, m.Height, m.Width, m.HomePlanets[hw].DW[dw].Position.X, m.HomePlanets[hw].DW[dw].Position.Y) assert.Equal(t, s.DWSize, m.HomePlanets[hw].DW[dw].Size, "hw #%d dw #%d: size", hw, dw) assert.Equal(t, s.DWResources, m.HomePlanets[hw].DW[dw].Resources, "hw #%d dw #%d: resources", hw, dw) d := m.ShortDistance(m.HomePlanets[hw].HW.Position, m.HomePlanets[hw].DW[dw].Position) @@ -43,7 +45,7 @@ func TestGenerator(t *testing.T) { freePlanetCount := make(map[generator.PlanetClass]int) for fp := range m.FreePlanets { ps := planetSettings(t, m.FreePlanets[fp].PlanetClass, s) - testPlanetParameters(t, ps, m.FreePlanets[fp]) + testPlanetParameters(t, ps, m.Height, m.Width, m.FreePlanets[fp]) if v, ok := freePlanetCount[m.FreePlanets[fp].PlanetClass]; !ok { freePlanetCount[m.FreePlanets[fp].PlanetClass] = 1 } else { @@ -68,13 +70,21 @@ func TestGenerator(t *testing.T) { } } -func testPlanetParameters(t *testing.T, s generator.PlanetSetting, p generator.Planet) { +func testPlanetParameters(t *testing.T, s generator.PlanetSetting, mapW, mapH uint32, p generator.Planet) { + testPlanetPositionOnMap(t, p.PlanetClass, mapW, mapH, p.Position.X, p.Position.Y) assert.LessOrEqualf(t, s.MinResource, p.Resources, "planet class=%s min resources", p.PlanetClass) assert.GreaterOrEqualf(t, s.MaxResource, p.Resources, "planet class=%s max resources", p.PlanetClass) assert.LessOrEqualf(t, s.MinSize, p.Size, "planet class=%s min size", p.PlanetClass) assert.GreaterOrEqualf(t, s.MaxSize, p.Size, "planet class=%s max size", p.PlanetClass) } +func testPlanetPositionOnMap(t *testing.T, cls generator.PlanetClass, mapW, mapH uint32, x, y float64) { + assert.GreaterOrEqual(t, x, 0.0, "planet class=%s x=%f < 0", cls, x) + assert.Less(t, x, float64(mapW), "planet class=%s x=%f >= %d", cls, x, mapW) + assert.GreaterOrEqual(t, y, 0.0, "planet class=%s y=%f < 0", cls, y) + assert.Less(t, y, float64(mapH), "planet class=%s y=%f >= %d", cls, y, mapH) +} + func planetSettings(t *testing.T, pc generator.PlanetClass, s generator.MapSetting) generator.PlanetSetting { switch pc { case generator.PlanetClassGiant: diff --git a/pkg/model/report/planet.go b/pkg/model/report/planet.go index 6bcebae..d697b21 100644 --- a/pkg/model/report/planet.go +++ b/pkg/model/report/planet.go @@ -11,7 +11,7 @@ type LocalPlanet struct { Population Float `json:"population"` // P - Население Colonists Float `json:"colonists"` // COL C - Количество колонистов Production string `json:"production"` - FreeIndustry Float `json:"freeInductry"` // Параметр "L" - Свободный производственный потенциал + FreeIndustry Float `json:"freeIndustry"` // Параметр "L" - Свободный производственный потенциал // [ ] FreeIndustry - неактуальная информация, т.к. модернизация происходит в процессе производства хода } diff --git a/pkg/util/map.go b/pkg/util/map.go index ad8333a..85edb4a 100644 --- a/pkg/util/map.go +++ b/pkg/util/map.go @@ -2,6 +2,32 @@ package util import "math" +// WrapF maps value into the half-open interval [0, float64(size)). +// It supports negative input values and is used for torus coordinates +// when the coordinate is represented as float64. +// +// size must be greater than zero. +func WrapF(value float64, size int) float64 { + if size <= 0 { + panic("WrapF: size must be > 0") + } + + s := float64(size) + + r := math.Mod(value, s) + if r < 0 { + r += s + } + + // Protect against a possible boundary artifact and keep result in [0, s). + // In normal cases math.Mod already gives a value with |r| < s. + if r >= s { + r -= s + } + + return r +} + func ShortDistance(w, h uint32, x1, y1, x2, y2 float64) float64 { return math.Hypot(deltas(w, h, x1, y1, x2, y2)) } diff --git a/pkg/util/map_test.go b/pkg/util/map_test.go index 24b24a5..1ef5ddf 100644 --- a/pkg/util/map_test.go +++ b/pkg/util/map_test.go @@ -2,11 +2,13 @@ package util_test import ( "fmt" + "math" "testing" "galaxy/util" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestShortDistance(t *testing.T) { @@ -54,3 +56,96 @@ func TestNextTravelCoord(t *testing.T) { }) } } + +func TestWrapF(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + value float64 + size int + want float64 + }{ + { + name: "already in range integer", + value: 3, + size: 10, + want: 3, + }, + { + name: "already in range fractional", + value: 3.25, + size: 10, + want: 3.25, + }, + { + name: "positive overflow integer", + value: 12, + size: 10, + want: 2, + }, + { + name: "positive overflow fractional", + value: 12.75, + size: 10, + want: 2.75, + }, + { + name: "negative small fractional", + value: -0.25, + size: 10, + want: 9.75, + }, + { + name: "negative overflow integer", + value: -12, + size: 10, + want: 8, + }, + { + name: "negative overflow fractional", + value: -12.75, + size: 10, + want: 7.25, + }, + { + name: "exact multiple positive", + value: 20, + size: 10, + want: 0, + }, + { + name: "exact multiple negative", + value: -20, + size: 10, + want: 0, + }, + { + name: "size one wraps into [0,1)", + value: 123.456, + size: 1, + want: math.Mod(123.456, 1), + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + got := util.WrapF(tt.value, tt.size) + require.InDelta(t, tt.want, got, 1e-12) + require.GreaterOrEqual(t, got, 0.0) + require.Less(t, got, float64(tt.size)) + }) + } +} + +// TestWrapF_NaNAndInf verifies IEEE 754 behavior inherited from math.Mod. +func TestWrapF_NaNAndInf(t *testing.T) { + t.Parallel() + + require.True(t, math.IsNaN(util.WrapF(math.NaN(), 10))) + require.True(t, math.IsNaN(util.WrapF(math.Inf(1), 10))) + require.True(t, math.IsNaN(util.WrapF(math.Inf(-1), 10))) +}