package main import ( "math/rand" "testing" "time" ) func TestNewStoryModeStateStartsWithThreeOfEach(t *testing.T) { now := time.Now() s := NewStoryModeState(StoryDifficultyMedium, now) for i := 0; i < int(storyResourceCount); i++ { if s.Resources[i] != storyStartingAmount { t.Errorf("ресурс %d: ожидалось стартовое количество %d, получено %d", i, storyStartingAmount, s.Resources[i]) } if s.Locked[i] { t.Errorf("ресурс %d: ничего не должно быть заперто в начале", i) } } } func TestRecordWinIncrementsResource(t *testing.T) { s := NewStoryModeState(StoryDifficultyEasy, time.Now()) s.RecordWin(StoryResTonk) if s.Resources[StoryResTonk] != storyStartingAmount+1 { t.Errorf("ожидалось увеличение ресурса Тонка на 1, получено %d", s.Resources[StoryResTonk]) } if s.Resources[StoryResDurak] != storyStartingAmount { t.Errorf("ресурс Дурака не должен был измениться, получено %d", s.Resources[StoryResDurak]) } } func TestRecordWinLocksResourceAtTarget(t *testing.T) { s := NewStoryModeState(StoryDifficultyEasy, time.Now()) s.Resources[StoryResCheckers] = StoryTargetCheckers - 1 s.RecordWin(StoryResCheckers) if s.Resources[StoryResCheckers] != StoryTargetCheckers { t.Errorf("ожидалось достижение цели %d, получено %d", StoryTargetCheckers, s.Resources[StoryResCheckers]) } if !s.Locked[StoryResCheckers] { t.Errorf("ресурс должен был запереться в сейф по достижении цели") } } func TestRecordWinOnLockedResourceKeepsAccumulating(t *testing.T) { s := NewStoryModeState(StoryDifficultyEasy, time.Now()) s.Resources[StoryResChess] = StoryTargetChess s.Locked[StoryResChess] = true s.RecordWin(StoryResChess) if s.Resources[StoryResChess] != StoryTargetChess+1 { t.Errorf("после фиксации ресурс должен был продолжить копиться без ограничения, получено %d", s.Resources[StoryResChess]) } if !s.Locked[StoryResChess] { t.Errorf("ресурс должен был остаться зафиксированным") } } func TestTickNoLongerDecaysResources(t *testing.T) { now := time.Now() s := NewStoryModeState(StoryDifficultyEasy, now) s.Tick(now.Add(10 * 24 * time.Hour)) // 10 суток — раньше это списало бы 10 if s.Resources[StoryResTonk] != storyStartingAmount { t.Errorf("ресурсы больше не должны расходоваться со временем, получено %d, ожидалось %d", s.Resources[StoryResTonk], storyStartingAmount) } } func TestTickResetsDailyCountersAfterAFullDay(t *testing.T) { now := time.Now() s := NewStoryModeState(StoryDifficultyEasy, now) s.KlondikeWinsToday = 2 s.DailyGrantsUsed[StoryResChess] = 3 s.DailyGrantsUsed[StoryResTicTacToe] = 2 s.Tick(now.Add(25 * time.Hour)) // чуть больше суток if s.KlondikeWinsToday != 0 { t.Errorf("счётчик побед Косынки за день должен был сброситься, получено %d", s.KlondikeWinsToday) } if s.DailyGrantsUsed[StoryResChess] != 0 || s.DailyGrantsUsed[StoryResTicTacToe] != 0 { t.Errorf("счётчики суточных начислений должны были сброситься у всех ресурсов, получено Chess=%d TicTacToe=%d", s.DailyGrantsUsed[StoryResChess], s.DailyGrantsUsed[StoryResTicTacToe]) } } func TestTickDoesNothingBeforeAFullDay(t *testing.T) { now := time.Now() s := NewStoryModeState(StoryDifficultyEasy, now) s.Tick(now.Add(23 * time.Hour)) if s.Resources[StoryResTonk] != storyStartingAmount { t.Errorf("до истечения полных суток расход не должен был случиться, получено %d", s.Resources[StoryResTonk]) } } func TestTickAccumulatesMultipleDaysAtOnce(t *testing.T) { now := time.Now() s := NewStoryModeState(StoryDifficultyEasy, now) s.DailyGrantsUsed[StoryResChess] = 3 days := s.Tick(now.Add(72 * time.Hour)) // ровно 3 суток if days != 3 { t.Errorf("ожидалось 3 учтённых суток, получено %d", days) } if s.DailyGrantsUsed[StoryResChess] != 0 { t.Errorf("суточный счётчик должен был сброситься независимо от числа прошедших суток, получено %d", s.DailyGrantsUsed[StoryResChess]) } } func TestAllResourcesLockedFalseUntilEveryOneIsLocked(t *testing.T) { s := NewStoryModeState(StoryDifficultyEasy, time.Now()) if s.AllResourcesLocked() { t.Fatalf("в начале не должно быть уже всё заперто") } for i := 0; i < int(storyResourceCount); i++ { s.Locked[i] = true } if !s.AllResourcesLocked() { t.Errorf("после запирания всех ресурсов AllResourcesLocked должен был вернуть true") } } func TestCompleteMatchesAllResourcesLocked(t *testing.T) { s := NewStoryModeState(StoryDifficultyEasy, time.Now()) if s.Complete() { t.Fatalf("сторимод не должен считаться завершённым в начале") } for i := 0; i < int(storyResourceCount); i++ { s.Locked[i] = true } if !s.Complete() { t.Errorf("сторимод должен был считаться завершённым после запирания всех ресурсов") } } func TestNewStoryModeStateDefaultVitals(t *testing.T) { s := NewStoryModeState(StoryDifficultyEasy, time.Now()) if s.Stress != StoryStressFloor { t.Errorf("ожидался стартовый стресс %d, получено %d", StoryStressFloor, s.Stress) } if s.Poisoning != 0 { t.Errorf("ожидалось стартовое отравление 0, получено %d", s.Poisoning) } if s.ToxinLevel != 0 { t.Errorf("ожидался стартовый токсин 0, получено %d", s.ToxinLevel) } } func TestAwardResourceRespectsDailyLimit(t *testing.T) { s := NewStoryModeState(StoryDifficultyEasy, time.Now()) for i := 0; i < StoryDailyGrantsLimit; i++ { if !s.AwardResource(StoryResChess) { t.Fatalf("начисление %d должно было пройти (лимит %d)", i+1, StoryDailyGrantsLimit) } } if s.AwardResource(StoryResChess) { t.Errorf("начисление сверх дневного лимита не должно было пройти") } if s.Resources[StoryResChess] != storyStartingAmount+StoryDailyGrantsLimit { t.Errorf("ожидалось %d очков ресурса, получено %d", storyStartingAmount+StoryDailyGrantsLimit, s.Resources[StoryResChess]) } } func TestAwardResourceLimitResetsAfterADay(t *testing.T) { now := time.Now() s := NewStoryModeState(StoryDifficultyEasy, now) for i := 0; i < StoryDailyGrantsLimit; i++ { s.AwardResource(StoryResChess) } s.Tick(now.Add(25 * time.Hour)) if !s.AwardResource(StoryResChess) { t.Errorf("после сброса суточного счётчика начисление снова должно было пройти") } } func TestAwardResourceDailyLimitIsPerResourceNotGlobal(t *testing.T) { s := NewStoryModeState(StoryDifficultyEasy, time.Now()) for i := 0; i < StoryDailyGrantsLimit; i++ { if !s.AwardResource(StoryResTicTacToe) { t.Fatalf("начисление %d крестикам-ноликам должно было пройти", i+1) } } if s.AwardResource(StoryResTicTacToe) { t.Errorf("крестики-нолики должны были исчерпать именно свой лимит") } // у шашек лимит совершенно свой, не должен быть затронут if !s.AwardResource(StoryResCheckers) { t.Errorf("исчерпанный лимит крестиков-ноликов не должен был повлиять на лимит шашек") } } func TestSpendAntidepressantCanGoBelowFloor(t *testing.T) { now := time.Now() s := NewStoryModeState(StoryDifficultyEasy, now) s.Stress = 35 s.Resources[StoryResKlondike] = 5 if !s.SpendAntidepressant(now) { t.Fatalf("трата антидепрессанта должна была пройти") } if s.Stress != 15 { t.Errorf("ожидался стресс 15 (35-20, ниже обычного пола), получено %d", s.Stress) } if s.Resources[StoryResKlondike] != 4 { t.Errorf("ожидалось списание 1 антидепрессанта, получено %d осталось", s.Resources[StoryResKlondike]) } if !s.StressRecoveryActive { t.Errorf("должно было запуститься восстановление стресса — он ниже обычного пола") } } func TestSpendAntidepressantNeverGoesBelowZero(t *testing.T) { now := time.Now() s := NewStoryModeState(StoryDifficultyEasy, now) s.Stress = 5 s.Resources[StoryResKlondike] = 5 s.SpendAntidepressant(now) if s.Stress != 0 { t.Errorf("стресс не должен был уйти ниже 0, получено %d", s.Stress) } } func TestSpendAntidepressantFailsWithoutResource(t *testing.T) { now := time.Now() s := NewStoryModeState(StoryDifficultyEasy, now) s.Resources[StoryResKlondike] = 0 if s.SpendAntidepressant(now) { t.Errorf("трата без ресурса не должна была пройти") } } func TestApplyStressRecoveryReturnsToFloorGradually(t *testing.T) { now := time.Now() s := NewStoryModeState(StoryDifficultyEasy, now) s.Stress = 40 s.Resources[StoryResKlondike] = 1 s.SpendAntidepressant(now) // стресс 20 (на полу — восстановление не нужно) s.Stress = 60 s.Resources[StoryResKlondike] = 1 s.SpendAntidepressant(now) // стресс 40, всё ещё выше пола — тоже без восстановления if s.StressRecoveryActive { t.Fatalf("восстановление не должно было запуститься — стресс всё ещё не ниже пола") } s.Stress = 25 s.Resources[StoryResKlondike] = 1 s.SpendAntidepressant(now) // стресс 5, ниже пола — запускается восстановление s.ApplyStressRecovery(now.Add(30 * time.Minute)) if s.Stress < 10 || s.Stress > 15 { t.Errorf("через полчаса ожидался стресс примерно посередине между 5 и 20, получено %d", s.Stress) } if !s.StressRecoveryActive { t.Errorf("восстановление ещё не должно было завершиться через полчаса") } } func TestApplyStressRecoveryCompletesAfterAnHour(t *testing.T) { now := time.Now() s := NewStoryModeState(StoryDifficultyEasy, now) s.Stress = 25 s.Resources[StoryResKlondike] = 1 s.SpendAntidepressant(now) // стресс 5, ниже пола s.ApplyStressRecovery(now.Add(time.Hour)) if s.Stress != StoryStressFloor { t.Errorf("через час стресс должен был вернуться к полу %d, получено %d", StoryStressFloor, s.Stress) } if s.StressRecoveryActive { t.Errorf("восстановление должно было завершиться (деактивироваться) через час") } } func TestApplyStressRecoveryDoesNothingWhenInactive(t *testing.T) { s := NewStoryModeState(StoryDifficultyEasy, time.Now()) s.Stress = 20 s.ApplyStressRecovery(time.Now()) if s.Stress != 20 { t.Errorf("без активного восстановления стресс не должен был измениться, получено %d", s.Stress) } } func TestSpendAntidoteReducesPoisoningWithFloorZero(t *testing.T) { s := NewStoryModeState(StoryDifficultyEasy, time.Now()) s.Poisoning = 15 s.Resources[StoryResMinesweeper] = 2 if !s.SpendAntidote() { t.Fatalf("трата антидота должна была пройти") } if s.Poisoning != 0 { t.Errorf("ожидалось отравление 0 (15-20, ограничено полом), получено %d", s.Poisoning) } } func TestSpendAirFilterMarksUsedToday(t *testing.T) { s := NewStoryModeState(StoryDifficultyEasy, time.Now()) s.Resources[StoryResGo] = 1 if !s.SpendAirFilter() { t.Fatalf("трата фильтра должна была пройти") } if !s.FilterUsedToday { t.Errorf("фильтр должен был отметиться использованным сегодня") } if s.Resources[StoryResGo] != 0 { t.Errorf("фильтр должен был списаться, получено %d", s.Resources[StoryResGo]) } } func TestSpendBatteryStartsAirPurification(t *testing.T) { now := time.Now() s := NewStoryModeState(StoryDifficultyEasy, now) s.Resources[StoryResOneOhOne] = 1 s.ToxinLevel = 80 if !s.SpendBattery(now) { t.Fatalf("трата батарейки должна была пройти") } if !s.AirPurificationActive { t.Errorf("очистка воздуха должна была запуститься") } if s.AirPurificationFromLevel != 80 { t.Errorf("ожидалось запомненное начальное значение токсина 80, получено %d", s.AirPurificationFromLevel) } } func TestApplyAirPurificationReducesToxinGradually(t *testing.T) { now := time.Now() s := NewStoryModeState(StoryDifficultyEasy, now) s.Resources[StoryResOneOhOne] = 1 s.ToxinLevel = 80 s.SpendBattery(now) s.ApplyAirPurification(now.Add(30 * time.Minute)) // половина часа — примерно половина токсина if s.ToxinLevel < 30 || s.ToxinLevel > 50 { t.Errorf("через полчаса ожидался токсин примерно в районе 40, получено %d", s.ToxinLevel) } if !s.AirPurificationActive { t.Errorf("очистка ещё не должна была завершиться через полчаса") } } func TestApplyAirPurificationCompletesAfterAnHour(t *testing.T) { now := time.Now() s := NewStoryModeState(StoryDifficultyEasy, now) s.Resources[StoryResOneOhOne] = 1 s.ToxinLevel = 80 s.SpendBattery(now) s.ApplyAirPurification(now.Add(time.Hour)) if s.ToxinLevel != 0 { t.Errorf("через час токсин должен был дойти до 0, получено %d", s.ToxinLevel) } if s.AirPurificationActive { t.Errorf("очистка должна была завершиться (деактивироваться) через час") } } func TestApplyAirPurificationDoesNothingWhenInactive(t *testing.T) { s := NewStoryModeState(StoryDifficultyEasy, time.Now()) s.ToxinLevel = 50 s.ApplyAirPurification(time.Now()) if s.ToxinLevel != 50 { t.Errorf("без активной очистки токсин не должен был измениться, получено %d", s.ToxinLevel) } } func TestSpendResourceUnlocksIfDropsBelowTarget(t *testing.T) { s := NewStoryModeState(StoryDifficultyEasy, time.Now()) s.Resources[StoryResGo] = StoryTargetGo s.Locked[StoryResGo] = true s.spendResource(StoryResGo, 1) if s.Locked[StoryResGo] { t.Errorf("ресурс должен был перестать считаться запертым, опустившись ниже цели") } } func TestSpendResourceKeepsLockedIfStillAtOrAboveTarget(t *testing.T) { s := NewStoryModeState(StoryDifficultyEasy, time.Now()) s.Resources[StoryResGo] = StoryTargetGo + 5 s.Locked[StoryResGo] = true s.spendResource(StoryResGo, 1) if !s.Locked[StoryResGo] { t.Errorf("ресурс должен был остаться запертым — он всё ещё выше цели") } } func TestStoryNeurotoxinProbabilityMapping(t *testing.T) { cases := []struct { days int want float64 }{ {0, 0}, {1, 0.5}, {2, 0.75}, {3, 1.0}, {10, 1.0}, } for _, c := range cases { if got := storyNeurotoxinProbability(c.days); got != c.want { t.Errorf("для %d дней без фильтра ожидалась вероятность %v, получено %v", c.days, c.want, got) } } } func TestStoryDoorKnockProbabilityMapping(t *testing.T) { cases := []struct { days int want float64 }{ {0, 0}, {1, 0.1}, {2, 0.5}, {3, 1.0}, {10, 1.0}, } for _, c := range cases { if got := storyDoorKnockProbability(c.days); got != c.want { t.Errorf("для %d дней без стука ожидалась вероятность %v, получено %v", c.days, c.want, got) } } } func TestProcessDayEventsNoReleaseWhenFilterUsedToday(t *testing.T) { s := NewStoryModeState(StoryDifficultyEasy, time.Now()) s.FilterUsedToday = true s.DaysSinceFilterUsed = 5 // неважно, каким было раньше — фильтр использован сегодня rnd := rand.New(rand.NewSource(1)) s.processDayEvents(rnd) if s.DaysSinceFilterUsed != 0 { t.Errorf("счётчик дней без фильтра должен был сброситься в 0, получено %d", s.DaysSinceFilterUsed) } if s.PendingNeurotoxinAlert { t.Errorf("выброс не должен был произойти — фильтр использован сегодня") } if s.FilterUsedToday { t.Errorf("флаг 'использован сегодня' должен был сброситься к следующему дню") } } func TestProcessDayEventsGuaranteedReleaseAfterThreeDays(t *testing.T) { s := NewStoryModeState(StoryDifficultyEasy, time.Now()) s.DaysSinceFilterUsed = 2 // станет 3 после инкремента — гарантированный выброс before := s.ToxinLevel rnd := rand.New(rand.NewSource(1)) s.processDayEvents(rnd) if !s.PendingNeurotoxinAlert { t.Fatalf("на третьи сутки без фильтра выброс должен был произойти гарантированно") } if s.ToxinLevel <= before { t.Errorf("содержание токсина должно было вырасти, получено %d (было %d)", s.ToxinLevel, before) } increase := s.ToxinLevel - before if increase < 10 || increase > 70 { t.Errorf("мощность выброса должна была быть в диапазоне 10-70, получено %d", increase) } } func TestProcessDayEventsNoReleaseOnFirstNeglectedDay(t *testing.T) { // вероятность на первый день без фильтра — 50%, а не 0 и не 100%; // проверяем хотя бы то, что при вероятности 0 (только что менялся) // выброса точно не будет — это уже покрыто выше. Здесь же // проверяем, что счётчик корректно увеличивается на 1 при // отсутствии использования. s := NewStoryModeState(StoryDifficultyEasy, time.Now()) s.DaysSinceFilterUsed = 0 s.FilterUsedToday = false rnd := rand.New(rand.NewSource(1)) s.processDayEvents(rnd) if s.DaysSinceFilterUsed != 1 { t.Errorf("счётчик дней без фильтра должен был увеличиться до 1, получено %d", s.DaysSinceFilterUsed) } } func TestProcessDayEventsGuaranteedKnockAfterThreeDays(t *testing.T) { s := NewStoryModeState(StoryDifficultyEasy, time.Now()) s.DaysSinceDoorKnock = 2 // станет 3 — гарантированный стук s.Stress = 30 rnd := rand.New(rand.NewSource(1)) s.processDayEvents(rnd) if !s.PendingDoorKnockAlert { t.Fatalf("на третьи сутки без стука он должен был произойти гарантированно") } if s.Stress != 50 { t.Errorf("стресс должен был вырасти на 20 (было 30), получено %d", s.Stress) } if s.DaysSinceDoorKnock != 0 { t.Errorf("счётчик дней без стука должен был сброситься, получено %d", s.DaysSinceDoorKnock) } } func TestProcessDayEventsStressCappedAtHundred(t *testing.T) { s := NewStoryModeState(StoryDifficultyEasy, time.Now()) s.DaysSinceDoorKnock = 2 s.Stress = 95 rnd := rand.New(rand.NewSource(1)) s.processDayEvents(rnd) if s.Stress != 100 { t.Errorf("стресс не должен был превысить 100, получено %d", s.Stress) } } func TestProcessDayEventsToxinCappedAtHundred(t *testing.T) { s := NewStoryModeState(StoryDifficultyEasy, time.Now()) s.DaysSinceFilterUsed = 2 s.ToxinLevel = 90 rnd := rand.New(rand.NewSource(1)) s.processDayEvents(rnd) if s.ToxinLevel != 100 { t.Errorf("токсин не должен был превысить 100, получено %d", s.ToxinLevel) } } func TestTickProcessesEachDaySeparatelyOverMultipleDays(t *testing.T) { now := time.Now() s := NewStoryModeState(StoryDifficultyEasy, now) // 5 суток без фильтра — на третий и далее день выброс гарантирован, // так что хотя бы один точно должен был случиться days := s.Tick(now.Add(5 * 24 * time.Hour)) if days != 5 { t.Fatalf("ожидалось 5 учтённых суток, получено %d", days) } if !s.PendingNeurotoxinAlert { t.Errorf("за 5 суток без фильтра выброс должен был произойти хотя бы раз (гарантированно с 3-х суток)") } if !s.PendingDoorKnockAlert { t.Errorf("за 5 суток стук в дверь тоже должен был произойти хотя бы раз (гарантированно с 3-х суток)") } }