adventure: draw the dungeon as a fog-of-war map, not a room tally
This commit is contained in:
148
internal/web/who_map_test.go
Normal file
148
internal/web/who_map_test.go
Normal file
@@ -0,0 +1,148 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"pete/internal/storage"
|
||||
"time"
|
||||
)
|
||||
|
||||
// fogMap is the shape gogobee sends: two visited rooms (entry, elite), and two
|
||||
// frontier rooms reachable one hop out — n4 is really the boss but arrives with
|
||||
// its kind withheld as "unknown". n3 sits behind a perception-locked door, n4
|
||||
// behind a key-locked one.
|
||||
func fogMap() *whoMap {
|
||||
return &whoMap{
|
||||
ZoneID: "ossuary",
|
||||
CurrentNode: "n2",
|
||||
Visited: []string{"n1", "n2"},
|
||||
Nodes: []whoMapNode{
|
||||
{ID: "n1", Kind: "entry"},
|
||||
{ID: "n2", Kind: "elite"},
|
||||
{ID: "n3", Kind: "unknown"},
|
||||
{ID: "n4", Kind: "unknown"},
|
||||
},
|
||||
Edges: []whoMapEdge{
|
||||
{From: "n1", To: "n2", Lock: ""},
|
||||
{From: "n1", To: "n3", Lock: "perception_check"},
|
||||
{From: "n2", To: "n4", Lock: "key_required"},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildMapView_Layout(t *testing.T) {
|
||||
v := buildMapView(fogMap())
|
||||
if v == nil {
|
||||
t.Fatal("nil view for a non-empty map")
|
||||
}
|
||||
if len(v.Nodes) != 4 {
|
||||
t.Fatalf("want 4 nodes, got %d", len(v.Nodes))
|
||||
}
|
||||
|
||||
// Depth places entry in column 0, its two successors in column 1, the boss
|
||||
// frontier in column 2 — a left-to-right dungeon.
|
||||
var n1, n2, n4 mapNode
|
||||
for _, n := range v.Nodes {
|
||||
switch {
|
||||
case n.Kind == "entry":
|
||||
n1 = n
|
||||
case n.Kind == "elite":
|
||||
n2 = n
|
||||
case n.Kind == "unknown" && n.X > n1.X+mapColGap:
|
||||
n4 = n // the deepest unknown is n4
|
||||
}
|
||||
}
|
||||
if !(n1.X < n2.X && n2.X < n4.X) {
|
||||
t.Errorf("columns not left-to-right: entry=%d elite=%d frontier=%d", n1.X, n2.X, n4.X)
|
||||
}
|
||||
// The boss frontier must NOT reveal its kind or its glyph.
|
||||
if n4.Kind != "unknown" || n4.Glyph != "?" {
|
||||
t.Errorf("frontier boss leaked: kind=%q glyph=%q", n4.Kind, n4.Glyph)
|
||||
}
|
||||
if n2.Current != true {
|
||||
t.Errorf("current node n2 (elite) should be marked current")
|
||||
}
|
||||
if n1.Current {
|
||||
t.Errorf("entry is not the current node")
|
||||
}
|
||||
|
||||
// Edges: three total, the two locked ones dashed, the visited-visited one open.
|
||||
if len(v.Edges) != 3 {
|
||||
t.Fatalf("want 3 edges, got %d", len(v.Edges))
|
||||
}
|
||||
locked := 0
|
||||
for _, e := range v.Edges {
|
||||
if e.Locked {
|
||||
locked++
|
||||
}
|
||||
}
|
||||
if locked != 2 {
|
||||
t.Errorf("want 2 locked doors (perception, key), got %d", locked)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildMapView_EmptyIsNil(t *testing.T) {
|
||||
if buildMapView(nil) != nil {
|
||||
t.Error("nil map should give nil view")
|
||||
}
|
||||
if buildMapView(&whoMap{ZoneID: "x"}) != nil {
|
||||
t.Error("map with no nodes should give nil view")
|
||||
}
|
||||
}
|
||||
|
||||
// publicDetailWithMap is publicDetail plus a fog-of-war map, the way the roster
|
||||
// push carries both.
|
||||
func publicDetailWithMap(t *testing.T) json.RawMessage {
|
||||
t.Helper()
|
||||
raw, err := json.Marshal(map[string]any{
|
||||
"hp_current": 30,
|
||||
"hp_max": 42,
|
||||
"armor_class": 17,
|
||||
"abilities": [6]int{16, 14, 15, 10, 12, 8},
|
||||
"modifiers": [6]int{3, 2, 2, 0, 1, -1},
|
||||
"supplies": 8,
|
||||
"room": "2 / 4",
|
||||
"map": fogMap(),
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return raw
|
||||
}
|
||||
|
||||
// TestWhoMapRenders drives the real template with a map present and asserts the
|
||||
// SVG lands, the current room is marked, and the fog holds: the frontier boss
|
||||
// is drawn as an unexplored room, never as a boss.
|
||||
func TestWhoMapRenders(t *testing.T) {
|
||||
s, _ := newAdvServer(t, "tok")
|
||||
s.auth = &Authenticator{secret: []byte("test-secret-key-at-least-16")}
|
||||
now := time.Now().Unix()
|
||||
|
||||
e := entry("tok-josie", "Josie", "expedition", "holymachina")
|
||||
e.Detail = publicDetailWithMap(t)
|
||||
if w := postRoster(t, s, "tok", rosterPush{SnapshotAt: now, Adventurers: []storage.RosterEntry{e}}); w.Code != 200 {
|
||||
t.Fatalf("seed roster = %d", w.Code)
|
||||
}
|
||||
|
||||
w := getWho(t, s, "tok-josie", "")
|
||||
if w.Code != 200 {
|
||||
t.Fatalf("who = %d: %s", w.Code, w.Body.String())
|
||||
}
|
||||
body := w.Body.String()
|
||||
|
||||
for _, want := range []string{"The map", "map-svg", "map-node-current", "map-edge-locked"} {
|
||||
if !strings.Contains(body, want) {
|
||||
t.Errorf("map render missing %q", want)
|
||||
}
|
||||
}
|
||||
// The elite room glyph shows (visited); the boss glyph must not (the boss is
|
||||
// only reachable as an unrevealed frontier door).
|
||||
if !strings.Contains(body, "◆") {
|
||||
t.Error("visited elite room should show its glyph")
|
||||
}
|
||||
if strings.Contains(body, "♛") {
|
||||
t.Error("fog leak: the boss glyph rendered for an unexplored frontier room")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user