go_games_collection/wayfarer_combat_test.go

457 lines
20 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package main
import (
"math/rand"
"testing"
)
func TestApplyDamageAbsorbedByShieldFirst(t *testing.T) {
shield, hull := 10, 50
applyDamage(&shield, &hull, 6)
if shield != 4 || hull != 50 {
t.Errorf("урон должен был полностью уйти в щит: shield=%d hull=%d", shield, hull)
}
}
func TestApplyDamageOverflowsToHull(t *testing.T) {
shield, hull := 5, 50
applyDamage(&shield, &hull, 12)
if shield != 0 || hull != 43 {
t.Errorf("избыток урона должен был перейти в корпус: shield=%d hull=%d", shield, hull)
}
}
func TestApplyDamageHullNeverNegative(t *testing.T) {
shield, hull := 0, 5
applyDamage(&shield, &hull, 100)
if hull != 0 {
t.Errorf("корпус не должен уходить в минус, получено %d", hull)
}
}
func TestAttackCanDefeatEnemy(t *testing.T) {
rnd := rand.New(rand.NewSource(1))
c := NewWayfarerCombat("enemy.pirate", 5, 5, 100, 100, 50, 50, 100, true, WayfarerWeaponLaser, 0, rnd)
c.Attack(WayfarerShieldFore, rnd)
if c.Outcome != CombatWon {
t.Fatalf("огромный урон игрока должен был сразу уничтожить слабого врага, получен исход %v (EnemyHull=%d)",
c.Outcome, c.EnemyHull)
}
if c.LastEnemyDamage != 0 {
t.Errorf("уничтоженный враг не должен успевать ответным ударом")
}
}
func TestAttackEnemySurvivesAndRetaliates(t *testing.T) {
rnd := rand.New(rand.NewSource(1))
c := NewWayfarerCombat("enemy.pirate", 1000, 5, 100, 100, 50, 50, 5, true, WayfarerWeaponLaser, 0, rnd)
beforeHull := c.PlayerHull + c.TotalShield()
c.Attack(WayfarerShieldFore, rnd)
if c.Outcome != CombatOngoing {
t.Fatalf("бой должен был продолжиться, получен исход %v", c.Outcome)
}
afterHull := c.PlayerHull + c.TotalShield()
if afterHull >= beforeHull {
t.Errorf("выживший враг должен был нанести ответный урон")
}
}
func TestAttackCanDefeatPlayer(t *testing.T) {
rnd := rand.New(rand.NewSource(1))
c := NewWayfarerCombat("enemy.pirate", 1000, 500, 1, 100, 0, 50, 1, true, WayfarerWeaponLaser, 0, rnd)
c.Attack(WayfarerShieldFore, rnd)
if c.Outcome != CombatLost {
t.Fatalf("слабый игрок против огромного урона должен был проиграть, получен исход %v (PlayerHull=%d)",
c.Outcome, c.PlayerHull)
}
}
func TestFleeEventuallySucceedsOrFails(t *testing.T) {
successSeen, failSeen := false, false
for seed := int64(0); seed < 200 && !(successSeen && failSeen); seed++ {
rnd := rand.New(rand.NewSource(seed))
c := NewWayfarerCombat("enemy.pirate", 1000, 5, 1000, 1000, 100, 100, 5, true, WayfarerWeaponLaser, 0, rnd)
if c.Flee(rnd) {
successSeen = true
} else {
failSeen = true
}
}
if !successSeen || !failSeen {
t.Errorf("ожидались оба исхода бегства среди множества попыток: успех=%v неудача=%v", successSeen, failSeen)
}
}
func TestFailedFleeStillTakesDamage(t *testing.T) {
rnd := rand.New(rand.NewSource(1))
c := NewWayfarerCombat("enemy.pirate", 1000, 20, 1000, 1000, 0, 0, 5, true, WayfarerWeaponLaser, 0, rnd)
for i := 0; i < 50 && c.Outcome == CombatOngoing; i++ {
fled := c.Flee(rnd)
if !fled {
if c.LastEnemyDamage <= 0 {
t.Errorf("неудачное бегство должно было нанести урон игроку")
}
return
}
}
}
func TestNegotiateOnlyAvailableWhenNegotiable(t *testing.T) {
rnd := rand.New(rand.NewSource(1))
c := NewWayfarerCombat("enemy.pirate", 1000, 5, 1000, 1000, 100, 100, 5, false, WayfarerWeaponLaser, 0, rnd)
before := *c
ok := c.Negotiate(rnd)
if ok {
t.Errorf("переговоры не должны быть доступны, если Negotiable=false")
}
if c.Round != before.Round {
t.Errorf("недоступная попытка переговоров не должна тратить раунд")
}
}
func TestNegotiateEventuallySucceedsOrFails(t *testing.T) {
successSeen, failSeen := false, false
for seed := int64(0); seed < 200 && !(successSeen && failSeen); seed++ {
rnd := rand.New(rand.NewSource(seed))
c := NewWayfarerCombat("enemy.pirate", 1000, 5, 1000, 1000, 100, 100, 5, true, WayfarerWeaponLaser, 0, rnd)
if c.Negotiate(rnd) {
successSeen = true
} else {
failSeen = true
}
}
if !successSeen || !failSeen {
t.Errorf("ожидались оба исхода переговоров среди множества попыток: успех=%v неудача=%v", successSeen, failSeen)
}
}
// --- Ракеты: урон слабее по щиту, сильнее по голому корпусу --------
func TestApplyWeaponDamageMissileWeakerVsShield(t *testing.T) {
shieldLaser, hullLaser := 20, 50
applyWeaponDamage(&shieldLaser, &hullLaser, 10, WayfarerWeaponLaser)
shieldMissile, hullMissile := 20, 50
applyWeaponDamage(&shieldMissile, &hullMissile, 10, WayfarerWeaponMissileRegular)
if shieldMissile <= shieldLaser {
t.Errorf("ракета должна была снять со щита меньше, чем лазер: щит после лазера=%d, после ракеты=%d", shieldLaser, shieldMissile)
}
if hullMissile != 50 || hullLaser != 50 {
t.Errorf("при достаточном щите урон не должен доходить до корпуса ни у лазера, ни у ракеты")
}
}
func TestApplyWeaponDamageMissileStrongerVsBareHull(t *testing.T) {
shieldLaser, hullLaser := 0, 50
applyDamage(&shieldLaser, &hullLaser, 10)
shieldMissile, hullMissile := 0, 50
applyWeaponDamage(&shieldMissile, &hullMissile, 10, WayfarerWeaponMissileRegular)
if (50 - hullMissile) <= (50 - hullLaser) {
t.Errorf("ракета по голому корпусу должна наносить больше урона, чем лазер: лазер снял %d, ракета %d", 50-hullLaser, 50-hullMissile)
}
}
func TestApplyWeaponDamageHomingSameFormulaAsRegular(t *testing.T) {
shieldA, hullA := 5, 50
applyWeaponDamage(&shieldA, &hullA, 10, WayfarerWeaponMissileRegular)
shieldB, hullB := 5, 50
applyWeaponDamage(&shieldB, &hullB, 10, WayfarerWeaponMissileHoming)
if shieldA != shieldB || hullA != hullB {
t.Errorf("обычная и самонаводящаяся ракета должны наносить одинаковый урон по формуле, получено (%d,%d) и (%d,%d)", shieldA, hullA, shieldB, hullB)
}
}
// --- AttackWithMissile ------------------------------------------------
func TestAttackWithMissileConsumesAmmoAndDamagesEnemy(t *testing.T) {
rnd := rand.New(rand.NewSource(1))
c := NewWayfarerCombat("enemy.pirate", 1000, 5, 100, 100, 50, 50, 20, true, WayfarerWeaponLaser, 0, rnd)
ship := &WayfarerShip{RegularMissiles: 2}
if err := c.AttackWithMissile(ship, WayfarerWeaponMissileRegular, WayfarerShieldFore, rnd); err != nil {
t.Fatalf("неожиданная ошибка: %v", err)
}
if ship.RegularMissiles != 1 {
t.Errorf("ожидалось списание одной ракеты, осталось %d", ship.RegularMissiles)
}
if c.EnemyHull >= 1000 {
t.Errorf("враг должен был получить урон")
}
}
func TestAttackWithMissileFailsWithoutAmmo(t *testing.T) {
rnd := rand.New(rand.NewSource(1))
c := NewWayfarerCombat("enemy.pirate", 1000, 5, 100, 100, 50, 50, 20, true, WayfarerWeaponLaser, 0, rnd)
ship := &WayfarerShip{RegularMissiles: 0}
if err := c.AttackWithMissile(ship, WayfarerWeaponMissileRegular, WayfarerShieldFore, rnd); err != errWayfarerNoMissiles {
t.Errorf("ожидалась ошибка нехватки ракет, получено: %v", err)
}
}
func TestAttackWithMissileUsesCorrectAmmoType(t *testing.T) {
rnd := rand.New(rand.NewSource(1))
c := NewWayfarerCombat("enemy.pirate", 1000, 5, 100, 100, 50, 50, 20, true, WayfarerWeaponLaser, 0, rnd)
ship := &WayfarerShip{HomingMissiles: 1}
if err := c.AttackWithMissile(ship, WayfarerWeaponMissileHoming, WayfarerShieldFore, rnd); err != nil {
t.Fatalf("неожиданная ошибка: %v", err)
}
if ship.HomingMissiles != 0 {
t.Errorf("ожидалось списание самонаводящейся ракеты, осталось %d", ship.HomingMissiles)
}
}
func TestAttackWithMissileTargetsChosenFacing(t *testing.T) {
rnd := rand.New(rand.NewSource(1))
c := NewWayfarerCombat("enemy.pirate", 1000, 5, 100, 100, 50, 50, 20, true, WayfarerWeaponLaser, 40, rnd)
ship := &WayfarerShip{RegularMissiles: 1}
rightBefore := c.EnemyShieldRight
if err := c.AttackWithMissile(ship, WayfarerWeaponMissileRegular, WayfarerShieldRight, rnd); err != nil {
t.Fatalf("неожиданная ошибка: %v", err)
}
if c.EnemyShieldRight >= rightBefore {
t.Errorf("щит врага именно с правого борта должен был уменьшиться, было %d, стало %d", rightBefore, c.EnemyShieldRight)
}
}
// --- Dodge / UseDecoy --------------------------------------------------
func TestDodgeOnlyWorksAgainstRegularMissile(t *testing.T) {
rnd := rand.New(rand.NewSource(1))
cLaser := NewWayfarerCombat("enemy.pirate", 1000, 5, 100, 100, 50, 50, 20, true, WayfarerWeaponLaser, 0, rnd)
if cLaser.Dodge(rnd) {
t.Errorf("уклонение не должно приносить успех против лазера — оно вообще неприменимо")
}
if cLaser.Round != 0 {
t.Errorf("неприменимое уклонение не должно тратить раунд")
}
cHoming := NewWayfarerCombat("enemy.pirate", 1000, 5, 100, 100, 50, 50, 20, true, WayfarerWeaponMissileHoming, 0, rnd)
if cHoming.Dodge(rnd) {
t.Errorf("уклонение не должно работать против самонаводящейся ракеты")
}
if cHoming.Round != 0 {
t.Errorf("неприменимое против самонаводящейся ракеты уклонение не должно тратить раунд")
}
}
func TestDodgeEventuallySucceedsAndFailsAgainstRegularMissile(t *testing.T) {
successSeen, failSeen := false, false
for seed := int64(0); seed < 200 && !(successSeen && failSeen); seed++ {
rnd := rand.New(rand.NewSource(seed))
c := NewWayfarerCombat("enemy.pirate", 1000, 5, 1000, 1000, 100, 100, 5, true, WayfarerWeaponMissileRegular, 0, rnd)
if c.Dodge(rnd) {
successSeen = true
} else {
failSeen = true
}
}
if !successSeen || !failSeen {
t.Errorf("ожидались оба исхода уклонения: успех=%v неудача=%v", successSeen, failSeen)
}
}
func TestUseDecoyOnlyWorksAgainstHomingMissileWithDecoysAvailable(t *testing.T) {
rnd := rand.New(rand.NewSource(1))
c := NewWayfarerCombat("enemy.pirate", 1000, 5, 100, 100, 50, 50, 20, true, WayfarerWeaponMissileHoming, 0, rnd)
shipNoDecoys := &WayfarerShip{Decoys: 0}
if c.UseDecoy(shipNoDecoys) {
t.Errorf("ловушка не должна срабатывать без запаса ловушек")
}
cLaser := NewWayfarerCombat("enemy.pirate", 1000, 5, 100, 100, 50, 50, 20, true, WayfarerWeaponLaser, 0, rnd)
shipWithDecoys := &WayfarerShip{Decoys: 3}
if cLaser.UseDecoy(shipWithDecoys) {
t.Errorf("ловушка не должна применяться против лазера — она нужна только против самонаводящихся ракет")
}
if shipWithDecoys.Decoys != 3 {
t.Errorf("неприменимая ловушка не должна расходоваться")
}
}
func TestUseDecoySucceedsAndConsumesOne(t *testing.T) {
rnd := rand.New(rand.NewSource(1))
c := NewWayfarerCombat("enemy.pirate", 1000, 5, 100, 100, 50, 50, 20, true, WayfarerWeaponMissileHoming, 0, rnd)
ship := &WayfarerShip{Decoys: 2}
if !c.UseDecoy(ship) {
t.Fatalf("ловушка должна была сработать против самонаводящейся ракеты при наличии запаса")
}
if ship.Decoys != 1 {
t.Errorf("ожидалось списание одной ловушки, осталось %d", ship.Decoys)
}
if c.LastEnemyDamage != 0 {
t.Errorf("успешное применение ловушки должно было полностью нейтрализовать урон в этом раунде")
}
}
// --- DistributeShields --------------------------------------------------
func TestDistributeShieldsRejectsExceedingTotal(t *testing.T) {
rnd := rand.New(rand.NewSource(1))
c := NewWayfarerCombat("enemy.pirate", 1000, 5, 100, 100, 50, 50, 20, true, WayfarerWeaponLaser, 0, rnd)
roundBefore := c.Round
if err := c.DistributeShields(30, 30, 30, 30, rnd); err != errWayfarerInvalidShieldDistribution {
t.Errorf("ожидалась ошибка превышения общего запаса, получено: %v", err)
}
if c.Round != roundBefore {
t.Errorf("отклонённое распределение не должно тратить раунд")
}
}
func TestDistributeShieldsAppliesNewSplit(t *testing.T) {
rnd := rand.New(rand.NewSource(1))
c := NewWayfarerCombat("enemy.pirate", 1000, 5, 100, 100, 50, 50, 20, true, WayfarerWeaponLaser, 0, rnd)
if err := c.DistributeShields(50, 0, 0, 0, rnd); err != nil {
t.Fatalf("неожиданная ошибка: %v", err)
}
if c.PlayerShieldFore != 50 || c.PlayerShieldAft != 0 || c.PlayerShieldLeft != 0 || c.PlayerShieldRight != 0 {
t.Errorf("новое распределение не применилось: нос=%d хвост=%d лево=%d право=%d",
c.PlayerShieldFore, c.PlayerShieldAft, c.PlayerShieldLeft, c.PlayerShieldRight)
}
}
func TestDistributeShieldsSpendsRoundAndEnemyStrikes(t *testing.T) {
rnd := rand.New(rand.NewSource(1))
c := NewWayfarerCombat("enemy.pirate", 1000, 500, 1000, 1000, 30, 30, 20, true, WayfarerWeaponLaser, 0, rnd)
roundBefore := c.Round
c.DistributeShields(10, 10, 5, 5, rnd)
if c.Round != roundBefore+1 {
t.Errorf("подтверждённое распределение должно было потратить раунд")
}
if c.LastEnemyDamage <= 0 {
t.Errorf("после распределения враг должен был ответить атакой")
}
}
// --- Щиты врага (симметричные щитам игрока) -----------------------------
func TestEnemyShieldsSplitEvenlyAtStart(t *testing.T) {
rnd := rand.New(rand.NewSource(1))
c := NewWayfarerCombat("enemy.pirate", 1000, 5, 100, 100, 0, 0, 20, true, WayfarerWeaponLaser, 40, rnd)
if c.TotalEnemyShield() != 40 {
t.Errorf("ожидался суммарный щит врага 40, получено %d", c.TotalEnemyShield())
}
if c.EnemyShieldAft != 10 || c.EnemyShieldLeft != 10 || c.EnemyShieldRight != 10 {
t.Errorf("щит врага должен был разложиться поровну по 4 направлениям, получено нос=%d хвост=%d лево=%d право=%d",
c.EnemyShieldFore, c.EnemyShieldAft, c.EnemyShieldLeft, c.EnemyShieldRight)
}
}
func TestAttackDepletesOnlyChosenEnemyFacing(t *testing.T) {
rnd := rand.New(rand.NewSource(1))
c := NewWayfarerCombat("enemy.pirate", 1000, 5, 100, 100, 0, 0, 20, true, WayfarerWeaponLaser, 40, rnd)
foreBefore, aftBefore, leftBefore, rightBefore := c.EnemyShieldFore, c.EnemyShieldAft, c.EnemyShieldLeft, c.EnemyShieldRight
c.Attack(WayfarerShieldLeft, rnd)
if c.EnemyShieldLeft >= leftBefore {
t.Errorf("щит врага слева должен был уменьшиться, было %d, стало %d", leftBefore, c.EnemyShieldLeft)
}
if c.EnemyShieldFore != foreBefore || c.EnemyShieldAft != aftBefore || c.EnemyShieldRight != rightBefore {
t.Errorf("атака в левый борт не должна была задеть другие направления щита врага")
}
}
// --- Заход врага (NextApproach) сужает возможные направления удара ------
func TestApproachCandidatesMatchSpecifiedMapping(t *testing.T) {
tests := []struct {
approach WayfarerApproach
want []WayfarerShieldFacing
}{
{WayfarerApproachFront, []WayfarerShieldFacing{WayfarerShieldFore}},
{WayfarerApproachBehind, []WayfarerShieldFacing{WayfarerShieldAft}},
{WayfarerApproachLeft, []WayfarerShieldFacing{WayfarerShieldLeft, WayfarerShieldAft}},
{WayfarerApproachRight, []WayfarerShieldFacing{WayfarerShieldRight, WayfarerShieldAft}},
{WayfarerApproachVertical, []WayfarerShieldFacing{WayfarerShieldFore, WayfarerShieldAft}},
}
for _, tt := range tests {
got := wayfarerApproachCandidates[tt.approach]
if len(got) != len(tt.want) {
t.Fatalf("заход %v: ожидалось %d направлений, получено %d (%v)", tt.approach, len(tt.want), len(got), got)
}
for i := range tt.want {
if got[i] != tt.want[i] {
t.Errorf("заход %v: ожидался набор %v, получено %v", tt.approach, tt.want, got)
}
}
}
}
func TestEnemyStrikeOnlyHitsFacingsAllowedByApproach(t *testing.T) {
rnd := rand.New(rand.NewSource(1))
c := NewWayfarerCombat("enemy.pirate", 1000, 5, 1000, 1000, 100, 100, 5, true, WayfarerWeaponLaser, 0, rnd)
for i := 0; i < 300 && c.Outcome == CombatOngoing; i++ {
c.NextApproach = WayfarerApproachLeft // фиксируем заход слева на весь тест
c.enemyStrikes(rnd)
if c.LastHitFacing != WayfarerShieldLeft && c.LastHitFacing != WayfarerShieldAft {
t.Fatalf("при заходе слева удар должен приходиться только в левый борт или хвост, получено %v", c.LastHitFacing)
}
}
}
func TestFrontApproachAlwaysHitsFore(t *testing.T) {
rnd := rand.New(rand.NewSource(1))
c := NewWayfarerCombat("enemy.pirate", 1000, 5, 1000, 1000, 100, 100, 5, true, WayfarerWeaponLaser, 0, rnd)
for i := 0; i < 50 && c.Outcome == CombatOngoing; i++ {
c.NextApproach = WayfarerApproachFront
c.enemyStrikes(rnd)
if c.LastHitFacing != WayfarerShieldFore {
t.Fatalf("при заходе строго спереди удар должен идти точно в нос, получено %v", c.LastHitFacing)
}
}
}
// --- Покупка ракет/ловушек на корабле ------------------------------------
func TestBuyMissileRespectsCapacity(t *testing.T) {
s := &WayfarerShip{Credits: 100000, MissileCapacity: 1}
if err := s.BuyMissile(WayfarerWeaponMissileRegular, 100); err != nil {
t.Fatalf("неожиданная ошибка: %v", err)
}
if err := s.BuyMissile(WayfarerWeaponMissileHoming, 100); err != errWayfarerNoMissileSlots {
t.Errorf("ожидалась ошибка нехватки слотов, получено: %v", err)
}
}
func TestBuyMissileInsufficientFunds(t *testing.T) {
s := &WayfarerShip{Credits: 10, MissileCapacity: 2}
if err := s.BuyMissile(WayfarerWeaponMissileRegular, 100); err != errWayfarerInsufficientFunds {
t.Errorf("ожидалась ошибка нехватки средств, получено: %v", err)
}
}
func TestBuyDecoyIncrementsCount(t *testing.T) {
s := &WayfarerShip{Credits: 1000}
if err := s.BuyDecoy(80); err != nil {
t.Fatalf("неожиданная ошибка: %v", err)
}
if s.Decoys != 1 {
t.Errorf("ожидалась 1 ловушка, получено %d", s.Decoys)
}
}
func TestActionsAfterCombatOverAreNoOps(t *testing.T) {
rnd := rand.New(rand.NewSource(1))
c := NewWayfarerCombat("enemy.pirate", 5, 5, 100, 100, 50, 50, 100, true, WayfarerWeaponLaser, 0, rnd)
c.Attack(WayfarerShieldFore, rnd)
if c.Outcome != CombatWon {
t.Fatalf("бой должен был завершиться победой для настройки теста")
}
roundAfter := c.Round
c.Attack(WayfarerShieldFore, rnd)
c.Flee(rnd)
c.Negotiate(rnd)
if c.Round != roundAfter {
t.Errorf("действия после завершения боя не должны ничего менять")
}
}