game generation process

This commit is contained in:
Ilia Denisov
2025-09-25 02:13:16 +03:00
parent 99035fd95d
commit 46066d890b
12 changed files with 206 additions and 101 deletions
+74 -31
View File
@@ -3,6 +3,7 @@ package game
import (
"fmt"
"math"
"math/rand/v2"
"github.com/google/uuid"
"github.com/iliadenisov/galaxy/pkg/generator"
@@ -15,9 +16,9 @@ type Repo interface {
}
func NewGame(r Repo, races []string) (uuid.UUID, error) {
id, err := uuid.NewRandom()
gameID, err := uuid.NewRandom()
if err != nil {
return uuid.Nil, fmt.Errorf("generate uuid: %s", err)
return uuid.Nil, fmt.Errorf("generate game uuid: %s", err)
}
m, err := generator.Generate(func(ms *generator.MapSetting) {
ms.Players = uint32(len(races))
@@ -29,7 +30,7 @@ func NewGame(r Repo, races []string) (uuid.UUID, error) {
return uuid.Nil, fmt.Errorf("generate map: wrong number of home planets: %d, expected: %d ", len(m.HomePlanets), len(races))
}
g := &game.Game{
ID: id,
ID: gameID,
Race: make([]game.Race, len(races)),
}
@@ -38,44 +39,71 @@ func NewGame(r Repo, races []string) (uuid.UUID, error) {
Height: m.Height,
Planet: make([]game.Planet, 0),
}
for hw := range races {
g.Race[hw] = game.Race{
Name: races[hw],
Votes: 1, // TODO: check with rules
VoteFor: races[hw],
var planetCount uint = 0
for i := range races {
raceID, err := uuid.NewRandom()
if err != nil {
return uuid.Nil, fmt.Errorf("generate race uuid: %s", err)
}
g.Race[i] = game.Race{
ID: raceID,
Name: races[i],
Ally: raceID,
Drive: 1,
Weapons: 1,
Shields: 1,
Cargo: 1,
}
gameMap.Planet = append(gameMap.Planet, game.Planet{
Owner: races[hw],
X: m.HomePlanets[hw].HW.Position.X,
Y: m.HomePlanets[hw].HW.Position.Y,
Size: m.HomePlanets[hw].HW.Size,
Resources: m.HomePlanets[hw].HW.Resources,
Production: game.ProductionCapital.AsType(""), // TODO: check default production
})
for dw := range m.HomePlanets[hw].DW {
gameMap.Planet = append(gameMap.Planet, game.Planet{
X: m.HomePlanets[hw].DW[dw].Position.X,
Y: m.HomePlanets[hw].DW[dw].Position.Y,
Size: m.HomePlanets[hw].DW[dw].Size,
Resources: m.HomePlanets[hw].DW[dw].Resources,
Production: game.ProductionNone.AsType(""),
})
gameMap.Planet = append(gameMap.Planet, newPlanet(
planetCount,
m.HomePlanets[i].HW.RandomName(),
raceID,
m.HomePlanets[i].HW.Position.X,
m.HomePlanets[i].HW.Position.Y,
m.HomePlanets[i].HW.Size,
m.HomePlanets[i].HW.Size, // HW's pop & ind = size
m.HomePlanets[i].HW.Size,
m.HomePlanets[i].HW.Resources,
game.ResearchDrive.AsType(""),
))
planetCount++
for dw := range m.HomePlanets[i].DW {
gameMap.Planet = append(gameMap.Planet, newPlanet(
planetCount,
m.HomePlanets[i].DW[dw].RandomName(),
raceID,
m.HomePlanets[i].DW[dw].Position.X,
m.HomePlanets[i].DW[dw].Position.Y,
m.HomePlanets[i].DW[dw].Size,
m.HomePlanets[i].DW[dw].Size, // DW's pop & ind = size
m.HomePlanets[i].DW[dw].Size,
m.HomePlanets[i].DW[dw].Resources,
game.ResearchDrive.AsType(""),
))
planetCount++
}
}
for i := range m.FreePlanets {
gameMap.Planet = append(gameMap.Planet, game.Planet{
X: m.FreePlanets[i].Position.X,
Y: m.FreePlanets[i].Position.Y,
Size: m.FreePlanets[i].Size,
Resources: m.FreePlanets[i].Resources,
Production: game.ProductionNone.AsType(""),
})
gameMap.Planet = append(gameMap.Planet, newPlanet(
planetCount,
m.FreePlanets[i].RandomName(),
uuid.Nil,
m.FreePlanets[i].Position.X,
m.FreePlanets[i].Position.Y,
m.FreePlanets[i].Size,
0,
0,
m.FreePlanets[i].Resources,
game.ProductionNone.AsType(""),
))
planetCount++
}
// TODO: check code below actually works
rand.Shuffle(len(gameMap.Planet), func(i, j int) {
gameMap.Planet[i].Number, gameMap.Planet[j].Number = gameMap.Planet[j].Number, gameMap.Planet[i].Number
})
g.Map = *gameMap
if err := r.Persist(*g); err != nil {
@@ -84,6 +112,21 @@ func NewGame(r Repo, races []string) (uuid.UUID, error) {
return g.ID, nil
}
func newPlanet(num uint, name string, owner uuid.UUID, x, y, size, pop, ind, res float64, prod game.ProductionType) game.Planet {
return game.Planet{
Name: name,
Number: num,
Owner: owner,
X: x,
Y: y,
Size: size,
Population: pop,
Industry: ind,
Resources: res,
Production: prod,
}
}
func (r Race) FlightDistance() float64 {
return r.Drive * 40
}
+1 -1
View File
@@ -22,7 +22,7 @@ func Generate(cfg ...func(*MapSetting)) (Map, error) {
freePlanets := ms.NobodysPlanets()
createPlanets := func(pc PlanetClass, ps PlanetSetting) error {
return m.CreatePlanets(pc, rand.Intn(ps.MaxCount(freePlanets))+1, float64(ps.MinDistanceHW), RandIFn(ps.MinSize, ps.MaxSize), RandIFn(ps.MinResource, ps.MaxResource))
return m.CreatePlanets(pc, ps.Number(freePlanets), float64(ps.MinDistanceHW), RandIFn(ps.MinSize, ps.MaxSize), RandIFn(ps.MinResource, ps.MaxResource))
}
// 1. Place Giant planets
+5 -4
View File
@@ -21,11 +21,11 @@ func TestGenerator(t *testing.T) {
t.Fatalf("generate: %s", err)
return
}
assert.Equal(t, players, len(m.HomePlanets), "hw count")
assert.Equal(t, players, len(m.HomePlanets), "hw-s count")
for hw := range m.HomePlanets {
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 count", 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 {
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)
@@ -38,6 +38,7 @@ func TestGenerator(t *testing.T) {
m.HomePlanets[hw].DW[dw].Position.X, m.HomePlanets[hw].DW[dw].Position.Y)
}
}
assert.LessOrEqualf(t, int(s.NobodysPlanets()), len(m.FreePlanets), "free planets clount")
freePlanetCount := make(map[generator.PlanetClass]int)
for fp := range m.FreePlanets {
ps := planetSettings(t, m.FreePlanets[fp].PlanetClass, s)
@@ -67,8 +68,8 @@ func TestGenerator(t *testing.T) {
}
for pc, num := range freePlanetCount {
ps := planetSettings(t, pc, s)
maxNum := ps.MaxCount(s.NobodysPlanets())
assert.LessOrEqualf(t, num, maxNum, "planet_class=%v probability=%f of total %d", pc, ps.Probability, s.NobodysPlanets())
maxNum := ps.Number(s.NobodysPlanets())
assert.Equalf(t, num, maxNum, "planet_class=%v ratio=%f of total %d", pc, ps.Ratio, s.NobodysPlanets())
}
})
}
-33
View File
@@ -20,30 +20,6 @@ type Coordinate struct {
X, Y float64
}
type PlanetClass int
const (
PlanetClassHW PlanetClass = iota
PlanetClassDW
PlanetClassGiant
PlanetClassBig
PlanetClassNormal
PlanetClassRich
PlanetClassAsterioid
)
type Planet struct {
PlanetClass PlanetClass
Position Coordinate
Size float64
Resources float64 // Сырьё
}
type PlanetarySystem struct {
HW Planet
DW []Planet
}
func NewMap(width, height, players uint32) (*Map, error) {
p, err := plotter.NewPlotter(width, height, defaultFactor)
if err != nil {
@@ -93,15 +69,6 @@ func (m Map) ShortDistance(from, to Coordinate) float64 {
return math.Sqrt(math.Pow(dx, 2) + math.Pow(dy, 2))
}
func NewPlanet(pc PlanetClass, c Coordinate, size, resources float64) Planet {
return Planet{
PlanetClass: pc,
Position: c,
Size: size,
Resources: resources,
}
}
// RandI returns a random float64 value between min and max
func RandI(min, max float64) float64 {
return min + rand.Float64()*(max-min)
+43
View File
@@ -0,0 +1,43 @@
package generator
import (
"fmt"
"math/rand"
)
type PlanetClass string
const (
PlanetClassHW PlanetClass = "HW"
PlanetClassDW PlanetClass = "DW"
PlanetClassGiant PlanetClass = "Giant"
PlanetClassBig PlanetClass = "Big"
PlanetClassNormal PlanetClass = "Normal"
PlanetClassRich PlanetClass = "Rich"
PlanetClassAsterioid PlanetClass = "Asteroid"
)
type Planet struct {
PlanetClass PlanetClass
Position Coordinate
Size float64
Resources float64 // Сырьё
}
type PlanetarySystem struct {
HW Planet
DW []Planet
}
func (p Planet) RandomName() string {
return fmt.Sprintf("%s-%04d-%04d", p.PlanetClass, rand.Intn(1000), rand.Intn(1000))
}
func NewPlanet(pc PlanetClass, c Coordinate, size, resources float64) Planet {
return Planet{
PlanetClass: pc,
Position: c,
Size: size,
Resources: resources,
}
}
+30
View File
@@ -0,0 +1,30 @@
package generator_test
import (
"regexp"
"testing"
g "github.com/iliadenisov/galaxy/pkg/generator"
"github.com/stretchr/testify/assert"
)
func TestPlanetRandomName(t *testing.T) {
re, err := regexp.Compile(`^([a-zA-Z]+)-(\d{4})-(\d{4})$`)
assert.NoError(t, err)
if err != nil {
return
}
for _, pc := range []g.PlanetClass{g.PlanetClassHW, g.PlanetClassDW, g.PlanetClassGiant, g.PlanetClassBig, g.PlanetClassNormal, g.PlanetClassRich, g.PlanetClassAsterioid} {
t.Run(string(pc), func(t *testing.T) {
name := g.NewPlanet(pc, g.Coordinate{0, 0}, 0, 0).RandomName()
g := re.FindStringSubmatch(name)
assert.NotNilf(t, g, "cannot parse: %q", name)
if g == nil {
return
}
assert.Equalf(t, 4, len(g), "regexp groups")
assert.Equal(t, string(pc), g[1])
assert.NotEqual(t, g[2], g[3])
})
}
}
+9 -9
View File
@@ -31,7 +31,6 @@ func (ms MapSetting) String() string {
}
func (ms MapSetting) ExpectedSize() uint32 {
// 1.5 coefficient is enough if all free planet probability will be 1.0
return uint32(math.Sqrt(float64(ms.Players)) * float64(ms.HWMinDistance) * 1.5)
}
@@ -49,11 +48,12 @@ type PlanetSetting struct {
MaxSize float64
MinResource float64
MaxResource float64
Probability float64
Ratio float64 // The proportion of the total number of free planets in the galaxy
}
func (ps PlanetSetting) MaxCount(freePlanets uint32) int {
return int(math.Ceil(float64(freePlanets) * ps.Probability))
// Number of planets need to be placed within freePlanets amount
func (ps PlanetSetting) Number(freePlanets uint32) int {
return int(math.Ceil(float64(freePlanets) * ps.Ratio))
}
func DefaultMapSetting() MapSetting {
@@ -73,7 +73,7 @@ func DefaultMapSetting() MapSetting {
MaxSize: 2500,
MinResource: 0,
MaxResource: 3,
Probability: 0.06,
Ratio: 0.06,
},
BigPlanets: PlanetSetting{
MinDistanceHW: 10,
@@ -81,7 +81,7 @@ func DefaultMapSetting() MapSetting {
MaxSize: 2000,
MinResource: 1,
MaxResource: 10,
Probability: 0.18,
Ratio: 0.18,
},
OthersMinDistance: defaultFactor, // min. is 1 pixel on the plotter
NormalPlanets: PlanetSetting{
@@ -90,7 +90,7 @@ func DefaultMapSetting() MapSetting {
MaxSize: 1000,
MinResource: 0,
MaxResource: 10,
Probability: 0.5,
Ratio: 0.5,
},
RichPlanets: PlanetSetting{
MinDistanceHW: 0,
@@ -98,7 +98,7 @@ func DefaultMapSetting() MapSetting {
MaxSize: 500,
MinResource: 5,
MaxResource: 25,
Probability: 0.18,
Ratio: 0.18,
},
Asterioids: PlanetSetting{
MinDistanceHW: 0,
@@ -106,7 +106,7 @@ func DefaultMapSetting() MapSetting {
MaxSize: 0,
MinResource: 0,
MaxResource: 0,
Probability: 0.08,
Ratio: 0.08,
},
}
}
+12
View File
@@ -4,6 +4,18 @@ import "github.com/google/uuid"
type Game struct {
ID uuid.UUID
Age uint // Game's turn number
Map Map
Race []Race
}
func (g Game) Votes(raceID uuid.UUID) float64 {
// XXX: calculate [Race]Population once when loading Game from Storage?
var pop float64
for i := range g.Map.Planet {
if g.Map.Planet[i].Owner == raceID {
pop += g.Map.Planet[i].Population
}
}
return pop / 1000.
}
-1
View File
@@ -3,6 +3,5 @@ package game
type Map struct {
Width uint32
Height uint32
Planet []Planet
}
+15 -9
View File
@@ -1,27 +1,33 @@
package game
import "math"
import (
"math"
"github.com/google/uuid"
)
type Planet struct {
X, Y float64
Size float64
Name string
Owner string
Name string
Number uint
Owner uuid.UUID
Production ProductionType
Resources float64 // Сырьё
Industry float64 // Промышленность
Population float64 // Население
Population float64 // P - Население
Industry float64 // I - Промышленность
Resources float64 // R - Ресурсы / сырьё
Capital float64 // CAP $ - Запасы промышленности
Material float64 // MAT M - Запасы сырья
Material float64 // MAT M - Запасы ресурсов / сырья
Colonists float64 // COL C - Количество колонистов
// Параметр "L" означает количество свободных производственных единиц.
// Параметр "L" - Свободный производственный потенциал
}
// Производственный потенциал (I)
// Свободный производственный потенциал (L)
// промышленность * 0.75 + население * 0.25
// TODO: за вычетом затрат, расходуемых в течение хода на модернизацию кораблей
func (p Planet) ProductionCapacity() float64 {
return p.Industry*0.75 + p.Population*0.25
}
+12 -11
View File
@@ -3,26 +3,27 @@ package game
type PlanetProduction string
const (
ProductionNone PlanetProduction = "NONE"
ProductionMaterial PlanetProduction = "MAT"
ProductionCapital PlanetProduction = "CAP"
ProductionDrive PlanetProduction = "DRIVE"
ProductionWeapons PlanetProduction = "WEAPONS"
ProductionShields PlanetProduction = "SHIELDS"
ProductionCargo PlanetProduction = "CARGO"
ProductionNone PlanetProduction = "-"
ProductionMaterial PlanetProduction = "MAT" // Сырьё
ProductionCapital PlanetProduction = "CAP" // Промышленность
ProductionScience PlanetProduction = "SCIENCE"
ProductionShip PlanetProduction = "SHIP"
ResearchDrive PlanetProduction = "DRIVE"
ResearchWeapons PlanetProduction = "WEAPONS"
ResearchShields PlanetProduction = "SHIELDS"
ResearchCargo PlanetProduction = "CARGO"
ResearchScience PlanetProduction = "SCIENCE"
ProductionShip PlanetProduction = "SHIP"
)
type ProductionType struct {
Production PlanetProduction
SubjectName string
SubjectName string // TODO: change to UUID
}
func (p PlanetProduction) AsType(subject string) ProductionType {
switch p {
case ProductionScience, ProductionShip:
case ResearchScience, ProductionShip:
return ProductionType{Production: p, SubjectName: subject}
default:
return ProductionType{Production: p}
+5 -2
View File
@@ -1,11 +1,14 @@
package game
import "github.com/google/uuid"
type Race struct {
ID uuid.UUID
Name string
Killed bool
Votes float64
VoteFor string
// Votes float64
Ally uuid.UUID // Race's Votes receiver
Drive float64
Weapons float64