go_games_collection/story_game_test.go
2026-07-16 09:53:25 +03:00

523 lines
21 KiB
Go
Raw 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 (
"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-х суток)")
}
}