feat: store battles and bombings

This commit is contained in:
Ilia Denisov
2026-01-30 18:57:43 +03:00
parent 824f6609ab
commit 4c14234afb
16 changed files with 274 additions and 103 deletions
+11 -2
View File
@@ -14,6 +14,7 @@ type Battle struct {
ID uuid.UUID
Planet uint
observerGroups map[int]bool // True = In_Battle, False = Out_Battle
initialNumbers map[int]uint // Initial number of ships in the group
Protocol []BattleAction
shipAmmo map[int]uint
@@ -22,7 +23,7 @@ type Battle struct {
type BattleAction struct {
Attacker int
Defenter int
Defender int
Destroyed bool
}
@@ -94,6 +95,11 @@ func ProduceBattles(c *Cache) []*Battle {
result := make([]*Battle, 0)
// TODO: check this behavior:
// Multiple battles on single planet shoul be produced as single battle too:
// A <--> B
// C <--> D
// where: A in peace with [C, D], B in peace with [C, D], and so on.
for pl, observerGroups := range planetGroups {
battleGroups := FilterBattleGroups(c, observerGroups)
b := &Battle{
@@ -102,6 +108,9 @@ func ProduceBattles(c *Cache) []*Battle {
attacker: make(map[int]map[int]float64),
shipAmmo: make(map[int]uint),
}
for sgi := range observerGroups {
b.initialNumbers[sgi] = c.ShipGroup(sgi).Number
}
for i := range battleGroups {
attIdx := battleGroups[i]
@@ -159,7 +168,7 @@ func SingleBattle(c *Cache, b *Battle) {
b.Protocol = append(b.Protocol, BattleAction{
Attacker: attIdx,
Defenter: defIdx,
Defender: defIdx,
Destroyed: destroyed,
})
+57 -23
View File
@@ -1,46 +1,80 @@
package controller
import "github.com/iliadenisov/galaxy/internal/model/game"
import (
"github.com/google/uuid"
"github.com/iliadenisov/galaxy/internal/model/game"
"github.com/iliadenisov/galaxy/internal/model/report"
)
func TransformBattle(c *Cache, b *Battle) *game.BattleReport {
r := &game.BattleReport{
func TransformBattle(c *Cache, b *Battle) *report.BattleReport {
r := &report.BattleReport{
ID: b.ID,
Planet: b.Planet,
PlanetName: c.MustPlanet(b.Planet).Name,
Races: make(map[int]string),
Ships: make(map[int]string),
Protocol: make([]game.BattleActionReport, len(b.Protocol)),
Races: make(map[int]uuid.UUID),
Ships: make(map[int]report.BattleReportGroup),
Protocol: make([]report.BattleActionReport, len(b.Protocol)),
}
cacheShipClass := make(map[string]int)
cacheRaceName := make(map[string]int)
cacheShipClass := make(map[uuid.UUID]int)
cacheRaceName := make(map[uuid.UUID]int)
cacher := func(shipClass string, cache map[string]int) int {
if v, ok := cache[shipClass]; ok {
addShipGroup := func(groupId int, inBattle bool) int {
shipClass := c.ShipGroupShipClass(groupId)
sg := c.ShipGroup(groupId)
itemNumber := len(r.Ships)
r.Ships[itemNumber] = report.BattleReportGroup{
OwnerID: sg.OwnerID,
InBattle: inBattle,
Number: b.initialNumbers[groupId],
NumberLeft: sg.Number,
ClassName: shipClass.Name,
LoadType: sg.CargoString(),
LoadQuantity: sg.Load,
Drive: sg.TechLevel(game.TechDrive),
Weapons: sg.TechLevel(game.TechWeapons),
Shields: sg.TechLevel(game.TechShields),
Cargo: sg.TechLevel(game.TechCargo),
}
cacheShipClass[shipClass.ID] = itemNumber
return itemNumber
}
ship := func(groupId int) int {
shipClass := c.ShipGroupShipClass(groupId)
if v, ok := cacheShipClass[shipClass.ID]; ok {
return v
} else {
itemNumber := len(r.Ships)
r.Ships[itemNumber] = shipClass
cache[shipClass] = itemNumber
return addShipGroup(groupId, true)
}
}
race := func(groupId int) int {
race := c.ShipGroupOwnerRace(groupId)
if v, ok := cacheRaceName[race.ID]; ok {
return v
} else {
itemNumber := len(r.Races)
r.Races[itemNumber] = race.ID
cacheRaceName[race.ID] = itemNumber
return itemNumber
}
}
for i := range b.Protocol {
r.Protocol[i] = game.BattleActionReport{
Attacker: cacher(c.ShipGroupOwnerRace(b.Protocol[i].Attacker).Name, cacheRaceName),
AttackerShipClass: cacher(c.ShipGroupShipClass(b.Protocol[i].Attacker).Name, cacheShipClass),
Defender: cacher(c.ShipGroupOwnerRace(b.Protocol[i].Defenter).Name, cacheRaceName),
DefenderShipClass: cacher(c.ShipGroupShipClass(b.Protocol[i].Defenter).Name, cacheShipClass),
r.Protocol[i] = report.BattleActionReport{
Attacker: race(b.Protocol[i].Attacker),
AttackerShipClass: ship(b.Protocol[i].Attacker),
Defender: race(b.Protocol[i].Defender),
DefenderShipClass: ship(b.Protocol[i].Defender),
Destroyed: b.Protocol[i].Destroyed,
}
}
for name, index := range cacheRaceName {
r.Races[index] = name
}
for name, index := range cacheShipClass {
r.Ships[index] = name
for sgi, inBattle := range b.observerGroups {
if !inBattle {
addShipGroup(sgi, false)
}
}
return r
+5 -24
View File
@@ -3,36 +3,17 @@ package controller
import (
"github.com/google/uuid"
"github.com/iliadenisov/galaxy/internal/model/game"
"github.com/iliadenisov/galaxy/internal/model/report"
)
type BombingReport struct {
Planets []BombingPlanetReport `json:"planets"`
}
type BombingPlanetReport struct {
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 {
func (c *Cache) bombingReport(p *game.Planet, ri int, groups []int) report.BombingPlanetReport {
attackPower := 0.
for _, i := range groups {
sg := c.ShipGroup(i)
st := c.ShipGroupShipClass(i)
attackPower += sg.BombingPower(st)
}
r := &BombingPlanetReport{
r := &report.BombingPlanetReport{
ID: uuid.New(),
Planet: p.Name,
Number: p.Number,
@@ -51,8 +32,8 @@ func (c *Cache) bombingReport(p *game.Planet, ri int, groups []int) BombingPlane
return *r
}
func (c *Cache) ProduceBombings() []BombingPlanetReport {
report := make([]BombingPlanetReport, 0)
func (c *Cache) ProduceBombings() []report.BombingPlanetReport {
report := make([]report.BombingPlanetReport, 0)
for pn, enemies := range c.collectBombingGroups() {
p := c.MustPlanet(pn)
for ri, groups := range enemies {
+6 -2
View File
@@ -4,6 +4,7 @@ import (
"fmt"
"github.com/iliadenisov/galaxy/internal/model/game"
"github.com/iliadenisov/galaxy/internal/model/report"
"github.com/iliadenisov/galaxy/internal/repo"
)
@@ -26,8 +27,11 @@ type Repo interface {
// LoadStateSafe retrieves game current state without preliminary locking
LoadStateSafe() (*game.Game, error)
// SaveBattle stores
SaveBattle(t uint, b *game.BattleReport) error
// SaveBattle stores a new battle protocol and battle meta data for turn t
SaveBattle(t uint, b *report.BattleReport, m *game.BattleMeta) error
// SaveBombing stores all prodused bombings for turn t
SaveBombings(t uint, b []report.BombingPlanetReport) error
}
type Controller struct {
+1 -1
View File
@@ -41,7 +41,7 @@ func buildGameOnMap(races []string, m generator.Map) (*game.Game, error) {
}
g := &game.Game{
ID: gameID,
Age: 0,
Turn: 0,
Race: make([]game.Race, len(races)),
}
gameMap := &game.Map{
+1 -1
View File
@@ -35,7 +35,7 @@ func TestNewGame(t *testing.T) {
g, err := r.LoadState()
assert.NoError(t, err)
assert.Equal(t, gameID, g.ID)
assert.Equal(t, uint(0), g.Age)
assert.Equal(t, uint(0), g.Turn)
assert.Equal(t, players, len(g.Race))
for r := range g.Race {
+32 -6
View File
@@ -2,12 +2,16 @@ package controller
import (
// "github.com/iliadenisov/galaxy/internal/game/battle"
"maps"
"slices"
"github.com/google/uuid"
"github.com/iliadenisov/galaxy/internal/model/game"
)
func MakeTurn(c *Controller, r Repo, g *game.Game) error {
func MakeTurn(c *Controller, r Repo) error {
// Next turn
g.Age += 1
c.Cache.g.Turn += 1
// 01. Корабли, где это возможно, объединяются в группы.
c.Cache.TurnMergeEqualShipGroups()
@@ -28,7 +32,7 @@ func MakeTurn(c *Controller, r Repo, g *game.Game) error {
battles = append(battles, ProduceBattles(c.Cache)...)
// 07. Корабли бомбят вражеские планеты.
_ = c.Cache.ProduceBombings()
bombings := c.Cache.ProduceBombings()
// 08. На планетах строятся корабли.
// 09. Корабли, где это возможно, объединяются в группы.
@@ -49,12 +53,33 @@ func MakeTurn(c *Controller, r Repo, g *game.Game) error {
/*** Last steps ***/
// Store bombings
if len(bombings) > 0 {
if err := r.SaveBombings(c.Cache.g.Turn, bombings); err != nil {
return err
}
}
// Store battles
if len(battles) > 0 {
battleMeta := make([]game.BattleMeta, len(battles))
for i := range battles {
// TODO: add In_Battle / Out_Battle participants?
br := TransformBattle(c.Cache, battles[i])
if err := r.SaveBattle(g.Age, br); err != nil {
b := battles[i]
observers := make(map[uuid.UUID]bool)
for sgi := range b.observerGroups {
observers[c.Cache.ShipGroup(sgi).OwnerID] = true
}
battleMeta[i] = game.BattleMeta{
Turn: c.Cache.g.Turn,
Planet: b.Planet,
BattleID: b.ID,
ObserverIDs: slices.Collect(maps.Keys(observers)),
}
report := TransformBattle(c.Cache, b)
if err := r.SaveBattle(c.Cache.g.Turn, report, &battleMeta[i]); err != nil {
return err
}
}
@@ -68,5 +93,6 @@ func MakeTurn(c *Controller, r Repo, g *game.Game) error {
// TODO: Store individual reports
_ = winners
// [ ] monitor memory consumption at this point?
return nil
}
+1
View File
@@ -105,6 +105,7 @@ func (c *Cache) ShipGroupOwnerRace(groupIndex int) *game.Race {
func (c *Cache) ShipGroupNumber(i int, n uint) {
c.validateShipGroupIndex(i)
c.g.ShipGroups[i].Number = n
// FIXME: cargo load must be decreased proportionally
}
func (c *Cache) DeleteShipGroup(i int) {
+1 -1
View File
@@ -7,7 +7,7 @@ import (
func MakeTurn(configure func(*controller.Param), race string, number int, name string) (err error) {
control(configure, func(c *controller.Controller) {
c.ExecuteGame(func(r controller.Repo, g *game.Game) { controller.MakeTurn(c, r, g) })
c.ExecuteGame(func(r controller.Repo, g *game.Game) { controller.MakeTurn(c, r) })
})
return
}
-32
View File
@@ -1,32 +0,0 @@
package game
import (
"encoding/json"
"github.com/google/uuid"
)
type BattleReport struct {
ID uuid.UUID `json:"id"`
Planet uint `json:"planet"`
PlanetName string `json:"planet_name"`
Races map[int]string `json:"races"`
Ships map[int]string `json:"ships"`
Protocol []BattleActionReport `json:"protocol"`
}
type BattleActionReport struct {
Attacker int `json:"r1"`
AttackerShipClass int `json:"s1"`
Defender int `json:"r2"`
DefenderShipClass int `json:"s2"`
Destroyed bool `json:"d"`
}
func (b BattleReport) MarshalBinary() (data []byte, err error) {
return json.Marshal(&b)
}
func (b *BattleReport) UnmarshalBinary(data []byte) error {
return json.Unmarshal(data, b)
}
+22 -1
View File
@@ -6,6 +6,7 @@ import (
"maps"
"github.com/google/uuid"
"github.com/iliadenisov/galaxy/internal/model/report"
)
type TechSet map[Tech]float64
@@ -26,7 +27,7 @@ func (ts TechSet) Set(t Tech, v float64) TechSet {
type Game struct {
ID uuid.UUID `json:"id"`
Age uint `json:"turn"` // Game's turn number
Turn uint `json:"turn"`
Map Map `json:"map"`
Race []Race `json:"races"`
Votes float64 `json:"votes"`
@@ -34,6 +35,18 @@ type Game struct {
Fleets []Fleet `json:"fleet,omitempty"`
}
type GameMeta struct {
Battles []BattleMeta `json:"battles,omitempty"`
Bombings []report.BombingPlanetReport `json:"bombings,omitempty"`
}
type BattleMeta struct {
Turn uint `json:"turn"`
Planet uint `json:"planet"`
BattleID uuid.UUID `json:"battle_id"`
ObserverIDs []uuid.UUID `json:"observer_ids"`
}
// TODO: remove if not needed
func (g Game) RaceVotes(raceID uuid.UUID) float64 {
var result float64
@@ -52,3 +65,11 @@ func (g Game) MarshalBinary() (data []byte, err error) {
func (g *Game) UnmarshalBinary(data []byte) error {
return json.Unmarshal(data, g)
}
func (b GameMeta) MarshalBinary() (data []byte, err error) {
return json.Marshal(&b)
}
func (b *GameMeta) UnmarshalBinary(data []byte) error {
return json.Unmarshal(data, b)
}
+7
View File
@@ -233,3 +233,10 @@ func (sg ShipGroup) BombingPower(st *ShipType) float64 {
float64(st.Armament) *
float64(sg.Number)
}
func (sg ShipGroup) CargoString() string {
if sg.CargoType == nil {
return "-"
}
return sg.CargoType.String()
}
+47
View File
@@ -0,0 +1,47 @@
package report
import (
"encoding/json"
"github.com/google/uuid"
)
type BattleReport struct {
ID uuid.UUID `json:"id"`
Turn uint `json:"turn"`
Planet uint `json:"planet"`
PlanetName string `json:"planet_name"`
Races map[int]uuid.UUID `json:"races"`
Ships map[int]BattleReportGroup `json:"ships"`
Protocol []BattleActionReport `json:"protocol"`
}
type BattleReportGroup struct {
OwnerID uuid.UUID `json:"ownerId"`
InBattle bool `json:"inBattle"`
Number uint `json:"num"`
NumberLeft uint `json:"numLeft"`
ClassName string `json:"className"`
LoadType string `json:"loadType"`
LoadQuantity float64 `json:"loadQuantity"`
Drive float64 `json:"drive"`
Weapons float64 `json:"wwapons"`
Shields float64 `json:"shields"`
Cargo float64 `json:"cargo"`
}
type BattleActionReport struct {
Attacker int `json:"a"`
AttackerShipClass int `json:"sa"`
Defender int `json:"d"`
DefenderShipClass int `json:"sd"`
Destroyed bool `json:"x"`
}
func (b BattleReport) MarshalBinary() (data []byte, err error) {
return json.Marshal(&b)
}
func (b *BattleReport) UnmarshalBinary(data []byte) error {
return json.Unmarshal(data, b)
}
+19
View File
@@ -0,0 +1,19 @@
package report
import "github.com/google/uuid"
type BombingPlanetReport struct {
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"`
}
+63 -9
View File
@@ -1,20 +1,25 @@
package repo
/*
/state.json
TODO: only state will be saved once (current, turn); meta and bombings are saved at turn generation and saved twice
/state.json
/0001/state.json
/0001/race/{UUID}/report.json
/0001/meta.json
/0001/bombing.json
/0001/battle/{UUID}.json
/0001/report/{UUID}.json
*/
import (
"fmt"
"github.com/iliadenisov/galaxy/internal/model/game"
"github.com/iliadenisov/galaxy/internal/model/report"
)
const (
statePath = "state.json"
metaPath = "meta.json"
)
func (r *repo) SaveTurn(t uint, g *game.Game) error {
@@ -65,7 +70,7 @@ func (r *repo) LoadStateSafe() (*game.Game, error) {
}
func loadState(s Storage, locked bool) (*game.Game, error) {
var g *game.Game = new(game.Game)
var result *game.Game = new(game.Game)
path := statePath
exist, err := s.Exists(path)
if err != nil {
@@ -75,22 +80,62 @@ func loadState(s Storage, locked bool) (*game.Game, error) {
return nil, NewGameNotInitializedError()
}
if locked {
if err := s.Read(path, g); err != nil {
if err := s.Read(path, result); err != nil {
return nil, NewStorageError(err)
}
} else {
if err := s.ReadSafe(path, g); err != nil {
if err := s.ReadSafe(path, result); err != nil {
return nil, NewStorageError(err)
}
}
return g, nil
return result, nil
}
func (r *repo) SaveBattle(t uint, b *game.BattleReport) error {
return saveBattle(r.s, t, b)
func loadMeta(s Storage) (*game.GameMeta, error) {
var result *game.GameMeta = new(game.GameMeta)
path := metaPath
exist, err := s.Exists(path)
if err != nil {
return nil, NewStorageError(err)
}
if !exist {
return result, nil
}
// TODO: create separate Read func for meta ops
if err := s.ReadSafe(path, result); err != nil {
return nil, NewStorageError(err)
}
return result, nil
}
func saveBattle(s Storage, t uint, b *game.BattleReport) error {
func saveMeta(s Storage, t uint, gm *game.GameMeta) error {
// save turn's meta
path := fmt.Sprintf("%s/%s", turnDir(t), metaPath)
if err := s.Write(path, gm); err != nil {
return NewStorageError(err)
}
// also save as latest meta
path = metaPath
if err := s.Write(path, gm); err != nil {
return NewStorageError(err)
}
return nil
}
func (r *repo) SaveBattle(t uint, b *report.BattleReport, m *game.BattleMeta) error {
meta, err := loadMeta(r.s)
if err != nil {
return err
}
err = saveBattle(r.s, t, b)
if err != nil {
return err
}
meta.Battles = append(meta.Battles, *m)
return saveMeta(r.s, t, meta)
}
func saveBattle(s Storage, t uint, b *report.BattleReport) error {
path := fmt.Sprintf("%s/battle/%s.json", turnDir(t), b.ID)
exist, err := s.Exists(path)
if err != nil {
@@ -105,6 +150,15 @@ func saveBattle(s Storage, t uint, b *game.BattleReport) error {
return nil
}
func (r *repo) SaveBombings(t uint, b []report.BombingPlanetReport) error {
meta, err := loadMeta(r.s)
if err != nil {
return err
}
meta.Bombings = b
return saveMeta(r.s, t, meta)
}
func turnDir(t uint) string {
return fmt.Sprintf("%04d", t)
}
+1 -1
View File
@@ -17,7 +17,7 @@ func StatusHandler(c *gin.Context, config controller.Config) {
}
c.JSON(http.StatusOK, rest.Status{
Turn: g.Age,
Turn: g.Turn,
Players: len(g.Race),
})
}