wip: turn generation
This commit is contained in:
@@ -41,6 +41,7 @@ func buildGameOnMap(races []string, m generator.Map) (*game.Game, error) {
|
|||||||
}
|
}
|
||||||
g := &game.Game{
|
g := &game.Game{
|
||||||
ID: gameID,
|
ID: gameID,
|
||||||
|
Age: 0,
|
||||||
Race: make([]game.Race, len(races)),
|
Race: make([]game.Race, len(races)),
|
||||||
}
|
}
|
||||||
gameMap := &game.Map{
|
gameMap := &game.Map{
|
||||||
@@ -98,8 +99,8 @@ func buildGameOnMap(races []string, m generator.Map) (*game.Game, error) {
|
|||||||
}
|
}
|
||||||
for i := range g.Race {
|
for i := range g.Race {
|
||||||
rel := slices.Clone(relations)
|
rel := slices.Clone(relations)
|
||||||
ri := slices.IndexFunc(rel, func(a game.RaceRelation) bool { return a.RaceID == g.Race[i].ID })
|
selfIdx := 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:]...)
|
g.Race[i].Relations = append(rel[:selfIdx], rel[selfIdx+1:]...)
|
||||||
}
|
}
|
||||||
|
|
||||||
for i := range m.FreePlanets {
|
for i := range m.FreePlanets {
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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.)
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
@@ -234,6 +234,14 @@ func (sg ShipGroup) BombingPower(st *ShipType) float64 {
|
|||||||
return number.Fixed3(result)
|
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 {
|
func (g *Game) JoinEqualGroups(raceName string) error {
|
||||||
ri, err := g.raceIndex(raceName)
|
ri, err := g.raceIndex(raceName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -94,6 +94,12 @@ func (g *Game) GiveVotes(race, recipient string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (g Game) relationInternal(ri, other int) (RaceRelation, 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 })
|
rel := slices.IndexFunc(g.Race[ri].Relations, func(r RaceRelation) bool { return r.RaceID == g.Race[other].ID })
|
||||||
if rel < 0 {
|
if rel < 0 {
|
||||||
return RaceRelation{}, e.NewGameStateError("Relation: opponent not found")
|
return RaceRelation{}, e.NewGameStateError("Relation: opponent not found")
|
||||||
|
|||||||
Reference in New Issue
Block a user