diff --git a/internal/controller/controller.go b/internal/controller/controller.go index 95cdb13..5a31de1 100644 --- a/internal/controller/controller.go +++ b/internal/controller/controller.go @@ -20,10 +20,14 @@ type Repo interface { // SaveState stores current game state updated between turns SaveState(*game.Game) error - // LoadState retrieves game current state + // LoadState retrieves game current state with required lock acquisition LoadState() (*game.Game, error) + // LoadStateSafe retrieves game current state without preliminary locking LoadStateSafe() (*game.Game, error) + + // SaveBattle stores + SaveBattle(t uint, b *game.BattleReport) error } type Controller struct { diff --git a/internal/game/turn/turn.go b/internal/game/turn/turn.go index 31c2dfd..36aaddf 100644 --- a/internal/game/turn/turn.go +++ b/internal/game/turn/turn.go @@ -1,13 +1,76 @@ package turn -import "github.com/iliadenisov/galaxy/internal/model/game" +import ( + "fmt" + + "github.com/iliadenisov/galaxy/internal/controller" + e "github.com/iliadenisov/galaxy/internal/error" + "github.com/iliadenisov/galaxy/internal/model/game" +) + +func MakeTurn(r controller.Repo, g *game.Game) error { + // Next turn + g.Age += 1 -func MakeTurn(g *game.Game) error { // 01. Корабли, где это возможно, объединяются в группы. game.JoinEqualGroups(g) // 02. Враждующие корабли вступают в схватку. - game.ProduceBattles(g) + battles := game.ProduceBattles(g) + // Internal control: after battles there are can't be groups with no ships left + for i := range g.ShipGroups { + if g.ShipGroups[i].Number == 0 { + return e.NewGameStateError("") + } + } + + // Last step: storing battles + if len(battles) > 0 { + for i := range battles { + br := TransformBattle(g, battles[i]) + if err := r.SaveBattle(g.Age, br); err != nil { + return err + } + } + } return nil } + +func TransformBattle(g *game.Game, b *game.Battle) *game.BattleReport { + p, ok := game.PlanetByNum(g, b.Planet) + if !ok { + panic(fmt.Sprintf("TransformBattle: no planet with number #%d", b.Planet)) + } + r := &game.BattleReport{ + ID: b.ID, + Planet: b.Planet, + PlanetName: p.Name, + Races: make(map[int]string), + Ships: make(map[int]string), + Protocol: make([]game.BattleActionReport, len(b.Protocol)), + } + + cacheShipClass := make(map[string]int) + + shipClass := func(shipClass string) int { + if v, ok := cacheShipClass[shipClass]; ok { + return v + } else { + l := len(r.Ships) + r.Ships[l] = shipClass + cacheShipClass[shipClass] = l + return l + } + } + + for i := range b.Protocol { + r.Protocol[i] = game.BattleActionReport{ + AttackerShipClass: shipClass(b.ShipClassName(b.Protocol[i].Attacker)), + DefenderShipClass: shipClass(b.ShipClassName(b.Protocol[i].Defenter)), + Destroyed: b.Protocol[i].Destroyed, + } + } + + return r +} diff --git a/internal/model/game/battle.go b/internal/model/game/battle.go index 2de2b7b..fcb9ff4 100644 --- a/internal/model/game/battle.go +++ b/internal/model/game/battle.go @@ -1,6 +1,7 @@ package game import ( + "encoding/json" "fmt" "maps" "math" @@ -11,29 +12,67 @@ import ( ) type Battle struct { - Planet uint - BattleReport BattleReport + ID uuid.UUID + Planet uint + // True = In_Battle, False = Out_Battle + observerGroups map[int]bool + Protocol []BattleAction shipAmmo map[int]uint + shipName map[int]string attacker map[int]map[int]float64 // a group able to attack and destroy an opponent with some probability > 0 } -type BattleReport struct { - BattleAction []BattleAction -} - type BattleAction struct { Attacker int Defenter int Destroyed bool } -func CollectPlanetGroups(g *Game, cacheShipGroupRaceID map[int]int, cacheShipClass map[int]*ShipType) map[uint][]int { - planetGroup := make(map[uint][]int) +func (b Battle) ShipClassName(groupIndex int) string { + if v, ok := b.shipName[groupIndex]; ok { + return v + } else { + panic(fmt.Sprintf("Battle.ShipClassName: no name stored for groupIndex=%d", groupIndex)) + } +} + +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"` +} + +type ShipClassBattle struct { + ClassName string `json:"class"` + Tech TechSet `json:"tech"` + Number uint `json:"number"` + CargoType *CargoType `json:"loadType,omitempty"` + Quantity float64 `json:"quantity"` + Left uint `json:"left"` +} + +func CollectPlanetGroups(g *Game, cacheShipGroupRaceID map[int]int, cacheShipClass map[int]*ShipType) map[uint]map[int]bool { + planetGroup := make(map[uint]map[int]bool) for groupIndex := range g.ShipGroups { - if g.ShipGroups[groupIndex].State() == StateInOrbit { + state := g.ShipGroups[groupIndex].State() + if state == StateInOrbit || state == StateUpgrade { planetNumber := g.ShipGroups[groupIndex].Destination - planetGroup[planetNumber] = append(planetGroup[planetNumber], groupIndex) + if _, ok := planetGroup[planetNumber]; !ok { + planetGroup[planetNumber] = make(map[int]bool) + } + planetGroup[planetNumber][groupIndex] = false if _, ok := cacheShipGroupRaceID[groupIndex]; !ok { cacheShipGroupRaceID[groupIndex] = RaceIndex(g, g.ShipGroups[groupIndex].OwnerID) @@ -57,6 +96,10 @@ func CollectPlanetGroups(g *Game, cacheShipGroupRaceID map[int]int, cacheShipCla return planetGroup } +func FilterBattleGroups(g *Game, groups map[int]bool) []int { + return slices.DeleteFunc(slices.Collect(maps.Keys(groups)), func(groupIndex int) bool { return g.ShipGroups[groupIndex].State() != StateInOrbit }) +} + func CacheRelations(g *Game, cacheShipGroupRaceID map[int]int) map[int]map[int]Relation { cache := make(map[int]map[int]Relation) ri := make(map[int]bool) @@ -132,8 +175,8 @@ func ProduceBattles(g *Game) []*Battle { clear(cacheProbability) }() - planetGroup := CollectPlanetGroups(g, cacheShipGroupRaceID, cacheShipClass) - if len(planetGroup) == 0 { + planetGroups := CollectPlanetGroups(g, cacheShipGroupRaceID, cacheShipClass) + if len(planetGroups) == 0 { return nil } @@ -144,35 +187,42 @@ func ProduceBattles(g *Game) []*Battle { result := make([]*Battle, 0) - for pl, groups := range planetGroup { + for pl, observerGroups := range planetGroups { + battleGroups := FilterBattleGroups(g, observerGroups) b := &Battle{ - Planet: pl, - attacker: make(map[int]map[int]float64), - shipAmmo: make(map[int]uint), + Planet: pl, + observerGroups: observerGroups, + attacker: make(map[int]map[int]float64), + shipAmmo: make(map[int]uint), + shipName: make(map[int]string), } - for i := range groups { - attIdx := groups[i] + for i := range battleGroups { + attIdx := battleGroups[i] // Ships with no Ammo will never attack somebody if cacheShipClass[attIdx].Armament == 0 { continue } - // TODO: remove slices.Clone? - opponents := slices.DeleteFunc(slices.Clone(groups), func(defIdx int) bool { + opponents := slices.DeleteFunc(slices.Clone(battleGroups), func(defIdx int) bool { return FilterBattleOpponents(g, attIdx, defIdx, cacheShipGroupRaceID, cacheRelation, cacheShipClass, cacheProbability) }) if len(opponents) > 0 { b.shipAmmo[attIdx] = cacheShipClass[attIdx].Armament + b.shipName[attIdx] = cacheShipClass[attIdx].Name + b.observerGroups[attIdx] = true for _, defIdx := range opponents { b.attacker[attIdx][defIdx] = cacheProbability[attIdx][defIdx] + b.shipName[defIdx] = cacheShipClass[defIdx].Name + b.observerGroups[defIdx] = true } } } if len(b.attacker) > 0 { SingleBattle(g, b) + b.ID = uuid.New() result = append(result, b) } @@ -203,7 +253,7 @@ func SingleBattle(g *Game, b *Battle) { panic("SingleBattle: probability unexpected: value <= 0") } - b.BattleReport.BattleAction = append(b.BattleReport.BattleAction, BattleAction{ + b.Protocol = append(b.Protocol, BattleAction{ Attacker: attIdx, Defenter: defIdx, Destroyed: destroyed, @@ -220,6 +270,8 @@ func SingleBattle(g *Game, b *Battle) { delete(b.attacker, attIdx) // Remove attacker if he lost all opponents } } + // FIXME: удалять ShipGroups после генерирования пользовательского отчёта + // g.ShipGroups = append(g.ShipGroups[:defIdx], g.ShipGroups[defIdx+1:]...) } if len(b.attacker) == 0 { break @@ -245,3 +297,11 @@ func DestructionProbability(attWeapons, attWeaponsTech, defShields, defShiledsTe func EffectiveDefence(defShields, defShiledsTech, defFullMass float64) float64 { return defShields * defShiledsTech / math.Pow(defFullMass, 1./3.) * math.Pow(30., 1./3.) } + +func (b BattleReport) MarshalBinary() (data []byte, err error) { + return json.Marshal(&b) +} + +func (b *BattleReport) UnmarshalBinary(data []byte) error { + return json.Unmarshal(data, b) +} diff --git a/internal/model/game/battle_test.go b/internal/model/game/battle_test.go index d40cead..f92d788 100644 --- a/internal/model/game/battle_test.go +++ b/internal/model/game/battle_test.go @@ -1,6 +1,7 @@ package game_test import ( + "slices" "testing" "github.com/iliadenisov/galaxy/internal/model/game" @@ -161,3 +162,20 @@ func TestFilterBattleOpponents(t *testing.T) { assert.True(t, game.FilterBattleOpponents(g, 1, 3, cacheShipGroupRaceID, cacheRelation, cacheShipClass, cacheProbability)) assert.NotContains(t, cacheProbability[1], 3) } + +func TestSlicesDeleteFunc(t *testing.T) { + type Container struct { + S []int + } + c := Container{S: []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}} + r1 := slices.DeleteFunc(c.S, func(e int) bool { return e%2 != 0 }) + assert.Len(t, r1, 5) + for i := range r1 { + assert.Equal(t, i*2, r1[i], "elem #%d", i) + assert.Equal(t, i*2, c.S[i], "elem #%d", i) + } + assert.Len(t, c.S, 10) + for i := len(r1); i < len(c.S); i++ { + assert.Equal(t, 0, c.S[i], "elem #%d", i) + } +} diff --git a/internal/repo/game.go b/internal/repo/game.go index 48cedd6..0b74119 100644 --- a/internal/repo/game.go +++ b/internal/repo/game.go @@ -2,10 +2,9 @@ package repo /* /state.json - /000/state.json - /000/race/{UUID}/order/001.json - /000/race/{UUID}/report.json - /000/battle/{planet_UUID} + /0001/state.json + /0001/race/{UUID}/report.json + /0001/battle/{UUID}.json */ import ( @@ -23,7 +22,7 @@ func (r *repo) SaveTurn(t uint, g *game.Game) error { } func saveTurn(s Storage, t uint, g *game.Game) error { - path := fmt.Sprintf("%03d/state.json", t) + path := fmt.Sprintf("%s/state.json", turnDir(t)) exist, err := s.Exists(path) if err != nil { return NewStorageError(err) @@ -86,3 +85,26 @@ func loadState(s Storage, locked bool) (*game.Game, error) { } return g, nil } + +func (r *repo) SaveBattle(t uint, b *game.BattleReport) error { + return saveBattle(r.s, t, b) +} + +func saveBattle(s Storage, t uint, b *game.BattleReport) error { + path := fmt.Sprintf("%s/battle/%s.json", turnDir(t), b.ID) + exist, err := s.Exists(path) + if err != nil { + return NewStorageError(err) + } + if exist { + return NewStateError(fmt.Sprintf("battle %s for turn %d already has been saved", b.ID, t)) + } + if err := s.Write(path, b); err != nil { + return NewStorageError(err) + } + return nil +} + +func turnDir(t uint) string { + return fmt.Sprintf("%04d", t) +}