diff --git a/README.md b/README.md index 4ba95a9..e9f40ef 100644 --- a/README.md +++ b/README.md @@ -1 +1 @@ -# galaxy-game \ No newline at end of file +# galaxy-game diff --git a/pkg/bitmap/bitmap.go b/pkg/bitmap/bitmap.go index 4b6a106..4274eab 100644 --- a/pkg/bitmap/bitmap.go +++ b/pkg/bitmap/bitmap.go @@ -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 diff --git a/pkg/bitmap/export_test.go b/pkg/bitmap/export_test.go index 166adfd..6563e66 100644 --- a/pkg/bitmap/export_test.go +++ b/pkg/bitmap/export_test.go @@ -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 diff --git a/pkg/generator/generator.go b/pkg/generator/generator.go index 806260d..cd0956b 100644 --- a/pkg/generator/generator.go +++ b/pkg/generator/generator.go @@ -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 } diff --git a/pkg/generator/generator_test.go b/pkg/generator/generator_test.go index d527dca..9276ca8 100644 --- a/pkg/generator/generator_test.go +++ b/pkg/generator/generator_test.go @@ -7,5 +7,5 @@ import ( ) func Test_Generator(t *testing.T) { - generator.Generate(generator.DefaultMapSetting()) + generator.Generate() } diff --git a/pkg/generator/map.go b/pkg/generator/map.go index 9e098ee..28f60bb 100644 --- a/pkg/generator/map.go +++ b/pkg/generator/map.go @@ -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) + } +} diff --git a/pkg/generator/plotter.go b/pkg/generator/plotter.go deleted file mode 100644 index 478a4c4..0000000 --- a/pkg/generator/plotter.go +++ /dev/null @@ -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) -} diff --git a/pkg/generator/plotter/plotter.go b/pkg/generator/plotter/plotter.go new file mode 100644 index 0000000..c86e75a --- /dev/null +++ b/pkg/generator/plotter/plotter.go @@ -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))) +} diff --git a/pkg/generator/plotter/plotter_test.go b/pkg/generator/plotter/plotter_test.go new file mode 100644 index 0000000..15ed866 --- /dev/null +++ b/pkg/generator/plotter/plotter_test.go @@ -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) + } +} diff --git a/pkg/generator/settings.go b/pkg/generator/settings.go index b9b6b3e..c0c46fb 100644 --- a/pkg/generator/settings.go +++ b/pkg/generator/settings.go @@ -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 diff --git a/pkg/server/server.go b/pkg/server/server.go index 4aca619..6c38ba0 100644 --- a/pkg/server/server.go +++ b/pkg/server/server.go @@ -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") }