chore: refactor structure
This commit is contained in:
@@ -0,0 +1,102 @@
|
||||
package game
|
||||
|
||||
import (
|
||||
"math"
|
||||
"slices"
|
||||
|
||||
"github.com/google/uuid"
|
||||
e "github.com/iliadenisov/galaxy/internal/error"
|
||||
)
|
||||
|
||||
type Fleet struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
OwnerID uuid.UUID `json:"ownerId"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
// TODO: Hello! Wanna know fleet's speed? Good. Implement & test this func first.
|
||||
func (g Game) FleetSpeed(fl Fleet) float64 {
|
||||
result := math.MaxFloat64
|
||||
for sg := range g.ShipGroups {
|
||||
if g.ShipGroups[sg].FleetID == nil || *g.ShipGroups[sg].FleetID != fl.ID {
|
||||
continue
|
||||
}
|
||||
st := g.mustShipType(g.ShipGroups[sg].TypeID)
|
||||
typeSpeed := g.ShipGroups[sg].Speed(st)
|
||||
if typeSpeed < result {
|
||||
result = typeSpeed
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func (g Game) JoinShipGroupToFleet(raceName, fleetName string, group, count uint) error {
|
||||
ri, err := g.raceIndex(raceName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return g.joinShipGroupToFleetInternal(ri, fleetName, group, count)
|
||||
}
|
||||
|
||||
func (g Game) joinShipGroupToFleetInternal(ri int, fleetName string, group, count uint) (err error) {
|
||||
name, ok := validateTypeName(fleetName)
|
||||
if !ok {
|
||||
return e.NewEntityTypeNameValidationError("%q", name)
|
||||
}
|
||||
sgi := -1
|
||||
var maxIndex uint
|
||||
for i, sg := range g.listShipGroups(ri) {
|
||||
if sgi < 0 && sg.Index == group {
|
||||
sgi = i
|
||||
}
|
||||
if sg.Index > maxIndex {
|
||||
maxIndex = sg.Index
|
||||
}
|
||||
}
|
||||
if sgi < 0 {
|
||||
return e.NewEntityNotExistsError("group #%d", group)
|
||||
}
|
||||
|
||||
if g.ShipGroups[sgi].Number < count {
|
||||
return e.NewJoinFleetGroupNumberNotEnoughError("%d<%d", g.ShipGroups[sgi].Number, count)
|
||||
}
|
||||
|
||||
fi := g.fleetIndex(ri, name)
|
||||
if fi < 0 {
|
||||
fi, err = g.createFleet(ri, name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if g.ShipGroups[sgi].Number != count && count > 0 {
|
||||
newGroup := g.ShipGroups[sgi]
|
||||
newGroup.Number -= count
|
||||
g.ShipGroups[sgi].Number = count
|
||||
newGroup.Index = maxIndex + 1
|
||||
g.ShipGroups = append(g.ShipGroups, newGroup)
|
||||
}
|
||||
|
||||
g.ShipGroups[sgi].FleetID = &g.Fleets[fi].ID
|
||||
return nil
|
||||
}
|
||||
|
||||
func (g Game) fleetIndex(ri int, name string) int {
|
||||
return slices.IndexFunc(g.Fleets, func(f Fleet) bool { return f.OwnerID == g.Race[ri].ID && f.Name == name })
|
||||
}
|
||||
|
||||
func (g Game) createFleet(ri int, name string) (int, error) {
|
||||
n, ok := validateTypeName(name)
|
||||
if !ok {
|
||||
return 0, e.NewEntityTypeNameValidationError("%q", n)
|
||||
}
|
||||
if fl := g.fleetIndex(ri, n); fl >= 0 {
|
||||
return 0, e.NewEntityTypeNameDuplicateError("fleet %w", g.Fleets[fl].Name)
|
||||
}
|
||||
g.Fleets = append(g.Fleets, Fleet{
|
||||
ID: uuid.New(),
|
||||
OwnerID: g.Race[ri].ID,
|
||||
Name: n,
|
||||
})
|
||||
return len(g.Fleets) - 1, nil
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
package game
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/google/uuid"
|
||||
e "github.com/iliadenisov/galaxy/internal/error"
|
||||
)
|
||||
|
||||
type Game struct {
|
||||
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 {
|
||||
// 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.
|
||||
}
|
||||
|
||||
func (g Game) raceIndex(name string) (int, error) {
|
||||
i := slices.IndexFunc(g.Race, func(r Race) bool { return r.Name == name })
|
||||
if i < 0 {
|
||||
return i, e.NewRaceUnknownError(name)
|
||||
}
|
||||
return i, nil
|
||||
}
|
||||
|
||||
func (g Game) UpdateRelation(race, opponent string, rel Relation) error {
|
||||
ri, err := g.raceIndex(race)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var other int
|
||||
if race == opponent {
|
||||
other = ri
|
||||
} else if other, err = g.raceIndex(opponent); err != nil {
|
||||
return err
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return g.updateRelationInternal(ri, other, rel)
|
||||
}
|
||||
|
||||
func (g Game) updateRelationInternal(ri, other int, rel Relation) error {
|
||||
for o := range g.Race[ri].Relations {
|
||||
switch {
|
||||
case ri == other:
|
||||
g.Race[ri].Relations[o].Relation = rel
|
||||
case g.Race[ri].Relations[o].RaceID == g.Race[other].ID:
|
||||
g.Race[ri].Relations[o].Relation = rel
|
||||
return nil
|
||||
}
|
||||
}
|
||||
if ri != other {
|
||||
return e.NewGameStateError("UpdateRelation: opponent not found")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (g Game) Relation(hostRace, opponentRace string) (RaceRelation, error) {
|
||||
ri, err := g.raceIndex(hostRace)
|
||||
if err != nil {
|
||||
return RaceRelation{}, err
|
||||
}
|
||||
other, err := g.raceIndex(opponentRace)
|
||||
if err != nil {
|
||||
return RaceRelation{}, err
|
||||
}
|
||||
return g.relationInternal(ri, other)
|
||||
}
|
||||
|
||||
func (g Game) relationInternal(ri, other int) (RaceRelation, error) {
|
||||
rel := slices.IndexFunc(g.Race[ri].Relations, func(r RaceRelation) bool { return r.RaceID == g.Race[other].ID })
|
||||
if rel < 0 {
|
||||
return RaceRelation{}, e.NewGameStateError("Relation: opponent not found")
|
||||
}
|
||||
return g.Race[ri].Relations[rel], nil
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
// validateTypeName always return v without leading and trailing spaces
|
||||
func validateTypeName(v string) (string, bool) {
|
||||
s := strings.TrimSpace(v)
|
||||
if len(s) > 0 {
|
||||
return s, true
|
||||
}
|
||||
// TODO: special symbols
|
||||
return s, false
|
||||
}
|
||||
|
||||
func (g Game) MarshalBinary() (data []byte, err error) {
|
||||
return json.Marshal(&g)
|
||||
}
|
||||
|
||||
func (g *Game) UnmarshalBinary(data []byte) error {
|
||||
return json.Unmarshal(data, g)
|
||||
}
|
||||
@@ -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.Seq2[int, ShipGroup] {
|
||||
return g.listShipGroups(ri)
|
||||
}
|
||||
@@ -0,0 +1,196 @@
|
||||
package game
|
||||
|
||||
import (
|
||||
"iter"
|
||||
"maps"
|
||||
"math"
|
||||
"slices"
|
||||
|
||||
"github.com/google/uuid"
|
||||
e "github.com/iliadenisov/galaxy/internal/error"
|
||||
"github.com/iliadenisov/galaxy/internal/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(maps.Values(maps.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.Seq2[int, ShipGroup] {
|
||||
return func(yield func(int, ShipGroup) bool) {
|
||||
for sg := range g.ShipGroups {
|
||||
if g.ShipGroups[sg].OwnerID == g.Race[ri].ID {
|
||||
if !yield(sg, g.ShipGroups[sg]) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func maxUint(a, b uint) uint {
|
||||
if b > a {
|
||||
return b
|
||||
}
|
||||
return a
|
||||
}
|
||||
@@ -0,0 +1,399 @@
|
||||
package game_test
|
||||
|
||||
import (
|
||||
"iter"
|
||||
"math/rand/v2"
|
||||
"slices"
|
||||
"testing"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/iliadenisov/galaxy/internal/controller"
|
||||
e "github.com/iliadenisov/galaxy/internal/error"
|
||||
"github.com/iliadenisov/galaxy/internal/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] = controller.NewPlanet(0, "Planet_0", g.Race[0].ID, 0, 0, 100, 0, 0, 0, game.ProductionNone.AsType(uuid.Nil))
|
||||
g.Map.Planet[1] = controller.NewPlanet(1, "Planet_1", g.Race[1].ID, 1, 1, 100, 0, 0, 0, game.ProductionNone.AsType(uuid.Nil))
|
||||
g.Map.Planet[2] = controller.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, collectGroups(g.ListShipGroups(raceIdx)), 1)
|
||||
|
||||
err = g.CreateShips(1, "R1_Freighter", 1, 1)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, collectGroups(g.ListShipGroups(1)), 1)
|
||||
|
||||
err = g.CreateShips(raceIdx, "R0_Freighter", 0, 6) // (2)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, collectGroups(g.ListShipGroups(raceIdx)), 2)
|
||||
|
||||
err = g.CreateShips(raceIdx, "R0_Gunship", 0, 2) // (3)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, collectGroups(g.ListShipGroups(raceIdx)), 3)
|
||||
|
||||
err = g.CreateShips(1, "R1_Gunship", 1, 1)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, collectGroups(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, collectGroups(g.ListShipGroups(raceIdx)), 4)
|
||||
err = g.CreateShips(raceIdx, "R0_Freighter", 0, 7) // 5 -> 7
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, collectGroups(g.ListShipGroups(raceIdx)), 5)
|
||||
err = g.CreateShips(raceIdx, "R0_Gunship", 0, 4) // (6)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, collectGroups(g.ListShipGroups(raceIdx)), 6)
|
||||
err = g.CreateShips(raceIdx, "R0_Freighter", 0, 4) // (7)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, collectGroups(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, collectGroups(g.ListShipGroups(1)), 3)
|
||||
|
||||
err = g.JoinEqualGroups("Race_0")
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Len(t, collectGroups(g.ListShipGroups(1)), 3)
|
||||
assert.Len(t, collectGroups(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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func collectGroups(i iter.Seq2[int, game.ShipGroup]) []game.ShipGroup {
|
||||
result := make([]game.ShipGroup, 0)
|
||||
for _, sg := range i {
|
||||
result = append(result, sg)
|
||||
}
|
||||
return result
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package game
|
||||
|
||||
type Map struct {
|
||||
Width uint32 `json:"width"`
|
||||
Height uint32 `json:"height"`
|
||||
Planet []Planet `json:"planets"`
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
package game
|
||||
|
||||
import (
|
||||
"math"
|
||||
"slices"
|
||||
|
||||
"github.com/google/uuid"
|
||||
e "github.com/iliadenisov/galaxy/internal/error"
|
||||
)
|
||||
|
||||
type UnidentifiedPlanet struct {
|
||||
X float64 `json:"x"`
|
||||
Y float64 `json:"y"`
|
||||
Number uint `json:"number"`
|
||||
}
|
||||
|
||||
type UninhabitedPlanet struct {
|
||||
UnidentifiedPlanet
|
||||
Size float64 `json:"size"`
|
||||
Name string `json:"name"`
|
||||
Resources float64 `json:"resources"` // R - Ресурсы / сырьё
|
||||
Capital float64 `json:"capital"` // CAP $ - Запасы промышленности
|
||||
Material float64 `json:"material"` // MAT M - Запасы ресурсов / сырья
|
||||
}
|
||||
|
||||
type PlanetReport struct {
|
||||
UninhabitedPlanet
|
||||
Industry float64 `json:"industry"` // I - Промышленность
|
||||
Population float64 `json:"population"` // P - Население
|
||||
Colonists float64 `json:"colonists"` // COL C - Количество колонистов
|
||||
Production ProductionType `json:"production"` // TODO: internal/report format
|
||||
// Параметр "L" - Свободный производственный потенциал
|
||||
}
|
||||
|
||||
type Planet struct {
|
||||
Owner uuid.UUID `json:"owner"`
|
||||
PlanetReport
|
||||
}
|
||||
|
||||
type PlanetReportForeign struct {
|
||||
RaceName string
|
||||
PlanetReport
|
||||
}
|
||||
|
||||
// Свободный производственный потенциал (L)
|
||||
// промышленность * 0.75 + население * 0.25
|
||||
// TODO: за вычетом затрат, расходуемых в течение хода на модернизацию кораблей
|
||||
func (p Planet) ProductionCapacity() float64 {
|
||||
return p.Industry*0.75 + p.Population*0.25
|
||||
}
|
||||
|
||||
// Производство промышленности
|
||||
// TODO: test on real values
|
||||
func (p *Planet) IncreaseIndustry() {
|
||||
prod := p.ProductionCapacity() / 5
|
||||
industryIncrement := math.Min(prod, p.Material)
|
||||
p.Industry += industryIncrement
|
||||
if p.Industry > p.Population {
|
||||
p.Industry = p.Population
|
||||
p.Capital += p.Population - p.Industry
|
||||
}
|
||||
}
|
||||
|
||||
// Производство материалов
|
||||
// TODO: test on real values
|
||||
func (p *Planet) IncreaseMaterial() {
|
||||
p.Material += p.ProductionCapacity() * p.Industry
|
||||
}
|
||||
|
||||
// Автоматическое увеличение населения на каждом ходу
|
||||
func (p *Planet) IncreasePopulation() {
|
||||
p.Population *= 1.08
|
||||
var extraPopulation = p.Size - p.Population
|
||||
if extraPopulation > 0 {
|
||||
p.Colonists += extraPopulation / 8
|
||||
p.Population -= extraPopulation
|
||||
}
|
||||
}
|
||||
|
||||
func (g Game) RenamePlanet(raceName string, planetNumber int, typeName string) error {
|
||||
ri, err := g.raceIndex(raceName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return g.renamePlanetInternal(ri, planetNumber, typeName)
|
||||
}
|
||||
|
||||
func (g Game) renamePlanetInternal(ri int, number int, name string) error {
|
||||
n, ok := validateTypeName(name)
|
||||
if !ok {
|
||||
return e.NewEntityTypeNameValidationError("%q", n)
|
||||
}
|
||||
if number < 0 {
|
||||
return e.NewPlanetNumberError(number)
|
||||
}
|
||||
pl := slices.IndexFunc(g.Map.Planet, func(p Planet) bool { return p.Number == uint(number) })
|
||||
if pl < 0 {
|
||||
return e.NewEntityNotExistsError("planet #%d", number)
|
||||
}
|
||||
if g.Map.Planet[pl].Owner != g.Race[ri].ID {
|
||||
return e.NewEntityNotOwnedError("planet %#d", number)
|
||||
}
|
||||
g.Map.Planet[pl].Name = n
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
package game
|
||||
|
||||
import (
|
||||
"slices"
|
||||
|
||||
"github.com/google/uuid"
|
||||
e "github.com/iliadenisov/galaxy/internal/error"
|
||||
)
|
||||
|
||||
type PlanetProduction string
|
||||
|
||||
const (
|
||||
ProductionNone PlanetProduction = "-"
|
||||
ProductionMaterial PlanetProduction = "MAT" // Сырьё
|
||||
ProductionCapital PlanetProduction = "CAP" // Промышленность
|
||||
|
||||
ResearchDrive PlanetProduction = "DRIVE"
|
||||
ResearchWeapons PlanetProduction = "WEAPONS"
|
||||
ResearchShields PlanetProduction = "SHIELDS"
|
||||
ResearchCargo PlanetProduction = "CARGO"
|
||||
|
||||
ResearchScience PlanetProduction = "SCIENCE"
|
||||
ProductionShip PlanetProduction = "SHIP"
|
||||
)
|
||||
|
||||
type ProductionType struct {
|
||||
Production PlanetProduction `json:"type"`
|
||||
SubjectID *uuid.UUID `json:"subjectId"`
|
||||
Progress *float64 `json:"progress"`
|
||||
}
|
||||
|
||||
func (p PlanetProduction) AsType(subject uuid.UUID) ProductionType {
|
||||
switch p {
|
||||
case ResearchScience, ProductionShip:
|
||||
return ProductionType{Production: p, SubjectID: &subject}
|
||||
default:
|
||||
return ProductionType{Production: p, SubjectID: nil}
|
||||
}
|
||||
}
|
||||
|
||||
func (g Game) PlanetProduction(raceName string, planetNumber int, prodType, subject string) error {
|
||||
ri, err := g.raceIndex(raceName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var prod PlanetProduction
|
||||
switch PlanetProduction(prodType) {
|
||||
case ProductionMaterial:
|
||||
prod = ProductionMaterial
|
||||
case ProductionCapital:
|
||||
prod = ProductionCapital
|
||||
case ResearchDrive:
|
||||
prod = ResearchDrive
|
||||
case ResearchWeapons:
|
||||
prod = ResearchWeapons
|
||||
case ResearchShields:
|
||||
prod = ResearchShields
|
||||
case ResearchCargo:
|
||||
prod = ResearchCargo
|
||||
case ResearchScience:
|
||||
prod = ResearchScience
|
||||
case ProductionShip:
|
||||
prod = ProductionShip
|
||||
default:
|
||||
return e.NewProductionInvalidError(prodType)
|
||||
}
|
||||
return g.planetProductionInternal(ri, planetNumber, prod, subject)
|
||||
}
|
||||
|
||||
func (g Game) planetProductionInternal(ri int, number int, prod PlanetProduction, subj string) error {
|
||||
if number < 0 {
|
||||
return e.NewPlanetNumberError(number)
|
||||
}
|
||||
i := slices.IndexFunc(g.Map.Planet, func(p Planet) bool { return p.Number == uint(number) })
|
||||
if i < 0 {
|
||||
return e.NewEntityNotExistsError("planet #%d", number)
|
||||
}
|
||||
if g.Map.Planet[i].Owner != g.Race[ri].ID {
|
||||
return e.NewEntityNotOwnedError("planet %#d", number)
|
||||
}
|
||||
g.Map.Planet[i].Production.Progress = nil
|
||||
var subjectID *uuid.UUID
|
||||
if (prod == ResearchScience || prod == ProductionShip) && subj == "" {
|
||||
return e.NewEntityTypeNameValidationError("%s=%q", prod, subj)
|
||||
}
|
||||
if prod == ResearchScience {
|
||||
i := slices.IndexFunc(g.Race[ri].Sciences, func(s Science) bool { return s.Name == subj })
|
||||
if i < 0 {
|
||||
return e.NewEntityNotExistsError("science %w", subj)
|
||||
}
|
||||
subjectID = &g.Race[ri].Sciences[i].ID
|
||||
}
|
||||
if prod == ProductionShip {
|
||||
i := slices.IndexFunc(g.Race[ri].ShipTypes, func(st ShipType) bool { return st.Name == subj })
|
||||
if i < 0 {
|
||||
return e.NewEntityNotExistsError("ship type %w", subj)
|
||||
}
|
||||
if g.Map.Planet[i].Production.Production == ProductionShip &&
|
||||
g.Map.Planet[i].Production.SubjectID != nil &&
|
||||
*g.Map.Planet[i].Production.SubjectID == g.Race[ri].ShipTypes[i].ID {
|
||||
// Planet already produces this ship type, keeping progress intact
|
||||
return nil
|
||||
}
|
||||
subjectID = &g.Race[ri].ShipTypes[i].ID
|
||||
var progress float64 = 0.
|
||||
g.Map.Planet[i].Production.Progress = &progress
|
||||
}
|
||||
if g.Map.Planet[i].Production.Production == ProductionShip {
|
||||
if g.Map.Planet[i].Production.SubjectID == nil {
|
||||
return e.NewGameStateError("planet #%d produces ship but SubjectID is empty", g.Map.Planet[i].Number)
|
||||
}
|
||||
s := *g.Map.Planet[i].Production.SubjectID
|
||||
if g.Map.Planet[i].Production.Progress == nil {
|
||||
return e.NewGameStateError("planet #%d produces ship but Progress is empty", g.Map.Planet[i].Number)
|
||||
}
|
||||
progress := *g.Map.Planet[i].Production.Progress
|
||||
i := slices.IndexFunc(g.Race[ri].ShipTypes, func(st ShipType) bool { return st.ID == s })
|
||||
if i < 0 {
|
||||
return e.NewGameStateError("planet #%d produces ship but ShipType was not found for race %s", g.Map.Planet[i].Number, g.Race[ri].Name)
|
||||
}
|
||||
mat, _ := g.Race[ri].ShipTypes[i].ProductionCost()
|
||||
extra := mat * progress
|
||||
g.Map.Planet[i].Material += extra
|
||||
}
|
||||
g.Map.Planet[i].Production.Production = prod
|
||||
g.Map.Planet[i].Production.SubjectID = subjectID
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package game
|
||||
|
||||
import "github.com/google/uuid"
|
||||
|
||||
type Race struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Extinct bool `json:"extinct"`
|
||||
|
||||
Vote uuid.UUID `json:"vote"`
|
||||
Relations []RaceRelation `json:"relations"`
|
||||
|
||||
Drive float64 `json:"drive"`
|
||||
Weapons float64 `json:"weapons"`
|
||||
Shields float64 `json:"shields"`
|
||||
Cargo float64 `json:"cargo"`
|
||||
|
||||
Sciences []Science `json:"science,omitempty"`
|
||||
|
||||
ShipTypes []ShipType `json:"shipType,omitempty"`
|
||||
}
|
||||
|
||||
type Relation string
|
||||
|
||||
const (
|
||||
RelationWar Relation = "War"
|
||||
RelationPeace Relation = "Peace"
|
||||
)
|
||||
|
||||
type RaceRelation struct {
|
||||
RaceID uuid.UUID `json:"raceId"`
|
||||
Relation Relation `json:"relation"`
|
||||
}
|
||||
|
||||
func (r Race) FlightDistance() float64 {
|
||||
return r.Drive * 40
|
||||
}
|
||||
|
||||
func (r Race) VisibilityDistance() float64 {
|
||||
return r.Drive * 30
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
package game
|
||||
|
||||
type Report struct {
|
||||
Width, Height uint32
|
||||
PlanetCount uint32 // do we need that?
|
||||
PlayersLeft uint32 // do we need that?
|
||||
|
||||
Votes float64
|
||||
VoteFor string
|
||||
|
||||
Statuses []PlayerStatus
|
||||
|
||||
Sciences []ScienceReport
|
||||
ForeignSciences []ScienceReportForeign
|
||||
|
||||
ShipTypes []ShipTypeReport
|
||||
ForeignShipTypes []ShipTypeReportForeign
|
||||
|
||||
Battles []any // TODO: tbd
|
||||
|
||||
Bombings []any // TODO: tbd
|
||||
|
||||
IncomingGroups []IncomingGroup
|
||||
|
||||
Planets []PlanetReport
|
||||
ForeignPlanets []PlanetReportForeign
|
||||
UninhabitedPlanets []UninhabitedPlanet
|
||||
UnidentifiedPlanets []UnidentifiedPlanet
|
||||
|
||||
ShipsInProduction []any // TODO: tbd
|
||||
|
||||
Routes []any // TODO: tbd
|
||||
|
||||
Fleets []any // TODO: tbd
|
||||
|
||||
ShipGroups []any // TODO: tbd
|
||||
|
||||
ForeignShipGroups []any // TODO: tbd
|
||||
|
||||
UnidentifiedGroups []any // TODO: tbd
|
||||
}
|
||||
|
||||
type IncomingGroup struct {
|
||||
SourcePlanetNumber uint
|
||||
TargetPlanetNumber uint
|
||||
Distance float64
|
||||
Speed float64
|
||||
Mass float64
|
||||
}
|
||||
|
||||
type ReportRelation struct {
|
||||
RaceName string
|
||||
Relation string
|
||||
}
|
||||
|
||||
type PlayerStatus struct {
|
||||
Name string
|
||||
Drive float64 `json:"drive"`
|
||||
Weapons float64 `json:"weapons"`
|
||||
Shields float64 `json:"shields"`
|
||||
Cargo float64 `json:"cargo"`
|
||||
Population float64
|
||||
Industry float64
|
||||
Planets uint16
|
||||
Relation ReportRelation
|
||||
Votes float64
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
package game
|
||||
|
||||
import (
|
||||
"slices"
|
||||
|
||||
"github.com/google/uuid"
|
||||
e "github.com/iliadenisov/galaxy/internal/error"
|
||||
)
|
||||
|
||||
type Science struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
ScienceReport
|
||||
}
|
||||
|
||||
type ScienceReportForeign struct {
|
||||
RaceName string
|
||||
ScienceReport
|
||||
}
|
||||
|
||||
type ScienceReport struct {
|
||||
Name string `json:"name"`
|
||||
Drive float64 `json:"drive"`
|
||||
Weapons float64 `json:"weapons"`
|
||||
Shields float64 `json:"shields"`
|
||||
Cargo float64 `json:"cargo"`
|
||||
}
|
||||
|
||||
func (g Game) Sciences(raceName string) ([]Science, error) {
|
||||
ri, err := g.raceIndex(raceName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return g.sciencesInternal(ri), nil
|
||||
}
|
||||
|
||||
func (g Game) sciencesInternal(ri int) []Science {
|
||||
return g.Race[ri].Sciences
|
||||
}
|
||||
|
||||
func (g Game) DeleteScience(raceName, typeName string) error {
|
||||
ri, err := g.raceIndex(raceName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return g.deleteScienceInternal(ri, typeName)
|
||||
}
|
||||
|
||||
func (g Game) deleteScienceInternal(ri int, name string) error {
|
||||
sc := slices.IndexFunc(g.Race[ri].Sciences, func(s Science) bool { return s.Name == name })
|
||||
if sc < 0 {
|
||||
return e.NewEntityNotExistsError("science %w", name)
|
||||
}
|
||||
if pl := slices.IndexFunc(g.Map.Planet, func(p Planet) bool {
|
||||
return p.Production.Production == ResearchScience &&
|
||||
p.Production.SubjectID != nil &&
|
||||
*p.Production.SubjectID == g.Race[ri].Sciences[sc].ID
|
||||
}); pl >= 0 {
|
||||
return e.NewDeleteSciencePlanetProductionError(g.Map.Planet[pl].Name)
|
||||
}
|
||||
g.Race[ri].Sciences = append(g.Race[ri].Sciences[:sc], g.Race[ri].Sciences[sc+1:]...)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (g Game) CreateScience(raceName, typeName string, d, w, s, c float64) error {
|
||||
ri, err := g.raceIndex(raceName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return g.createScienceInternal(ri, typeName, d, w, s, c)
|
||||
}
|
||||
|
||||
func (g Game) createScienceInternal(ri int, name string, d, w, s, c float64) error {
|
||||
n, ok := validateTypeName(name)
|
||||
if !ok {
|
||||
return e.NewEntityTypeNameValidationError("%q", n)
|
||||
}
|
||||
if sc := slices.IndexFunc(g.Race[ri].Sciences, func(s Science) bool { return s.Name == n }); sc >= 0 {
|
||||
return e.NewEntityTypeNameDuplicateError("science %w", g.Race[ri].Sciences[sc].Name)
|
||||
}
|
||||
if d < 0 {
|
||||
return e.NewDriveValueError(d)
|
||||
}
|
||||
if w < 0 {
|
||||
return e.NewWeaponsValueError(w)
|
||||
}
|
||||
if s < 0 {
|
||||
return e.NewShieldsValueError(s)
|
||||
}
|
||||
if c < 0 {
|
||||
return e.NewCargoValueError(c)
|
||||
}
|
||||
sum := d + w + s + c
|
||||
if sum != 1 {
|
||||
return e.NewScienceSumValuesError("D=%f W=%f S=%f C=%f sum=%f", d, w, s, c, sum)
|
||||
}
|
||||
g.Race[ri].Sciences = append(g.Race[ri].Sciences, Science{
|
||||
ID: uuid.New(),
|
||||
ScienceReport: ScienceReport{
|
||||
Name: n,
|
||||
Drive: d,
|
||||
Weapons: w,
|
||||
Shields: s,
|
||||
Cargo: c,
|
||||
},
|
||||
})
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package game
|
||||
|
||||
type GameParameter struct {
|
||||
Series string
|
||||
Players uint
|
||||
Public bool
|
||||
}
|
||||
@@ -0,0 +1,209 @@
|
||||
package game
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"slices"
|
||||
|
||||
"github.com/google/uuid"
|
||||
e "github.com/iliadenisov/galaxy/internal/error"
|
||||
)
|
||||
|
||||
type ShipTypeReport struct {
|
||||
Name string `json:"name"`
|
||||
Drive float64 `json:"drive"`
|
||||
Armament uint `json:"armament"`
|
||||
Weapons float64 `json:"weapons"`
|
||||
Shields float64 `json:"shields"`
|
||||
Cargo float64 `json:"cargo"`
|
||||
}
|
||||
|
||||
type ShipType struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
ShipTypeReport
|
||||
}
|
||||
|
||||
type ShipTypeReportForeign struct {
|
||||
RaceName string
|
||||
ShipTypeReport
|
||||
}
|
||||
|
||||
func (st ShipType) Equal(o ShipType) bool {
|
||||
return st.Drive == o.Drive &&
|
||||
st.Weapons == o.Weapons &&
|
||||
st.Armament == o.Armament &&
|
||||
st.Shields == o.Shields &&
|
||||
st.Cargo == o.Cargo
|
||||
}
|
||||
|
||||
func (st ShipType) EmptyMass() float64 {
|
||||
shipMass := st.Drive + st.Shields + st.Cargo + st.WeaponsMass()
|
||||
return shipMass
|
||||
}
|
||||
|
||||
func (st ShipType) WeaponsMass() float64 {
|
||||
if st.Armament == 0 || st.Weapons == 0 {
|
||||
return 0
|
||||
}
|
||||
return float64(st.Armament+1) * (st.Weapons / 2)
|
||||
}
|
||||
|
||||
// ProductionCost returns Material (MAT) and Population (POP) to produce this [ShipType]
|
||||
func (st ShipType) ProductionCost() (mat float64, pop float64) {
|
||||
mat = st.EmptyMass()
|
||||
pop = mat * 10
|
||||
return
|
||||
}
|
||||
|
||||
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 {
|
||||
return nil, err
|
||||
}
|
||||
return g.shipTypesInternal(ri), nil
|
||||
}
|
||||
|
||||
func (g Game) shipTypesInternal(ri int) []ShipType {
|
||||
return g.Race[ri].ShipTypes
|
||||
}
|
||||
|
||||
func (g Game) DeleteShipType(raceName, typeName string) error {
|
||||
ri, err := g.raceIndex(raceName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return g.deleteShipTypeInternal(ri, typeName)
|
||||
}
|
||||
|
||||
func (g Game) deleteShipTypeInternal(ri int, name string) error {
|
||||
st := slices.IndexFunc(g.Race[ri].ShipTypes, func(st ShipType) bool { return st.Name == name })
|
||||
if st < 0 {
|
||||
return e.NewEntityNotExistsError("ship type %w", name)
|
||||
}
|
||||
if pl := slices.IndexFunc(g.Map.Planet, func(p Planet) bool {
|
||||
return p.Production.Production == ProductionShip &&
|
||||
p.Production.SubjectID != nil &&
|
||||
g.Race[ri].ShipTypes[st].ID == *p.Production.SubjectID
|
||||
}); pl >= 0 {
|
||||
return e.NewDeleteShipTypePlanetProductionError(g.Map.Planet[pl].Name)
|
||||
}
|
||||
g.Race[ri].ShipTypes = append(g.Race[ri].ShipTypes[:st], g.Race[ri].ShipTypes[st+1:]...)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (g Game) CreateShipType(raceName, typeName string, d, w, s, c float64, a int) error {
|
||||
ri, err := g.raceIndex(raceName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return g.createShipTypeInternal(ri, typeName, d, w, s, c, a)
|
||||
}
|
||||
|
||||
func (g Game) createShipTypeInternal(ri int, name string, d, w, s, c float64, a int) error {
|
||||
if err := checkShipTypeValues(d, w, s, c, a); err != nil {
|
||||
return err
|
||||
}
|
||||
n, ok := validateTypeName(name)
|
||||
if !ok {
|
||||
return e.NewEntityTypeNameValidationError("%q", n)
|
||||
}
|
||||
if st := slices.IndexFunc(g.Race[ri].ShipTypes, func(st ShipType) bool { return st.Name == name }); st >= 0 {
|
||||
return e.NewEntityTypeNameDuplicateError("ship type %w", g.Race[ri].ShipTypes[st].Name)
|
||||
}
|
||||
g.Race[ri].ShipTypes = append(g.Race[ri].ShipTypes, ShipType{
|
||||
ID: uuid.New(),
|
||||
ShipTypeReport: ShipTypeReport{
|
||||
Name: n,
|
||||
Drive: d,
|
||||
Weapons: w,
|
||||
Shields: s,
|
||||
Cargo: c,
|
||||
Armament: uint(a),
|
||||
},
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (g Game) MergeShipType(race, name, targetName string) error {
|
||||
ri, err := g.raceIndex(race)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return g.mergeShipTypeInternal(ri, name, targetName)
|
||||
}
|
||||
|
||||
func (g Game) mergeShipTypeInternal(ri int, name, targetName string) error {
|
||||
st := slices.IndexFunc(g.Race[ri].ShipTypes, func(st ShipType) bool { return st.Name == name })
|
||||
if st < 0 {
|
||||
return e.NewEntityNotExistsError("source ship type %w", name)
|
||||
}
|
||||
if name == targetName {
|
||||
return e.NewEntityTypeNameEqualityError("ship type %q", targetName)
|
||||
}
|
||||
tt := slices.IndexFunc(g.Race[ri].ShipTypes, func(st ShipType) bool { return st.Name == targetName })
|
||||
if tt < 0 {
|
||||
return e.NewEntityNotExistsError("target ship type %w", name)
|
||||
}
|
||||
if !g.Race[ri].ShipTypes[st].Equal(g.Race[ri].ShipTypes[tt]) {
|
||||
return e.NewMergeShipTypeNotEqualError()
|
||||
}
|
||||
|
||||
// switch planet productions to the new type
|
||||
for pl := range g.Map.Planet {
|
||||
if g.Map.Planet[pl].Owner == g.Race[ri].ID &&
|
||||
g.Map.Planet[pl].Production.Production == ProductionShip &&
|
||||
g.Map.Planet[pl].Production.SubjectID != nil &&
|
||||
*g.Map.Planet[pl].Production.SubjectID == g.Race[ri].ShipTypes[st].ID {
|
||||
g.Map.Planet[pl].Production.SubjectID = &g.Race[ri].ShipTypes[tt].ID
|
||||
}
|
||||
}
|
||||
|
||||
// switch ship groups to the new type
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
// remove the source type
|
||||
g.Race[ri].ShipTypes = append(g.Race[ri].ShipTypes[:st], g.Race[ri].ShipTypes[st+1:]...)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func checkShipTypeValues(d, w, s, c float64, a int) error {
|
||||
if !checkShipTypeValueDWSC(d) {
|
||||
return e.NewDriveValueError(d)
|
||||
}
|
||||
if !checkShipTypeValueDWSC(w) {
|
||||
return e.NewWeaponsValueError(w)
|
||||
}
|
||||
if !checkShipTypeValueDWSC(s) {
|
||||
return e.NewShieldsValueError(s)
|
||||
}
|
||||
if !checkShipTypeValueDWSC(c) {
|
||||
return e.NewCargoValueError(s)
|
||||
}
|
||||
if a < 0 {
|
||||
return e.NewShipTypeArmamentValueError(a)
|
||||
}
|
||||
if (w == 0 && a > 0) || (a == 0 && w > 0) {
|
||||
return e.NewShipTypeArmamentAndWeaponsValueError("A=%d W=%.0f", a, w)
|
||||
}
|
||||
if d == 0 && w == 0 && s == 0 && c == 0 && a == 0 {
|
||||
return e.NewShipTypeShipTypeZeroValuesError()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func checkShipTypeValueDWSC(v float64) bool {
|
||||
return v == 0 || v >= 1
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package game_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/iliadenisov/galaxy/internal/model/game"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestEmptyMass(t *testing.T) {
|
||||
Freighter := game.ShipType{
|
||||
ShipTypeReport: game.ShipTypeReport{
|
||||
Name: "Freighter",
|
||||
Drive: 8,
|
||||
Armament: 0,
|
||||
Weapons: 0,
|
||||
Shields: 2,
|
||||
Cargo: 10,
|
||||
},
|
||||
}
|
||||
assert.Equal(t, 20., Freighter.EmptyMass())
|
||||
|
||||
Gunship := game.ShipType{
|
||||
ShipTypeReport: game.ShipTypeReport{
|
||||
Name: "Gunship",
|
||||
Drive: 4,
|
||||
Armament: 2,
|
||||
Weapons: 2,
|
||||
Shields: 4,
|
||||
Cargo: 0,
|
||||
},
|
||||
}
|
||||
assert.Equal(t, 11., Gunship.EmptyMass())
|
||||
|
||||
Cruiser := game.ShipType{
|
||||
ShipTypeReport: game.ShipTypeReport{
|
||||
Name: "Cruiser",
|
||||
Drive: 15,
|
||||
Armament: 1,
|
||||
Weapons: 15,
|
||||
Shields: 15,
|
||||
Cargo: 0,
|
||||
},
|
||||
}
|
||||
assert.Equal(t, 45., Cruiser.EmptyMass())
|
||||
}
|
||||
Reference in New Issue
Block a user