diff --git a/internal/controller/bombing.go b/internal/controller/bombing.go index d80d037..f55be31 100644 --- a/internal/controller/bombing.go +++ b/internal/controller/bombing.go @@ -10,18 +10,19 @@ type BombingReport struct { } type BombingPlanetReport struct { - Planet string `json:"name"` - Number uint `json:"number"` - Owner string `json:"owner"` - Attacker string `json:"attacker"` - Production string `json:"production"` - Industry float64 `json:"industry"` // I - Промышленность - Population float64 `json:"population"` // P - Население - Colonists float64 `json:"colonists"` // COL C - Количество колонистов - Capital float64 `json:"capital"` // CAP $ - Запасы промышленности - Material float64 `json:"material"` // MAT M - Запасы ресурсов / сырья - AttackPower float64 `json:"attack"` - Wiped bool `json:"wiped"` + ID uuid.UUID `json:"id"` + Planet string `json:"name"` + Number uint `json:"number"` + Owner string `json:"owner"` + Attacker string `json:"attacker"` + Production string `json:"production"` + Industry float64 `json:"industry"` // I - Промышленность + Population float64 `json:"population"` // P - Население + Colonists float64 `json:"colonists"` // COL C - Количество колонистов + Capital float64 `json:"capital"` // CAP $ - Запасы промышленности + Material float64 `json:"material"` // MAT M - Запасы ресурсов / сырья + AttackPower float64 `json:"attack"` + Wiped bool `json:"wiped"` } func (c *Cache) bombingReport(p *game.Planet, ri int, groups []int) BombingPlanetReport { @@ -32,6 +33,7 @@ func (c *Cache) bombingReport(p *game.Planet, ri int, groups []int) BombingPlane attackPower += sg.BombingPower(st) } r := &BombingPlanetReport{ + ID: uuid.New(), Planet: p.Name, Number: p.Number, Owner: c.g.Race[c.RaceIndex(p.Owner)].Name, diff --git a/internal/controller/bombing_test.go b/internal/controller/bombing_test.go index 6ddbfa9..fe85f0b 100644 --- a/internal/controller/bombing_test.go +++ b/internal/controller/bombing_test.go @@ -121,6 +121,7 @@ func TestProduceBombings(t *testing.T) { reports := c.ProduceBombings() assert.Len(t, reports, 2) for _, r := range reports { + assert.NotEqual(t, uuid.Nil, r.ID) switch pn := r.Number; pn { case R1_Planet_1_num: assert.Equal(t, Race_1.Name, r.Owner) diff --git a/internal/controller/generate_turn.go b/internal/controller/generate_turn.go index 4d9a55c..ecde741 100644 --- a/internal/controller/generate_turn.go +++ b/internal/controller/generate_turn.go @@ -28,7 +28,10 @@ func MakeTurn(c *Controller, r Repo, g *game.Game) error { battles = append(battles, ProduceBattles(c.Cache)...) // 07. Корабли бомбят вражеские планеты. - _ = c.Cache.ProduceBombings() // TODO: store bombings for reports + _ = c.Cache.ProduceBombings() + + // 08. На планетах строятся корабли. + c.Cache.ProduceShips() /*** Last steps ***/ diff --git a/internal/controller/planet.go b/internal/controller/planet.go index 9272491..fe3f643 100644 --- a/internal/controller/planet.go +++ b/internal/controller/planet.go @@ -78,11 +78,11 @@ func (c *Cache) PlanetProduction(ri int, number int, prod game.ProductionType, s if p.Owner != c.g.Race[ri].ID { return e.NewEntityNotOwnedError("planet #%d", number) } - i := c.MustPlanetIndex(p.Number) var subjectID *uuid.UUID if (prod == game.ResearchScience || prod == game.ProductionShip) && subj == "" { return e.NewEntityTypeNameValidationError("%s=%q", prod, subj) } + if prod == game.ResearchScience { i := slices.IndexFunc(c.g.Race[ri].Sciences, func(s game.Science) bool { return s.Name == subj }) if i < 0 { @@ -90,6 +90,7 @@ func (c *Cache) PlanetProduction(ri int, number int, prod game.ProductionType, s } subjectID = &c.g.Race[ri].Sciences[i].ID } + if prod == game.ProductionShip { st, _, ok := c.ShipClass(ri, subj) if !ok { @@ -102,30 +103,24 @@ func (c *Cache) PlanetProduction(ri int, number int, prod game.ProductionType, s return nil } subjectID = &st.ID + } + + if p.Production.Type == game.ProductionShip && (prod != game.ProductionShip || *subjectID != *p.Production.SubjectID) { + mat, _ := c.MustShipType(ri, *p.Production.SubjectID).ProductionCost() + p.Material += mat * (*p.Production.Progress) + *p.Production.Progress = 0. + } else if prod == game.ProductionShip { + // new ship class to produce; otherwise we must have been returned from the func earlier var progress float64 = 0. - c.g.Map.Planet[i].Production.Progress = &progress - } else { - c.g.Map.Planet[i].Production.Progress = nil + p.Production.Progress = &progress } - if p.Production.Type == game.ProductionShip && prod != game.ProductionShip { - if p.Production.SubjectID == nil { - return e.NewGameStateError("planet #%d produces ship but SubjectID is empty", p.Number) - } - s := *p.Production.SubjectID - if p.Production.Progress == nil { - return e.NewGameStateError("planet #%d produces ship but Progress is empty", p.Number) - } - progress := *p.Production.Progress - st, ok := c.ShipType(ri, s) - if !ok { - return e.NewGameStateError("planet #%d produces ship but ShipType was not found for race %s", p.Number, c.g.Race[ri].Name) - } - mat, _ := st.ProductionCost() - extra := mat * progress - c.g.Map.Planet[i].Material += extra + + if prod != game.ProductionShip { + p.Production.Progress = nil } - c.g.Map.Planet[i].Production.Type = prod - c.g.Map.Planet[i].Production.SubjectID = subjectID + + p.Production.Type = prod + p.Production.SubjectID = subjectID return nil } @@ -201,6 +196,23 @@ func (c *Cache) PlanetProductionCapacity(planetNumber uint) float64 { return p.ProductionCapacity() - busyResources } +func (c *Cache) ProduceShips() { + for pl := range c.g.Map.Planet { + p := c.MustPlanet(c.g.Map.Planet[pl].Number) + if p.Owner == uuid.Nil || p.Production.Type != game.ProductionShip { + continue + } + ri := c.RaceIndex(p.Owner) + st := c.MustShipType(ri, *p.Production.SubjectID) + ships := ProduceShip(p, st.EmptyMass()) + if ships > 0 { + if err := c.CreateShips(ri, st.Name, p.Number, ships); err != nil { + panic(fmt.Sprintf("ProduceShips: CreateShip: %s", err)) + } + } + } +} + // Internal funcs func (c *Cache) putPopulation(pn uint, v float64) { @@ -230,16 +242,14 @@ func ProduceShip(p *game.Planet, shipMass float64) int { if CAP_deficit := p.Capital - CAP_perShip; CAP_deficit < 0 { productionExtraCAP = -CAP_deficit } - - ship_prod := productionExtraCAP + productionForMass - - if productionAvailable >= ship_prod { - productionAvailable -= ship_prod + productionCost := productionExtraCAP + productionForMass + if productionAvailable >= productionCost { + productionAvailable -= productionCost p.Capital = p.Capital - (CAP_perShip - productionExtraCAP) ships++ } else { - progress := productionAvailable / ship_prod - productionAvailable -= ship_prod * progress + progress := productionAvailable / productionCost + productionAvailable -= productionCost * progress p.Production.Progress = &progress break } diff --git a/internal/controller/planet_test.go b/internal/controller/planet_test.go index f497ea6..34bd611 100644 --- a/internal/controller/planet_test.go +++ b/internal/controller/planet_test.go @@ -1,6 +1,7 @@ package controller_test import ( + "slices" "testing" "github.com/google/uuid" @@ -122,6 +123,41 @@ func TestPlanetProductionCapacity(t *testing.T) { assert.Equal(t, 53.125, c.PlanetProductionCapacity(R0_Planet_0_num)) } +func TestProduceShips(t *testing.T) { + c, g := newCache() + + pn := int(R0_Planet_0_num) + assert.NoError(t, g.PlanetProduction(Race_0.Name, pn, "SHIP", Race_0_Gunship)) + assert.Equal(t, game.ProductionShip, c.MustPlanet(R0_Planet_0_num).Production.Type) + assert.NotNil(t, c.MustPlanet(R0_Planet_0_num).Production.Progress) + assert.NotNil(t, c.MustPlanet(R0_Planet_0_num).Production.SubjectID) + st := c.MustShipClass(Race_0_idx, Race_0_Gunship) + assert.Equal(t, st.ID, *c.MustPlanet(R0_Planet_0_num).Production.SubjectID) + + c.MustPlanet(R0_Planet_0_num).Population = 1000. + c.MustPlanet(R0_Planet_0_num).Industry = 1000. + c.MustPlanet(R0_Planet_0_num).Resources = 10. + shipMass := st.EmptyMass() + + c.ProduceShips() + + assert.Len(t, slices.Collect(c.RaceShipGroups(Race_0_idx)), 0) + assert.NotNil(t, c.MustPlanet(R0_Planet_0_num).Production.Progress) + progress := *c.MustPlanet(R0_Planet_0_num).Production.Progress + assert.InDelta(t, 0.45, progress, 0.001) + + assert.NoError(t, c.CreateShipType(Race_0_idx, "Drone", 1, 0, 0, 0, 0)) + assert.NoError(t, g.PlanetProduction(Race_0.Name, pn, "SHIP", "Drone")) + assert.InDelta(t, shipMass*progress, c.MustPlanet(R0_Planet_0_num).Material, 0.00001) // 99.(0099) material build + + c.ProduceShips() + + assert.Len(t, slices.Collect(c.RaceShipGroups(Race_0_idx)), 1) + assert.Equal(t, uint(99), c.ShipGroup(0).Number) + progress = *c.MustPlanet(R0_Planet_0_num).Production.Progress + assert.InDelta(t, 0.0099, progress, 0.00001) // 1.(0099) drones with no CAP on planet +} + func TestProduceShip(t *testing.T) { Drone := game.ShipType{ ShipTypeReport: game.ShipTypeReport{ diff --git a/internal/model/game/production.go b/internal/model/game/production.go index 8038cc0..bfe8b77 100644 --- a/internal/model/game/production.go +++ b/internal/model/game/production.go @@ -22,7 +22,7 @@ const ( type Production struct { Type ProductionType `json:"type"` - SubjectID *uuid.UUID `json:"subjectId"` + SubjectID *uuid.UUID `json:"subjectId"` // TODO: get rid of Nils? Progress *float64 `json:"progress"` } diff --git a/internal/model/game/ship.go b/internal/model/game/ship.go index 860c249..5699f69 100644 --- a/internal/model/game/ship.go +++ b/internal/model/game/ship.go @@ -71,6 +71,7 @@ func (st ShipType) EmptyMass() float64 { } // ProductionCost returns Material (MAT) and Population (POP) to produce this [ShipType] +// TODO: do we need this? func (st ShipType) ProductionCost() (mat float64, pop float64) { mat = st.EmptyMass() pop = mat * 10