package main import ( "testing" "time" ) func csT(offsetSec int) time.Time { base := time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC) return base.Add(time.Duration(offsetSec) * time.Second) } func TestNewCorpseStarchGameInitialState(t *testing.T) { g := NewCorpseStarchGame(csT(0)) if g.Starch != 0 || g.TotalEarned != 0 { t.Fatalf("новая партия должна начинаться с нулевого запаса") } if g.ClickYield != 1 { t.Errorf("ожидался базовый доход клика 1, получено %v", g.ClickYield) } if len(g.ServitorCounts) != len(corpseStarchServitorDefs) { t.Errorf("количество счётчиков сервиторов должно совпадать с числом тиров") } } func TestRequisitionAddsClickYield(t *testing.T) { g := NewCorpseStarchGame(csT(0)) g.Requisition(csT(0)) if g.Starch != 1 || g.TotalEarned != 1 { t.Errorf("ожидался запас 1 после одной реквизиции, получено %v/%v", g.Starch, g.TotalEarned) } g.Requisition(csT(1)) if g.Starch != 2 { t.Errorf("ожидался запас 2 после двух реквизиций, получено %v", g.Starch) } } func TestTickAccruesPassiveIncome(t *testing.T) { g := NewCorpseStarchGame(csT(0)) g.ServitorCounts[0] = 2 // 2 сервочерепа по 0.5/сек = 1/сек g.Tick(csT(10)) if g.Starch != 10 { t.Errorf("ожидался запас 10 после 10 секунд по 1/сек, получено %v", g.Starch) } if g.TotalEarned != 10 { t.Errorf("TotalEarned тоже должен был вырасти на 10, получено %v", g.TotalEarned) } } func TestTickWithNoElapsedTimeDoesNothing(t *testing.T) { g := NewCorpseStarchGame(csT(5)) g.ServitorCounts[0] = 5 g.Tick(csT(5)) if g.Starch != 0 { t.Errorf("без прошедшего времени доход не должен начисляться, получено %v", g.Starch) } } func TestSectionsUnlockProgressively(t *testing.T) { g := NewCorpseStarchGame(csT(0)) if g.ServitorsUnlocked() || g.ShopUnlocked() || g.SumpUnlocked() || g.MissionsUnlocked() { t.Fatalf("в начале партии ни один раздел не должен быть открыт") } g.TotalEarned = corpseStarchUnlockServitors if !g.ServitorsUnlocked() { t.Errorf("сервиторы должны открыться на пороге %v", corpseStarchUnlockServitors) } if g.ShopUnlocked() { t.Errorf("магазин не должен быть открыт раньше своего порога") } g.TotalEarned = corpseStarchUnlockMissions if !g.ShopUnlocked() || !g.SumpUnlocked() || !g.MissionsUnlocked() { t.Errorf("на пороге зачисток все более ранние разделы тоже должны быть открыты") } } func TestBuyServitorRespectsUnlockAndCost(t *testing.T) { g := NewCorpseStarchGame(csT(0)) g.Starch = 5 if g.BuyServitor(0) { t.Fatalf("покупка не должна была пройти при нехватке средств") } g.Starch = 10 if !g.BuyServitor(0) { t.Fatalf("покупка должна была пройти при ровно достаточных средствах") } if g.ServitorCounts[0] != 1 { t.Errorf("счётчик тира 0 должен был увеличиться") } if g.Starch != 0 { t.Errorf("стоимость должна была списаться полностью, получено %v", g.Starch) } g.Starch = 1000 if g.BuyServitor(1) { t.Errorf("тир 1 не должен быть доступен без достаточного TotalEarned") } } func TestServitorCostScalesUp(t *testing.T) { g := NewCorpseStarchGame(csT(0)) firstCost := g.servitorCost(0) g.Starch = firstCost g.BuyServitor(0) secondCost := g.servitorCost(0) if secondCost <= firstCost { t.Errorf("стоимость следующей покупки должна расти, было %v стало %v", firstCost, secondCost) } } func TestBuyClickUpgrade(t *testing.T) { g := NewCorpseStarchGame(csT(0)) g.Starch = corpseStarchClickUpgradeBaseCost before := g.ClickYield if !g.BuyClickUpgrade() { t.Fatalf("апгрейд должен был пройти при достаточных средствах") } if g.ClickYield <= before { t.Errorf("доход клика должен был вырасти") } if g.Starch != 0 { t.Errorf("стоимость апгрейда должна была списаться полностью") } } func TestDipChainswordBonusOutcome(t *testing.T) { g := NewCorpseStarchGame(csT(0)) before := g.Starch outcome := g.DipChainsword(csT(0), 0.1) if outcome != DipBonusStarch { t.Fatalf("ожидался исход DipBonusStarch при малом roll, получено %v", outcome) } if g.Starch <= before { t.Errorf("запас должен был вырасти при бонусном исходе") } } func TestDipChainswordBlessingGrantsTemporaryMultiplier(t *testing.T) { g := NewCorpseStarchGame(csT(0)) outcome := g.DipChainsword(csT(0), 0.6) if outcome != DipMachineSpiritBlessing { t.Fatalf("ожидался исход DipMachineSpiritBlessing, получено %v", outcome) } if g.currentClickYield(csT(0)) != g.ClickYield*2 { t.Errorf("во время благословения доход клика должен удваиваться") } if g.currentClickYield(csT(31)) != g.ClickYield { t.Errorf("после истечения 30 секунд бонус должен был закончиться") } } func TestDipChainswordNothingOutcome(t *testing.T) { g := NewCorpseStarchGame(csT(0)) before := g.Starch outcome := g.DipChainsword(csT(0), 0.8) if outcome != DipNothing { t.Fatalf("ожидался исход DipNothing, получено %v", outcome) } if g.Starch != before { t.Errorf("запас не должен был измениться при пустом исходе") } } func TestDipChainswordCorruptionOutcome(t *testing.T) { g := NewCorpseStarchGame(csT(0)) g.Starch = 100 outcome := g.DipChainsword(csT(0), 0.95) if outcome != DipCorruption { t.Fatalf("ожидался исход DipCorruption, получено %v", outcome) } if g.Corruption != 1 { t.Errorf("скверна должна была увеличиться на 1") } if g.Starch >= 100 { t.Errorf("запас должен был уменьшиться от порчи, получено %v", g.Starch) } } func TestWarpBreachTriggersAtCorruptionThreshold(t *testing.T) { g := NewCorpseStarchGame(csT(0)) g.Starch = 1000 g.TotalEarned = 1000 g.ServitorCounts[0] = 10 g.ServitorCounts[1] = 4 // два неудачных окунания — Скверна 1, потом 2, выброс ещё не должен случиться g.DipChainsword(csT(0), 0.95) g.DipChainsword(csT(1), 0.95) if g.Corruption != 2 { t.Fatalf("ожидалась Скверна 2 после двух неудач, получено %d", g.Corruption) } if g.InChaosIncursion(csT(1)) { t.Fatalf("вторжение не должно было начаться раньше порога") } // третье неудачное окунание достигает порога — должен случиться выброс starchBefore := g.Starch g.DipChainsword(csT(2), 0.95) if g.Corruption != 0 { t.Errorf("Скверна должна была сброситься в 0 после выброса, получено %d", g.Corruption) } if !g.InChaosIncursion(csT(2)) { t.Fatalf("вторжение хаоситов должно было начаться сразу после выброса") } // после выброса запас должен был уменьшиться примерно вдвое от // того, что было ДО штрафа за само неудачное окунание (10%), — // проверяем лишь то, что итог заметно меньше половины исходного // запаса до всей цепочки событий, без привязки к точной формуле if g.Starch >= starchBefore { t.Errorf("запас должен был заметно уменьшиться при выбросе, было %v, стало %v", starchBefore, g.Starch) } if g.ServitorCounts[0] != 5 || g.ServitorCounts[1] != 2 { t.Errorf("количество сервиторов должно было уполовиниться (10->5, 4->2), получено %v", g.ServitorCounts) } } func TestWarpBreachHalvesServitorsWithFloorDivision(t *testing.T) { g := NewCorpseStarchGame(csT(0)) g.ServitorCounts[0] = 7 // нечётное число — проверяем округление вниз g.Corruption = corpseStarchCorruptionBreachThreshold - 1 g.DipChainsword(csT(0), 0.95) // добивает Скверну до порога if g.ServitorCounts[0] != 3 { t.Errorf("7 сервиторов должны были уполовиниться с округлением вниз до 3, получено %d", g.ServitorCounts[0]) } } func TestChaosIncursionReducesServitorYield(t *testing.T) { g := NewCorpseStarchGame(csT(0)) g.ServitorCounts[0] = 10 // 10 * 0.5/сек = 5/сек в обычном режиме // ChaosIncursionUntil остаётся нулевым — вторжения нет вообще g.Tick(csT(10)) normalEarn := g.Starch g2 := NewCorpseStarchGame(csT(0)) g2.ServitorCounts[0] = 10 g2.ChaosIncursionUntil = csT(100) // вторжение идёт весь этот тик (10 < 100) g2.Tick(csT(10)) if g2.Starch >= normalEarn { t.Errorf("доход во время вторжения должен быть меньше обычного: обычный %v, во время вторжения %v", normalEarn, g2.Starch) } if g2.Starch != normalEarn*corpseStarchIncursionYieldMultiplier { t.Errorf("ожидался ровно множитель %v от обычного дохода, получено %v (обычный: %v)", corpseStarchIncursionYieldMultiplier, g2.Starch, normalEarn) } } func TestDefendAgainstChaosOnlyWorksDuringIncursion(t *testing.T) { g := NewCorpseStarchGame(csT(0)) if g.DefendAgainstChaos(csT(0)) { t.Errorf("защита не должна была сработать без активного вторжения") } if g.ChaosDefendClicks != 0 { t.Errorf("счётчик защиты не должен был увеличиться вне вторжения") } g.ChaosIncursionUntil = csT(100) if !g.DefendAgainstChaos(csT(0)) { t.Errorf("защита должна была сработать во время вторжения") } if g.ChaosDefendClicks != 1 { t.Errorf("ожидался счётчик защиты 1, получено %d", g.ChaosDefendClicks) } } func TestIncursionResolvesAfterDurationWithoutBonus(t *testing.T) { g := NewCorpseStarchGame(csT(0)) g.ChaosIncursionUntil = csT(10) g.ChaosDefendClicks = corpseStarchDefendClicksForBonus - 1 // недостаточно для бонуса starchBefore := g.Starch g.Tick(csT(11)) // время вышло за отметку окончания вторжения if g.InChaosIncursion(csT(11)) { t.Errorf("вторжение должно было завершиться") } if !g.ChaosIncursionUntil.IsZero() { t.Errorf("ChaosIncursionUntil должен был сброситься в нулевое значение") } if g.ChaosDefendClicks != 0 { t.Errorf("счётчик защиты должен был сброситься после разрешения вторжения") } if g.Starch != starchBefore { t.Errorf("без достаточной защиты бонус не должен был начислиться, было %v, стало %v", starchBefore, g.Starch) } } func TestIncursionResolvesWithBonusWhenDefendedEnough(t *testing.T) { g := NewCorpseStarchGame(csT(0)) g.ChaosIncursionUntil = csT(10) g.ChaosDefendClicks = corpseStarchDefendClicksForBonus starchBefore := g.Starch g.Tick(csT(11)) if g.Starch != starchBefore+corpseStarchDefendBonusReward { t.Errorf("ожидался бонус %v за успешную защиту, было %v, стало %v", corpseStarchDefendBonusReward, starchBefore, g.Starch) } } func TestChaosIncursionRemainingCountdown(t *testing.T) { g := NewCorpseStarchGame(csT(0)) g.ChaosIncursionUntil = csT(100) remaining := g.ChaosIncursionRemaining(csT(40)) if remaining != 60*time.Second { t.Errorf("ожидалось 60 секунд до конца вторжения, получено %v", remaining) } if g.ChaosIncursionRemaining(csT(200)) != 0 { t.Errorf("после окончания вторжения обратный отсчёт должен быть 0") } } func TestStartMissionRequiresUnlockAndNoActiveMission(t *testing.T) { g := NewCorpseStarchGame(csT(0)) if g.StartMission(csT(0), 0) { t.Fatalf("миссия не должна была начаться без открытого порога") } g.TotalEarned = corpseStarchUnlockMissions if !g.StartMission(csT(0), 0) { t.Fatalf("миссия должна была начаться при открытом пороге") } if g.StartMission(csT(1), 1) { t.Errorf("нельзя начать вторую миссию, пока активна первая") } } func TestMissionRemainingAndCollect(t *testing.T) { g := NewCorpseStarchGame(csT(0)) g.TotalEarned = corpseStarchUnlockMissions g.StartMission(csT(0), 0) // "vermin", 20 секунд if g.MissionRemaining(csT(5)) <= 0 { t.Fatalf("через 5 секунд миссия ещё не должна была закончиться") } if _, ok := g.CollectMission(csT(5), 0.5); ok { t.Errorf("нельзя забрать награду до завершения миссии") } if g.MissionRemaining(csT(25)) != 0 { t.Errorf("через 25 секунд миссия (20 сек) должна была завершиться") } reward, ok := g.CollectMission(csT(25), 0.5) if !ok { t.Fatalf("награда должна была быть получена после завершения") } def := corpseStarchMissionDefs[0] wantReward := def.RewardMin + 0.5*(def.RewardMax-def.RewardMin) if reward != wantReward { t.Errorf("ожидалась награда %v, получено %v", wantReward, reward) } if g.ActiveMission != nil { t.Errorf("активная миссия должна была сброситься после получения награды") } if g.MissionsDone != 1 { t.Errorf("счётчик завершённых миссий должен был увеличиться") } } func TestLogTrimsToLimit(t *testing.T) { g := NewCorpseStarchGame(csT(0)) for i := 0; i < corpseStarchLogLimit+5; i++ { g.addLog("test.key") } if len(g.Log) != corpseStarchLogLimit { t.Errorf("лог должен обрезаться до лимита %d, получено %d", corpseStarchLogLimit, len(g.Log)) } }