go_games_collection/wayfarer_tui_test.go

460 lines
16 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 (
"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)
}
}