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
+1 -1
View File
@@ -1 +1 @@
# galaxy-game
# galaxy-game
+15 -15
View File
@@ -9,35 +9,35 @@ import (
const intSize = 32
type bitmap struct {
type Bitmap struct {
width uint32
height uint32
bitVector []uint32
}
func NewBitmap(width uint32, height uint32) bitmap {
return bitmap{width: width, height: height, bitVector: make([]uint32, int(math.Ceil(float64(width*height)/intSize)))}
func NewBitmap(width uint32, height uint32) Bitmap {
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
boundY := (p.height + uint32(y)) % p.height
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))
}
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)
}
func (p bitmap) isSet(number uint32) bool {
func (p Bitmap) isSet(number uint32) bool {
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)
for i := range p.bitVector {
result -= bits.OnesCount32(p.bitVector[i])
@@ -45,7 +45,7 @@ func (p bitmap) FreeCount() (result int) {
return
}
func (p bitmap) GetFreeN(number int) (int, int, error) {
func (p Bitmap) GetFreeN(number int) (int, int, error) {
if p.FreeCount() == 0 {
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)
}
func (p bitmap) SetFreeN(number int) error {
func (p Bitmap) SetFreeN(number int) error {
if p.FreeCount() == 0 {
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)
}
func (p bitmap) Circle(x, y int, r float32, fill bool) {
func (p Bitmap) Circle(x, y int, r float32, fill bool) {
plotX := 0
plotY := int(math.Ceil(float64(r)))
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
plotY := int(math.Ceil(r))
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)
@@ -150,13 +150,13 @@ func (p bitmap) octant(x, y int, plotX, plotY int) {
p.Set(x-plotY, y-plotX)
}
func (p bitmap) Clear() {
func (p Bitmap) Clear() {
for i := range p.bitVector {
p.bitVector[i] &= 0
}
}
func (p bitmap) String() string {
func (p Bitmap) String() string {
px := map[bool]string{true: "██", false: "░░"}
var result string
cnt := 0
+2 -2
View File
@@ -2,8 +2,8 @@ package bitmap
import "slices"
func (p bitmap) value() []uint32 {
func (p Bitmap) value() []uint32 {
return slices.Clone(p.bitVector)
}
var Value = (bitmap).value
var Value = (Bitmap).value
+42 -44
View File
@@ -3,74 +3,72 @@ package generator
import (
"fmt"
"math"
"math/rand"
)
func Generate(ms MapSetting) (Map, error) {
pl := func(c Coordinate, ps PlanetSetting) Planet {
return Planet{
Position: c,
Size: ps.MinSize + rand.Float32()*(ps.MaxSize-ps.MinSize),
Resources: float32(ps.MinResource) + rand.Float32()*(ps.MaxResource-ps.MinResource)}
func (m *Map) CreatePlanets(num int, deadZoneRadius float32, size, resources func() float32) error {
for range num {
coord, err := m.NewCoordinate(deadZoneRadius)
if err != nil {
return err
}
planet := NewPlanet(coord, size(), resources())
m.AddPlanet(planet)
}
// mapSize := uint(math.Ceil(math.Sqrt(float64(param.Players)))) * param.HW_MinDistance
var mapSize uint = 200
return nil
}
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 {
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)
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)))
fmt.Println("generating", giantsNum, "giant planets")
for range giantsNum {
coord, err := result.NewCoordinate(float32(ms.GiantPlanets.MinDistanceHW))
if err != nil {
return Map{}, err
}
planet := pl(coord, ms.GiantPlanets)
result.AddPlanet(planet)
}
m.CreatePlanets(giantsNum, float32(ms.GiantPlanets.MinDistanceHW),
RandIFn(ms.GiantPlanets.MinSize, ms.GiantPlanets.MaxSize),
RandIFn(ms.GiantPlanets.MinResource, ms.GiantPlanets.MaxResource),
)
// 2. Place Big planets
bigsNum := int(math.Ceil(float64(freePlanets) * float64(ms.BigPlanets.Probability)))
fmt.Println("generating", bigsNum, "big planets")
for range bigsNum {
coord, err := result.NewCoordinate(float32(ms.BigPlanets.MinDistanceHW))
if err != nil {
return Map{}, err
}
planet := pl(coord, ms.BigPlanets)
result.AddPlanet(planet)
}
m.CreatePlanets(bigsNum, float32(ms.BigPlanets.MinDistanceHW),
RandIFn(ms.BigPlanets.MinSize, ms.BigPlanets.MaxSize),
RandIFn(ms.BigPlanets.MinResource, ms.BigPlanets.MaxResource),
)
// X. Place players' Home Worlds
for player := 0; player < int(ms.Players); player++ {
fmt.Println("generating HW #", player)
coord, err := result.NewCoordinate(float32(ms.HWMinDistance))
coord, err := m.NewCoordinate(float32(ms.HWMinDistance))
if err != nil {
return Map{}, err
}
planet := Planet{Position: coord, Size: float32(ms.HWSize), Resources: float32(ms.HWResources)}
result.HomePlanets[player] = PlanetarySystem{HW: planet}
planet := NewPlanet(coord, float32(ms.HWSize), float32(ms.HWResources))
m.HomePlanets[player] = PlanetarySystem{HW: planet}
}
result.plotter.clearFn()
m.plotter.Clear()
for i := range result.HomePlanets {
result.plotter.MarkDeadZone(result.HomePlanets[i].HW.Position.X, result.HomePlanets[i].HW.Position.Y, 5)
for j := range result.HomePlanets[i].DW {
result.plotter.MarkDeadZone(result.HomePlanets[i].DW[j].Position.X, result.HomePlanets[i].DW[j].Position.Y, 5)
for i := range m.HomePlanets {
m.plotter.MarkDeadZone(m.HomePlanets[i].HW.Position.X, m.HomePlanets[i].HW.Position.Y, 5)
for j := range m.HomePlanets[i].DW {
m.plotter.MarkDeadZone(m.HomePlanets[i].DW[j].Position.X, m.HomePlanets[i].DW[j].Position.Y, 5)
}
}
for i := range result.FreePlanets {
result.plotter.MarkDeadZone(result.FreePlanets[i].Position.X, result.FreePlanets[i].Position.Y, 5)
for i := range m.FreePlanets {
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) {
generator.Generate(generator.DefaultMapSetting())
generator.Generate()
}
+28 -5
View File
@@ -2,14 +2,17 @@ package generator
import (
"fmt"
"math/rand"
"github.com/iliadenisov/galaxy/pkg/generator/plotter"
)
type Map struct {
Width uint
Height uint
Width uint32
Height uint32
HomePlanets []PlanetarySystem
FreePlanets []Planet
plotter Plotter
plotter plotter.Plotter
}
type Coordinate struct {
@@ -27,8 +30,8 @@ type PlanetarySystem struct {
DW []Planet
}
func NewMap(width, height, players uint) (*Map, error) {
p, err := NewPlotter(width, height, 1.0)
func NewMap(width, height, players uint32) (*Map, error) {
p, err := plotter.NewPlotter(width, height, 1.0)
if err != nil {
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
}
}
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
type MapSetting struct {
Players uint
HWSize uint
HWResources uint
HWMinDistance uint
DWCount uint
DWSize uint
DWResources uint
DWMinDistance uint
DWMaxDistance uint
Players uint32
HWSize uint32
HWResources uint32
HWMinDistance uint32
DWCount uint32
DWSize uint32
DWResources uint32
DWMinDistance uint32
DWMaxDistance uint32
GiantPlanets PlanetSetting
BigPlanets PlanetSetting
@@ -19,7 +19,7 @@ type MapSetting struct {
}
type PlanetSetting struct {
MinDistanceHW uint
MinDistanceHW uint32
MinSize float32
MaxSize float32
MinResource float32
+2 -2
View File
@@ -16,7 +16,7 @@ func New(storage storage.Storage) Server {
return Server{storage: storage}
}
func (s Server) CreateGame(gameParam game.GameParameter, mapParam generator.MapSetting) (game.Game, error) {
_, _ = generator.Generate(mapParam)
func (s Server) CreateGame(gameParam game.GameParameter) (game.Game, error) {
_, _ = generator.Generate()
return game.Game{}, errors.New("not yet implemented")
}