diff --git a/pkg/game/cmd_group.go b/pkg/game/cmd_group.go new file mode 100644 index 0000000..a11372d --- /dev/null +++ b/pkg/game/cmd_group.go @@ -0,0 +1,19 @@ +package game + +import "github.com/iliadenisov/galaxy/pkg/model/game" + +func JoinEqualGroups(configure func(*Param), race string) (err error) { + control(configure, func(c *ctrl) { + c.execute(func(r Repo, g game.Game) { + err = joinEqualGroups(r, g, race) + }) + }) + return +} + +func joinEqualGroups(r Repo, g game.Game, race string) error { + if err := g.JoinEqualGroups(race); err != nil { + return err + } + return r.SaveState(g) +} diff --git a/pkg/game/cmd_group_test.go b/pkg/game/cmd_group_test.go new file mode 100644 index 0000000..f2d40f3 --- /dev/null +++ b/pkg/game/cmd_group_test.go @@ -0,0 +1,16 @@ +package game_test + +import ( + "testing" + + "github.com/iliadenisov/galaxy/pkg/game" + mg "github.com/iliadenisov/galaxy/pkg/model/game" + "github.com/stretchr/testify/assert" +) + +func TestJoinEqualGroups(t *testing.T) { + g(t, func(p func(*game.Param), g func() mg.Game) { + err := game.JoinEqualGroups(p, "race_01") + assert.NoError(t, err) + }) +} diff --git a/pkg/game/generator.go b/pkg/game/generator.go index b50e14f..bf37990 100644 --- a/pkg/game/generator.go +++ b/pkg/game/generator.go @@ -65,7 +65,7 @@ func buildGameOnMap(races []string, m generator.Map) (*game.Game, error) { Shields: 1, Cargo: 1, } - gameMap.Planet = append(gameMap.Planet, newPlanet( + gameMap.Planet = append(gameMap.Planet, NewPlanet( planetCount, m.HomePlanets[i].HW.RandomName(), raceID, @@ -79,7 +79,7 @@ func buildGameOnMap(races []string, m generator.Map) (*game.Game, error) { )) planetCount++ for dw := range m.HomePlanets[i].DW { - gameMap.Planet = append(gameMap.Planet, newPlanet( + gameMap.Planet = append(gameMap.Planet, NewPlanet( planetCount, m.HomePlanets[i].DW[dw].RandomName(), raceID, @@ -101,7 +101,7 @@ func buildGameOnMap(races []string, m generator.Map) (*game.Game, error) { } for i := range m.FreePlanets { - gameMap.Planet = append(gameMap.Planet, newPlanet( + gameMap.Planet = append(gameMap.Planet, NewPlanet( planetCount, m.FreePlanets[i].RandomName(), uuid.Nil, @@ -125,7 +125,7 @@ func buildGameOnMap(races []string, m generator.Map) (*game.Game, error) { return g, nil } -func newPlanet(num uint, name string, owner uuid.UUID, x, y, size, pop, ind, res float64, prod game.ProductionType) game.Planet { +func NewPlanet(num uint, name string, owner uuid.UUID, x, y, size, pop, ind, res float64, prod game.ProductionType) game.Planet { return game.Planet{ Owner: owner, PlanetReport: game.PlanetReport{ diff --git a/pkg/model/game/game.go b/pkg/model/game/game.go index 3b5ef1f..dae1097 100644 --- a/pkg/model/game/game.go +++ b/pkg/model/game/game.go @@ -10,10 +10,12 @@ import ( ) type Game struct { - ID uuid.UUID `json:"id"` - Age uint `json:"turn"` // Game's turn number - Map Map `json:"map"` - Race []Race `json:"races"` + ID uuid.UUID `json:"id"` + Age uint `json:"turn"` // Game's turn number + Map Map `json:"map"` + Race []Race `json:"races"` + ShipGroups []ShipGroup `json:"shipGroup,omitempty"` + Fleets []Fleet `json:"fleet,omitempty"` } func (g Game) Votes(raceID uuid.UUID) float64 { diff --git a/pkg/model/game/game_export_test.go b/pkg/model/game/game_export_test.go new file mode 100644 index 0000000..36a67c8 --- /dev/null +++ b/pkg/model/game/game_export_test.go @@ -0,0 +1,11 @@ +package game + +import "iter" + +func (g *Game) CreateShips(ri int, shipTypeName string, planetNumber int, quantity int) error { + return g.createShips(ri, shipTypeName, planetNumber, quantity) +} + +func (g Game) ListShipGroups(ri int) iter.Seq[ShipGroup] { + return g.listShipGroups(ri) +} diff --git a/pkg/model/game/group.go b/pkg/model/game/group.go new file mode 100644 index 0000000..9234fce --- /dev/null +++ b/pkg/model/game/group.go @@ -0,0 +1,195 @@ +package game + +import ( + "iter" + "math" + "slices" + + "github.com/google/uuid" + e "github.com/iliadenisov/galaxy/pkg/error" + "github.com/iliadenisov/galaxy/pkg/number" +) + +type CargoType string + +const ( + // CargoNone CargoType = "-" + CargoColonist CargoType = "COL" // Колонисты + CargoMaterial CargoType = "MAT" // Сырьё + CargoCapital CargoType = "CAP" // Промышленность +) + +type ShipGroup struct { + Index uint `json:"index"` // Group index (ordered) + OwnerID uuid.UUID `json:"ownerId"` // Race link + TypeID uuid.UUID `json:"typeId"` // ShipType link + FleetID *uuid.UUID `json:"fleetId,omitempty"` // ShipType link + Number uint `json:"number"` // Number (quantity) ships of Type + State string `json:"state"` // TODO: kinda enum: In_Orbit, In_Space, Transfer_State, Upgrade + + CargoType *CargoType `json:"loadType,omitempty"` + Load float64 `json:"load"` // Cargo loaded - "Масса груза" + + Drive float64 `json:"drive"` + Weapons float64 `json:"weapons"` + Shields float64 `json:"shields"` + Cargo float64 `json:"cargo"` + + // TODO: append AND TEST: Destination, Origin, Range + Destination uint `json:"destination"` +} + +func (sg ShipGroup) Equal(other ShipGroup) bool { + return sg.OwnerID == other.OwnerID && + sg.TypeID == other.TypeID && + sg.FleetID == other.FleetID && + sg.Drive == other.Drive && + sg.Weapons == other.Weapons && + sg.Shields == other.Shields && + sg.Cargo == other.Cargo && + sg.CargoType == other.CargoType && + sg.Load == other.Load && + sg.State == other.State +} + +// Грузоподъёмность +func (sg ShipGroup) CargoCapacity(st *ShipType) float64 { + return sg.Cargo * (st.Cargo + (st.Cargo*st.Cargo)/20) * float64(sg.Number) +} + +// Масса перевозимого груза - +// общее количество единиц груза, деленное на технологический уровень Грузоперевозок +func (sg ShipGroup) CarryingMass() float64 { + return sg.Load / sg.Cargo +} + +// Полная масса - +// массу корабля самого по себе плюс масса перевозимого груза. +func (sg ShipGroup) FullMass(st *ShipType) float64 { + return st.EmptyMass() + sg.CarryingMass() +} + +// Эффективность двигателя - +// равна мощности Двигателей, умноженной на технологический уровень блока Двигателей +func (sg ShipGroup) DriveEffective(st *ShipType) float64 { + return st.Drive * sg.Drive +} + +// Корабли перемещаются за один ход на количество световых лет, равное +// эффективности двигателя, умноженной на 20 и деленной на "Полную массу" корабля. +func (sg ShipGroup) Speed(st *ShipType) float64 { + return sg.DriveEffective(st) * 20 / sg.FullMass(st) +} + +func (sg ShipGroup) UpgradeDriveCost(st *ShipType, drive float64) float64 { + return (1 - sg.Drive/drive) * 10 * st.Drive +} + +// TODO: test on other values +func (sg ShipGroup) UpgradeWeaponsCost(st *ShipType, weapons float64) float64 { + return (1 - sg.Weapons/weapons) * 10 * st.WeaponsMass() +} + +func (sg ShipGroup) UpgradeShieldsCost(st *ShipType, shields float64) float64 { + return (1 - sg.Shields/shields) * 10 * st.Shields +} + +func (sg ShipGroup) UpgradeCargoCost(st *ShipType, cargo float64) float64 { + return (1 - sg.Cargo/cargo) * 10 * st.Cargo +} + +// Мощность бомбардировки +// TODO: maybe rounding must be done only for display? +func (sg ShipGroup) BombingPower(st *ShipType) float64 { + // return math.Sqrt(sg.Type.Weapons * sg.Weapons) + result := (math.Sqrt(st.Weapons*sg.Weapons)/10. + 1.) * + st.Weapons * + sg.Weapons * + float64(st.Armament) * + float64(sg.Number) + return number.Fixed3(result) +} + +func (g *Game) JoinEqualGroups(raceName string) error { + ri, err := g.raceIndex(raceName) + if err != nil { + return err + } + g.joinEqualGroupsInternal(ri) + return nil +} + +func (g *Game) joinEqualGroupsInternal(ri int) { + shipGroups := slices.Collect(g.listShipGroups(ri)) + origin := len(shipGroups) + if origin < 2 { + return + } + for i := 0; i < len(shipGroups)-1; i++ { + for j := len(shipGroups) - 1; j > i; j-- { + if shipGroups[i].Equal(shipGroups[j]) { + shipGroups[i].Index = maxUint(shipGroups[i].Index, shipGroups[j].Index) + shipGroups[i].Number += shipGroups[j].Number + shipGroups = append(shipGroups[:j], shipGroups[j+1:]...) + } + } + } + if len(shipGroups) == origin { + return + } + g.ShipGroups = slices.DeleteFunc(g.ShipGroups, func(v ShipGroup) bool { return v.OwnerID == g.Race[ri].ID }) + g.ShipGroups = append(g.ShipGroups, shipGroups...) +} + +func (g *Game) createShips(ri int, shipTypeName string, planetNumber int, quantity int) error { + st := slices.IndexFunc(g.Race[ri].ShipTypes, func(st ShipType) bool { return st.Name == shipTypeName }) + if st < 0 { + return e.NewEntityNotExistsError("ship type %w", shipTypeName) + } + pl := slices.IndexFunc(g.Map.Planet, func(p Planet) bool { return p.Number == uint(planetNumber) }) + if pl < 0 { + return e.NewEntityNotExistsError("planet #%d", planetNumber) + } + if g.Map.Planet[pl].Owner != g.Race[ri].ID { + return e.NewEntityNotOwnedError("planet %#d", planetNumber) + } + + var maxIndex uint + for sg := range g.listShipGroups(ri) { + if sg.Index > maxIndex { + maxIndex = sg.Index + } + } + g.ShipGroups = append(g.ShipGroups, ShipGroup{ + Index: maxIndex + 1, + OwnerID: g.Race[ri].ID, + TypeID: g.Race[ri].ShipTypes[st].ID, + Destination: g.Map.Planet[pl].Number, + Number: uint(quantity), + State: "In_Orbit", + Drive: g.Race[ri].Drive, + Weapons: g.Race[ri].Weapons, + Shields: g.Race[ri].Shields, + Cargo: g.Race[ri].Cargo, + }) + return nil +} + +func (g Game) listShipGroups(ri int) iter.Seq[ShipGroup] { + return func(yield func(ShipGroup) bool) { + for sg := range g.ShipGroups { + if g.ShipGroups[sg].OwnerID == g.Race[ri].ID { + if !yield(g.ShipGroups[sg]) { + return + } + } + } + } +} + +func maxUint(a, b uint) uint { + if b > a { + return b + } + return a +} diff --git a/pkg/model/game/group_test.go b/pkg/model/game/group_test.go new file mode 100644 index 0000000..c60c0aa --- /dev/null +++ b/pkg/model/game/group_test.go @@ -0,0 +1,390 @@ +package game_test + +import ( + "math/rand/v2" + "slices" + "testing" + + "github.com/google/uuid" + e "github.com/iliadenisov/galaxy/pkg/error" + gg "github.com/iliadenisov/galaxy/pkg/game" + "github.com/iliadenisov/galaxy/pkg/model/game" + "github.com/stretchr/testify/assert" +) + +func TestCargoCapacity(t *testing.T) { + test := func(cargoSize float64, expectCapacity float64) { + ship := game.ShipType{ + ShipTypeReport: game.ShipTypeReport{ + Drive: 1, + Armament: 1, + Weapons: 1, + Shields: 1, + Cargo: cargoSize, + }, + } + sg := game.ShipGroup{ + Number: 1, + State: "In_Orbit", + Drive: 1.5, + Weapons: 1.1, + Shields: 2.0, + Cargo: 1.0, + } + assert.Equal(t, expectCapacity, sg.CargoCapacity(&ship)) + } + test(1, 1.05) + test(5, 6.25) + test(10, 15) + test(50, 175) + test(100, 600) +} + +func TestCarryingAndFullMass(t *testing.T) { + Freighter := &game.ShipType{ + ShipTypeReport: game.ShipTypeReport{ + Name: "Freighter", + Drive: 8, + Armament: 0, + Weapons: 0, + Shields: 2, + Cargo: 10, + }, + } + sg := &game.ShipGroup{ + Number: 1, + State: "In_Orbit", + Drive: 1.0, + Weapons: 1.0, + Shields: 1.0, + Cargo: 1.0, + Load: 0.0, + } + em := Freighter.EmptyMass() + assert.Equal(t, 0.0, sg.CarryingMass()) + assert.Equal(t, em, sg.FullMass(Freighter)) + + sg.Load = 10.0 + assert.Equal(t, 10.0, sg.CarryingMass()) + assert.Equal(t, em+10.0, sg.FullMass(Freighter)) + + sg.Cargo = 2.5 + assert.Equal(t, 4.0, sg.CarryingMass()) + assert.Equal(t, em+4.0, sg.FullMass(Freighter)) +} + +func TestSpeed(t *testing.T) { + Freighter := &game.ShipType{ + ShipTypeReport: game.ShipTypeReport{ + Name: "Freighter", + Drive: 8, + Armament: 0, + Weapons: 0, + Shields: 2, + Cargo: 10, + }, + } + sg := &game.ShipGroup{ + Number: 1, + State: "In_Orbit", + Drive: 1.0, + Weapons: 1.0, + Shields: 1.0, + Cargo: 1.0, + Load: 0.0, + } + assert.Equal(t, 8.0, sg.Speed(Freighter)) + sg.Load = 5.0 + assert.Equal(t, 6.4, sg.Speed(Freighter)) + sg.Drive = 1.5 + assert.Equal(t, 9.6, sg.Speed(Freighter)) + sg.Load = 10 + sg.Cargo = 1.5 + assert.Equal(t, 9.0, sg.Speed(Freighter)) +} + +func TestBombingPower(t *testing.T) { + Gunship := game.ShipType{ + ShipTypeReport: game.ShipTypeReport{ + Drive: 60.0, + Armament: 3, + Weapons: 30.0, + Shields: 100.0, + Cargo: 0.0, + }, + } + sg := game.ShipGroup{ + Number: 1, + State: "In_Orbit", + Drive: 1.0, + Weapons: 1.0, + Shields: 1.0, + Cargo: 1.0, + } + expectedBombingPower := 139.295 + result := sg.BombingPower(&Gunship) + assert.Equal(t, expectedBombingPower, result) +} + +func TestUpgradeCost(t *testing.T) { + Cruiser := game.ShipType{ + ShipTypeReport: game.ShipTypeReport{ + Name: "Cruiser", + Drive: 15, + Armament: 1, + Weapons: 15, + Shields: 15, + Cargo: 0, + }, + } + + sg := game.ShipGroup{ + Number: 1, + State: "In_Orbit", + Drive: 1.0, + Weapons: 1.0, + Shields: 1.0, + Cargo: 1.0, + } + upgradeCost := sg.UpgradeDriveCost(&Cruiser, 2.0) + + sg.UpgradeWeaponsCost(&Cruiser, 2.0) + + sg.UpgradeShieldsCost(&Cruiser, 2.0) + + sg.UpgradeCargoCost(&Cruiser, 2.0) + assert.Equal(t, 225., upgradeCost) +} + +func TestDriveEffective(t *testing.T) { + tc := []struct { + driveShipType float64 + driveTech float64 + expectDriveEffective float64 + }{ + {1, 1, 1}, + {1, 2, 2}, + {2, 1, 2}, + {0, 1, 0}, + {0, 1.5, 0}, + {0, 10, 0}, + {1.5, 1.5, 2.25}, + } + for i := range tc { + someShip := game.ShipType{ + ShipTypeReport: game.ShipTypeReport{ + Drive: tc[i].driveShipType, + Armament: rand.UintN(30) + 1, + Weapons: rand.Float64()*30 + 1, + Shields: rand.Float64()*100 + 1, + Cargo: rand.Float64()*20 + 1, + }, + } + sg := game.ShipGroup{ + Number: rand.UintN(4) + 1, + State: "In_Orbit", + Drive: tc[i].driveTech, + Weapons: rand.Float64()*5 + 1, + Shields: rand.Float64()*5 + 1, + Cargo: rand.Float64()*5 + 1, + } + assert.Equal(t, tc[i].expectDriveEffective, sg.DriveEffective(&someShip)) + } +} + +func TestShipGroupEqual(t *testing.T) { + fleetId := uuid.New() + someUUID := uuid.New() + mat := game.CargoMaterial + cap := game.CargoCapital + left := &game.ShipGroup{ + Index: 1, + Number: 1, + + OwnerID: uuid.New(), + TypeID: uuid.New(), + FleetID: &fleetId, + State: "In_Orbit", + CargoType: &mat, + Load: 123.45, + Drive: 1.0, + Weapons: 1.0, + Shields: 1.0, + Cargo: 1.0, + } + + // essential properties + right := *left + assert.True(t, left.Equal(right)) + + left.OwnerID = someUUID + assert.False(t, left.Equal(right)) + + right = *left + left.TypeID = someUUID + assert.False(t, left.Equal(right)) + + right = *left + left.FleetID = &someUUID + assert.False(t, left.Equal(right)) + + right = *left + left.FleetID = nil + assert.False(t, left.Equal(right)) + + right = *left + left.State = "In_Space" + assert.False(t, left.Equal(right)) + + right = *left + left.CargoType = &cap + assert.False(t, left.Equal(right)) + + right = *left + left.CargoType = nil + assert.False(t, left.Equal(right)) + + right = *left + left.Load = 45.123 + assert.False(t, left.Equal(right)) + + right = *left + left.Drive = 1.1 + assert.False(t, left.Equal(right)) + + right = *left + left.Weapons = 1.1 + assert.False(t, left.Equal(right)) + + right = *left + left.Shields = 1.1 + assert.False(t, left.Equal(right)) + + right = *left + left.Cargo = 1.1 + assert.False(t, left.Equal(right)) + + // non-essential properties + right = *left + + left.Index = 2 + assert.True(t, left.Equal(right)) + left.Number = 5 + assert.True(t, left.Equal(right)) +} + +func TestJoinEqualGroups(t *testing.T) { + g := &game.Game{ + Race: make([]game.Race, 2), + } + raceIdx := 0 + g.Race[raceIdx] = game.Race{ + ID: uuid.New(), + Name: "Race_0", + Drive: 1.1, + Weapons: 1.2, + Shields: 1.3, + Cargo: 1.4, + } + g.Race[1] = game.Race{ + ID: uuid.New(), + Name: "Race_1", + Drive: 2.1, + Weapons: 2.2, + Shields: 2.3, + Cargo: 2.4, + } + g.Map = game.Map{ + Width: 10, + Height: 10, + Planet: make([]game.Planet, 3), + } + g.Map.Planet[0] = gg.NewPlanet(0, "Planet_0", g.Race[0].ID, 0, 0, 100, 0, 0, 0, game.ProductionNone.AsType(uuid.Nil)) + g.Map.Planet[1] = gg.NewPlanet(1, "Planet_1", g.Race[1].ID, 1, 1, 100, 0, 0, 0, game.ProductionNone.AsType(uuid.Nil)) + g.Map.Planet[2] = gg.NewPlanet(1, "Planet_2", g.Race[0].ID, 2, 2, 100, 0, 0, 0, game.ProductionNone.AsType(uuid.Nil)) + + err := g.CreateShipType("Race_0", "R0_Gunship", 60, 30, 100, 0, 3) + assert.NoError(t, err) + err = g.CreateShipType("Race_0", "R0_Freighter", 8, 0, 2, 10, 0) + assert.NoError(t, err) + + err = g.CreateShipType("Race_1", "R1_Gunship", 60, 30, 100, 0, 3) + assert.NoError(t, err) + err = g.CreateShipType("Race_1", "R1_Freighter", 8, 0, 2, 10, 0) + assert.NoError(t, err) + + err = g.CreateShips(raceIdx, "Freighter", 0, 2) + assert.ErrorContains(t, err, e.GenericErrorText(e.ErrInputEntityNotExists)) + err = g.CreateShips(raceIdx, "R0_Gunship", 1, 2) + assert.ErrorContains(t, err, e.GenericErrorText(e.ErrInputEntityNotOwned)) + + err = g.CreateShips(raceIdx, "R0_Freighter", 0, 1) // 1 -> 2 + assert.NoError(t, err) + assert.Len(t, slices.Collect(g.ListShipGroups(raceIdx)), 1) + + err = g.CreateShips(1, "R1_Freighter", 1, 1) + assert.NoError(t, err) + assert.Len(t, slices.Collect(g.ListShipGroups(1)), 1) + + err = g.CreateShips(raceIdx, "R0_Freighter", 0, 6) // (2) + assert.NoError(t, err) + assert.Len(t, slices.Collect(g.ListShipGroups(raceIdx)), 2) + + err = g.CreateShips(raceIdx, "R0_Gunship", 0, 2) // (3) + assert.NoError(t, err) + assert.Len(t, slices.Collect(g.ListShipGroups(raceIdx)), 3) + + err = g.CreateShips(1, "R1_Gunship", 1, 1) + assert.NoError(t, err) + assert.Len(t, slices.Collect(g.ListShipGroups(1)), 2) + + g.Race[raceIdx].Drive = 1.5 + err = g.CreateShips(raceIdx, "R0_Gunship", 0, 9) // 4 -> 6 + assert.NoError(t, err) + assert.Len(t, slices.Collect(g.ListShipGroups(raceIdx)), 4) + err = g.CreateShips(raceIdx, "R0_Freighter", 0, 7) // 5 -> 7 + assert.NoError(t, err) + assert.Len(t, slices.Collect(g.ListShipGroups(raceIdx)), 5) + err = g.CreateShips(raceIdx, "R0_Gunship", 0, 4) // (6) + assert.NoError(t, err) + assert.Len(t, slices.Collect(g.ListShipGroups(raceIdx)), 6) + err = g.CreateShips(raceIdx, "R0_Freighter", 0, 4) // (7) + assert.NoError(t, err) + assert.Len(t, slices.Collect(g.ListShipGroups(raceIdx)), 7) + + g.Race[1].Shields = 2.0 + err = g.CreateShips(1, "R1_Freighter", 1, 1) + assert.NoError(t, err) + assert.Len(t, slices.Collect(g.ListShipGroups(1)), 3) + + err = g.JoinEqualGroups("Race_0") + assert.NoError(t, err) + + assert.Len(t, slices.Collect(g.ListShipGroups(1)), 3) + assert.Len(t, slices.Collect(g.ListShipGroups(raceIdx)), 4) + + shipTypeID := func(ri int, name string) uuid.UUID { + st := slices.IndexFunc(g.Race[ri].ShipTypes, func(v game.ShipType) bool { return v.Name == name }) + if st < 0 { + t.Fatalf("ShipType not found: %s", name) + return uuid.Nil + } + return g.Race[ri].ShipTypes[st].ID + } + + for sg := range g.ListShipGroups(raceIdx) { + switch { + case sg.TypeID == shipTypeID(raceIdx, "R0_Freighter") && sg.Drive == 1.1: + assert.Equal(t, uint(7), sg.Number) + assert.Equal(t, uint(2), sg.Index) + case sg.TypeID == shipTypeID(raceIdx, "R0_Freighter") && sg.Drive == 1.5: + assert.Equal(t, uint(11), sg.Number) + assert.Equal(t, uint(7), sg.Index) + case sg.TypeID == shipTypeID(raceIdx, "R0_Gunship") && sg.Drive == 1.1: + assert.Equal(t, uint(2), sg.Number) + assert.Equal(t, uint(3), sg.Index) + case sg.TypeID == shipTypeID(raceIdx, "R0_Gunship") && sg.Drive == 1.5: + assert.Equal(t, uint(13), sg.Number) + assert.Equal(t, uint(6), sg.Index) + default: + t.Error("not all ship groups covered") + } + } +} diff --git a/pkg/model/game/race.go b/pkg/model/game/race.go index eed52c6..dcc63b6 100644 --- a/pkg/model/game/race.go +++ b/pkg/model/game/race.go @@ -17,9 +17,7 @@ type Race struct { Sciences []Science `json:"science,omitempty"` - ShipTypes []ShipType `json:"shipType,omitempty"` - ShipGroups []ShipGroup `json:"shipGroup,omitempty"` - Fleets []Fleet `json:"fleet,omitempty"` + ShipTypes []ShipType `json:"shipType,omitempty"` } type Relation string diff --git a/pkg/model/game/ship.go b/pkg/model/game/ship.go index 7bf0f33..fa4acbf 100644 --- a/pkg/model/game/ship.go +++ b/pkg/model/game/ship.go @@ -1,12 +1,12 @@ package game import ( + "fmt" "math" "slices" "github.com/google/uuid" e "github.com/iliadenisov/galaxy/pkg/error" - "github.com/iliadenisov/galaxy/pkg/number" ) type ShipTypeReport struct { @@ -28,19 +28,8 @@ type ShipTypeReportForeign struct { ShipTypeReport } -type ShipGroup struct { - TypeID uuid.UUID `json:"id"` - Type ShipType `json:"-"` // TODO: fill upon load from store - Number uint `json:"number"` - State string `json:"state"` // TODO: kinda enum: In_Orbit, In_Space, Transfer_State, Upgrade - Load float64 `json:"load"` // Cargo loaded - "Масса груза" - Drive float64 `json:"drive"` - Weapons float64 `json:"weapons"` - Shields float64 `json:"shields"` - Cargo float64 `json:"cargo"` -} - type Fleet struct { + OwnerID uuid.UUID `json:"ownerId"` ShipGroups []ShipGroup `json:"group"` } @@ -71,71 +60,27 @@ func (st ShipType) ProductionCost() (mat float64, pop float64) { return } -// Грузоподъёмность -func (sg ShipGroup) CargoCapacity() float64 { - return sg.Drive * (sg.Type.Cargo + (sg.Type.Cargo*sg.Type.Cargo)/20) -} - -// "Масса перевозимого груза" -func (sg ShipGroup) CarryingMass() float64 { - return sg.Load / sg.Cargo -} - -func (sg ShipGroup) FullMass() float64 { - return sg.Type.EmptyMass() + sg.CarryingMass() -} - -// "Эффективность двигателя" -// равна мощности Двигателей умноженной на текущий технологический уровень блока Двигателей -func (sg ShipGroup) DriveEffective() float64 { - return sg.Type.Drive * sg.Drive -} - // TODO: test this -func (sg ShipGroup) Speed() float64 { - return sg.DriveEffective() * 20 / sg.FullMass() -} - -func (sg ShipGroup) UpgradeDriveCost(drive float64) float64 { - return (1 - sg.Drive/drive) * 10 * sg.Type.Drive -} - -// TODO: test on other values -func (sg ShipGroup) UpgradeWeaponsCost(weapons float64) float64 { - return (1 - sg.Weapons/weapons) * 10 * sg.Type.WeaponsMass() -} - -func (sg ShipGroup) UpgradeShieldsCost(shields float64) float64 { - return (1 - sg.Shields/shields) * 10 * sg.Type.Shields -} - -func (sg ShipGroup) UpgradeCargoCost(cargo float64) float64 { - return (1 - sg.Cargo/cargo) * 10 * sg.Type.Cargo -} - -// Мощность бомбардировки -// TODO: maybe rounding must be done only for display? -func (sg ShipGroup) BombingPower() float64 { - // return math.Sqrt(sg.Type.Weapons * sg.Weapons) - result := (math.Sqrt(sg.Type.Weapons*sg.Weapons)/10. + 1.) * - sg.Type.Weapons * - sg.Weapons * - float64(sg.Type.Armament) * - float64(sg.Number) - return number.Fixed3(result) -} - -// TODO: test this -func (fl Fleet) Speed() float64 { +func (g Game) FleetSpeed(fl *Fleet) float64 { result := math.MaxFloat64 for _, sg := range fl.ShipGroups { - if sg.Speed() < result { - result = sg.Speed() + st := g.mustShipType(sg.TypeID) + if sg.Speed(st) < result { + result = sg.Speed(st) } } return result } +func (g Game) mustShipType(id uuid.UUID) *ShipType { + for ri := range g.Race { + if st := slices.IndexFunc(g.Race[ri].ShipTypes, func(st ShipType) bool { return st.ID == id }); st >= 0 { + return &g.Race[ri].ShipTypes[st] + } + } + panic(fmt.Sprintf("mustShipType: ShipType not found: %v", id)) +} + func (g Game) ShipTypes(raceName string) ([]ShipType, error) { ri, err := g.raceIndex(raceName) if err != nil { @@ -240,9 +185,9 @@ func (g Game) mergeShipTypeInternal(ri int, name, targetName string) error { } // switch ship groups to the new type - for sg := range g.Race[ri].ShipGroups { - if g.Race[ri].ShipGroups[sg].TypeID == g.Race[ri].ShipTypes[st].ID { - g.Race[ri].ShipGroups[sg].TypeID = g.Race[ri].ShipTypes[tt].ID + for sg := range g.ShipGroups { + if g.ShipGroups[sg].OwnerID == g.Race[ri].ID && g.ShipGroups[sg].TypeID == g.Race[ri].ShipTypes[st].ID { + g.ShipGroups[sg].TypeID = g.Race[ri].ShipTypes[tt].ID } } diff --git a/pkg/model/game/ship_test.go b/pkg/model/game/ship_test.go index b2ce769..1ec4d73 100644 --- a/pkg/model/game/ship_test.go +++ b/pkg/model/game/ship_test.go @@ -7,7 +7,7 @@ import ( "github.com/stretchr/testify/assert" ) -func TestShipType(t *testing.T) { +func TestEmptyMass(t *testing.T) { Freighter := game.ShipType{ ShipTypeReport: game.ShipTypeReport{ Name: "Freighter", @@ -43,72 +43,4 @@ func TestShipType(t *testing.T) { }, } assert.Equal(t, 45., Cruiser.EmptyMass()) - - sg := game.ShipGroup{ - Type: Cruiser, - Number: 1, - State: "In_Orbit", - Drive: 1.0, - Weapons: 1.0, - Shields: 1.0, - Cargo: 1.0, - } - upgradeCost := sg.UpgradeDriveCost(2.0) + - sg.UpgradeWeaponsCost(2.0) + - sg.UpgradeShieldsCost(2.0) + - sg.UpgradeCargoCost(2.0) - assert.Equal(t, 225., upgradeCost) -} - -func TestCargoCapacity(t *testing.T) { - test := func(cargoSize float64, expectCapacity float64) { - ship := game.ShipType{ - ShipTypeReport: game.ShipTypeReport{ - Drive: 1, - Armament: 1, - Weapons: 1, - Shields: 1, - Cargo: cargoSize, - }, - } - sg := game.ShipGroup{ - Type: ship, - Number: 1, - State: "In_Orbit", - Drive: 1.0, - Weapons: 1.0, - Shields: 1.0, - Cargo: 1.0, - } - assert.Equal(t, expectCapacity, sg.CargoCapacity()) - } - test(1, 1.05) - test(5, 6.25) - test(10, 15) - test(50, 175) - test(100, 600) -} - -func TestBombingPower(t *testing.T) { - Gunship := game.ShipType{ - ShipTypeReport: game.ShipTypeReport{ - Drive: 60.0, - Armament: 3, - Weapons: 30.0, - Shields: 100.0, - Cargo: 0.0, - }, - } - sg := game.ShipGroup{ - Type: Gunship, - Number: 1, - State: "In_Orbit", - Drive: 1.0, - Weapons: 1.0, - Shields: 1.0, - Cargo: 1.0, - } - expectedBombingPower := 139.295 - result := sg.BombingPower() - assert.Equal(t, expectedBombingPower, result) }