Fixes
This commit is contained in:
parent
9353ab527a
commit
ceb9bbc37a
7 changed files with 108 additions and 10 deletions
|
|
@ -110,7 +110,7 @@ func (m BlackjackModel) handleKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if m.game.Phase == BJPhaseRoundOver {
|
if m.game.Phase == BJPhaseRoundOver {
|
||||||
if key == "n" {
|
if key == "n" && !m.humanIsBankrupt() {
|
||||||
m.game = m.game.NextRound()
|
m.game = m.game.NextRound()
|
||||||
m.message = ""
|
m.message = ""
|
||||||
m.isError = false
|
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))
|
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 {
|
func (m BlackjackModel) View() string {
|
||||||
if m.quitting {
|
if m.quitting {
|
||||||
return T("common.goodbye")
|
return T("common.goodbye")
|
||||||
|
|
@ -228,7 +235,13 @@ func (m BlackjackModel) View() string {
|
||||||
if m.game.Phase == BJPhaseRoundOver {
|
if m.game.Phase == BJPhaseRoundOver {
|
||||||
fmt.Fprintln(&b, m.renderResult())
|
fmt.Fprintln(&b, m.renderResult())
|
||||||
fmt.Fprintln(&b)
|
fmt.Fprintln(&b)
|
||||||
|
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")))
|
fmt.Fprintln(&b, dimStyle.Render(T("blackjack.hint.gameover")))
|
||||||
|
}
|
||||||
return b.String()
|
return b.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,9 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import "testing"
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
func TestBlackjackModelDealsCorrectPlayerCount(t *testing.T) {
|
func TestBlackjackModelDealsCorrectPlayerCount(t *testing.T) {
|
||||||
for n := 1; n <= 6; n++ {
|
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)
|
m := NewBlackjackModel(1, 100, 10)
|
||||||
for m.game.Phase == BJPhasePlayerTurn {
|
for m.game.Phase == BJPhasePlayerTurn {
|
||||||
if err := m.game.Stand(); err != nil {
|
if err := m.game.Stand(); err != nil {
|
||||||
|
|
@ -187,13 +190,22 @@ func TestBlackjackModelShowsMessageWhenNoFundsLeft(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
m.game.Players[m.humanIdx].Balance = 0
|
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"))
|
next, _ := m.Update(key("n"))
|
||||||
m = next.(BlackjackModel)
|
m = next.(BlackjackModel)
|
||||||
if m.game.Players[m.humanIdx].Bet != 0 {
|
if m.game.Phase != BJPhaseRoundOver {
|
||||||
t.Errorf("ставка при отсутствии денег должна была стать 0, получено %d", m.game.Players[m.humanIdx].Bet)
|
t.Errorf("нажатие 'n' при банкротстве не должно было начинать новый раунд")
|
||||||
}
|
}
|
||||||
if m.message == "" {
|
if len(m.game.Players[m.humanIdx].Hand) != handsBefore {
|
||||||
t.Errorf("ожидалось информационное сообщение об отсутствии денег")
|
t.Errorf("рука не должна была измениться — новый раунд не должен был начаться")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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{
|
actions = append(actions, corpseStarchAction{
|
||||||
Label: T("corpsestarch.action.dip"),
|
Label: T("corpsestarch.action.dip"),
|
||||||
Handle: func(m *CorpseStarchModel, now time.Time) {
|
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")
|
m.setInfo("corpsestarch.log.dip_nothing")
|
||||||
case DipCorruption:
|
case DipCorruption:
|
||||||
if m.game.InChaosIncursion(now) {
|
if m.game.InChaosIncursion(now) {
|
||||||
|
// именно этот бросок пробил порог и запустил
|
||||||
|
// вторжение прямо сейчас — раньше несколько
|
||||||
|
// таких пробоев подряд могли перезапускать
|
||||||
|
// вторжение, сбрасывая его таймер и прогресс
|
||||||
|
// защиты; теперь пока идёт вторжение, само
|
||||||
|
// действие окунания недоступно (см. условие
|
||||||
|
// добавления этого действия выше), так что
|
||||||
|
// сюда мы попадаем только один раз — в момент
|
||||||
|
// самого пробоя
|
||||||
m.setInfo("corpsestarch.log.warp_breach")
|
m.setInfo("corpsestarch.log.warp_breach")
|
||||||
} else {
|
} else {
|
||||||
m.setInfo("corpsestarch.log.dip_corruption")
|
m.setInfo("corpsestarch.log.dip_corruption")
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
func TestCorpseStarchModelBuyServitorActionHiddenDuringIncursion(t *testing.T) {
|
||||||
withTempConfigDir(t)
|
withTempConfigDir(t)
|
||||||
m := NewCorpseStarchModel()
|
m := NewCorpseStarchModel()
|
||||||
|
|
|
||||||
4
menu.go
4
menu.go
|
|
@ -1621,7 +1621,9 @@ func (m AppModel) viewMenu() string {
|
||||||
}
|
}
|
||||||
fmt.Fprintf(&b, "%s%s\n", cursor, label)
|
fmt.Fprintf(&b, "%s%s\n", cursor, label)
|
||||||
if d := opt.desc(); d != "" {
|
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) {
|
if end < len(m.options) {
|
||||||
|
|
|
||||||
BIN
tonk
BIN
tonk
Binary file not shown.
|
|
@ -336,6 +336,8 @@ func init() {
|
||||||
"blackjack.title": {"=== БЛЭКДЖЕК ===", "=== BLACKJACK ==="},
|
"blackjack.title": {"=== БЛЭКДЖЕК ===", "=== BLACKJACK ==="},
|
||||||
"blackjack.dealer_line": {"Дилер:\n%s\n\n", "Dealer:\n%s\n\n"},
|
"blackjack.dealer_line": {"Дилер:\n%s\n\n", "Dealer:\n%s\n\n"},
|
||||||
"blackjack.hint.gameover": {"[n] новый раунд [q] в меню", "[n] new round [q] menu"},
|
"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.hint.turn": {"[h] взять карту [s] остановиться [d] удвоить ставку [q] в меню", "[h] hit [s] stand [d] double down [q] menu"},
|
||||||
"blackjack.msg.hit": {"Вы взяли карту.", "You took a card."},
|
"blackjack.msg.hit": {"Вы взяли карту.", "You took a card."},
|
||||||
"blackjack.msg.stand": {"Вы остановились.", "You stood."},
|
"blackjack.msg.stand": {"Вы остановились.", "You stood."},
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue