Files
galaxy-game/game/internal/controller/controller_export_test.go
T
Ilia Denisov cc67364113
Tests · Go / test (push) Successful in 2m2s
Tests · Integration / integration (pull_request) Successful in 1m48s
Tests · Go / test (pull_request) Successful in 2m0s
fix(game): resolve battles ship by ship, matching the combat rules
The battle engine diverged from the documented combat model
(game/rules.txt "Сражения") in three ways:

- the destruction roll was inverted (rand >= p), so a near-certain hit
  destroyed its target only ~(1-p) of the time;
- a whole group fired as a single ship (Armament shots per round)
  regardless of its ship count, so fleet size never affected offence;
- the defending mass used the whole group's full mass instead of one
  target ship's, weakening grouped ships' shields by ~Number^(1/3).

SingleBattle now resolves ship by ship: every living ship fires once per
round in random order across all groups, each gun targets a random enemy
ship (weighted by group size), and the destruction roll matches the
documented probability. FilterBattleOpponents evaluates per-ship mass.

Also fixes opponent-map initialisation in ProduceBattles that kept only
an attacker's last opponent.

The rules already describe this model, so no documentation change is
needed. Tests: per-ship one-sided wipe, destruction-roll direction, and
the updated per-ship-mass probability expectation.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-30 23:57:27 +02:00

156 lines
3.5 KiB
Go

package controller
import (
"iter"
e "galaxy/error"
"galaxy/game/internal/model/game"
"github.com/google/uuid"
)
func (c *Cache) CreateShips(ri int, shipTypeName string, planetNumber uint, quantity int) error {
class, _, ok := c.ShipClass(ri, shipTypeName)
if !ok {
return e.NewEntityNotExistsError("ship class q", shipTypeName)
}
p, ok := c.Planet(planetNumber)
if !ok {
return e.NewEntityNotExistsError("planet #%d", planetNumber)
}
if !p.OwnedBy(c.g.Race[ri].ID) {
return e.NewEntityNotOwnedError("planet #%d", planetNumber)
}
c.unsafeCreateShips(ri, class.ID, p.Number, uint(quantity))
return nil
}
func (c *Cache) AddRace(n string) (int, uuid.UUID) {
id := uuid.New()
r := &game.Race{
ID: id,
VoteFor: id,
Name: n,
Tech: game.NewTechSet(),
Relations: make([]game.RaceRelation, len(c.g.Race)),
}
c.g.Race = append(c.g.Race, *r)
for i := range c.listRaceIdx() {
if c.g.Race[i].ID != id {
c.g.Race[i].Relations = append(c.g.Race[i].Relations, game.RaceRelation{RaceID: id, Relation: game.RelationPeace})
continue
}
for j := range c.g.Race[i].Relations {
c.g.Race[i].Relations[j].RaceID = c.g.Race[j].ID
c.g.Race[i].Relations[j].Relation = game.RelationPeace
}
}
return len(c.g.Race) - 1, id
}
func (c *Cache) Race(i int) *game.Race {
c.validateRaceIndex(i)
return &c.g.Race[i]
}
func (c *Cache) RaceShipGroups(ri int) iter.Seq[*game.ShipGroup] {
return c.listShipGroups(ri)
}
func (c *Cache) RaceScience(ri int) []game.Science {
return c.raceScience(ri)
}
func (c *Cache) ListFleets(ri int) iter.Seq[*game.Fleet] {
return c.listFleets(ri)
}
func (c *Cache) MustFleetID(ri int, name string) uuid.UUID {
for f := range c.listFleets(ri) {
if f.Name == name {
return f.ID
}
}
panic("fleet not found")
}
func (c *Cache) MustShipClass(ri int, name string) *game.ShipType {
st, _, ok := c.ShipClass(ri, name)
if !ok {
panic("ship class not found")
}
return st
}
func (c *Cache) PutPopulation(pn uint, v float64) {
c.putPopulation(pn, v)
}
func (c *Cache) PutColonists(pn uint, v float64) {
c.putColonists(pn, v)
}
func (c *Cache) PutMaterial(pn uint, v float64) {
c.putMaterial(pn, v)
}
func (c *Cache) RaceTechLevel(ri int, t game.Tech, v float64) {
c.raceTechLevel(ri, t, v)
}
func (c *Cache) ListRoutedSendGroupIds(pn uint) iter.Seq[int] {
return c.listRoutedSendGroupIds(pn)
}
func (c *Cache) ListRoutedUnloadShipGroupIds(pn uint, rt game.RouteType) iter.Seq[int] {
return c.listRoutedUnloadShipGroupIds(pn, rt)
}
func (c *Cache) SelectColUnloadGroup(groups []int) (result iter.Seq[int]) {
return c.selectColUnloadGroup(groups)
}
func (c *Cache) ListMoveableGroupIds() iter.Seq[int] {
return c.listMoveableGroupIds()
}
func (c *Cache) CollectBombingGroups() map[uint]map[int][]int {
return c.collectBombingGroups()
}
func BombPlanet(p *game.Planet, power float64) {
bombPlanet(p, power)
}
func (c *Cache) ListProducingPlanets() iter.Seq[uint] {
return c.listProducingPlanets()
}
func (c *Cache) VotesByRace() map[int]float64 {
return c.votesByRace()
}
func VotingWinners(calc []*VoteGroup, gameVotes float64) []int {
return votingWinners(calc, gameVotes)
}
func (c *Cache) CreateShipsUnsafe_T(ri int, classID uuid.UUID, planet uint, quantity uint) int {
return c.unsafeCreateShips(ri, classID, planet, quantity)
}
func (c *Cache) WipeRace(ri int) {
c.wipeRace(ri)
}
func (c *Cache) UnsafeDeleteShipGroup(sgi int) {
c.unsafeDeleteShipGroup(sgi)
}
func DestructionRoll(probability float64) bool {
return destructionRoll(probability)
}