go_games_collection/corpsestarch_game_test.go

555 lines
22 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 (
"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, csT(0)) {
t.Fatalf("покупка не должна была пройти при нехватке средств")
}
g.Starch = 10
if !g.BuyServitor(0, csT(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, csT(0)) {
t.Errorf("тир 1 не должен быть доступен без достаточного TotalEarned")
}
}
func TestServitorCostScalesUp(t *testing.T) {
g := NewCorpseStarchGame(csT(0))
firstCost := g.servitorCost(0)
g.Starch = firstCost
g.BuyServitor(0, csT(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 TestWarpBreachDoesNotLowerServitorPrice(t *testing.T) {
g := NewCorpseStarchGame(csT(0))
g.Starch = 1_000_000
for i := 0; i < 10; i++ {
g.BuyServitor(0, csT(0))
}
priceBefore := g.servitorCost(0)
if g.ServitorCounts[0] != 10 {
t.Fatalf("ожидалось 10 купленных сервиторов тира 0, получено %d", g.ServitorCounts[0])
}
g.Corruption = corpseStarchCorruptionBreachThreshold - 1
g.DipChainsword(csT(1), 0.95) // добивает Скверну до порога — варп-выброс, сервиторы уполовиниваются
if g.ServitorCounts[0] != 5 {
t.Fatalf("ожидалось уполовинивание до 5, получено %d", g.ServitorCounts[0])
}
priceAfter := g.servitorCost(0)
if priceAfter != priceBefore {
t.Errorf("цена следующей покупки не должна была измениться от потери инфраструктуры: было %v, стало %v", priceBefore, priceAfter)
}
}
func TestBuyServitorBlockedDuringIncursion(t *testing.T) {
g := NewCorpseStarchGame(csT(0))
g.Starch = 1_000_000
g.ChaosIncursionUntil = csT(100)
if g.BuyServitor(0, csT(0)) {
t.Errorf("покупка сервитора не должна была пройти во время вторжения")
}
if g.ServitorCounts[0] != 0 {
t.Errorf("счётчик не должен был увеличиться")
}
}
func TestBuyServitorWorksAgainAfterIncursionEnds(t *testing.T) {
g := NewCorpseStarchGame(csT(0))
g.Starch = 1_000_000
g.ChaosIncursionUntil = csT(100)
if !g.BuyServitor(0, csT(200)) { // время уже после конца вторжения
t.Errorf("покупка должна была пройти после окончания вторжения")
}
}
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 TestWarpBreachIncrementsChaosMarks(t *testing.T) {
g := NewCorpseStarchGame(csT(0))
for i := 0; i < 3; i++ {
g.triggerWarpBreach(csT(i))
}
if g.ChaosMarks != 3 {
t.Errorf("ожидалось 3 метки хаоса после трёх выбросов, получено %d", g.ChaosMarks)
}
}
func TestChaosOfferPendingAtThreshold(t *testing.T) {
g := NewCorpseStarchGame(csT(0))
for i := 0; i < corpseStarchChaosMarksForOffer-1; i++ {
g.triggerWarpBreach(csT(i))
}
if g.ChaosOfferPending() {
t.Fatalf("предложение не должно было появиться раньше порога")
}
g.triggerWarpBreach(csT(100))
if !g.ChaosOfferPending() {
t.Fatalf("предложение должно было появиться по достижении порога меток")
}
}
func TestDeclineChaosOfferResetsMarksAndGivesReward(t *testing.T) {
g := NewCorpseStarchGame(csT(0))
g.ChaosMarks = corpseStarchChaosMarksForOffer
starchBefore := g.Starch
g.DeclineChaosOffer()
if g.ChaosMarks != 0 {
t.Errorf("счётчик меток должен был сброситься в 0, получено %d", g.ChaosMarks)
}
if g.Starch != starchBefore+corpseStarchChaosDeclineReward {
t.Errorf("ожидалась награда %v за отказ, было %v, стало %v", corpseStarchChaosDeclineReward, starchBefore, g.Starch)
}
if g.ChaosOfferPending() {
t.Errorf("предложение не должно оставаться активным после отказа")
}
}
func TestAcceptChaosOfferStartsRebirth(t *testing.T) {
g := NewCorpseStarchGame(csT(0))
g.ChaosMarks = corpseStarchChaosMarksForOffer
g.AcceptChaosOffer(csT(0))
if !g.InRebirth(csT(0)) {
t.Fatalf("перерождение должно было начаться сразу после принятия")
}
if g.ChaosMarks != 0 {
t.Errorf("счётчик меток должен был сброситься при принятии, получено %d", g.ChaosMarks)
}
if g.Mode != CorpseStarchModeImperial {
t.Errorf("режим не должен меняться сразу — только по завершении перерождения")
}
remaining := g.RebirthRemaining(csT(0))
if remaining != corpseStarchRebirthDuration {
t.Errorf("ожидалось %v до конца перерождения, получено %v", corpseStarchRebirthDuration, remaining)
}
}
func TestRebirthBlocksIncomeEntirely(t *testing.T) {
g := NewCorpseStarchGame(csT(0))
g.ServitorCounts[0] = 100 // солидный пассивный доход, если бы не блокировка
g.AcceptChaosOffer(csT(0))
starchBefore := g.Starch
g.Tick(csT(3600)) // час прошёл — обычно немалый доход, но идёт перерождение
if g.Starch != starchBefore {
t.Errorf("во время перерождения доход не должен начисляться вообще, было %v, стало %v", starchBefore, g.Starch)
}
if g.Mode != CorpseStarchModeImperial {
t.Errorf("перерождение длится сутки — часа недостаточно для завершения")
}
}
func TestRebirthCompletesAndSwitchesToDemonicMode(t *testing.T) {
g := NewCorpseStarchGame(csT(0))
g.AcceptChaosOffer(csT(0))
g.Tick(csT(int(corpseStarchRebirthDuration.Seconds()) + 10)) // чуть больше суток
if g.InRebirth(csT(int(corpseStarchRebirthDuration.Seconds()) + 10)) {
t.Errorf("перерождение должно было завершиться")
}
if g.Mode != CorpseStarchModeDemonic {
t.Errorf("режим должен был переключиться на демонический")
}
if !g.RebirthUntil.IsZero() {
t.Errorf("RebirthUntil должен был сброситься в нулевое значение")
}
}
func TestRebirthResumesIncomeAfterCompletion(t *testing.T) {
g := NewCorpseStarchGame(csT(0))
g.ServitorCounts[0] = 10 // 5/сек
g.AcceptChaosOffer(csT(0))
rebirthEnd := int(corpseStarchRebirthDuration.Seconds())
g.Tick(csT(rebirthEnd)) // перерождение как раз завершается этим тиком
starchAfterRebirth := g.Starch
g.Tick(csT(rebirthEnd + 10)) // ещё 10 секунд уже в демоническом режиме
if g.Starch <= starchAfterRebirth {
t.Errorf("доход должен был возобновиться после завершения перерождения")
}
}
func TestServitorEffectiveNameKeySwitchesWithMode(t *testing.T) {
def := corpseStarchServitorDefs[0]
if def.effectiveNameKey(CorpseStarchModeImperial) != def.NameKey {
t.Errorf("в обычном режиме ожидалось имперское имя")
}
if def.effectiveNameKey(CorpseStarchModeDemonic) != def.DemonicNameKey {
t.Errorf("в демоническом режиме ожидалось демоническое имя")
}
if def.DemonicNameKey == "" {
t.Errorf("демоническое имя не должно быть пустым")
}
}
func TestMissionEffectiveNameKeySwitchesWithMode(t *testing.T) {
def := corpseStarchMissionDefs[0]
if def.effectiveNameKey(CorpseStarchModeImperial) != def.NameKey {
t.Errorf("в обычном режиме ожидалось имперское имя")
}
if def.effectiveNameKey(CorpseStarchModeDemonic) != def.DemonicNameKey {
t.Errorf("в демоническом режиме ожидалось демоническое имя")
}
if def.DemonicNameKey == "" {
t.Errorf("демоническое имя не должно быть пустым")
}
}
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))
}
}