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") } }