mirror of
https://github.com/prosolis/gogobee.git
synced 2026-07-15 16:42:41 +00:00
Mechanical `gofmt -w ./internal ./cmd`. Mostly struct-field realignment that had drifted, plus a few trailing-newline fixes. No behaviour change — gofmt is semantics-preserving, and build/vet/test are green either side. Split out from the code-review fixes that follow so those stay reviewable instead of hiding inside a wall of realignment.
243 lines
6.9 KiB
Go
243 lines
6.9 KiB
Go
package plugin
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestEvaluateEdgeLock_None(t *testing.T) {
|
|
e := ZoneEdge{From: "a", To: "b", Lock: LockNone}
|
|
ok, _ := evaluateEdgeLock(e, edgeUnlockCtx{})
|
|
if !ok {
|
|
t.Fatal("LockNone should always unlock")
|
|
}
|
|
}
|
|
|
|
func TestEvaluateEdgeLock_Perception(t *testing.T) {
|
|
e := ZoneEdge{
|
|
From: "a",
|
|
To: "b",
|
|
Lock: LockPerception,
|
|
LockData: map[string]any{"dc": 1}, // trivially passes
|
|
}
|
|
ok, _ := evaluateEdgeLock(e, edgeUnlockCtx{
|
|
RunID: "run", FromNode: "a",
|
|
AbilityMods: [6]int{0, 0, 0, 0, 5, 0},
|
|
})
|
|
if !ok {
|
|
t.Fatal("Perception roll+5 vs DC 1 should unlock")
|
|
}
|
|
|
|
// DC 99 + bad WIS → never unlock
|
|
e.LockData = map[string]any{"dc": 99}
|
|
ok, reason := evaluateEdgeLock(e, edgeUnlockCtx{
|
|
RunID: "run", FromNode: "a",
|
|
AbilityMods: [6]int{0, 0, 0, 0, -2, 0},
|
|
})
|
|
if ok {
|
|
t.Fatal("Perception vs DC 99 should fail")
|
|
}
|
|
if !strings.Contains(reason, "Perception") {
|
|
t.Errorf("reason = %q, want Perception phrasing", reason)
|
|
}
|
|
}
|
|
|
|
func TestPerceptionRollForEdge_Deterministic(t *testing.T) {
|
|
r1 := perceptionRollForEdge("run-x", "a", "b")
|
|
r2 := perceptionRollForEdge("run-x", "a", "b")
|
|
if r1 != r2 {
|
|
t.Fatalf("perception roll non-deterministic: %d vs %d", r1, r2)
|
|
}
|
|
if r1 < 1 || r1 > 20 {
|
|
t.Errorf("roll %d out of 1..20", r1)
|
|
}
|
|
r3 := perceptionRollForEdge("run-y", "a", "b")
|
|
r4 := perceptionRollForEdge("run-x", "a", "c")
|
|
// Just sanity-check the seed actually mixes inputs.
|
|
if r1 == r3 && r1 == r4 {
|
|
t.Error("perception roll insensitive to seed inputs")
|
|
}
|
|
}
|
|
|
|
func TestEvaluateEdgeLock_Key(t *testing.T) {
|
|
e := ZoneEdge{
|
|
From: "a",
|
|
To: "b",
|
|
Lock: LockKey,
|
|
LockData: map[string]any{"key_id": "Brass Key"},
|
|
}
|
|
ok, _ := evaluateEdgeLock(e, edgeUnlockCtx{
|
|
InventoryNames: map[string]bool{"brass key": true},
|
|
})
|
|
if !ok {
|
|
t.Fatal("matching key should unlock")
|
|
}
|
|
ok, reason := evaluateEdgeLock(e, edgeUnlockCtx{
|
|
InventoryNames: map[string]bool{},
|
|
})
|
|
if ok {
|
|
t.Fatal("missing key should not unlock")
|
|
}
|
|
if !strings.Contains(reason, "key") {
|
|
t.Errorf("reason = %q", reason)
|
|
}
|
|
}
|
|
|
|
func TestEvaluateEdgeLock_LevelMin(t *testing.T) {
|
|
e := ZoneEdge{From: "a", To: "b", Lock: LockLevelMin, LockData: map[string]any{"min_level": 5}}
|
|
if ok, _ := evaluateEdgeLock(e, edgeUnlockCtx{CharLevel: 5}); !ok {
|
|
t.Fatal("level 5 should pass min 5")
|
|
}
|
|
if ok, _ := evaluateEdgeLock(e, edgeUnlockCtx{CharLevel: 4}); ok {
|
|
t.Fatal("level 4 should fail min 5")
|
|
}
|
|
}
|
|
|
|
func TestEvaluateEdgeLock_StatCheck(t *testing.T) {
|
|
e := ZoneEdge{
|
|
From: "a", To: "b", Lock: LockStatCheck,
|
|
LockData: map[string]any{"stat": "STR", "dc": 1},
|
|
}
|
|
ok, _ := evaluateEdgeLock(e, edgeUnlockCtx{
|
|
RunID: "x", FromNode: "a",
|
|
AbilityMods: [6]int{5, 0, 0, 0, 0, 0},
|
|
})
|
|
if !ok {
|
|
t.Fatal("STR+5 vs DC 1 should unlock")
|
|
}
|
|
}
|
|
|
|
func TestPendingFork_Roundtrip(t *testing.T) {
|
|
pf := pendingFork{
|
|
PendingAt: "fork",
|
|
Options: []pendingChoice{
|
|
{Index: 1, To: "left", Label: "Left", Unlocked: true},
|
|
{Index: 2, To: "right", Label: "Right", Unlocked: false, Hint: "you sense a draft", Reason: "Perception 4 vs DC 12"},
|
|
},
|
|
}
|
|
m, err := encodePendingFork(pf)
|
|
if err != nil {
|
|
t.Fatalf("encode: %v", err)
|
|
}
|
|
got, err := decodePendingFork(m)
|
|
if err != nil {
|
|
t.Fatalf("decode: %v", err)
|
|
}
|
|
if got == nil || got.PendingAt != "fork" || len(got.Options) != 2 {
|
|
t.Fatalf("decoded pendingFork mismatch: %+v", got)
|
|
}
|
|
if got.Options[0].To != "left" || got.Options[1].Hint != "you sense a draft" {
|
|
t.Errorf("decoded options bad: %+v", got.Options)
|
|
}
|
|
}
|
|
|
|
func TestDecodePendingFork_Empty(t *testing.T) {
|
|
if pf, _ := decodePendingFork(nil); pf != nil {
|
|
t.Error("nil map should decode to nil fork")
|
|
}
|
|
if pf, _ := decodePendingFork(map[string]any{}); pf != nil {
|
|
t.Error("empty map should decode to nil fork")
|
|
}
|
|
// Non-fork content (some future schema in the same column) is also nil.
|
|
if pf, _ := decodePendingFork(map[string]any{"unrelated": 1}); pf != nil {
|
|
t.Error("non-fork content should decode to nil fork")
|
|
}
|
|
}
|
|
|
|
func TestResolveForkChoice(t *testing.T) {
|
|
pf := &pendingFork{
|
|
PendingAt: "fork",
|
|
Options: []pendingChoice{
|
|
{Index: 1, To: "left", Unlocked: true},
|
|
{Index: 2, To: "right", Unlocked: false, Reason: "DC 12"},
|
|
},
|
|
}
|
|
if c, err := resolveForkChoice(pf, 1); err != nil || c.To != "left" {
|
|
t.Errorf("choice 1 = %+v err=%v", c, err)
|
|
}
|
|
if _, err := resolveForkChoice(pf, 2); err == nil {
|
|
t.Error("locked choice should error")
|
|
}
|
|
if _, err := resolveForkChoice(pf, 3); err == nil {
|
|
t.Error("out-of-range should error")
|
|
}
|
|
if _, err := resolveForkChoice(nil, 1); err == nil {
|
|
t.Error("nil fork should error")
|
|
}
|
|
}
|
|
|
|
func TestRenderForkPrompt_LockedHint(t *testing.T) {
|
|
zone := ZoneDefinition{ID: "z", Display: "Test"}
|
|
pf := pendingFork{
|
|
Options: []pendingChoice{
|
|
{Index: 1, Label: "Left", Unlocked: true},
|
|
{Index: 2, Label: "Right", Unlocked: false, Hint: "you sense a draft"},
|
|
{Index: 3, Label: "Hidden", Unlocked: false},
|
|
},
|
|
}
|
|
got := renderForkPrompt(zone, pf)
|
|
if !strings.Contains(got, "**1.** Left") {
|
|
t.Errorf("unlocked option not rendered: %q", got)
|
|
}
|
|
if !strings.Contains(got, "you sense a draft") {
|
|
t.Errorf("hint not rendered: %q", got)
|
|
}
|
|
if !strings.Contains(got, "**3.** Hidden _(locked)_") {
|
|
t.Errorf("plain locked option not rendered: %q", got)
|
|
}
|
|
}
|
|
|
|
func TestEvaluateForkEdges_OrdersByGraph(t *testing.T) {
|
|
g := BuildGraph("z",
|
|
[]ZoneNode{
|
|
{NodeID: "e", IsEntry: true, Kind: NodeKindEntry, Label: "Entry"},
|
|
{NodeID: "fork", Kind: NodeKindFork, Label: "Fork"},
|
|
{NodeID: "left", Kind: NodeKindExploration, Label: "Left Hall"},
|
|
{NodeID: "right", Kind: NodeKindExploration, Label: "Right Hall"},
|
|
{NodeID: "b", IsBoss: true, Kind: NodeKindBoss, Label: "Boss"},
|
|
},
|
|
[]ZoneEdge{
|
|
{From: "e", To: "fork"},
|
|
{From: "fork", To: "right", Weight: 2,
|
|
Lock: LockLevelMin, LockData: map[string]any{"min_level": 99}},
|
|
{From: "fork", To: "left", Weight: 1},
|
|
{From: "left", To: "b"},
|
|
{From: "right", To: "b"},
|
|
},
|
|
)
|
|
ctx := edgeUnlockCtx{RunID: "run-1", FromNode: "fork", CharLevel: 5}
|
|
choices := evaluateForkEdges(g, "fork", ctx)
|
|
if len(choices) != 2 {
|
|
t.Fatalf("want 2 choices, got %d", len(choices))
|
|
}
|
|
// Lowest weight first → left.
|
|
if choices[0].To != "left" || !choices[0].Unlocked {
|
|
t.Errorf("choice 0 = %+v", choices[0])
|
|
}
|
|
if choices[1].To != "right" || choices[1].Unlocked {
|
|
t.Errorf("choice 1 = %+v", choices[1])
|
|
}
|
|
if choices[0].Label != "Left Hall" {
|
|
t.Errorf("label not propagated: %q", choices[0].Label)
|
|
}
|
|
}
|
|
|
|
func TestNodeKindToRoomType(t *testing.T) {
|
|
cases := map[ZoneNodeKind]RoomType{
|
|
NodeKindEntry: RoomEntry,
|
|
NodeKindExploration: RoomExploration,
|
|
NodeKindSecret: RoomExploration,
|
|
NodeKindHarvest: RoomExploration,
|
|
NodeKindFork: RoomExploration,
|
|
NodeKindMerge: RoomExploration,
|
|
NodeKindTrap: RoomTrap,
|
|
NodeKindElite: RoomElite,
|
|
NodeKindBoss: RoomBoss,
|
|
}
|
|
for k, want := range cases {
|
|
if got := nodeKindToRoomType(k); got != want {
|
|
t.Errorf("%s → %s, want %s", k, got, want)
|
|
}
|
|
}
|
|
}
|