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("действия после завершения боя не должны ничего менять") } }