460 lines
16 KiB
Go
460 lines
16 KiB
Go
package main
|
||
|
||
import (
|
||
"fmt"
|
||
"strings"
|
||
"testing"
|
||
"unicode/utf8"
|
||
)
|
||
|
||
func TestWfWrapSplitsLongText(t *testing.T) {
|
||
longText := "Это довольно длинный текст, который заведомо не поместится в одну строку узкого терминала и должен быть перенесён на несколько строк."
|
||
// termWidth=50 -> реальная ширина переноса 50-4=46 (выше минимума
|
||
// в 40, так что запасной вариант 60 тут не сработает)
|
||
wrapped := wfWrap(longText, 50)
|
||
lines := strings.Split(strings.TrimRight(wrapped, "\n"), "\n")
|
||
if len(lines) < 2 {
|
||
t.Fatalf("ожидался перенос минимум на 2 строки, получена 1: %q", wrapped)
|
||
}
|
||
for _, line := range lines {
|
||
if n := utf8.RuneCountInString(line); n > 46 {
|
||
t.Errorf("строка длиннее заданной ширины 46 рун: %q (рун: %d)", line, n)
|
||
}
|
||
}
|
||
}
|
||
|
||
func TestWfWrapShortTextStaysOnOneLine(t *testing.T) {
|
||
short := "Короткая фраза."
|
||
wrapped := wfWrap(short, 80)
|
||
lines := strings.Split(strings.TrimRight(wrapped, " \n"), "\n")
|
||
if len(lines) != 1 {
|
||
t.Errorf("короткий текст не должен переноситься, получено %d строк: %q", len(lines), wrapped)
|
||
}
|
||
}
|
||
|
||
func TestWfWrapUsesFallbackWidthWhenTermWidthUnset(t *testing.T) {
|
||
longText := strings.Repeat("слово ", 30)
|
||
wrapped := wfWrap(longText, 0)
|
||
lines := strings.Split(strings.TrimRight(wrapped, " \n"), "\n")
|
||
if len(lines) < 2 {
|
||
t.Errorf("даже с запасной шириной длинный текст должен был перенестись, получено: %q", wrapped)
|
||
}
|
||
}
|
||
|
||
func TestWayfarerQuestOfferAccept(t *testing.T) {
|
||
withTempConfigDir(t)
|
||
m := NewWayfarerModel()
|
||
m.pendingQuestKey = WayfarerQuestDefs[0].Key
|
||
m.view = wfViewQuestOffer
|
||
|
||
next, _ := m.Update(key("1"))
|
||
m = next.(WayfarerModel)
|
||
if m.view != wfViewQuestStep {
|
||
t.Fatalf("после принятия задания должен был открыться экран его шага, получено view=%v", m.view)
|
||
}
|
||
if m.ship.ActiveQuest == nil || m.ship.ActiveQuest.Key != WayfarerQuestDefs[0].Key {
|
||
t.Fatalf("задание должно было стать активным")
|
||
}
|
||
if m.pendingQuestKey != "" {
|
||
t.Errorf("pendingQuestKey должен был очиститься после принятия")
|
||
}
|
||
}
|
||
|
||
func TestWayfarerQuestOfferDecline(t *testing.T) {
|
||
withTempConfigDir(t)
|
||
m := NewWayfarerModel()
|
||
m.pendingQuestKey = WayfarerQuestDefs[0].Key
|
||
m.view = wfViewQuestOffer
|
||
|
||
next, _ := m.Update(key("2"))
|
||
m = next.(WayfarerModel)
|
||
if m.view != wfViewHub {
|
||
t.Fatalf("после отказа должен был открыться хаб, получено view=%v", m.view)
|
||
}
|
||
if m.ship.ActiveQuest != nil {
|
||
t.Errorf("задание не должно было стать активным после отказа")
|
||
}
|
||
if m.pendingQuestKey != "" {
|
||
t.Errorf("pendingQuestKey должен был очиститься после отказа")
|
||
}
|
||
}
|
||
|
||
func TestWayfarerMarketPricesStableUntilNextDocking(t *testing.T) {
|
||
withTempConfigDir(t)
|
||
m := NewWayfarerModel()
|
||
m.view = wfViewMarket
|
||
|
||
before := append([]float64{}, m.marketPrices...)
|
||
for i := 0; i < 10; i++ {
|
||
next, _ := m.Update(key("down"))
|
||
m = next.(WayfarerModel)
|
||
next, _ = m.Update(key("up"))
|
||
m = next.(WayfarerModel)
|
||
}
|
||
for i, p := range m.marketPrices {
|
||
if p != before[i] {
|
||
t.Errorf("цена товара %d изменилась просто от навигации по рынку: было %v, стало %v", i, before[i], p)
|
||
}
|
||
}
|
||
}
|
||
|
||
func TestWayfarerMarketPricesChangeAfterJump(t *testing.T) {
|
||
withTempConfigDir(t)
|
||
m := NewWayfarerModel()
|
||
before := append([]float64{}, m.marketPrices...)
|
||
|
||
nearby := wayfarerNearbySystems(m.ship.CurrentSystem, m.ship.FuelRange())
|
||
if len(nearby) == 0 {
|
||
t.Fatal("нет достижимых систем для проверки")
|
||
}
|
||
next, _ := m.performJump(nearby[0])
|
||
m2 := next.(WayfarerModel)
|
||
|
||
changed := false
|
||
for i, p := range m2.marketPrices {
|
||
if p != before[i] {
|
||
changed = true
|
||
break
|
||
}
|
||
}
|
||
if !changed {
|
||
t.Errorf("после прыжка в новую систему цены должны были пересчитаться")
|
||
}
|
||
}
|
||
|
||
func TestWayfarerBuySellUsesCachedPriceNotFreshRoll(t *testing.T) {
|
||
withTempConfigDir(t)
|
||
m := NewWayfarerModel()
|
||
m.view = wfViewMarket
|
||
m.cursor = 0
|
||
cachedPrice := m.marketPrices[0]
|
||
|
||
next, _ := m.Update(key("b"))
|
||
m = next.(WayfarerModel)
|
||
// после покупки кэш цены не должен был поменяться сам по себе
|
||
if m.marketPrices[0] != cachedPrice {
|
||
t.Errorf("покупка не должна была пересчитать цену: было %v, стало %v", cachedPrice, m.marketPrices[0])
|
||
}
|
||
wantCredits := 1000 - cachedPrice
|
||
if m.ship.Credits != wantCredits {
|
||
t.Errorf("покупка должна была списать ровно закэшированную цену %v, кредитов осталось %v (ожидалось %v)",
|
||
cachedPrice, m.ship.Credits, wantCredits)
|
||
}
|
||
}
|
||
|
||
func TestWayfarerResetRequestShowsConfirmation(t *testing.T) {
|
||
withTempConfigDir(t)
|
||
m := NewWayfarerModel()
|
||
next, _ := m.Update(key("N"))
|
||
m = next.(WayfarerModel)
|
||
if !m.confirmingReset {
|
||
t.Fatalf("ожидался запрос подтверждения после нажатия N")
|
||
}
|
||
}
|
||
|
||
func TestWayfarerResetCancelKeepsCareer(t *testing.T) {
|
||
withTempConfigDir(t)
|
||
m := NewWayfarerModel()
|
||
m.ship.Credits = 5000
|
||
|
||
next, _ := m.Update(key("N"))
|
||
m = next.(WayfarerModel)
|
||
next, _ = m.Update(key("n"))
|
||
m = next.(WayfarerModel)
|
||
|
||
if m.confirmingReset {
|
||
t.Errorf("запрос подтверждения должен был закрыться после отмены")
|
||
}
|
||
if m.ship.Credits != 5000 {
|
||
t.Errorf("карьера не должна была сброситься при отмене, получено кредитов: %v", m.ship.Credits)
|
||
}
|
||
}
|
||
|
||
func TestWayfarerResetConfirmStartsFreshCareer(t *testing.T) {
|
||
withTempConfigDir(t)
|
||
m := NewWayfarerModel()
|
||
m.ship.Credits = 5000
|
||
m.ship.CurrentSystem = WayfarerSystemID{Galaxy: 2, Index: 50}
|
||
|
||
next, _ := m.Update(key("N"))
|
||
m = next.(WayfarerModel)
|
||
next, _ = m.Update(key("y"))
|
||
m = next.(WayfarerModel)
|
||
|
||
if m.confirmingReset {
|
||
t.Errorf("запрос подтверждения должен был закрыться после подтверждения")
|
||
}
|
||
if m.ship.Credits != 1000 {
|
||
t.Errorf("ожидались стартовые 1000 кредитов после новой карьеры, получено %v", m.ship.Credits)
|
||
}
|
||
if m.ship.CurrentSystem != wayfarerStartSystem {
|
||
t.Errorf("новая карьера должна была начаться со стартовой системы")
|
||
}
|
||
if m.view != wfViewHub {
|
||
t.Errorf("после сброса должен был открыться хаб")
|
||
}
|
||
}
|
||
|
||
func TestGalacticCoordsAdjustGalaxyAndIndex(t *testing.T) {
|
||
withTempConfigDir(t)
|
||
m := NewWayfarerModel()
|
||
m.view = wfViewGalacticCoords
|
||
m.galacticTargetGalaxy = 0
|
||
m.galacticTargetIndex = 0
|
||
|
||
next, _ := m.Update(key("right"))
|
||
m = next.(WayfarerModel)
|
||
if m.galacticTargetGalaxy != 1 {
|
||
t.Errorf("ожидалась галактика 1, получено %d", m.galacticTargetGalaxy)
|
||
}
|
||
|
||
next, _ = m.Update(key("up"))
|
||
m = next.(WayfarerModel)
|
||
if m.galacticTargetIndex != 1 {
|
||
t.Errorf("ожидался индекс 1, получено %d", m.galacticTargetIndex)
|
||
}
|
||
|
||
next, _ = m.Update(key("shift+up"))
|
||
m = next.(WayfarerModel)
|
||
if m.galacticTargetIndex != 11 {
|
||
t.Errorf("ожидался индекс 11 после +10, получено %d", m.galacticTargetIndex)
|
||
}
|
||
}
|
||
|
||
func TestGalacticCoordsGalaxyWrapsAround(t *testing.T) {
|
||
withTempConfigDir(t)
|
||
m := NewWayfarerModel()
|
||
m.view = wfViewGalacticCoords
|
||
m.galacticTargetGalaxy = 0
|
||
|
||
next, _ := m.Update(key("left"))
|
||
m = next.(WayfarerModel)
|
||
if m.galacticTargetGalaxy != WayfarerGalaxyCount-1 {
|
||
t.Errorf("ожидалось зацикливание на последнюю галактику, получено %d", m.galacticTargetGalaxy)
|
||
}
|
||
}
|
||
|
||
func TestGalacticCoordsIndexWrapsAround(t *testing.T) {
|
||
withTempConfigDir(t)
|
||
m := NewWayfarerModel()
|
||
m.view = wfViewGalacticCoords
|
||
m.galacticTargetIndex = 0
|
||
|
||
next, _ := m.Update(key("down"))
|
||
m = next.(WayfarerModel)
|
||
if m.galacticTargetIndex != WayfarerSystemsPerGalaxy-1 {
|
||
t.Errorf("ожидалось зацикливание на последний индекс, получено %d", m.galacticTargetIndex)
|
||
}
|
||
}
|
||
|
||
func TestGalacticJumpUsesChosenCoordinates(t *testing.T) {
|
||
withTempConfigDir(t)
|
||
m := NewWayfarerModel()
|
||
m.ship.Fuel = m.ship.FuelCapacity
|
||
startGalaxy := m.ship.CurrentSystem.Galaxy
|
||
m.view = wfViewGalacticCoords
|
||
m.galacticTargetGalaxy = (startGalaxy + 2) % WayfarerGalaxyCount
|
||
m.galacticTargetIndex = 42
|
||
|
||
next, _ := m.Update(key("enter"))
|
||
m = next.(WayfarerModel)
|
||
want := WayfarerSystemID{Galaxy: (startGalaxy + 2) % WayfarerGalaxyCount, Index: 42}
|
||
if m.ship.CurrentSystem != want {
|
||
t.Errorf("прыжок должен был привести именно в выбранные координаты %v, получено %v", want, m.ship.CurrentSystem)
|
||
}
|
||
}
|
||
|
||
func TestGalacticJumpRejectsSameGalaxy(t *testing.T) {
|
||
withTempConfigDir(t)
|
||
m := NewWayfarerModel()
|
||
m.ship.Fuel = m.ship.FuelCapacity
|
||
m.view = wfViewGalacticCoords
|
||
m.galacticTargetGalaxy = m.ship.CurrentSystem.Galaxy
|
||
m.galacticTargetIndex = 5
|
||
|
||
next, _ := m.Update(key("enter"))
|
||
m = next.(WayfarerModel)
|
||
if !m.isError {
|
||
t.Errorf("ожидалась ошибка при попытке прыжка в свою же галактику")
|
||
}
|
||
if m.view != wfViewGalacticCoords {
|
||
t.Errorf("при ошибке экран не должен был закрыться")
|
||
}
|
||
}
|
||
|
||
func TestCombatActionsIncludeMissilesWhenAvailable(t *testing.T) {
|
||
withTempConfigDir(t)
|
||
m := NewWayfarerModel()
|
||
m.ship.RegularMissiles = 1
|
||
m.combat = NewWayfarerCombat("wf.enemy.pirate", 100, 10, m.ship.Hull, m.ship.MaxHull, m.ship.Shield, m.ship.MaxShield, m.ship.LaserPower, true, WayfarerWeaponLaser, 0, m.rng)
|
||
|
||
actions := m.combatActions()
|
||
found := false
|
||
for _, a := range actions {
|
||
if strings.Contains(a.Label, T("wf.combat.action.attack_missile_regular")[:10]) {
|
||
found = true
|
||
}
|
||
}
|
||
if !found {
|
||
t.Errorf("действие стрельбы обычной ракетой должно было появиться при наличии боезапаса")
|
||
}
|
||
}
|
||
|
||
func TestCombatActionsExcludeMissilesWithoutAmmo(t *testing.T) {
|
||
withTempConfigDir(t)
|
||
m := NewWayfarerModel()
|
||
m.ship.RegularMissiles = 0
|
||
m.ship.HomingMissiles = 0
|
||
m.combat = NewWayfarerCombat("wf.enemy.pirate", 100, 10, m.ship.Hull, m.ship.MaxHull, m.ship.Shield, m.ship.MaxShield, m.ship.LaserPower, true, WayfarerWeaponLaser, 0, m.rng)
|
||
|
||
for _, a := range m.combatActions() {
|
||
if strings.Contains(a.Label, "ракет") {
|
||
t.Errorf("без боезапаса действия стрельбы ракетой быть не должно: %q", a.Label)
|
||
}
|
||
}
|
||
}
|
||
|
||
func TestCombatActionsIncludeDodgeOnlyAgainstRegularMissile(t *testing.T) {
|
||
withTempConfigDir(t)
|
||
m := NewWayfarerModel()
|
||
m.combat = NewWayfarerCombat("wf.enemy.pirate", 100, 10, m.ship.Hull, m.ship.MaxHull, m.ship.Shield, m.ship.MaxShield, m.ship.LaserPower, true, WayfarerWeaponMissileRegular, 0, m.rng)
|
||
|
||
found := false
|
||
for _, a := range m.combatActions() {
|
||
if a.Label == T("wf.combat.action.dodge") {
|
||
found = true
|
||
}
|
||
}
|
||
if !found {
|
||
t.Errorf("уклонение должно быть доступно против обычной ракеты врага")
|
||
}
|
||
|
||
m.combat.EnemyWeapon = WayfarerWeaponLaser
|
||
for _, a := range m.combatActions() {
|
||
if a.Label == T("wf.combat.action.dodge") {
|
||
t.Errorf("уклонение не должно предлагаться против лазера")
|
||
}
|
||
}
|
||
}
|
||
|
||
func TestCombatActionsIncludeDecoyOnlyAgainstHomingMissileWithStock(t *testing.T) {
|
||
withTempConfigDir(t)
|
||
m := NewWayfarerModel()
|
||
m.ship.Decoys = 1
|
||
m.combat = NewWayfarerCombat("wf.enemy.pirate", 100, 10, m.ship.Hull, m.ship.MaxHull, m.ship.Shield, m.ship.MaxShield, m.ship.LaserPower, true, WayfarerWeaponMissileHoming, 0, m.rng)
|
||
|
||
found := false
|
||
for _, a := range m.combatActions() {
|
||
if strings.Contains(a.Label, "ловушк") {
|
||
found = true
|
||
}
|
||
}
|
||
if !found {
|
||
t.Errorf("ловушка должна быть доступна против самонаводящейся ракеты при наличии запаса")
|
||
}
|
||
|
||
m.ship.Decoys = 0
|
||
for _, a := range m.combatActions() {
|
||
if strings.Contains(a.Label, "ловушк") {
|
||
t.Errorf("без запаса ловушек действие не должно предлагаться")
|
||
}
|
||
}
|
||
}
|
||
|
||
func TestCombatShieldDistributionFullFlow(t *testing.T) {
|
||
withTempConfigDir(t)
|
||
m := NewWayfarerModel()
|
||
m.combat = NewWayfarerCombat("wf.enemy.pirate", 1000, 1, 1000, 1000, 30, 50, m.ship.LaserPower, true, WayfarerWeaponLaser, 0, m.rng)
|
||
m.view = wfViewCombat
|
||
|
||
actions := m.combatActions()
|
||
distIdx := len(actions) - 1 // распределение щитов — последний пункт
|
||
|
||
next, _ := m.Update(key(fmt.Sprintf("%d", distIdx+1)))
|
||
m = next.(WayfarerModel)
|
||
if !m.combatDistributing {
|
||
t.Fatalf("ожидался вход в подрежим распределения щитов")
|
||
}
|
||
roundBefore := m.combat.Round
|
||
|
||
next, _ = m.Update(key("up")) // +5 к носу (сейчас в фокусе)
|
||
m = next.(WayfarerModel)
|
||
if m.distFore != 14 {
|
||
t.Fatalf("ожидалось увеличение носа на 5 (было 9 при 4-стороннем сплите 30), получено %d", m.distFore)
|
||
}
|
||
|
||
next, _ = m.Update(key("enter"))
|
||
m = next.(WayfarerModel)
|
||
if m.combatDistributing {
|
||
t.Errorf("подрежим должен был закрыться после подтверждения")
|
||
}
|
||
if m.combat.Round != roundBefore+1 {
|
||
t.Errorf("подтверждённое распределение должно было потратить раунд")
|
||
}
|
||
if m.combat.PlayerShieldFore < 13 {
|
||
t.Errorf("новое распределение должно было примениться (±1 от возможного минимального ответного урона), нос=%d", m.combat.PlayerShieldFore)
|
||
}
|
||
}
|
||
|
||
func TestCombatShieldDistributionCancelDoesNotSpendRound(t *testing.T) {
|
||
withTempConfigDir(t)
|
||
m := NewWayfarerModel()
|
||
m.combat = NewWayfarerCombat("wf.enemy.pirate", 1000, 500, 1000, 1000, 30, 30, m.ship.LaserPower, true, WayfarerWeaponLaser, 0, m.rng)
|
||
m.view = wfViewCombat
|
||
roundBefore := m.combat.Round
|
||
|
||
actions := m.combatActions()
|
||
distIdx := len(actions) - 1
|
||
next, _ := m.Update(key(fmt.Sprintf("%d", distIdx+1)))
|
||
m = next.(WayfarerModel)
|
||
|
||
next, _ = m.Update(key("esc"))
|
||
m = next.(WayfarerModel)
|
||
if m.combatDistributing {
|
||
t.Errorf("esc должен был выйти из подрежима")
|
||
}
|
||
if m.combat.Round != roundBefore {
|
||
t.Errorf("отменённое распределение не должно тратить раунд")
|
||
}
|
||
}
|
||
|
||
func TestGalacticJumpRejectsWithoutFullTank(t *testing.T) {
|
||
withTempConfigDir(t)
|
||
m := NewWayfarerModel()
|
||
m.ship.Fuel = 1
|
||
m.view = wfViewGalacticCoords
|
||
m.galacticTargetGalaxy = (m.ship.CurrentSystem.Galaxy + 1) % WayfarerGalaxyCount
|
||
|
||
next, _ := m.Update(key("enter"))
|
||
m = next.(WayfarerModel)
|
||
if !m.isError {
|
||
t.Errorf("ожидалась ошибка нехватки топлива")
|
||
}
|
||
}
|
||
|
||
func TestWayfarerResetActionsIgnoredWhileConfirming(t *testing.T) {
|
||
withTempConfigDir(t)
|
||
m := NewWayfarerModel()
|
||
viewBefore := m.view
|
||
|
||
next, _ := m.Update(key("N"))
|
||
m = next.(WayfarerModel)
|
||
// нажатие "1" (переход на рынок) не должно срабатывать, пока ждём подтверждения
|
||
next, _ = m.Update(key("1"))
|
||
m = next.(WayfarerModel)
|
||
if m.view != viewBefore {
|
||
t.Errorf("обычная навигация не должна срабатывать во время запроса подтверждения")
|
||
}
|
||
}
|
||
|
||
func TestWfWrapClampsExcessiveWidth(t *testing.T) {
|
||
text := "текст"
|
||
wrapped := wfWrap(text, 500)
|
||
firstLine := strings.Split(wrapped, "\n")[0]
|
||
// при очень широком терминале перенос всё равно не должен
|
||
// растягиваться дальше 100 колонок (проверяем по длине padding'а,
|
||
// которым lipgloss дополняет строку до ширины стиля)
|
||
if n := utf8.RuneCountInString(firstLine); n > 100 {
|
||
t.Errorf("ширина не должна была превышать 100 колонок даже на широком терминале, получено %d", n)
|
||
}
|
||
}
|