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) } }