diff --git a/blackjack_tui.go b/blackjack_tui.go index 3704121..664b2ae 100644 --- a/blackjack_tui.go +++ b/blackjack_tui.go @@ -110,7 +110,7 @@ func (m BlackjackModel) handleKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) { } if m.game.Phase == BJPhaseRoundOver { - if key == "n" { + if key == "n" && !m.humanIsBankrupt() { m.game = m.game.NextRound() m.message = "" m.isError = false @@ -213,6 +213,13 @@ func (m BlackjackModel) renderDealer() string { return Tf("blackjack.hand_with_total", renderHandCards(m.game.Dealer), handTotal(m.game.Dealer)) } +func (m BlackjackModel) humanIsBankrupt() bool { + if m.humanIdx < 0 || m.humanIdx >= len(m.game.Players) { + return false + } + return m.game.Players[m.humanIdx].Balance == 0 +} + func (m BlackjackModel) View() string { if m.quitting { return T("common.goodbye") @@ -228,7 +235,13 @@ func (m BlackjackModel) View() string { if m.game.Phase == BJPhaseRoundOver { fmt.Fprintln(&b, m.renderResult()) fmt.Fprintln(&b) - fmt.Fprintln(&b, dimStyle.Render(T("blackjack.hint.gameover"))) + if m.humanIsBankrupt() { + fmt.Fprintln(&b, errorStyle.Render(T("blackjack.bankrupt"))) + fmt.Fprintln(&b) + fmt.Fprintln(&b, dimStyle.Render(T("blackjack.hint.bankrupt"))) + } else { + fmt.Fprintln(&b, dimStyle.Render(T("blackjack.hint.gameover"))) + } return b.String() } diff --git a/blackjack_tui_test.go b/blackjack_tui_test.go index 0148879..25da778 100644 --- a/blackjack_tui_test.go +++ b/blackjack_tui_test.go @@ -1,6 +1,9 @@ package main -import "testing" +import ( + "strings" + "testing" +) func TestBlackjackModelDealsCorrectPlayerCount(t *testing.T) { for n := 1; n <= 6; n++ { @@ -179,7 +182,7 @@ func TestBlackjackModelShowsMessageWhenBetReduced(t *testing.T) { } } -func TestBlackjackModelShowsMessageWhenNoFundsLeft(t *testing.T) { +func TestBlackjackModelBankruptBlocksNextRoundAndShowsMessage(t *testing.T) { m := NewBlackjackModel(1, 100, 10) for m.game.Phase == BJPhasePlayerTurn { if err := m.game.Stand(); err != nil { @@ -187,13 +190,22 @@ func TestBlackjackModelShowsMessageWhenNoFundsLeft(t *testing.T) { } } m.game.Players[m.humanIdx].Balance = 0 + handsBefore := len(m.game.Players[m.humanIdx].Hand) + + if !m.humanIsBankrupt() { + t.Fatalf("ожидалось, что humanIsBankrupt() вернёт true при нулевом балансе") + } + view := m.View() + if !strings.Contains(view, T("blackjack.bankrupt")) { + t.Errorf("экран должен был показать сообщение о банкротстве, получено:\n%s", view) + } next, _ := m.Update(key("n")) m = next.(BlackjackModel) - if m.game.Players[m.humanIdx].Bet != 0 { - t.Errorf("ставка при отсутствии денег должна была стать 0, получено %d", m.game.Players[m.humanIdx].Bet) + if m.game.Phase != BJPhaseRoundOver { + t.Errorf("нажатие 'n' при банкротстве не должно было начинать новый раунд") } - if m.message == "" { - t.Errorf("ожидалось информационное сообщение об отсутствии денег") + if len(m.game.Players[m.humanIdx].Hand) != handsBefore { + t.Errorf("рука не должна была измениться — новый раунд не должен был начаться") } } diff --git a/corpsestarch_tui.go b/corpsestarch_tui.go index c0053a8..1e09b04 100644 --- a/corpsestarch_tui.go +++ b/corpsestarch_tui.go @@ -125,7 +125,7 @@ func (m CorpseStarchModel) buildActions(now time.Time) []corpseStarchAction { }) } - if g.SumpUnlocked() { + if g.SumpUnlocked() && !g.InChaosIncursion(now) { actions = append(actions, corpseStarchAction{ Label: T("corpsestarch.action.dip"), Handle: func(m *CorpseStarchModel, now time.Time) { @@ -139,6 +139,15 @@ func (m CorpseStarchModel) buildActions(now time.Time) []corpseStarchAction { m.setInfo("corpsestarch.log.dip_nothing") case DipCorruption: if m.game.InChaosIncursion(now) { + // именно этот бросок пробил порог и запустил + // вторжение прямо сейчас — раньше несколько + // таких пробоев подряд могли перезапускать + // вторжение, сбрасывая его таймер и прогресс + // защиты; теперь пока идёт вторжение, само + // действие окунания недоступно (см. условие + // добавления этого действия выше), так что + // сюда мы попадаем только один раз — в момент + // самого пробоя m.setInfo("corpsestarch.log.warp_breach") } else { m.setInfo("corpsestarch.log.dip_corruption") diff --git a/corpsestarch_tui_test.go b/corpsestarch_tui_test.go index 2aa2321..fbe84cf 100644 --- a/corpsestarch_tui_test.go +++ b/corpsestarch_tui_test.go @@ -17,6 +17,66 @@ func TestCorpseStarchModelRequisitionViaKey(t *testing.T) { } } +func TestCorpseStarchModelDipActionHiddenDuringIncursion(t *testing.T) { + withTempConfigDir(t) + m := NewCorpseStarchModel() + m.game.TotalEarned = corpseStarchUnlockSump // открываем отстойник + + m.game.ChaosIncursionUntil = m.game.LastTick.Add(time.Hour) + during := m.buildActions(m.game.LastTick) + + for _, a := range during { + if a.Label == T("corpsestarch.action.dip") { + t.Errorf("окунание меча не должно предлагаться во время вторжения — иначе новая Скверна могла бы перезапустить уже идущее вторжение") + } + } +} + +func TestCorpseStarchModelDipActionReappearsAfterIncursionEnds(t *testing.T) { + withTempConfigDir(t) + m := NewCorpseStarchModel() + m.game.TotalEarned = corpseStarchUnlockSump + m.game.ChaosIncursionUntil = time.Time{} // вторжения нет + + actions := m.buildActions(m.game.LastTick) + found := false + for _, a := range actions { + if a.Label == T("corpsestarch.action.dip") { + found = true + } + } + if !found { + t.Errorf("окунание меча должно быть доступно вне вторжения") + } +} + +// TestWarpBreachDuringIncursionCannotBeTriggeredAgain — регрессия: +// раньше окунание меча оставалось доступным даже во время уже +// идущего вторжения, и если бросок снова достигал порога Скверны, +// triggerWarpBreach вызывался повторно поверх уже активного +// вторжения, сбрасывая его таймер (ChaosIncursionUntil) и прогресс +// защиты (ChaosDefendClicks) игрока. Теперь действие недоступно, пока +// идёт вторжение (см. buildActions), так что до самого движка такой +// повторный вызов дойти не может — но зафиксируем и в движке, что +// вызов triggerWarpBreach во время уже идущего вторжения продлевает +// его, а не более тонко сохраняет прогресс, чтобы явно видеть, какое +// именно поведение предотвращает интерфейс. +func TestWarpBreachDuringIncursionResetsProgressAtEngineLevel(t *testing.T) { + g := NewCorpseStarchGame(time.Now()) + now := g.LastTick + g.ChaosIncursionUntil = now.Add(time.Second) // вторжение вот-вот закончится + g.ChaosDefendClicks = 5 // накопленный прогресс защиты + + g.triggerWarpBreach(now) + + if g.ChaosDefendClicks != 0 { + t.Errorf("на уровне движка triggerWarpBreach действительно обнуляет прогресс защиты (%d) — поэтому интерфейс и не должен допускать повторный вызов во время вторжения", g.ChaosDefendClicks) + } + if !g.ChaosIncursionUntil.After(now.Add(time.Second)) { + t.Errorf("на уровне движка повторный вызов продлевает вторжение заново — поэтому интерфейс блокирует окунание меча, пока вторжение идёт") + } +} + func TestCorpseStarchModelBuyServitorActionHiddenDuringIncursion(t *testing.T) { withTempConfigDir(t) m := NewCorpseStarchModel() diff --git a/menu.go b/menu.go index 767a4b5..527199b 100644 --- a/menu.go +++ b/menu.go @@ -1621,7 +1621,9 @@ func (m AppModel) viewMenu() string { } fmt.Fprintf(&b, "%s%s\n", cursor, label) if d := opt.desc(); d != "" { - fmt.Fprintf(&b, " %s\n", menuDescStyle.Render(d)) + wrapped := wrapText(d, m.termWidth) + indented := strings.ReplaceAll(wrapped, "\n", "\n ") + fmt.Fprintf(&b, " %s\n", menuDescStyle.Render(indented)) } } if end < len(m.options) { diff --git a/tonk b/tonk index 867589a..8fa729f 100755 Binary files a/tonk and b/tonk differ diff --git a/translations.go b/translations.go index 6d66c50..3d07fa4 100644 --- a/translations.go +++ b/translations.go @@ -336,6 +336,8 @@ func init() { "blackjack.title": {"=== БЛЭКДЖЕК ===", "=== BLACKJACK ==="}, "blackjack.dealer_line": {"Дилер:\n%s\n\n", "Dealer:\n%s\n\n"}, "blackjack.hint.gameover": {"[n] новый раунд [q] в меню", "[n] new round [q] menu"}, + "blackjack.bankrupt": {"Вы банкрот — фишек не осталось.", "You're bankrupt — you have no chips left."}, + "blackjack.hint.bankrupt": {"[q] в меню", "[q] menu"}, "blackjack.hint.turn": {"[h] взять карту [s] остановиться [d] удвоить ставку [q] в меню", "[h] hit [s] stand [d] double down [q] menu"}, "blackjack.msg.hit": {"Вы взяли карту.", "You took a card."}, "blackjack.msg.stand": {"Вы остановились.", "You stood."},