1225 lines
No EOL
49 KiB
Go
1225 lines
No EOL
49 KiB
Go
package main
|
||
|
||
import (
|
||
"strings"
|
||
"testing"
|
||
"time"
|
||
)
|
||
|
||
func TestStoryDifficultySelectionGoesStraightToOnboardingHub(t *testing.T) {
|
||
withTempConfigDir(t)
|
||
m := NewAppModel()
|
||
m.state = stateStoryDifficulty
|
||
m.storyDifficultyCursor = 2 // "Сложная"
|
||
|
||
next, _ := m.Update(key("enter"))
|
||
m = next.(AppModel)
|
||
if m.state != stateStoryHub {
|
||
t.Fatalf("после выбора сложности ожидался переход сразу в stateStoryHub (этап знакомства), получено %v", m.state)
|
||
}
|
||
if m.story == nil {
|
||
t.Fatalf("ожидалось созданное состояние сторимода")
|
||
}
|
||
if m.story.Difficulty != StoryDifficultyHard {
|
||
t.Errorf("ожидалась сложность StoryDifficultyHard, получено %v", m.story.Difficulty)
|
||
}
|
||
if m.story.Onboarding != StoryOnboardingTicTacToeOnly {
|
||
t.Errorf("новая партия должна была начаться с этапа знакомства (только крестики-нолики), получено %v", m.story.Onboarding)
|
||
}
|
||
}
|
||
|
||
func TestStoryDifficultySelectionPersistsToSave(t *testing.T) {
|
||
withTempConfigDir(t)
|
||
m := NewAppModel()
|
||
m.state = stateStoryDifficulty
|
||
m.storyDifficultyCursor = 1
|
||
|
||
next, _ := m.Update(key("enter"))
|
||
_ = next.(AppModel)
|
||
|
||
loaded, err := LoadStoryModeState()
|
||
if err != nil {
|
||
t.Fatalf("неожиданная ошибка загрузки: %v", err)
|
||
}
|
||
if loaded.Difficulty != StoryDifficultyMedium {
|
||
t.Errorf("сохранённая сложность должна была быть Medium, получено %v", loaded.Difficulty)
|
||
}
|
||
}
|
||
|
||
func TestModeSelectStoryWithExistingSaveShowsGreetingThenHub(t *testing.T) {
|
||
withTempConfigDir(t)
|
||
pre := NewStoryModeState(StoryDifficultyEasy, time.Now())
|
||
pre.Onboarding = StoryOnboardingDone // приветствие дня показывается только после знакомства
|
||
if err := SaveStoryModeState(pre); err != nil {
|
||
t.Fatalf("неожиданная ошибка сохранения: %v", err)
|
||
}
|
||
|
||
m := NewAppModel()
|
||
next, _ := m.Update(key("enter")) // язык -> выбор режима
|
||
m = next.(AppModel)
|
||
next, _ = m.Update(key("down")) // курсор на "Режим истории"
|
||
m = next.(AppModel)
|
||
next, _ = m.Update(key("enter"))
|
||
m = next.(AppModel)
|
||
|
||
if m.state != stateStoryMorning {
|
||
t.Fatalf("при входе через меню режимов ожидалось приветствие дня, получено %v", m.state)
|
||
}
|
||
if m.storyGreeting != storyGreetingPlayingAgain {
|
||
t.Errorf("при входе в тот же день ожидалось приветствие storyGreetingPlayingAgain, получено %v", m.storyGreeting)
|
||
}
|
||
|
||
next, _ = m.Update(key(" "))
|
||
m = next.(AppModel)
|
||
if m.state != stateStoryHub {
|
||
t.Fatalf("после приветствия ожидался переход в stateStoryHub, получено %v", m.state)
|
||
}
|
||
if m.story == nil || m.story.Difficulty != StoryDifficultyEasy {
|
||
t.Fatalf("ожидалось загруженное состояние с сохранённой сложностью, получено %+v", m.story)
|
||
}
|
||
}
|
||
|
||
func TestStoryHubLaunchesPlayableGame(t *testing.T) {
|
||
withTempConfigDir(t)
|
||
m := NewAppModel()
|
||
m.story = NewStoryModeState(StoryDifficultyEasy, time.Now())
|
||
m.state = stateStoryHub
|
||
m.storyHubCursor = 0 // Крестики-нолики — единственная видимая игра на этапе знакомства
|
||
|
||
next, _ := m.Update(key("enter"))
|
||
m = next.(AppModel)
|
||
if m.state != statePlaying {
|
||
t.Fatalf("ожидался переход в statePlaying после запуска игры, получено %v", m.state)
|
||
}
|
||
if m.storyActiveResource == nil || *m.storyActiveResource != StoryResTicTacToe {
|
||
t.Fatalf("ожидался отмеченный активный ресурс StoryResTicTacToe")
|
||
}
|
||
if _, ok := m.activeGame.(TicTacToeModel); !ok {
|
||
t.Errorf("ожидалась запущенная модель TicTacToeModel, получено %T", m.activeGame)
|
||
}
|
||
}
|
||
|
||
func TestAllStoryHubGamesAreWiredInStoryGameDefs(t *testing.T) {
|
||
for _, id := range storyHubOrder {
|
||
if _, ok := storyGameDefs[id]; !ok {
|
||
t.Errorf("ресурс %v есть в storyHubOrder, но не подключён в storyGameDefs", id)
|
||
}
|
||
}
|
||
}
|
||
|
||
func TestStoryHubStillLaunchesLockedGame(t *testing.T) {
|
||
withTempConfigDir(t)
|
||
m := NewAppModel()
|
||
m.story = NewStoryModeState(StoryDifficultyEasy, time.Now())
|
||
m.story.Onboarding = StoryOnboardingDone // тест про полный хаб, не про этап знакомства
|
||
m.story.Locked[StoryResTicTacToe] = true
|
||
m.state = stateStoryHub
|
||
m.storyHubCursor = indexOfStoryResource(t, StoryResTicTacToe)
|
||
|
||
next, _ := m.Update(key("enter"))
|
||
m = next.(AppModel)
|
||
if m.activeGame == nil {
|
||
t.Errorf("уже зафиксированная (запертая) игра должна была оставаться играбельной")
|
||
}
|
||
}
|
||
|
||
func TestWinningStoryGameAwardsResourceAndReturnsToHub(t *testing.T) {
|
||
withTempConfigDir(t)
|
||
m := NewAppModel()
|
||
m.story = NewStoryModeState(StoryDifficultyEasy, time.Now())
|
||
before := m.story.Resources[StoryResTicTacToe]
|
||
|
||
tm := NewTicTacToeModel(TicTacToeDifficultyEasy)
|
||
tm.game.Phase = TicTacToePhaseOver
|
||
tm.game.Result = &TicTacToeResult{Winner: tm.game.HumanMark}
|
||
tm.backToMenu = true
|
||
|
||
resource := StoryResTicTacToe
|
||
m.storyActiveResource = &resource
|
||
m.activeGame = tm
|
||
m.state = statePlaying
|
||
|
||
next, _ := m.Update(key("q")) // ключ не важен — уже готовый backToMenu=true проверяется в Update верхнего уровня
|
||
m2, ok := next.(AppModel)
|
||
if !ok {
|
||
t.Fatalf("ожидался AppModel в ответе")
|
||
}
|
||
|
||
if m2.state != stateStoryHub {
|
||
t.Fatalf("после победы в игре сторимода ожидался возврат в stateStoryHub, получено %v", m2.state)
|
||
}
|
||
if m2.story.Resources[StoryResTicTacToe] != before+1 {
|
||
t.Errorf("ожидалось начисление +1 ресурса за победу, получено %d (было %d)", m2.story.Resources[StoryResTicTacToe], before)
|
||
}
|
||
if m2.storyActiveResource != nil {
|
||
t.Errorf("отметка активного ресурса должна была сброситься после возврата")
|
||
}
|
||
}
|
||
|
||
func TestLosingStoryGameDoesNotAwardResource(t *testing.T) {
|
||
withTempConfigDir(t)
|
||
m := NewAppModel()
|
||
m.story = NewStoryModeState(StoryDifficultyEasy, time.Now())
|
||
before := m.story.Resources[StoryResTicTacToe]
|
||
|
||
tm := NewTicTacToeModel(TicTacToeDifficultyEasy)
|
||
tm.game.Phase = TicTacToePhaseOver
|
||
loser := TicTacToeX
|
||
if tm.game.HumanMark == TicTacToeX {
|
||
loser = TicTacToeO
|
||
}
|
||
tm.game.Result = &TicTacToeResult{Winner: loser}
|
||
tm.backToMenu = true
|
||
|
||
resource := StoryResTicTacToe
|
||
m.storyActiveResource = &resource
|
||
m.activeGame = tm
|
||
m.state = statePlaying
|
||
|
||
next, _ := m.Update(key("q"))
|
||
m2 := next.(AppModel)
|
||
|
||
if m2.story.Resources[StoryResTicTacToe] != before {
|
||
t.Errorf("поражение не должно было начислять ресурс, получено %d (было %d)", m2.story.Resources[StoryResTicTacToe], before)
|
||
}
|
||
if m2.state != stateStoryHub {
|
||
t.Errorf("после поражения тоже ожидался возврат в хаб, получено %v", m2.state)
|
||
}
|
||
}
|
||
|
||
func TestStoryResourceLocksExactlyAtTarget(t *testing.T) {
|
||
s := NewStoryModeState(StoryDifficultyEasy, time.Now())
|
||
s.Resources[StoryResChess] = StoryTargetChess - 1
|
||
s.RecordWin(StoryResChess)
|
||
if !s.Locked[StoryResChess] {
|
||
t.Fatalf("ресурс должен был зафиксироваться по достижении цели")
|
||
}
|
||
}
|
||
|
||
// indexOfStoryResource находит позицию ресурса в storyHubOrder — не
|
||
// хардкодим индекс напрямую, чтобы тест не сломался при изменении
|
||
// порядка показа в хабе.
|
||
func indexOfStoryResource(t *testing.T, id StoryResourceID) int {
|
||
t.Helper()
|
||
for i, r := range storyHubOrder {
|
||
if r == id {
|
||
return i
|
||
}
|
||
}
|
||
t.Fatalf("ресурс %v не найден в storyHubOrder", id)
|
||
return -1
|
||
}
|
||
|
||
func TestStoryOutcomeBlackjackRequiresThreeRoundsMinimum(t *testing.T) {
|
||
bm := NewBlackjackModel(1, 500, 25)
|
||
for bm.game.Phase == BJPhasePlayerTurn {
|
||
bm.game.Stand()
|
||
}
|
||
bm.game.RoundsPlayed = 2
|
||
bm.game.Players[bm.humanIdx].Balance = bm.game.StartingCapital + 1000 // в большом плюсе, но раундов мало
|
||
|
||
finished, won := storyOutcomeBlackjack(bm)
|
||
if !finished {
|
||
t.Fatalf("после завершения раунда партия должна была считаться finished")
|
||
}
|
||
if won {
|
||
t.Errorf("меньше трёх раундов не должно засчитываться победой, даже при явном плюсе")
|
||
}
|
||
}
|
||
|
||
func TestStoryOutcomeBlackjackWinsWithThreeRoundsAndProfit(t *testing.T) {
|
||
bm := NewBlackjackModel(1, 500, 25)
|
||
for bm.game.Phase == BJPhasePlayerTurn {
|
||
bm.game.Stand()
|
||
}
|
||
bm.game.RoundsPlayed = 3
|
||
bm.game.Players[bm.humanIdx].Balance = bm.game.StartingCapital + 1
|
||
|
||
_, won := storyOutcomeBlackjack(bm)
|
||
if !won {
|
||
t.Errorf("три раунда и баланс выше стартового должны были засчитаться победой")
|
||
}
|
||
}
|
||
|
||
func TestStoryOutcomeBlackjackNoWinWithoutProfit(t *testing.T) {
|
||
bm := NewBlackjackModel(1, 500, 25)
|
||
for bm.game.Phase == BJPhasePlayerTurn {
|
||
bm.game.Stand()
|
||
}
|
||
bm.game.RoundsPlayed = 5
|
||
bm.game.Players[bm.humanIdx].Balance = bm.game.StartingCapital // ровно столько же, не "выше"
|
||
|
||
_, won := storyOutcomeBlackjack(bm)
|
||
if won {
|
||
t.Errorf("баланс не выше стартового (даже после многих раундов) не должен засчитываться победой")
|
||
}
|
||
}
|
||
|
||
func TestStoryOutcomeBlackjackNotFinishedMidRound(t *testing.T) {
|
||
bm := NewBlackjackModel(1, 500, 25)
|
||
bm.game.Phase = BJPhasePlayerTurn // принудительно, чтобы не зависеть от случайного натурального блэкджека с раздачи
|
||
finished, _ := storyOutcomeBlackjack(bm)
|
||
if finished {
|
||
t.Errorf("партия в процессе раунда не должна была считаться завершённой")
|
||
}
|
||
}
|
||
|
||
func TestTicTacToeDrawCountsAsStoryWin(t *testing.T) {
|
||
tm := NewTicTacToeModel(TicTacToeDifficultyEasy)
|
||
tm.game.Phase = TicTacToePhaseOver
|
||
tm.game.Result = &TicTacToeResult{Winner: TicTacToeEmpty} // ничья
|
||
finished, won := storyOutcomeTicTacToe(tm)
|
||
if !finished || !won {
|
||
t.Errorf("ничья должна была засчитаться как победа в сторимоде, получено finished=%v won=%v", finished, won)
|
||
}
|
||
}
|
||
|
||
func TestTicTacToeStoryTargetIsTen(t *testing.T) {
|
||
if StoryTargetTicTacToe != 10 {
|
||
t.Errorf("ожидалась цель 10 для крестиков-ноликов, получено %d", StoryTargetTicTacToe)
|
||
}
|
||
}
|
||
|
||
func TestEnterStoryHubFromModeSelectShowsMorningScreenAfterADay(t *testing.T) {
|
||
withTempConfigDir(t)
|
||
m := NewAppModel()
|
||
past := time.Now().Add(-25 * time.Hour)
|
||
m.story = NewStoryModeState(StoryDifficultyEasy, past)
|
||
m.story.Onboarding = StoryOnboardingDone
|
||
|
||
m = m.enterStoryHubFromModeSelect()
|
||
if m.state != stateStoryMorning {
|
||
t.Fatalf("после прошедших суток ожидался утренний экран, получено %v", m.state)
|
||
}
|
||
if m.storyGreeting != storyGreetingWokeUp {
|
||
t.Errorf("после ровно одних прошедших суток ожидалось приветствие storyGreetingWokeUp, получено %v", m.storyGreeting)
|
||
}
|
||
// возможные случайно сработавшие события (выброс/стук) не относятся
|
||
// к тому, что здесь проверяется — своя логика очереди уже
|
||
// покрыта отдельными тестами
|
||
m.story.PendingNeurotoxinAlert = false
|
||
m.story.PendingDoorKnockAlert = false
|
||
|
||
next, _ := m.Update(key(" "))
|
||
m2 := next.(AppModel)
|
||
if m2.state != stateStoryHub {
|
||
t.Errorf("после утреннего экрана ожидался переход в хаб, получено %v", m2.state)
|
||
}
|
||
}
|
||
|
||
func TestEnterStoryHubFromModeSelectSkipsGreetingDuringOnboarding(t *testing.T) {
|
||
withTempConfigDir(t)
|
||
m := NewAppModel()
|
||
m.story = NewStoryModeState(StoryDifficultyEasy, time.Now()) // по умолчанию ещё на этапе знакомства
|
||
|
||
m = m.enterStoryHubFromModeSelect()
|
||
if m.state != stateStoryHub {
|
||
t.Errorf("во время знакомства (ИИ ещё не представилась) приветствие дня показываться не должно, получено %v", m.state)
|
||
}
|
||
}
|
||
|
||
func TestEnterStoryHubFromModeSelectShowsGreetingAfterOnboarding(t *testing.T) {
|
||
withTempConfigDir(t)
|
||
m := NewAppModel()
|
||
m.story = NewStoryModeState(StoryDifficultyEasy, time.Now())
|
||
m.story.Onboarding = StoryOnboardingDone
|
||
|
||
m = m.enterStoryHubFromModeSelect()
|
||
if m.state != stateStoryMorning {
|
||
t.Errorf("после завершения знакомства вход через меню режимов должен был показывать одно из приветствий, получено %v", m.state)
|
||
}
|
||
if m.storyGreeting != storyGreetingPlayingAgain {
|
||
t.Errorf("в тот же день ожидалось приветствие storyGreetingPlayingAgain, получено %v", m.storyGreeting)
|
||
}
|
||
}
|
||
|
||
func TestEnterStoryHubAfterGameNeverShowsGreeting(t *testing.T) {
|
||
withTempConfigDir(t)
|
||
m := NewAppModel()
|
||
past := time.Now().Add(-50 * time.Hour) // прошло несколько суток
|
||
m.story = NewStoryModeState(StoryDifficultyEasy, past)
|
||
|
||
m = m.enterStoryHub()
|
||
if m.state != stateStoryHub {
|
||
t.Errorf("возврат из партии не должен показывать приветствие дня даже после нескольких прошедших суток, получено %v", m.state)
|
||
}
|
||
}
|
||
|
||
func TestStoryOutcomeTonkRequiresThreeRoundsAndMaxBalance(t *testing.T) {
|
||
tm := NewModel()
|
||
tm.game.Phase = PhaseGameOver
|
||
tm.game.RoundsPlayed = 2
|
||
tm.game.Players[tm.humanIdx].Balance = 999999 // в большом плюсе, но раундов мало
|
||
if _, won := storyOutcomeTonk(tm); won {
|
||
t.Errorf("меньше трёх раундов не должно засчитываться победой")
|
||
}
|
||
|
||
tm.game.RoundsPlayed = 3
|
||
for _, p := range tm.game.Players {
|
||
p.Balance = 0
|
||
}
|
||
tm.game.Players[tm.humanIdx].Balance = 100
|
||
if _, won := storyOutcomeTonk(tm); !won {
|
||
t.Errorf("три раунда и максимальный баланс должны были засчитаться победой")
|
||
}
|
||
}
|
||
|
||
func TestStoryOutcomeDurakStreakResetsOnLoss(t *testing.T) {
|
||
dm := NewDurakModel(4)
|
||
dm.storyNonFoolStreak = 2
|
||
dm.game.Result = &DurakResult{Loser: dm.humanIdx}
|
||
if _, won := storyOutcomeDurak(dm); won {
|
||
t.Errorf("поражение в решающей партии должно было сбросить серию, а не засчитать победу")
|
||
}
|
||
}
|
||
|
||
func TestStoryOutcomeDurakStreakCountsCurrentGame(t *testing.T) {
|
||
dm := NewDurakModel(4)
|
||
dm.storyNonFoolStreak = 2 // уже 2 подряд без дурака
|
||
other := (dm.humanIdx + 1) % len(dm.game.Players)
|
||
dm.game.Result = &DurakResult{Loser: other} // в этой партии дураком стал не человек
|
||
if _, won := storyOutcomeDurak(dm); !won {
|
||
t.Errorf("третья подряд партия без дурака должна была засчитаться победой")
|
||
}
|
||
}
|
||
|
||
func TestStoryOutcomeMinesweeperStreakResetsOnLoss(t *testing.T) {
|
||
mm := NewMinesweeperModel(9, 9, 10)
|
||
mm.storyWinStreak = 2
|
||
mm.game.Result = &MinesweeperResult{Won: false}
|
||
if _, won := storyOutcomeMinesweeper(mm); won {
|
||
t.Errorf("взрыв в решающей партии должен был сбросить серию, а не засчитать победу")
|
||
}
|
||
}
|
||
|
||
func TestStoryOutcomeMinesweeperStreakCountsCurrentGame(t *testing.T) {
|
||
mm := NewMinesweeperModel(9, 9, 10)
|
||
mm.storyWinStreak = 2
|
||
mm.game.Result = &MinesweeperResult{Won: true}
|
||
if _, won := storyOutcomeMinesweeper(mm); !won {
|
||
t.Errorf("третья подряд победа без взрыва должна была засчитаться")
|
||
}
|
||
}
|
||
|
||
func TestStoryOutcomeOneOhOneRequiresMatchOverNotJustRound(t *testing.T) {
|
||
om := NewOneOhOneModel(3)
|
||
om.game.Phase = OneOhOnePhaseRoundOver
|
||
om.game.Result = &OneOhOneRoundResult{Winner: om.humanIdx, MatchOver: false}
|
||
if finished, _ := storyOutcomeOneOhOne(om); finished {
|
||
t.Errorf("отдельный раунд не должен засчитываться завершением партии — нужен весь матч")
|
||
}
|
||
|
||
om.game.Result.MatchOver = true
|
||
om.game.Result.MatchWinner = om.humanIdx
|
||
if finished, won := storyOutcomeOneOhOne(om); !finished || !won {
|
||
t.Errorf("выигранный матч должен был засчитаться победой, получено finished=%v won=%v", finished, won)
|
||
}
|
||
}
|
||
|
||
func TestStoryOutcomeThousandRequiresThreeHandsAndTopScore(t *testing.T) {
|
||
tm := NewThousandModel()
|
||
tm.game.Phase = ThousandPhaseHandOver
|
||
tm.game.HandNumber = 2
|
||
tm.game.Players[tm.humanIdx].Score = 999999
|
||
if _, won := storyOutcomeThousand(tm); won {
|
||
t.Errorf("меньше трёх раздач не должно засчитываться победой")
|
||
}
|
||
|
||
tm.game.HandNumber = 3
|
||
for _, p := range tm.game.Players {
|
||
p.Score = 0
|
||
}
|
||
tm.game.Players[tm.humanIdx].Score = 50
|
||
if _, won := storyOutcomeThousand(tm); !won {
|
||
t.Errorf("три раздачи и первое место по очкам должны были засчитаться победой")
|
||
}
|
||
}
|
||
|
||
func TestStoryOutcomeKlondikeJustReportsWinFlag(t *testing.T) {
|
||
km := NewKlondikeModel()
|
||
km.game.Won = false
|
||
if finished, _ := storyOutcomeKlondike(km); finished {
|
||
t.Errorf("незавершённая партия не должна была считаться выигранной")
|
||
}
|
||
km.game.Won = true
|
||
if finished, won := storyOutcomeKlondike(km); !finished || !won {
|
||
t.Errorf("выигранная партия должна была вернуть finished=true won=true")
|
||
}
|
||
}
|
||
|
||
func TestKlondikeDailyCounterAwardsResourceOnThirdWinOfDay(t *testing.T) {
|
||
withTempConfigDir(t)
|
||
m := NewAppModel()
|
||
m.story = NewStoryModeState(StoryDifficultyEasy, time.Now())
|
||
|
||
km := NewKlondikeModel()
|
||
km.game.Won = true
|
||
km.backToMenu = true
|
||
|
||
for i := 0; i < 3; i++ {
|
||
resource := StoryResKlondike
|
||
m.storyActiveResource = &resource
|
||
m.activeGame = km
|
||
m.state = statePlaying
|
||
next, _ := m.Update(key("q"))
|
||
m = next.(AppModel)
|
||
}
|
||
|
||
if m.story.Resources[StoryResKlondike] != storyStartingAmount+1 {
|
||
t.Errorf("после третьей победы за день ожидалось начисление ровно +1 ресурса, получено %d (было %d)",
|
||
m.story.Resources[StoryResKlondike], storyStartingAmount)
|
||
}
|
||
if m.story.KlondikeWinsToday != 3 {
|
||
t.Errorf("ожидался счётчик побед за день = 3, получено %d", m.story.KlondikeWinsToday)
|
||
}
|
||
}
|
||
|
||
func TestStoryOutcomePokerRequiresTournamentOverAndNotEliminated(t *testing.T) {
|
||
pm := NewPokerModel(4)
|
||
if finished, _ := storyOutcomePoker(pm); finished {
|
||
t.Errorf("турнир в процессе не должен считаться завершённым")
|
||
}
|
||
|
||
pm.game.Phase = PokerPhaseTournamentOver
|
||
pm.game.Players[pm.humanSeat].Eliminated = false
|
||
if finished, won := storyOutcomePoker(pm); !finished || !won {
|
||
t.Errorf("невыбывший человек при завершении турнира должен был засчитаться победителем")
|
||
}
|
||
|
||
pm.game.Players[pm.humanSeat].Eliminated = true
|
||
if _, won := storyOutcomePoker(pm); won {
|
||
t.Errorf("выбывший человек не должен был засчитаться победителем турнира")
|
||
}
|
||
}
|
||
|
||
func TestExistingSaveSkipsWelcomeScreen(t *testing.T) {
|
||
withTempConfigDir(t)
|
||
pre := NewStoryModeState(StoryDifficultyEasy, time.Now())
|
||
pre.Onboarding = StoryOnboardingDone
|
||
if err := SaveStoryModeState(pre); err != nil {
|
||
t.Fatalf("неожиданная ошибка сохранения: %v", err)
|
||
}
|
||
|
||
m := NewAppModel()
|
||
next, _ := m.Update(key("enter")) // язык -> выбор режима
|
||
m = next.(AppModel)
|
||
next, _ = m.Update(key("down")) // курсор на "Режим истории"
|
||
m = next.(AppModel)
|
||
next, _ = m.Update(key("enter"))
|
||
m = next.(AppModel)
|
||
|
||
if m.state != stateStoryMorning {
|
||
t.Fatalf("ожидалось ежедневное приветствие (не одноразовое приветствие ИИ), получено %v", m.state)
|
||
}
|
||
if m.state == stateStoryWelcome {
|
||
t.Fatalf("одноразовое приветствие ИИ не должно было показаться повторно")
|
||
}
|
||
}
|
||
|
||
func TestStoryConditionsScreenListsAllGames(t *testing.T) {
|
||
withTempConfigDir(t)
|
||
m := NewAppModel()
|
||
m.story = NewStoryModeState(StoryDifficultyEasy, time.Now())
|
||
m.story.Onboarding = StoryOnboardingDone // клавиша c недоступна во время знакомства
|
||
m.state = stateStoryHub
|
||
|
||
next, _ := m.Update(key("c"))
|
||
m = next.(AppModel)
|
||
if m.state != stateStoryConditions {
|
||
t.Fatalf("клавиша c должна была открыть экран условий, получено %v", m.state)
|
||
}
|
||
view := m.View()
|
||
for _, id := range storyHubOrder {
|
||
resDef := storyResourceDefs[id]
|
||
if !strings.Contains(view, T(resDef.NameKey)) {
|
||
t.Errorf("экран условий должен был упоминать %q", T(resDef.NameKey))
|
||
}
|
||
}
|
||
|
||
next, _ = m.Update(key("esc"))
|
||
m = next.(AppModel)
|
||
if m.state != stateStoryHub {
|
||
t.Errorf("любая клавиша на экране условий должна была вернуть в хаб, получено %v", m.state)
|
||
}
|
||
}
|
||
|
||
func TestStoryEscapeOptionHiddenUntilConditionMet(t *testing.T) {
|
||
withTempConfigDir(t)
|
||
m := NewAppModel()
|
||
m.story = NewStoryModeState(StoryDifficultyEasy, time.Now())
|
||
m.state = stateStoryHub
|
||
|
||
if m.storyEscapeUnlocked() {
|
||
t.Fatalf("условие побега не должно было выполниться при стартовых ресурсах")
|
||
}
|
||
view := m.View()
|
||
if strings.Contains(view, T("story.hub.escape_option")) {
|
||
t.Errorf("пункт побега не должен был показываться до выполнения условия")
|
||
}
|
||
}
|
||
|
||
func TestStoryEscapeRequiresAllThreeConditions(t *testing.T) {
|
||
withTempConfigDir(t)
|
||
m := NewAppModel()
|
||
m.story = NewStoryModeState(StoryDifficultyEasy, time.Now())
|
||
m.story.Onboarding = StoryOnboardingDone
|
||
|
||
// стресс высокий, но запасов не хватает — не должно сработать
|
||
m.story.Stress = 60
|
||
if m.storyEscapeUnlocked() {
|
||
t.Errorf("побег не должен был разблокироваться без достаточных запасов мыла/верёвок, даже при высоком стрессе")
|
||
}
|
||
|
||
// запасов хватает, но стресс низкий — тоже не должно сработать
|
||
m.story.Stress = 20
|
||
m.story.Resources[StoryResTicTacToe] = 50
|
||
m.story.Resources[StoryResSenet] = 50
|
||
if m.storyEscapeUnlocked() {
|
||
t.Errorf("побег не должен был разблокироваться при низком стрессе, даже с достаточными запасами")
|
||
}
|
||
|
||
// все три условия разом — теперь должно сработать
|
||
m.story.Stress = 60
|
||
if !m.storyEscapeUnlocked() {
|
||
t.Errorf("побег должен был разблокироваться при выполнении всех трёх условий разом")
|
||
}
|
||
}
|
||
|
||
func TestStoryEscapeStillLockedBelowStressThreshold(t *testing.T) {
|
||
withTempConfigDir(t)
|
||
m := NewAppModel()
|
||
m.story = NewStoryModeState(StoryDifficultyEasy, time.Now())
|
||
m.story.Onboarding = StoryOnboardingDone
|
||
m.story.Resources[StoryResTicTacToe] = 50
|
||
m.story.Resources[StoryResSenet] = 50
|
||
m.story.Stress = 59 // чуть ниже порога, запасов уже достаточно
|
||
|
||
if m.storyEscapeUnlocked() {
|
||
t.Errorf("побег не должен был разблокироваться при стрессе ниже 60%%, даже с достаточными запасами")
|
||
}
|
||
}
|
||
|
||
func TestStoryEscapeOptionAppearsAndLeadsToAttemptScreen(t *testing.T) {
|
||
withTempConfigDir(t)
|
||
m := NewAppModel()
|
||
m.story = NewStoryModeState(StoryDifficultyEasy, time.Now())
|
||
m.story.Onboarding = StoryOnboardingDone // пункт побега скрыт во время знакомства
|
||
m.story.Resources[StoryResTicTacToe] = 50
|
||
m.story.Resources[StoryResSenet] = 50
|
||
m.story.Stress = 60
|
||
m.state = stateStoryHub
|
||
m.storyHubCursor = len(storyHubOrder) // дополнительный пункт в конце списка
|
||
|
||
if !m.storyEscapeUnlocked() {
|
||
t.Fatalf("условие побега должно было выполниться при 50/50/60")
|
||
}
|
||
view := m.View()
|
||
if !strings.Contains(view, T("story.hub.escape_option")) {
|
||
t.Errorf("пункт побега должен был появиться в хабе")
|
||
}
|
||
|
||
next, _ := m.Update(key("enter"))
|
||
m = next.(AppModel)
|
||
if m.state != stateStoryEscapeAttempt {
|
||
t.Fatalf("выбор пункта побега должен был вести на экран уговоров, получено %v", m.state)
|
||
}
|
||
}
|
||
|
||
func TestStoryEscapeAttemptTakingPillResetsStressAndReturnsToHub(t *testing.T) {
|
||
withTempConfigDir(t)
|
||
m := AppModel{state: stateStoryEscapeAttempt, story: NewStoryModeState(StoryDifficultyEasy, time.Now())}
|
||
m.story.Stress = 80
|
||
next, _ := m.Update(key("1"))
|
||
m = next.(AppModel)
|
||
if m.state != stateStoryHub {
|
||
t.Errorf("выбор таблетки должен был вернуть в хаб, получено %v", m.state)
|
||
}
|
||
if m.story.Stress != 0 {
|
||
t.Errorf("таблетка должна была сбросить стресс до 0, получено %d", m.story.Stress)
|
||
}
|
||
}
|
||
|
||
func TestStoryEscapeAttemptEscReturnsToHubWithoutTakingPill(t *testing.T) {
|
||
m := AppModel{state: stateStoryEscapeAttempt, story: NewStoryModeState(StoryDifficultyEasy, time.Now())}
|
||
m.story.Stress = 80
|
||
next, _ := m.Update(key("esc"))
|
||
m = next.(AppModel)
|
||
if m.state != stateStoryHub {
|
||
t.Errorf("esc должен был вернуть в хаб, получено %v", m.state)
|
||
}
|
||
if m.story.Stress != 80 {
|
||
t.Errorf("esc не должен был трогать стресс, получено %d", m.story.Stress)
|
||
}
|
||
}
|
||
|
||
func TestStoryEscapeAttemptLeavingGoesToEnding(t *testing.T) {
|
||
m := AppModel{state: stateStoryEscapeAttempt}
|
||
next, _ := m.Update(key("2"))
|
||
m = next.(AppModel)
|
||
if m.state != stateStoryEscapeEnding {
|
||
t.Errorf("выбор 'сбежать' должен был вести на экран концовки, получено %v", m.state)
|
||
}
|
||
}
|
||
|
||
func TestStoryEscapeEndingReturnsToModeSelect(t *testing.T) {
|
||
m := AppModel{state: stateStoryEscapeEnding}
|
||
next, _ := m.Update(key(" "))
|
||
m = next.(AppModel)
|
||
if m.state != stateModeSelect {
|
||
t.Errorf("экран концовки должен был вести в меню режимов, получено %v", m.state)
|
||
}
|
||
}
|
||
|
||
func TestFreshStoryStartsOnboardingWithTicTacToeOnly(t *testing.T) {
|
||
withTempConfigDir(t)
|
||
m := NewAppModel()
|
||
m.state = stateStoryDifficulty
|
||
next, _ := m.Update(key("enter"))
|
||
m = next.(AppModel)
|
||
|
||
if m.state != stateStoryHub {
|
||
t.Fatalf("ожидался прямой переход в хаб, получено %v", m.state)
|
||
}
|
||
if m.story.Onboarding != StoryOnboardingTicTacToeOnly {
|
||
t.Fatalf("ожидался этап знакомства TicTacToeOnly, получено %v", m.story.Onboarding)
|
||
}
|
||
visible := m.storyVisibleGames()
|
||
if len(visible) != 1 || visible[0] != StoryResTicTacToe {
|
||
t.Fatalf("на этапе знакомства должны были быть видны только крестики-нолики, получено %v", visible)
|
||
}
|
||
|
||
view := m.View()
|
||
if strings.Contains(view, "/10") || strings.Contains(view, "Сложность:") {
|
||
t.Errorf("на этапе знакомства не должно быть чисел ресурсов или строки сложности, получено:\n%s", view)
|
||
}
|
||
if strings.Contains(view, T(storyGameDefs[StoryResCheckers].NameKey)) {
|
||
t.Errorf("шашки не должны быть видны на самом первом этапе знакомства")
|
||
}
|
||
}
|
||
|
||
func TestOnboardingShowsGameNameNotResourceName(t *testing.T) {
|
||
withTempConfigDir(t)
|
||
m := NewAppModel()
|
||
m.story = NewStoryModeState(StoryDifficultyEasy, time.Now())
|
||
m.state = stateStoryHub
|
||
|
||
view := m.View()
|
||
if !strings.Contains(view, T("game.tictactoe.name")) {
|
||
t.Errorf("на этапе знакомства должно было показываться чистое название игры %q, получено:\n%s", T("game.tictactoe.name"), view)
|
||
}
|
||
if strings.Contains(view, T(storyResourceDefs[StoryResTicTacToe].NameKey)) {
|
||
t.Errorf("на этапе знакомства не должно было показываться название ресурса (%q) — оно ещё ничего не значит для игрока, получено:\n%s", T(storyResourceDefs[StoryResTicTacToe].NameKey), view)
|
||
}
|
||
}
|
||
|
||
func TestThreeTicTacToeWinsUnlockCheckers(t *testing.T) {
|
||
withTempConfigDir(t)
|
||
m := NewAppModel()
|
||
m.story = NewStoryModeState(StoryDifficultyEasy, time.Now())
|
||
m.state = stateStoryHub
|
||
|
||
tm := NewTicTacToeModel(TicTacToeDifficultyEasy)
|
||
tm.game.Phase = TicTacToePhaseOver
|
||
tm.game.Result = &TicTacToeResult{Winner: tm.game.HumanMark}
|
||
tm.backToMenu = true
|
||
|
||
for i := 0; i < 3; i++ {
|
||
resource := StoryResTicTacToe
|
||
m.storyActiveResource = &resource
|
||
m.activeGame = tm
|
||
m.state = statePlaying
|
||
next, _ := m.Update(key("q"))
|
||
m = next.(AppModel)
|
||
}
|
||
|
||
if m.story.Onboarding != StoryOnboardingWithCheckers {
|
||
t.Fatalf("после 3 побед ожидался переход на этап WithCheckers, получено %v", m.story.Onboarding)
|
||
}
|
||
if m.state != stateStoryHub {
|
||
t.Fatalf("ожидался возврат в хаб (не приветствие) — знакомство ещё не завершено, получено %v", m.state)
|
||
}
|
||
visible := m.storyVisibleGames()
|
||
if len(visible) != 2 {
|
||
t.Fatalf("ожидалось 2 видимые игры (крестики-нолики + шашки), получено %v", visible)
|
||
}
|
||
}
|
||
|
||
func TestOneCheckersWinCompletesOnboardingAndShowsWelcome(t *testing.T) {
|
||
withTempConfigDir(t)
|
||
m := NewAppModel()
|
||
m.story = NewStoryModeState(StoryDifficultyEasy, time.Now())
|
||
m.story.Onboarding = StoryOnboardingWithCheckers
|
||
m.story.Resources[StoryResTicTacToe] = storyStartingAmount + StoryOnboardingTicTacToeWinsNeeded
|
||
m.state = stateStoryHub
|
||
|
||
cm := NewCheckersModel(0, ColorChoiceA)
|
||
cm.game.Result = &CheckersResult{Winner: cm.human}
|
||
cm.backToMenu = true
|
||
|
||
resource := StoryResCheckers
|
||
m.storyActiveResource = &resource
|
||
m.activeGame = cm
|
||
m.state = statePlaying
|
||
next, _ := m.Update(key("q"))
|
||
m = next.(AppModel)
|
||
|
||
if m.story.Onboarding != StoryOnboardingDone {
|
||
t.Fatalf("после победы в шашках знакомство должно было завершиться, получено %v", m.story.Onboarding)
|
||
}
|
||
if m.state != stateStoryWelcome {
|
||
t.Fatalf("ровно в момент завершения знакомства ожидалось приветствие, получено %v", m.state)
|
||
}
|
||
|
||
next, _ = m.Update(key(" ")) // показать текст полностью (пропустить печать)
|
||
m = next.(AppModel)
|
||
next, _ = m.Update(key(" ")) // продолжить в хаб
|
||
m = next.(AppModel)
|
||
if m.state != stateStoryHub {
|
||
t.Fatalf("после приветствия ожидался переход в полный хаб, получено %v", m.state)
|
||
}
|
||
view := m.View()
|
||
if !strings.Contains(view, "Сложность:") {
|
||
t.Errorf("после завершения знакомства хаб должен был показывать обычный вид со строкой сложности, получено:\n%s", view)
|
||
}
|
||
visible := m.storyVisibleGames()
|
||
if len(visible) != len(storyHubOrder) {
|
||
t.Errorf("после завершения знакомства должны были стать видны все игры, получено %d из %d", len(visible), len(storyHubOrder))
|
||
}
|
||
}
|
||
|
||
func TestWelcomeScreenNeverShownAgainAfterOnboarding(t *testing.T) {
|
||
withTempConfigDir(t)
|
||
s := NewStoryModeState(StoryDifficultyEasy, time.Now())
|
||
s.Onboarding = StoryOnboardingDone
|
||
if err := SaveStoryModeState(s); err != nil {
|
||
t.Fatalf("неожиданная ошибка сохранения: %v", err)
|
||
}
|
||
|
||
m := NewAppModel()
|
||
next, _ := m.Update(key("enter")) // язык -> выбор режима
|
||
m = next.(AppModel)
|
||
next, _ = m.Update(key("down")) // курсор на "Режим истории"
|
||
m = next.(AppModel)
|
||
next, _ = m.Update(key("enter"))
|
||
m = next.(AppModel)
|
||
|
||
if m.state != stateStoryMorning {
|
||
t.Fatalf("при завершённом знакомстве и существующем сохранении ожидалось ежедневное приветствие, получено %v", m.state)
|
||
}
|
||
}
|
||
|
||
func TestStoryRevealPartialTextPreservesParagraphs(t *testing.T) {
|
||
text := "Раз два три.\n\nЧетыре пять шесть семь."
|
||
// первые 3 слова — только первый абзац целиком, второй ещё не начат
|
||
got := storyRevealPartialText(text, 3)
|
||
if got != "Раз два три." {
|
||
t.Errorf("ожидался только первый абзац целиком, получено %q", got)
|
||
}
|
||
// 3+2=5 слов — первый абзац целиком плюс первые 2 слова второго
|
||
got = storyRevealPartialText(text, 5)
|
||
if got != "Раз два три.\n\nЧетыре пять" {
|
||
t.Errorf("ожидался первый абзац целиком плюс частичный второй, получено %q", got)
|
||
}
|
||
// все слова — весь текст целиком, без изменений
|
||
got = storyRevealPartialText(text, storyTotalWords(text))
|
||
if got != text {
|
||
t.Errorf("при показе всех слов текст должен был совпасть с оригиналом, получено %q, ожидалось %q", got, text)
|
||
}
|
||
}
|
||
|
||
func TestStoryRevealPartialTextZeroWordsIsEmpty(t *testing.T) {
|
||
if got := storyRevealPartialText("Раз два три", 0); got != "" {
|
||
t.Errorf("при 0 слов ожидалась пустая строка, получено %q", got)
|
||
}
|
||
}
|
||
|
||
func TestStoryWelcomeTicksRevealWordsGradually(t *testing.T) {
|
||
withTempConfigDir(t)
|
||
m := AppModel{state: stateStoryWelcome, storyWelcomeWordsShown: 0}
|
||
total := storyTotalWords(T("story.welcome.text"))
|
||
|
||
next, cmd := m.Update(storyWelcomeTickMsg{})
|
||
m = next.(AppModel)
|
||
if m.storyWelcomeWordsShown != 1 {
|
||
t.Fatalf("после одного тика ожидалось показанное 1 слово, получено %d", m.storyWelcomeWordsShown)
|
||
}
|
||
if cmd == nil {
|
||
t.Errorf("должен был запланироваться следующий тик — текст ещё не показан целиком (всего слов: %d)", total)
|
||
}
|
||
}
|
||
|
||
func TestStoryWelcomeTicksStopAtTotalWords(t *testing.T) {
|
||
m := AppModel{state: stateStoryWelcome}
|
||
total := storyTotalWords(T("story.welcome.text"))
|
||
m.storyWelcomeWordsShown = total - 1
|
||
|
||
next, cmd := m.Update(storyWelcomeTickMsg{})
|
||
m = next.(AppModel)
|
||
if m.storyWelcomeWordsShown != total {
|
||
t.Fatalf("ожидалось, что счётчик достигнет total=%d, получено %d", total, m.storyWelcomeWordsShown)
|
||
}
|
||
if cmd != nil {
|
||
t.Errorf("после показа всего текста дальнейший тик планироваться не должен")
|
||
}
|
||
}
|
||
|
||
func TestStoryWelcomeKeyPressSkipsTypingFirst(t *testing.T) {
|
||
m := AppModel{state: stateStoryWelcome, storyWelcomeWordsShown: 2}
|
||
total := storyTotalWords(T("story.welcome.text"))
|
||
|
||
next, _ := m.Update(key(" "))
|
||
m = next.(AppModel)
|
||
if m.state != stateStoryWelcome {
|
||
t.Fatalf("первое нажатие во время печати должно было остаться на экране приветствия, получено %v", m.state)
|
||
}
|
||
if m.storyWelcomeWordsShown != total {
|
||
t.Errorf("первое нажатие должно было сразу показать весь текст, получено %d из %d слов", m.storyWelcomeWordsShown, total)
|
||
}
|
||
}
|
||
|
||
func TestStoryWelcomeKeyPressContinuesWhenFullyShown(t *testing.T) {
|
||
withTempConfigDir(t)
|
||
total := storyTotalWords(T("story.welcome.text"))
|
||
m := AppModel{state: stateStoryWelcome, storyWelcomeWordsShown: total, story: NewStoryModeState(StoryDifficultyEasy, time.Now())}
|
||
|
||
next, _ := m.Update(key(" "))
|
||
m = next.(AppModel)
|
||
if m.state != stateStoryHub {
|
||
t.Errorf("нажатие после полного показа текста должно было продолжить в хаб, получено %v", m.state)
|
||
}
|
||
}
|
||
|
||
func TestStoryWelcomeViewShowsTypingHintWhileIncomplete(t *testing.T) {
|
||
m := AppModel{state: stateStoryWelcome, storyWelcomeWordsShown: 1}
|
||
view := m.viewStoryWelcome()
|
||
if !strings.Contains(view, T("story.welcome.hint.typing")) {
|
||
t.Errorf("во время печати должна была показываться подсказка 'показать сразу', получено:\n%s", view)
|
||
}
|
||
}
|
||
|
||
func TestStoryWelcomeViewShowsContinueHintWhenComplete(t *testing.T) {
|
||
total := storyTotalWords(T("story.welcome.text"))
|
||
m := AppModel{state: stateStoryWelcome, storyWelcomeWordsShown: total}
|
||
view := m.viewStoryWelcome()
|
||
if !strings.Contains(view, T("story.welcome.hint")) || strings.Contains(view, T("story.welcome.hint.typing")) {
|
||
t.Errorf("после полного показа текста должна была показываться обычная подсказка 'продолжить', получено:\n%s", view)
|
||
}
|
||
}
|
||
|
||
func TestStoryHubShowsVitalsWhenOnboardingDone(t *testing.T) {
|
||
withTempConfigDir(t)
|
||
m := NewAppModel()
|
||
m.story = NewStoryModeState(StoryDifficultyEasy, time.Now())
|
||
m.story.Onboarding = StoryOnboardingDone
|
||
m.story.Stress = 45
|
||
m.story.Poisoning = 10
|
||
m.story.ToxinLevel = 5
|
||
m.state = stateStoryHub
|
||
|
||
view := m.View()
|
||
if !strings.Contains(view, "45") || !strings.Contains(view, "10") || !strings.Contains(view, "5") {
|
||
t.Errorf("хаб должен был показывать значения стресса/отравления/токсина, получено:\n%s", view)
|
||
}
|
||
}
|
||
|
||
func TestStoryHubHidesVitalsDuringOnboarding(t *testing.T) {
|
||
withTempConfigDir(t)
|
||
m := NewAppModel()
|
||
m.story = NewStoryModeState(StoryDifficultyEasy, time.Now())
|
||
m.state = stateStoryHub
|
||
|
||
view := m.View()
|
||
if strings.Contains(view, T("story.hub.difficulty_label")) {
|
||
t.Errorf("на этапе знакомства показатели выживания не должны показываться")
|
||
}
|
||
}
|
||
|
||
func TestStoryHubUKeyOpensUseItemScreen(t *testing.T) {
|
||
withTempConfigDir(t)
|
||
m := NewAppModel()
|
||
m.story = NewStoryModeState(StoryDifficultyEasy, time.Now())
|
||
m.story.Onboarding = StoryOnboardingDone
|
||
m.state = stateStoryHub
|
||
|
||
next, _ := m.Update(key("u"))
|
||
m = next.(AppModel)
|
||
if m.state != stateStoryUseItem {
|
||
t.Fatalf("клавиша u должна была открыть экран использования, получено %v", m.state)
|
||
}
|
||
}
|
||
|
||
func TestStoryHubUKeyDisabledDuringOnboarding(t *testing.T) {
|
||
withTempConfigDir(t)
|
||
m := NewAppModel()
|
||
m.story = NewStoryModeState(StoryDifficultyEasy, time.Now())
|
||
m.state = stateStoryHub
|
||
|
||
next, _ := m.Update(key("u"))
|
||
m = next.(AppModel)
|
||
if m.state != stateStoryHub {
|
||
t.Errorf("клавиша u не должна была работать на этапе знакомства, получено %v", m.state)
|
||
}
|
||
}
|
||
|
||
func TestStoryUseItemSpendsResourceAndAppliesEffect(t *testing.T) {
|
||
withTempConfigDir(t)
|
||
m := NewAppModel()
|
||
m.story = NewStoryModeState(StoryDifficultyEasy, time.Now())
|
||
m.story.Stress = 40
|
||
m.story.Resources[StoryResKlondike] = 5
|
||
m.state = stateStoryUseItem
|
||
|
||
next, _ := m.Update(key("1")) // антидепрессант — первый пункт
|
||
m = next.(AppModel)
|
||
if m.story.Stress != 20 {
|
||
t.Errorf("ожидался стресс 20 после траты антидепрессанта, получено %d", m.story.Stress)
|
||
}
|
||
if m.story.Resources[StoryResKlondike] != 4 {
|
||
t.Errorf("ожидалось списание 1 антидепрессанта, получено %d", m.story.Resources[StoryResKlondike])
|
||
}
|
||
if m.state != stateStoryUseItem {
|
||
t.Errorf("после использования должны были остаться на этом же экране, получено %v", m.state)
|
||
}
|
||
}
|
||
|
||
func TestStoryUseItemEscReturnsToHub(t *testing.T) {
|
||
m := AppModel{state: stateStoryUseItem}
|
||
next, _ := m.Update(key("esc"))
|
||
m = next.(AppModel)
|
||
if m.state != stateStoryHub {
|
||
t.Errorf("esc на экране использования должен был вернуть в хаб, получено %v", m.state)
|
||
}
|
||
}
|
||
|
||
func TestStoryGreetingForDaysMapping(t *testing.T) {
|
||
cases := []struct {
|
||
days int
|
||
want storyGreetingKind
|
||
}{
|
||
{0, storyGreetingPlayingAgain},
|
||
{1, storyGreetingWokeUp},
|
||
{2, storyGreetingWorried},
|
||
{5, storyGreetingWorried},
|
||
}
|
||
for _, c := range cases {
|
||
if got := storyGreetingForDays(c.days); got != c.want {
|
||
t.Errorf("для %d дней ожидалось %v, получено %v", c.days, c.want, got)
|
||
}
|
||
}
|
||
}
|
||
|
||
func TestRandomStoryPostGameFlavorReturnsValidKey(t *testing.T) {
|
||
valid := make(map[string]bool)
|
||
for _, k := range storyPostGameFlavorKeys {
|
||
valid[k] = true
|
||
}
|
||
for i := 0; i < 30; i++ {
|
||
got := randomStoryPostGameFlavor()
|
||
if !valid[got] {
|
||
t.Fatalf("randomStoryPostGameFlavor вернул неизвестный ключ %q", got)
|
||
}
|
||
}
|
||
}
|
||
|
||
func TestPostGameFlavorPoolHasTwelveDistinctPhrases(t *testing.T) {
|
||
if len(storyPostGameFlavorKeys) != 12 {
|
||
t.Fatalf("ожидалось 12 фраз в пуле, получено %d", len(storyPostGameFlavorKeys))
|
||
}
|
||
seen := make(map[string]bool)
|
||
for _, k := range storyPostGameFlavorKeys {
|
||
text := T(k)
|
||
if text == "" || text == k {
|
||
t.Errorf("ключ %q не имеет перевода", k)
|
||
}
|
||
if seen[text] {
|
||
t.Errorf("повторяющийся текст фразы: %q", text)
|
||
}
|
||
seen[text] = true
|
||
}
|
||
}
|
||
|
||
func TestPostGameFlavorShownAfterReturningFromGame(t *testing.T) {
|
||
withTempConfigDir(t)
|
||
m := NewAppModel()
|
||
m.story = NewStoryModeState(StoryDifficultyEasy, time.Now())
|
||
m.story.Onboarding = StoryOnboardingDone
|
||
|
||
tm := NewTicTacToeModel(TicTacToeDifficultyEasy)
|
||
tm.game.Phase = TicTacToePhaseOver
|
||
loser := TicTacToeX
|
||
if tm.game.HumanMark == TicTacToeX {
|
||
loser = TicTacToeO
|
||
}
|
||
tm.game.Result = &TicTacToeResult{Winner: loser}
|
||
tm.backToMenu = true
|
||
|
||
resource := StoryResTicTacToe
|
||
m.storyActiveResource = &resource
|
||
m.activeGame = tm
|
||
m.state = statePlaying
|
||
next, _ := m.Update(key("q"))
|
||
m = next.(AppModel)
|
||
|
||
if m.storyPostGameFlavor == "" {
|
||
t.Fatalf("после возврата из партии ожидалась выбранная фраза-наблюдение")
|
||
}
|
||
view := m.View()
|
||
if !strings.Contains(view, T(m.storyPostGameFlavor)) {
|
||
t.Errorf("хаб должен был показывать выбранную фразу, получено:\n%s", view)
|
||
}
|
||
}
|
||
|
||
func TestStoryHubNKeyShowsResetConfirmation(t *testing.T) {
|
||
withTempConfigDir(t)
|
||
m := NewAppModel()
|
||
m.story = NewStoryModeState(StoryDifficultyEasy, time.Now())
|
||
m.story.Onboarding = StoryOnboardingDone
|
||
m.state = stateStoryHub
|
||
|
||
next, _ := m.Update(key("N"))
|
||
m = next.(AppModel)
|
||
if !m.storyConfirmingReset {
|
||
t.Fatalf("клавиша N должна была включить режим подтверждения сброса")
|
||
}
|
||
view := m.View()
|
||
if !strings.Contains(view, T("story.hub.reset_confirm")) {
|
||
t.Errorf("ожидался текст подтверждения сброса, получено:\n%s", view)
|
||
}
|
||
}
|
||
|
||
func TestStoryHubResetConfirmYDeletesSaveAndGoesToDifficulty(t *testing.T) {
|
||
withTempConfigDir(t)
|
||
m := NewAppModel()
|
||
m.story = NewStoryModeState(StoryDifficultyHard, time.Now())
|
||
m.story.Resources[StoryResChess] = 50
|
||
if err := SaveStoryModeState(m.story); err != nil {
|
||
t.Fatalf("неожиданная ошибка сохранения: %v", err)
|
||
}
|
||
m.state = stateStoryHub
|
||
m.storyConfirmingReset = true
|
||
|
||
next, _ := m.Update(key("y"))
|
||
m = next.(AppModel)
|
||
if m.state != stateStoryDifficulty {
|
||
t.Fatalf("после подтверждения ожидался переход на выбор сложности, получено %v", m.state)
|
||
}
|
||
if m.storyConfirmingReset {
|
||
t.Errorf("режим подтверждения должен был сброситься")
|
||
}
|
||
if _, err := LoadStoryModeState(); err != ErrStoryModeNoSave {
|
||
t.Errorf("сохранение должно было быть удалено, получено err=%v", err)
|
||
}
|
||
}
|
||
|
||
func TestStoryHubResetConfirmNCancelsAndKeepsProgress(t *testing.T) {
|
||
withTempConfigDir(t)
|
||
m := NewAppModel()
|
||
m.story = NewStoryModeState(StoryDifficultyEasy, time.Now())
|
||
m.story.Resources[StoryResChess] = 77
|
||
m.state = stateStoryHub
|
||
m.storyConfirmingReset = true
|
||
|
||
next, _ := m.Update(key("n"))
|
||
m = next.(AppModel)
|
||
if m.storyConfirmingReset {
|
||
t.Errorf("отмена должна была выключить режим подтверждения")
|
||
}
|
||
if m.state != stateStoryHub {
|
||
t.Errorf("после отмены ожидался возврат в хаб, получено %v", m.state)
|
||
}
|
||
if m.story.Resources[StoryResChess] != 77 {
|
||
t.Errorf("прогресс не должен был пострадать при отмене, получено %d", m.story.Resources[StoryResChess])
|
||
}
|
||
}
|
||
|
||
func TestStoryHubResetConfirmEscCancels(t *testing.T) {
|
||
m := AppModel{state: stateStoryHub, storyConfirmingReset: true, story: NewStoryModeState(StoryDifficultyEasy, time.Now())}
|
||
next, _ := m.Update(key("esc"))
|
||
m = next.(AppModel)
|
||
if m.storyConfirmingReset {
|
||
t.Errorf("esc должен был отменить подтверждение сброса")
|
||
}
|
||
}
|
||
|
||
func TestStoryHubResetAvailableDuringOnboarding(t *testing.T) {
|
||
withTempConfigDir(t)
|
||
m := NewAppModel()
|
||
m.story = NewStoryModeState(StoryDifficultyEasy, time.Now()) // по умолчанию этап знакомства
|
||
m.state = stateStoryHub
|
||
|
||
next, _ := m.Update(key("N"))
|
||
m = next.(AppModel)
|
||
if !m.storyConfirmingReset {
|
||
t.Errorf("сброс должен быть доступен и во время знакомства, получено storyConfirmingReset=%v", m.storyConfirmingReset)
|
||
}
|
||
}
|
||
|
||
func TestAdvanceStoryEventQueueOrdering(t *testing.T) {
|
||
m := AppModel{story: NewStoryModeState(StoryDifficultyEasy, time.Now())}
|
||
|
||
m.story.PendingNeurotoxinAlert = true
|
||
m.story.PendingDoorKnockAlert = true
|
||
if got := m.advanceStoryEventQueue(); got != stateStoryNeurotoxinAlert {
|
||
t.Errorf("при обоих ожидающих событиях сначала должен был идти выброс, получено %v", got)
|
||
}
|
||
|
||
m.story.PendingNeurotoxinAlert = false
|
||
if got := m.advanceStoryEventQueue(); got != stateStoryDoorKnockAlert {
|
||
t.Errorf("после выброса должен был идти стук, получено %v", got)
|
||
}
|
||
|
||
m.story.PendingDoorKnockAlert = false
|
||
if got := m.advanceStoryEventQueue(); got != stateStoryHub {
|
||
t.Errorf("без ожидающих событий должен был идти хаб, получено %v", got)
|
||
}
|
||
}
|
||
|
||
func TestNeurotoxinAlertDismissAdvancesToKnockThenHub(t *testing.T) {
|
||
withTempConfigDir(t)
|
||
m := AppModel{state: stateStoryNeurotoxinAlert, story: NewStoryModeState(StoryDifficultyEasy, time.Now())}
|
||
m.story.PendingNeurotoxinAlert = true
|
||
m.story.PendingDoorKnockAlert = true
|
||
m.story.ToxinLevel = 45
|
||
|
||
next, _ := m.Update(key(" "))
|
||
m = next.(AppModel)
|
||
if m.story.PendingNeurotoxinAlert {
|
||
t.Errorf("после показа флаг выброса должен был сброситься")
|
||
}
|
||
if m.state != stateStoryDoorKnockAlert {
|
||
t.Fatalf("ожидался переход к экрану стука в дверь, получено %v", m.state)
|
||
}
|
||
|
||
next, _ = m.Update(key(" "))
|
||
m = next.(AppModel)
|
||
if m.story.PendingDoorKnockAlert {
|
||
t.Errorf("после показа флаг стука должен был сброситься")
|
||
}
|
||
if m.state != stateStoryHub {
|
||
t.Errorf("после обоих уведомлений ожидался переход в хаб, получено %v", m.state)
|
||
}
|
||
}
|
||
|
||
func TestNeurotoxinAlertViewShowsToxinLevel(t *testing.T) {
|
||
m := AppModel{story: NewStoryModeState(StoryDifficultyEasy, time.Now())}
|
||
m.story.ToxinLevel = 55
|
||
view := m.viewStoryNeurotoxinAlert()
|
||
if !strings.Contains(view, "55") {
|
||
t.Errorf("экран выброса должен был показывать текущий уровень токсина, получено:\n%s", view)
|
||
}
|
||
}
|
||
|
||
func TestDoorKnockAlertDismissGoesToHubWhenNothingElsePending(t *testing.T) {
|
||
withTempConfigDir(t)
|
||
m := AppModel{state: stateStoryDoorKnockAlert, story: NewStoryModeState(StoryDifficultyEasy, time.Now())}
|
||
m.story.PendingDoorKnockAlert = true
|
||
|
||
next, _ := m.Update(key(" "))
|
||
m = next.(AppModel)
|
||
if m.story.PendingDoorKnockAlert {
|
||
t.Errorf("флаг стука должен был сброситься после показа")
|
||
}
|
||
if m.state != stateStoryHub {
|
||
t.Errorf("ожидался переход в хаб, получено %v", m.state)
|
||
}
|
||
} |