Files
gogobee/internal/plugin/dnd_expedition_supplies_test.go
prosolis 2413f892e5 Adv 2.0 D&D Phase 12 E1b: supply system (procurement, burn, depletion)
Pure logic per gogobee_expedition_system.md §4. Tier→base SU/day and
harsh-conditions multiplier tables (§4.1). Standard pack 10 SU/50c
(max 3) and deluxe pack 20 SU/90c (max 1) per §4.2. SupplyPurchase
struct with Validate / Total / Cost. Depletion brackets (>25 / 10–25
/ 1–9 / 0) → roll modifier and long-rest gate per §4.3. applyDailyBurn
deducts and clamps; resets the per-day forage flag.

Procurement is exposed as a pure helper here; the !expedition start
flow wires it interactively in E1c.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 14:25:22 -07:00

161 lines
4.2 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 plugin
import "testing"
func TestSupplyDailyBurn_AllTiers(t *testing.T) {
cases := []struct {
tier ZoneTier
want float32
}{
{ZoneTierBeginner, 1.0},
{ZoneTierApprentice, 1.5},
{ZoneTierJourneyman, 2.0},
{ZoneTierVeteran, 3.0},
{ZoneTierLegendary, 4.0},
}
for _, c := range cases {
if got := supplyDailyBurn(c.tier); got != c.want {
t.Errorf("tier %d: got %v, want %v", c.tier, got, c.want)
}
}
}
func TestSupplyHarshMultiplier_AllTiers(t *testing.T) {
cases := []struct {
tier ZoneTier
want float32
}{
{ZoneTierBeginner, 1.0},
{ZoneTierApprentice, 1.5},
{ZoneTierJourneyman, 2.0},
{ZoneTierVeteran, 2.5},
{ZoneTierLegendary, 3.0},
}
for _, c := range cases {
if got := supplyHarshMultiplier(c.tier); got != c.want {
t.Errorf("tier %d: got %v, want %v", c.tier, got, c.want)
}
}
}
func TestSupplyDepletion_Brackets(t *testing.T) {
cases := []struct {
current float32
max float32
want SupplyDepletionState
}{
{10, 10, SupplyNormal}, // 100%
{3, 10, SupplyNormal}, // 30%
{2.5, 10, SupplyNormal}, // 25% — boundary stays normal
{2, 10, SupplyRationing}, // 20%
{1, 10, SupplyRationing}, // 10% — boundary stays rationing
{0.9, 10, SupplySevereRationing},
{0.1, 10, SupplySevereRationing},
{0, 10, SupplyStarvation},
}
for _, c := range cases {
s := ExpeditionSupplies{Current: c.current, Max: c.max}
if got := supplyDepletion(s); got != c.want {
t.Errorf("%v/%v: got %d, want %d", c.current, c.max, got, c.want)
}
}
}
func TestSupplyRollModifier(t *testing.T) {
cases := map[SupplyDepletionState]int{
SupplyNormal: 0,
SupplyRationing: -1,
SupplySevereRationing: -2,
SupplyStarvation: -2,
}
for state, want := range cases {
if got := supplyRollModifier(state); got != want {
t.Errorf("state %d: got %d, want %d", state, got, want)
}
}
}
func TestSupplyAllowsLongRest(t *testing.T) {
cases := map[SupplyDepletionState]bool{
SupplyNormal: true,
SupplyRationing: true,
SupplySevereRationing: false,
SupplyStarvation: false,
}
for state, want := range cases {
if got := supplyAllowsLongRest(state); got != want {
t.Errorf("state %d: got %v, want %v", state, got, want)
}
}
}
func TestSupplyPurchase_Validate(t *testing.T) {
cases := []struct {
p SupplyPurchase
wantErr bool
}{
{SupplyPurchase{StandardPacks: 1}, false},
{SupplyPurchase{StandardPacks: 3, DeluxePacks: 1}, false}, // max
{SupplyPurchase{StandardPacks: 4}, true}, // over standard cap
{SupplyPurchase{DeluxePacks: 2}, true}, // over deluxe cap
{SupplyPurchase{StandardPacks: -1}, true},
{SupplyPurchase{}, true}, // none purchased
}
for _, c := range cases {
err := c.p.Validate()
if (err != nil) != c.wantErr {
t.Errorf("%+v: err = %v, wantErr=%v", c.p, err, c.wantErr)
}
}
}
func TestSupplyPurchase_TotalAndCost(t *testing.T) {
p := SupplyPurchase{StandardPacks: 3, DeluxePacks: 1}
if p.Total() != 50 {
t.Errorf("total = %v, want 50", p.Total())
}
if p.Cost() != 240 {
t.Errorf("cost = %d, want 240", p.Cost())
}
}
func TestMakeSupplies_FillsFromTier(t *testing.T) {
p := SupplyPurchase{StandardPacks: 2}
s := makeSupplies(ZoneTierJourneyman, p)
if s.Max != 20 || s.Current != 20 {
t.Errorf("supplies: %+v", s)
}
if s.DailyBurn != 2.0 {
t.Errorf("daily burn = %v, want 2", s.DailyBurn)
}
if s.PacksStandard != 2 {
t.Errorf("packs std = %d", s.PacksStandard)
}
}
func TestApplyDailyBurn_DrainsAndClamps(t *testing.T) {
s := ExpeditionSupplies{Current: 5, Max: 10, DailyBurn: 2, HarshMod: 1.5, ForagedToday: true}
s, burn := applyDailyBurn(s, false)
if burn != 2 {
t.Errorf("burn = %v, want 2", burn)
}
if s.Current != 3 {
t.Errorf("current = %v, want 3", s.Current)
}
if s.ForagedToday {
t.Error("forage flag should reset on day rollover")
}
// Harsh active doubles via mult.
s, burn = applyDailyBurn(s, true)
if burn != 3 { // 2 × 1.5
t.Errorf("harsh burn = %v, want 3", burn)
}
// Drain to floor.
for i := 0; i < 5; i++ {
s, _ = applyDailyBurn(s, false)
}
if s.Current != 0 {
t.Errorf("expected current clamped to 0, got %v", s.Current)
}
}