refactor: plotter, generator

This commit is contained in:
Ilia Denisov
2025-09-12 21:32:50 +03:00
parent d3b00b5c8d
commit 05999687aa
11 changed files with 262 additions and 140 deletions
+15 -15
View File
@@ -9,35 +9,35 @@ import (
const intSize = 32 const intSize = 32
type bitmap struct { type Bitmap struct {
width uint32 width uint32
height uint32 height uint32
bitVector []uint32 bitVector []uint32
} }
func NewBitmap(width uint32, height uint32) bitmap { func NewBitmap(width uint32, height uint32) Bitmap {
return bitmap{width: width, height: height, bitVector: make([]uint32, int(math.Ceil(float64(width*height)/intSize)))} return Bitmap{width: width, height: height, bitVector: make([]uint32, int(math.Ceil(float64(width*height)/intSize)))}
} }
func (p bitmap) Set(x, y int) { func (p Bitmap) Set(x, y int) {
boundX := (p.width + uint32(x)) % p.width boundX := (p.width + uint32(x)) % p.width
boundY := (p.height + uint32(y)) % p.height boundY := (p.height + uint32(y)) % p.height
p.set(boundX + boundY*p.width) p.set(boundX + boundY*p.width)
} }
func (p bitmap) set(number uint32) { func (p Bitmap) set(number uint32) {
p.bitVector[number/intSize] |= (0b1 << (number % intSize)) p.bitVector[number/intSize] |= (0b1 << (number % intSize))
} }
func (p bitmap) IsSet(x, y int) bool { func (p Bitmap) IsSet(x, y int) bool {
return p.isSet(uint32(x) + uint32(y)*p.width) return p.isSet(uint32(x) + uint32(y)*p.width)
} }
func (p bitmap) isSet(number uint32) bool { func (p Bitmap) isSet(number uint32) bool {
return p.bitVector[number/intSize]&(0b1<<(number%intSize)) > 0 return p.bitVector[number/intSize]&(0b1<<(number%intSize)) > 0
} }
func (p bitmap) FreeCount() (result int) { func (p Bitmap) FreeCount() (result int) {
result = int(p.width) * int(p.height) result = int(p.width) * int(p.height)
for i := range p.bitVector { for i := range p.bitVector {
result -= bits.OnesCount32(p.bitVector[i]) result -= bits.OnesCount32(p.bitVector[i])
@@ -45,7 +45,7 @@ func (p bitmap) FreeCount() (result int) {
return return
} }
func (p bitmap) GetFreeN(number int) (int, int, error) { func (p Bitmap) GetFreeN(number int) (int, int, error) {
if p.FreeCount() == 0 { if p.FreeCount() == 0 {
return 0, 0, errors.New("no free pixels left") return 0, 0, errors.New("no free pixels left")
} }
@@ -65,7 +65,7 @@ func (p bitmap) GetFreeN(number int) (int, int, error) {
return 0, 0, fmt.Errorf("get free pixel: no such number=%d, max=%d", number, n) return 0, 0, fmt.Errorf("get free pixel: no such number=%d, max=%d", number, n)
} }
func (p bitmap) SetFreeN(number int) error { func (p Bitmap) SetFreeN(number int) error {
if p.FreeCount() == 0 { if p.FreeCount() == 0 {
return errors.New("no free pixels left") return errors.New("no free pixels left")
} }
@@ -84,7 +84,7 @@ func (p bitmap) SetFreeN(number int) error {
return fmt.Errorf("set free pixel: no such number=%d, max=%d", number, n) return fmt.Errorf("set free pixel: no such number=%d, max=%d", number, n)
} }
func (p bitmap) Circle(x, y int, r float32, fill bool) { func (p Bitmap) Circle(x, y int, r float32, fill bool) {
plotX := 0 plotX := 0
plotY := int(math.Ceil(float64(r))) plotY := int(math.Ceil(float64(r)))
delta := 3 - 2*plotY delta := 3 - 2*plotY
@@ -115,7 +115,7 @@ func (p bitmap) Circle(x, y int, r float32, fill bool) {
} }
} }
func (p bitmap) circleAdjacent(x, y int, r float64) { func (p Bitmap) circleAdjacent(x, y int, r float64) {
plotX := 0 plotX := 0
plotY := int(math.Ceil(r)) plotY := int(math.Ceil(r))
delta := 1 - 2*plotY delta := 1 - 2*plotY
@@ -139,7 +139,7 @@ func (p bitmap) circleAdjacent(x, y int, r float64) {
} }
} }
func (p bitmap) octant(x, y int, plotX, plotY int) { func (p Bitmap) octant(x, y int, plotX, plotY int) {
p.Set(x+plotX, y+plotY) p.Set(x+plotX, y+plotY)
p.Set(x+plotX, y-plotY) p.Set(x+plotX, y-plotY)
p.Set(x-plotX, y+plotY) p.Set(x-plotX, y+plotY)
@@ -150,13 +150,13 @@ func (p bitmap) octant(x, y int, plotX, plotY int) {
p.Set(x-plotY, y-plotX) p.Set(x-plotY, y-plotX)
} }
func (p bitmap) Clear() { func (p Bitmap) Clear() {
for i := range p.bitVector { for i := range p.bitVector {
p.bitVector[i] &= 0 p.bitVector[i] &= 0
} }
} }
func (p bitmap) String() string { func (p Bitmap) String() string {
px := map[bool]string{true: "██", false: "░░"} px := map[bool]string{true: "██", false: "░░"}
var result string var result string
cnt := 0 cnt := 0
+2 -2
View File
@@ -2,8 +2,8 @@ package bitmap
import "slices" import "slices"
func (p bitmap) value() []uint32 { func (p Bitmap) value() []uint32 {
return slices.Clone(p.bitVector) return slices.Clone(p.bitVector)
} }
var Value = (bitmap).value var Value = (Bitmap).value
+42 -44
View File
@@ -3,74 +3,72 @@ package generator
import ( import (
"fmt" "fmt"
"math" "math"
"math/rand"
) )
func Generate(ms MapSetting) (Map, error) { func (m *Map) CreatePlanets(num int, deadZoneRadius float32, size, resources func() float32) error {
pl := func(c Coordinate, ps PlanetSetting) Planet { for range num {
return Planet{ coord, err := m.NewCoordinate(deadZoneRadius)
Position: c, if err != nil {
Size: ps.MinSize + rand.Float32()*(ps.MaxSize-ps.MinSize), return err
Resources: float32(ps.MinResource) + rand.Float32()*(ps.MaxResource-ps.MinResource)} }
planet := NewPlanet(coord, size(), resources())
m.AddPlanet(planet)
} }
// mapSize := uint(math.Ceil(math.Sqrt(float64(param.Players)))) * param.HW_MinDistance return nil
var mapSize uint = 200 }
result, err := NewMap(mapSize, mapSize, ms.Players) func Generate(cfg ...func(*MapSetting)) (Map, error) {
ms := DefaultMapSetting()
for i := range cfg {
cfg[i](&ms)
}
// TODO: pre-calculate sufficient map size
var mapSize uint32 = 200
m, err := NewMap(mapSize, mapSize, ms.Players)
if err != nil { if err != nil {
return Map{}, fmt.Errorf("NewMap: %s", err) return Map{}, fmt.Errorf("NewMap: %s", err)
} }
totalPlanets := ms.Players * 10 totalPlanets := ms.Players * 10 // TODO: why 10?
freePlanets := totalPlanets - ms.Players*(ms.DWCount+1) freePlanets := totalPlanets - ms.Players*(ms.DWCount+1)
fmt.Println("map:", mapSize, "players:", ms.Players, "planets:", totalPlanets, "uninhabited:", freePlanets) // 1. Place Giant planets
giantsNum := int(math.Ceil(float64(freePlanets) * float64(ms.GiantPlanets.Probability))) giantsNum := int(math.Ceil(float64(freePlanets) * float64(ms.GiantPlanets.Probability)))
fmt.Println("generating", giantsNum, "giant planets") m.CreatePlanets(giantsNum, float32(ms.GiantPlanets.MinDistanceHW),
for range giantsNum { RandIFn(ms.GiantPlanets.MinSize, ms.GiantPlanets.MaxSize),
coord, err := result.NewCoordinate(float32(ms.GiantPlanets.MinDistanceHW)) RandIFn(ms.GiantPlanets.MinResource, ms.GiantPlanets.MaxResource),
if err != nil { )
return Map{}, err
}
planet := pl(coord, ms.GiantPlanets)
result.AddPlanet(planet)
}
// 2. Place Big planets
bigsNum := int(math.Ceil(float64(freePlanets) * float64(ms.BigPlanets.Probability))) bigsNum := int(math.Ceil(float64(freePlanets) * float64(ms.BigPlanets.Probability)))
fmt.Println("generating", bigsNum, "big planets") m.CreatePlanets(bigsNum, float32(ms.BigPlanets.MinDistanceHW),
for range bigsNum { RandIFn(ms.BigPlanets.MinSize, ms.BigPlanets.MaxSize),
coord, err := result.NewCoordinate(float32(ms.BigPlanets.MinDistanceHW)) RandIFn(ms.BigPlanets.MinResource, ms.BigPlanets.MaxResource),
if err != nil { )
return Map{}, err
}
planet := pl(coord, ms.BigPlanets)
result.AddPlanet(planet)
}
// X. Place players' Home Worlds
for player := 0; player < int(ms.Players); player++ { for player := 0; player < int(ms.Players); player++ {
fmt.Println("generating HW #", player) coord, err := m.NewCoordinate(float32(ms.HWMinDistance))
coord, err := result.NewCoordinate(float32(ms.HWMinDistance))
if err != nil { if err != nil {
return Map{}, err return Map{}, err
} }
planet := Planet{Position: coord, Size: float32(ms.HWSize), Resources: float32(ms.HWResources)} planet := NewPlanet(coord, float32(ms.HWSize), float32(ms.HWResources))
result.HomePlanets[player] = PlanetarySystem{HW: planet} m.HomePlanets[player] = PlanetarySystem{HW: planet}
} }
result.plotter.clearFn() m.plotter.Clear()
for i := range result.HomePlanets { for i := range m.HomePlanets {
result.plotter.MarkDeadZone(result.HomePlanets[i].HW.Position.X, result.HomePlanets[i].HW.Position.Y, 5) m.plotter.MarkDeadZone(m.HomePlanets[i].HW.Position.X, m.HomePlanets[i].HW.Position.Y, 5)
for j := range result.HomePlanets[i].DW { for j := range m.HomePlanets[i].DW {
result.plotter.MarkDeadZone(result.HomePlanets[i].DW[j].Position.X, result.HomePlanets[i].DW[j].Position.Y, 5) m.plotter.MarkDeadZone(m.HomePlanets[i].DW[j].Position.X, m.HomePlanets[i].DW[j].Position.Y, 5)
} }
} }
for i := range result.FreePlanets { for i := range m.FreePlanets {
result.plotter.MarkDeadZone(result.FreePlanets[i].Position.X, result.FreePlanets[i].Position.Y, 5) m.plotter.MarkDeadZone(m.FreePlanets[i].Position.X, m.FreePlanets[i].Position.Y, 5)
} }
return *result, nil return *m, nil
} }
+1 -1
View File
@@ -7,5 +7,5 @@ import (
) )
func Test_Generator(t *testing.T) { func Test_Generator(t *testing.T) {
generator.Generate(generator.DefaultMapSetting()) generator.Generate()
} }
+28 -5
View File
@@ -2,14 +2,17 @@ package generator
import ( import (
"fmt" "fmt"
"math/rand"
"github.com/iliadenisov/galaxy/pkg/generator/plotter"
) )
type Map struct { type Map struct {
Width uint Width uint32
Height uint Height uint32
HomePlanets []PlanetarySystem HomePlanets []PlanetarySystem
FreePlanets []Planet FreePlanets []Planet
plotter Plotter plotter plotter.Plotter
} }
type Coordinate struct { type Coordinate struct {
@@ -27,8 +30,8 @@ type PlanetarySystem struct {
DW []Planet DW []Planet
} }
func NewMap(width, height, players uint) (*Map, error) { func NewMap(width, height, players uint32) (*Map, error) {
p, err := NewPlotter(width, height, 1.0) p, err := plotter.NewPlotter(width, height, 1.0)
if err != nil { if err != nil {
return nil, fmt.Errorf("NewPlotter: %s", err) return nil, fmt.Errorf("NewPlotter: %s", err)
} }
@@ -51,3 +54,23 @@ func (m Map) NewCoordinate(deadZoneRaduis float32) (Coordinate, error) {
return Coordinate{X: x, Y: y}, nil return Coordinate{X: x, Y: y}, nil
} }
} }
func NewPlanet(c Coordinate, size, resources float32) Planet {
return Planet{
Position: c,
Size: size,
Resources: resources,
}
}
// RandI returns a random float32 value between min and max
func RandI(min, max float32) float32 {
return min + rand.Float32()*(max-min)
}
// RandIFn is a wrapper for the [RandI] func
func RandIFn(min, max float32) func() float32 {
return func() float32 {
return RandI(min, max)
}
}
-60
View File
@@ -1,60 +0,0 @@
package generator
import (
"errors"
"fmt"
"math/rand"
"github.com/iliadenisov/galaxy/pkg/bitmap"
)
type Plotter struct {
factor float32
clearFn func()
circleFn func(x, y int, r float32)
freeCountFn func() int
freeNumberToCoordFn func(int) (int, int)
}
func NewPlotter(width, height uint, factor float32) (Plotter, error) {
if factor > 1 || factor <= 0 {
return Plotter{}, fmt.Errorf("factor should be: 0 > F <= 1")
}
sectorsX := uint32(float32(width) / factor)
sectorsY := uint32(float32(height) / factor)
bm := bitmap.NewBitmap(sectorsX, sectorsY)
return Plotter{
factor: factor,
clearFn: bm.Clear,
circleFn: func(x, y int, r float32) { bm.Circle(x, y, r, true) },
freeCountFn: bm.FreeCount,
freeNumberToCoordFn: func(n int) (x int, y int) {
x, y, err := bm.GetFreeN(n)
if err != nil {
panic(err)
}
return
},
}, nil
}
func (p Plotter) RandomFreePoint(deadZoneRaduis float32) (float32, float32, error) {
fsCount := p.freeCountFn()
if fsCount == 0 {
return 0, 0, errors.New("no more space for planets")
}
next := rand.Intn(fsCount)
x, y := p.freeNumberToCoordFn(next)
p.PlotDeadZone(x, y, deadZoneRaduis)
planetX := float32(x)*p.factor + rand.Float32()*p.factor // TODO: correct shift?
planetY := float32(y)*p.factor + rand.Float32()*p.factor
return planetX, planetY, nil
}
func (p Plotter) MarkDeadZone(x, y float32, radius float32) {
p.PlotDeadZone(int(x/p.factor), int(y/p.factor), radius)
}
func (p Plotter) PlotDeadZone(x, y int, radius float32) {
p.circleFn(x, y, radius/p.factor)
}
+75
View File
@@ -0,0 +1,75 @@
package plotter
import (
"errors"
"fmt"
"math"
"math/rand"
"github.com/iliadenisov/galaxy/pkg/bitmap"
)
type Plotter struct {
factor float32
clearFn func()
circleFn func(x, y int, r float32)
freeCountFn func() int
freeNumberToCoordFn func(int) (int, int, error)
}
func NewPlotter(width, height uint32, factor float32) (Plotter, error) {
return NewBitmapPlotter(NewBitmap(width, height, factor), factor)
}
func NewBitmap(width, height uint32, factor float32) bitmap.Bitmap {
return bitmap.NewBitmap(AsPlotterSize(width, height, factor))
}
func NewBitmapPlotter(bm bitmap.Bitmap, factor float32) (Plotter, error) {
if factor > 1 || factor <= 0 {
return Plotter{}, fmt.Errorf("factor should be: 0 > F <= 1")
}
return Plotter{
factor: factor,
clearFn: bm.Clear,
circleFn: func(x, y int, r float32) { bm.Circle(x, y, r, true) },
freeCountFn: bm.FreeCount,
freeNumberToCoordFn: bm.GetFreeN,
}, nil
}
func (p Plotter) RandomFreePoint(deadZoneRaduis float32) (float32, float32, error) {
if deadZoneRaduis <= 0. {
return 0, 0, fmt.Errorf("radius must be positive value: %f", deadZoneRaduis)
}
fsCount := p.freeCountFn()
if fsCount == 0 {
return 0, 0, errors.New("RandomFreePoint: no free space left")
}
next := rand.Intn(fsCount)
x, y, err := p.freeNumberToCoordFn(next)
if err != nil {
return 0, 0, fmt.Errorf("RandomFreePoint: freeNumberToCoordFn: %s", err)
}
p.plotDeadZone(x, y, deadZoneRaduis)
planetX := float32(x)*p.factor + rand.Float32()*p.factor
planetY := float32(y)*p.factor + rand.Float32()*p.factor
return planetX, planetY, nil
}
func (p Plotter) MarkDeadZone(x, y float32, radius float32) {
p.plotDeadZone(int(x/p.factor), int(y/p.factor), radius)
}
func (p Plotter) plotDeadZone(x, y int, radius float32) {
p.circleFn(x, y, radius/p.factor)
}
func (p Plotter) Clear() { p.clearFn() }
func AsPlotterSize(width, height uint32, factor float32) (uint32, uint32) {
if factor > 1 || factor <= 0 {
return width, height
}
return uint32(math.Ceil(float64(width) / float64(factor))), uint32(math.Ceil(float64(height) / float64(factor)))
}
+86
View File
@@ -0,0 +1,86 @@
package plotter_test
import (
"testing"
"github.com/iliadenisov/galaxy/pkg/generator/plotter"
)
func TestNewPlotter(t *testing.T) {
_, err := plotter.NewPlotter(10, 10, 0)
if err == nil {
t.Error("expect: error when factor=0")
}
_, err = plotter.NewPlotter(10, 10, -0.01)
if err == nil {
t.Error("expect: error when factor<0")
}
_, err = plotter.NewPlotter(10, 10, 1.001)
if err == nil {
t.Error("expect: error when factor>1")
}
_, err = plotter.NewPlotter(10, 10, 1)
if err != nil {
t.Error("expect: no error when factor=1")
}
}
func TestAsPlotterSize(t *testing.T) {
for _, tc := range []struct {
w, h uint32
f float32
ew, eh uint32
}{
{10, 10, 0, 10, 10},
{10, 10, -1, 10, 10},
{10, 10, 1.1, 10, 10},
{10, 20, 0.5, 20, 40},
{30, 60, 0.3, 100, 200},
{10, 20, 0.25, 40, 80},
} {
w, h := plotter.AsPlotterSize(tc.w, tc.h, tc.f)
if tc.ew != w || tc.eh != h {
t.Errorf("expect: w=%d h=%d, got: w=%d h=%d", tc.ew, tc.eh, w, h)
}
}
}
func TestRandomFreePoint(t *testing.T) {
var factor float32 = 0.25
var w, h uint32 = 20, 20
bm := plotter.NewBitmap(w, h, factor)
p, err := plotter.NewBitmapPlotter(bm, factor) // 80x80
if err != nil {
t.Fatal(err)
}
x, y, err := p.RandomFreePoint(3)
if err != nil {
t.Errorf("expect: no error getting random point, got: %s", err)
}
if x > float32(w) || y > float32(w) {
t.Errorf("expect: point coordinates within map size %dx%d, got: x=%f y=%f", w, h, x, y)
}
_, _, err = p.RandomFreePoint(0)
if err == nil {
t.Error("expect: error when radius not positive, got: none")
}
_, _, err = p.RandomFreePoint(float32(w + h)) // guaranteed to mark whole area dead zone
if err != nil {
t.Errorf("expect: no error getting random point, got: %s", err)
}
_, _, err = p.RandomFreePoint(1)
if err == nil {
t.Error("expect: error when no free space left, got: none")
}
p.Clear()
_, _, err = p.RandomFreePoint(10)
if err != nil {
t.Errorf("expect: no error getting random point after clearing, got: %s", err)
}
}
+10 -10
View File
@@ -1,15 +1,15 @@
package generator package generator
type MapSetting struct { type MapSetting struct {
Players uint Players uint32
HWSize uint HWSize uint32
HWResources uint HWResources uint32
HWMinDistance uint HWMinDistance uint32
DWCount uint DWCount uint32
DWSize uint DWSize uint32
DWResources uint DWResources uint32
DWMinDistance uint DWMinDistance uint32
DWMaxDistance uint DWMaxDistance uint32
GiantPlanets PlanetSetting GiantPlanets PlanetSetting
BigPlanets PlanetSetting BigPlanets PlanetSetting
@@ -19,7 +19,7 @@ type MapSetting struct {
} }
type PlanetSetting struct { type PlanetSetting struct {
MinDistanceHW uint MinDistanceHW uint32
MinSize float32 MinSize float32
MaxSize float32 MaxSize float32
MinResource float32 MinResource float32
+2 -2
View File
@@ -16,7 +16,7 @@ func New(storage storage.Storage) Server {
return Server{storage: storage} return Server{storage: storage}
} }
func (s Server) CreateGame(gameParam game.GameParameter, mapParam generator.MapSetting) (game.Game, error) { func (s Server) CreateGame(gameParam game.GameParameter) (game.Game, error) {
_, _ = generator.Generate(mapParam) _, _ = generator.Generate()
return game.Game{}, errors.New("not yet implemented") return game.Game{}, errors.New("not yet implemented")
} }