diff --git a/internal/controller/generate.go b/internal/controller/generate.go index 9d51330..a53974c 100644 --- a/internal/controller/generate.go +++ b/internal/controller/generate.go @@ -41,6 +41,7 @@ func buildGameOnMap(races []string, m generator.Map) (*game.Game, error) { } g := &game.Game{ ID: gameID, + Age: 0, Race: make([]game.Race, len(races)), } gameMap := &game.Map{ @@ -98,8 +99,8 @@ func buildGameOnMap(races []string, m generator.Map) (*game.Game, error) { } for i := range g.Race { rel := slices.Clone(relations) - ri := slices.IndexFunc(rel, func(a game.RaceRelation) bool { return a.RaceID == g.Race[i].ID }) - g.Race[i].Relations = append(rel[:ri], rel[ri+1:]...) + selfIdx := slices.IndexFunc(rel, func(a game.RaceRelation) bool { return a.RaceID == g.Race[i].ID }) + g.Race[i].Relations = append(rel[:selfIdx], rel[selfIdx+1:]...) } for i := range m.FreePlanets { diff --git a/internal/game/turn/turn.go b/internal/game/turn/turn.go new file mode 100644 index 0000000..31c2dfd --- /dev/null +++ b/internal/game/turn/turn.go @@ -0,0 +1,13 @@ +package turn + +import "github.com/iliadenisov/galaxy/internal/model/game" + +func MakeTurn(g *game.Game) error { + // 01. Корабли, где это возможно, объединяются в группы. + game.JoinEqualGroups(g) + + // 02. Враждующие корабли вступают в схватку. + game.ProduceBattles(g) + + return nil +} diff --git a/internal/model/game/battle.go b/internal/model/game/battle.go new file mode 100644 index 0000000..4d1806f --- /dev/null +++ b/internal/model/game/battle.go @@ -0,0 +1,127 @@ +package game + +import ( + "fmt" + "math" + "math/rand/v2" + "slices" +) + +type Battle struct { + Planet uint + Groups []int // ShipGroup indexes + BattleReport BattleReport +} + +type BattleReport struct { + BattleAction []BattleAction +} + +type BattleAction struct { + Attacker int + Defenter int + Destroyed bool +} + +type BattleOpponent struct { + RaceIndex int + ShipGroupIndex int + ShipType ShipType +} + +func ProduceBattles(g *Game) error { + + battleOnPlanet := Battle{} + _ = battleOnPlanet + + return nil +} + +func SingleBattle(g *Game, b Battle) { + attacker := SelectAttackShip(g, b.Groups) + for shots := range attacker.ShipType.Armament { + // groupsCopy := slices.Clone(b.Groups) + // groupsWithoutAttacker := append(groupsCopy[:attackerIdx], groupsCopy[attackerIdx+1:]...) + _ = SelectDefendShip(g, slices.Clone(b.Groups), attacker) + + _ = shots + } +} + +func SelectAttackShip(g *Game, battleGroups []int) BattleOpponent { + sgi := rand.IntN(len(battleGroups)) + if sgi > len(g.ShipGroups)-1 { + panic("SelectAttackShip: battleGroups is bigger than game's ship groups") + } + sg := g.ShipGroups[sgi] + ri := slices.IndexFunc(g.Race, func(r Race) bool { return r.ID == sg.OwnerID }) + if ri < 0 { + panic(fmt.Sprintf("SelectAttackShip: ship group #%v owner race not found by ID=%v", sg.Index, sg.OwnerID)) + } + st, ok := ShipClass(g, ri, sg.TypeID) + if !ok { + panic(fmt.Sprintf("SelectAttackShip: ship class not found for race=%q group=%v", g.Race[ri].Name, sg.Index)) + } + if st.Weapons == 0 || st.Armament == 0 { + panic(fmt.Sprintf("SelectAttackShip: ship_class=%q of race=%q has no weapons for attack", st.Name, g.Race[ri].Name)) + } + return BattleOpponent{ + RaceIndex: ri, + ShipGroupIndex: sgi, + ShipType: st, + } +} + +func SelectDefendShip(g *Game, battleGroups []int, attacker BattleOpponent) BattleOpponent { + enemyGroups := FilterAttackingPretendent(g, attacker.RaceIndex, battleGroups) + sgi := rand.IntN(len(enemyGroups)) + if sgi > len(g.ShipGroups)-1 { + panic("SelectDefendShip: battleGroups is bigger than game's ship groups") + } + sg := g.ShipGroups[sgi] + ri := slices.IndexFunc(g.Race, func(r Race) bool { return r.ID == sg.OwnerID }) + if ri < 0 { + panic(fmt.Sprintf("SelectDefendShip: ship group #%v owner race not found by ID=%v", sg.Index, sg.OwnerID)) + } + st, ok := ShipClass(g, ri, sg.TypeID) + if !ok { + panic(fmt.Sprintf("SelectDefendShip: ship class not found for race=%q group=%v", g.Race[ri].Name, sg.Index)) + } + return BattleOpponent{ + RaceIndex: ri, + ShipGroupIndex: sgi, + ShipType: st, + } +} + +// attackerIdx - attacker race index +func FilterAttackingPretendent(g *Game, attackerIdx int, battleGroups []int) []int { + result := make([]int, 0) + for sgi := range battleGroups { + sg := g.ShipGroups[sgi] + enemyIdx := slices.IndexFunc(g.Race, func(r Race) bool { return r.ID == sg.OwnerID }) + if enemyIdx < 0 { + panic(fmt.Sprintf("FilterAttackingPretendent: ship group #%v owner race not found by ID=%v", sg.Index, sg.OwnerID)) + } + rel, err := g.relationInternal(attackerIdx, enemyIdx) + if err != nil { + panic(err) + } + // attacker race will be in peace with itself, so attacker ships will be filtered out as well + if rel.Relation == RelationPeace { + continue + } + result = append(result, sgi) + } + return result +} + +func DestroyProbability(attWeapons, attWeaponsTech, defShields, defShiledsTech, defFullMass float64) float64 { + effAttack := attWeapons * attWeaponsTech + effDefence := EffectiveDefence(defShields, defShiledsTech, defFullMass) + return (math.Log10(effAttack/effDefence)/math.Log10(4) + 1) / 2 +} + +func EffectiveDefence(defShields, defShiledsTech, defFullMass float64) float64 { + return defShields * defShiledsTech / math.Pow(defFullMass, 1./3.) * math.Pow(30., 1./3.) +} diff --git a/internal/model/game/battle_test.go b/internal/model/game/battle_test.go new file mode 100644 index 0000000..90c83c7 --- /dev/null +++ b/internal/model/game/battle_test.go @@ -0,0 +1,66 @@ +package game_test + +import ( + "testing" + + "github.com/iliadenisov/galaxy/internal/model/game" + "github.com/stretchr/testify/assert" +) + +var ( + attacker = game.ShipType{ + ShipTypeReport: game.ShipTypeReport{ + Name: "Attacker", + Drive: 8, + Armament: 1, + Weapons: 8, + Shields: 8, + Cargo: 0, + }, + } + defender = game.ShipType{ + ShipTypeReport: game.ShipTypeReport{ + Name: "Defender", + Drive: 1, + Armament: 1, + Weapons: 1, + Shields: 1, + Cargo: 0, + }, + } + ship = game.ShipType{ + ShipTypeReport: game.ShipTypeReport{ + Name: "Ship", + Drive: 10, + Armament: 1, + Weapons: 10, + Shields: 10, + Cargo: 0, + }, + } +) + +func TestDestroyProbability(t *testing.T) { + probability := game.DestroyProbability(ship.Weapons, 1, ship.Shields, 1, ship.EmptyMass()) + assert.Equal(t, .5, probability) + + unsinkableShip := ship + unsinkableShip.Shields = 55 + probability = game.DestroyProbability(ship.Weapons, 1, unsinkableShip.Shields, 1, unsinkableShip.EmptyMass()) + assert.LessOrEqual(t, probability, 0.) + + disruptiveShip := ship + disruptiveShip.Weapons = 40 + probability = game.DestroyProbability(disruptiveShip.Weapons, 1, ship.Shields, 1, ship.EmptyMass()) + assert.GreaterOrEqual(t, probability, 1.) +} + +func TestEffectiveDefence(t *testing.T) { + assert.Equal(t, 10., game.EffectiveDefence(ship.Shields, 1, ship.EmptyMass())) + + attackerEffectiveDefence := game.EffectiveDefence(attacker.Shields, 1, attacker.EmptyMass()) + defenderEffectiveDefence := game.EffectiveDefence(defender.Shields, 1, defender.EmptyMass()) + + // attacker's effective shields must be 'just' 4 times greater than defender's + assert.InDelta(t, defenderEffectiveDefence*4, attackerEffectiveDefence, 0) +} diff --git a/internal/model/game/group.go b/internal/model/game/group.go index a9a7d74..a5693bc 100644 --- a/internal/model/game/group.go +++ b/internal/model/game/group.go @@ -234,6 +234,14 @@ func (sg ShipGroup) BombingPower(st *ShipType) float64 { return number.Fixed3(result) } +// JoinEqualGroups iterates over all races and joins their respective equal ship groups. +// Used in turn production. +func JoinEqualGroups(g *Game) { + for i := range g.Race { + g.joinEqualGroupsInternal(i) + } +} + func (g *Game) JoinEqualGroups(raceName string) error { ri, err := g.raceIndex(raceName) if err != nil { diff --git a/internal/model/game/race.go b/internal/model/game/race.go index 6b8e50d..5e85355 100644 --- a/internal/model/game/race.go +++ b/internal/model/game/race.go @@ -94,6 +94,12 @@ func (g *Game) GiveVotes(race, recipient string) error { } func (g Game) relationInternal(ri, other int) (RaceRelation, error) { + if ri == other { + return RaceRelation{ + RaceID: g.Race[ri].ID, + Relation: RelationPeace, + }, nil + } 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")