Files
gogobee/internal/plugin/dnd_zone_narration_test.go
prosolis 3369d7d8fe gofmt: bring internal/ and cmd/ back to gofmt -l clean
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.
2026-07-10 07:18:07 -07:00

571 lines
20 KiB
Go
Raw Permalink 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 (
"strings"
"testing"
"time"
"gogobee/internal/flavor"
"maunium.net/go/mautrix/id"
)
// Phase 11 D1d — TwinBee narration + mood event tests.
func TestMoodBand_Boundaries(t *testing.T) {
cases := []struct {
score int
want MoodBand
}{
{0, MoodBandHostile}, {19, MoodBandHostile},
{20, MoodBandGrumpy}, {39, MoodBandGrumpy},
{40, MoodBandNeutral}, {59, MoodBandNeutral},
{60, MoodBandFriendly}, {79, MoodBandFriendly},
{80, MoodBandEffusive}, {100, MoodBandEffusive},
}
for _, c := range cases {
if got := moodBand(c.score); got != c.want {
t.Errorf("moodBand(%d) = %d, want %d", c.score, got, c.want)
}
}
}
func TestMoodEventDelta_DesignDocTable(t *testing.T) {
want := map[DMMoodEvent]int{
MoodEventZoneComplete: +10,
MoodEventPlayerDeath: -5,
MoodEventNat20: +3,
MoodEventNat1: -2,
MoodEventCommunityMilestone: +15,
MoodEventDowntime: -20,
MoodEventTaunt: -10,
MoodEventCompliment: +5,
}
for ev, delta := range want {
if got := MoodEventDelta(ev); got != delta {
t.Errorf("delta(%s) = %d, want %d", ev, got, delta)
}
}
}
func TestPassiveDecayMood_DriftsTowardFifty(t *testing.T) {
now := time.Date(2026, 5, 8, 12, 0, 0, 0, time.UTC)
cases := []struct {
name string
score int
hoursAgo int
want int
}{
{"high-score-decays-down", 80, 5, 70}, // -2/hr * 5
{"high-clamps-at-50", 80, 100, 50}, // would overshoot
{"low-score-decays-up", 10, 5, 20}, // +2/hr * 5
{"low-clamps-at-50", 10, 100, 50}, // would overshoot
{"already-50-no-change", 50, 100, 50}, // neutral stays neutral
{"fresh-no-decay", 80, 0, 80}, // no time elapsed
{"sub-hour-no-decay", 80, 0, 80}, // hours==0 → no drift
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
last := now.Add(-time.Duration(c.hoursAgo) * time.Hour)
if got := passiveDecayMood(c.score, last, now); got != c.want {
t.Errorf("decay(%d, %dh ago) = %d, want %d", c.score, c.hoursAgo, got, c.want)
}
})
}
}
func TestTwinBeeLine_DeterministicAcrossCalls(t *testing.T) {
a := twinBeeLine(ZoneGoblinWarrens, DMRoomEntry, "abc123", 2)
b := twinBeeLine(ZoneGoblinWarrens, DMRoomEntry, "abc123", 2)
if a != b {
t.Errorf("non-deterministic:\n a=%q\n b=%q", a, b)
}
if !strings.HasPrefix(a, "🎭 **TwinBee:**") {
t.Errorf("missing TwinBee prefix: %q", a)
}
// Different room index should typically yield a different line
// (chance of accidental collision is 1/N; with the Goblin+Generic
// pool that's tiny but theoretically possible — only assert prefix).
c := twinBeeLine(ZoneGoblinWarrens, DMRoomEntry, "abc123", 3)
if !strings.HasPrefix(c, "🎭 **TwinBee:**") {
t.Errorf("missing prefix on alt salt: %q", c)
}
}
func TestTwinBeeLine_BossEntryUsesNamedPool(t *testing.T) {
// Grol's pool is a single hand-crafted line.
got := twinBeeLine(ZoneGoblinWarrens, DMBossEntry, "any", 0)
if !strings.Contains(got, "Grol") {
t.Errorf("Goblin Warrens boss entry should mention Grol; got %q", got)
}
got = twinBeeLine(ZoneCryptValdris, DMBossEntry, "any", 0)
if !strings.Contains(got, "Valdris") {
t.Errorf("Crypt boss entry should mention Valdris; got %q", got)
}
}
func TestTwinBeeLine_UnknownKindReturnsEmpty(t *testing.T) {
if got := twinBeeLine(ZoneGoblinWarrens, DMNarrationType("not_a_real_kind"), "x", 0); got != "" {
t.Errorf("expected empty for unknown kind, got %q", got)
}
}
func TestApplyMoodEvent_PersistsClampedDelta(t *testing.T) {
setupAuditTestDB(t)
uid := id.UserID("@zone-mood-evt:example")
zoneCmdTestCharacter(t, uid, 1)
defer cleanupZoneRuns(uid)
run, err := startZoneRun(uid, ZoneGoblinWarrens, 1, nil)
if err != nil {
t.Fatalf("start: %v", err)
}
if run.DMMood != 50 {
t.Fatalf("starting mood = %d, want 50", run.DMMood)
}
// +10 → 60
got, err := applyMoodEvent(run.RunID, MoodEventZoneComplete)
if err != nil {
t.Fatalf("apply zone_complete: %v", err)
}
if got != 60 {
t.Errorf("after zone_complete: mood = %d, want 60", got)
}
// -20 → 40
got, _ = applyMoodEvent(run.RunID, MoodEventDowntime)
if got != 40 {
t.Errorf("after downtime: mood = %d, want 40", got)
}
// Clamp test: huge negative stack → 0
for i := 0; i < 20; i++ {
_, _ = applyMoodEvent(run.RunID, MoodEventDowntime)
}
r, _ := getZoneRun(run.RunID)
if r.DMMood != 0 {
t.Errorf("after extreme negative stack: mood = %d, want 0 (clamped)", r.DMMood)
}
// Clamp upward
for i := 0; i < 20; i++ {
_, _ = applyMoodEvent(run.RunID, MoodEventCommunityMilestone)
}
r, _ = getZoneRun(run.RunID)
if r.DMMood != 100 {
t.Errorf("after extreme positive stack: mood = %d, want 100 (clamped)", r.DMMood)
}
}
func TestApplyMoodEvent_UnknownEventErrors(t *testing.T) {
if _, err := applyMoodEvent("fake-run", DMMoodEvent("ghosts")); err == nil {
t.Error("expected error on unknown mood event")
}
}
// ── Phase 11 D2b — Tier 1 zone flavor file routing ──────────────────────────
func TestComposeBossEntry_AppendsAbilityCalloutForTier1(t *testing.T) {
got := composeBossEntry(ZoneGoblinWarrens, "run-d2b", 7)
if !strings.Contains(got, "Grol") {
t.Errorf("Warrens boss-entry should mention Grol: %q", got)
}
// Two TwinBee prefixes = entry + callout suffix.
if n := strings.Count(got, "🎭 **TwinBee:**"); n != 2 {
t.Errorf("Warrens boss-entry expected 2 TwinBee blocks (entry+callout), got %d:\n%s", n, got)
}
got = composeBossEntry(ZoneCryptValdris, "run-d2b", 7)
if !strings.Contains(got, "Valdris") {
t.Errorf("Crypt boss-entry should mention Valdris: %q", got)
}
if n := strings.Count(got, "🎭 **TwinBee:**"); n != 2 {
t.Errorf("Crypt boss-entry expected 2 TwinBee blocks (entry+callout), got %d:\n%s", n, got)
}
}
// (Dragon's Lair gained signature callouts and an elite pool in D5b; see
// the Tier 4-5 routing tests below for its current behavior.)
func TestComposeBossEntry_DeterministicAcrossCalls(t *testing.T) {
a := composeBossEntry(ZoneGoblinWarrens, "stable-run", 4)
b := composeBossEntry(ZoneGoblinWarrens, "stable-run", 4)
if a != b {
t.Errorf("compose not deterministic:\n a=%q\n b=%q", a, b)
}
}
func TestEliteRoomEntryLine_RoutesPerZone(t *testing.T) {
w := eliteRoomEntryLine(ZoneGoblinWarrens, "run-elite", 3)
if w == "" || !strings.HasPrefix(w, "🎭 **TwinBee:**") {
t.Errorf("Warrens elite line missing/no prefix: %q", w)
}
c := eliteRoomEntryLine(ZoneCryptValdris, "run-elite", 3)
if c == "" || !strings.HasPrefix(c, "🎭 **TwinBee:**") {
t.Errorf("Crypt elite line missing/no prefix: %q", c)
}
if w == c {
t.Errorf("Warrens and Crypt elite lines collided on same salt — pools shouldn't overlap")
}
}
func TestZoneLorePool_PrependsZoneSpecific(t *testing.T) {
w := zoneLorePool(ZoneGoblinWarrens)
if len(w) <= len(flavor.LoreLines) {
t.Errorf("Warrens lore pool should be larger than generic; got %d vs generic %d",
len(w), len(flavor.LoreLines))
}
}
// ── Phase 11 D3c — Tier 2 zone flavor file routing ──────────────────────────
func TestComposeBossEntry_AppendsAbilityCalloutForTier2(t *testing.T) {
got := composeBossEntry(ZoneForestShadows, "run-d3c", 5)
if !strings.Contains(got, "Hollow King") {
t.Errorf("Forest boss-entry should mention Hollow King: %q", got)
}
if n := strings.Count(got, "🎭 **TwinBee:**"); n != 2 {
t.Errorf("Forest boss-entry expected 2 TwinBee blocks (entry+callout), got %d:\n%s", n, got)
}
got = composeBossEntry(ZoneSunkenTemple, "run-d3c", 5)
if !strings.Contains(got, "Aboleth") {
t.Errorf("Sunken Temple boss-entry should mention Aboleth: %q", got)
}
if n := strings.Count(got, "🎭 **TwinBee:**"); n != 2 {
t.Errorf("Sunken Temple boss-entry expected 2 TwinBee blocks (entry+callout), got %d:\n%s", n, got)
}
}
func TestEliteRoomEntryLine_Tier2Routes(t *testing.T) {
f := eliteRoomEntryLine(ZoneForestShadows, "run-elite-t2", 3)
if f == "" || !strings.HasPrefix(f, "🎭 **TwinBee:**") {
t.Errorf("Forest elite line missing/no prefix: %q", f)
}
s := eliteRoomEntryLine(ZoneSunkenTemple, "run-elite-t2", 3)
if s == "" || !strings.HasPrefix(s, "🎭 **TwinBee:**") {
t.Errorf("Sunken Temple elite line missing/no prefix: %q", s)
}
if f == s {
t.Errorf("Forest and Sunken Temple elite lines collided on same salt")
}
}
func TestZoneLorePool_Tier2PrependsZoneSpecific(t *testing.T) {
f := zoneLorePool(ZoneForestShadows)
if len(f) <= len(flavor.LoreLines) {
t.Errorf("Forest lore pool should be larger than generic; got %d vs generic %d",
len(f), len(flavor.LoreLines))
}
s := zoneLorePool(ZoneSunkenTemple)
if len(s) <= len(flavor.LoreLines) {
t.Errorf("Sunken Temple lore pool should be larger than generic; got %d vs generic %d",
len(s), len(flavor.LoreLines))
}
}
func TestRoomEntryPool_Tier2PrependsZoneSpecific(t *testing.T) {
f := zoneRoomEntryPool(ZoneForestShadows)
if len(f) <= len(flavor.RoomEntryGeneric) {
t.Errorf("Forest room-entry pool should overlay zone-specific lines; got %d", len(f))
}
s := zoneRoomEntryPool(ZoneSunkenTemple)
if len(s) <= len(flavor.RoomEntryGeneric) {
t.Errorf("Sunken Temple room-entry pool should overlay zone-specific lines; got %d", len(s))
}
}
// ── Phase 11 D4c — Tier 3 zone flavor file routing ──────────────────────────
func TestComposeBossEntry_AppendsAbilityCalloutForTier3(t *testing.T) {
got := composeBossEntry(ZoneManorBlackspire, "run-d4c", 5)
if !strings.Contains(got, "Aldric") {
t.Errorf("Manor boss-entry should mention Aldric: %q", got)
}
if n := strings.Count(got, "🎭 **TwinBee:**"); n != 2 {
t.Errorf("Manor boss-entry expected 2 TwinBee blocks (entry+callout), got %d:\n%s", n, got)
}
got = composeBossEntry(ZoneUnderforge, "run-d4c", 5)
if !strings.Contains(got, "Thyrak") {
t.Errorf("Underforge boss-entry should mention Thyrak: %q", got)
}
if n := strings.Count(got, "🎭 **TwinBee:**"); n != 2 {
t.Errorf("Underforge boss-entry expected 2 TwinBee blocks (entry+callout), got %d:\n%s", n, got)
}
}
func TestEliteRoomEntryLine_Tier3Routes(t *testing.T) {
m := eliteRoomEntryLine(ZoneManorBlackspire, "run-elite-t3", 3)
if m == "" || !strings.HasPrefix(m, "🎭 **TwinBee:**") {
t.Errorf("Manor elite line missing/no prefix: %q", m)
}
u := eliteRoomEntryLine(ZoneUnderforge, "run-elite-t3", 3)
if u == "" || !strings.HasPrefix(u, "🎭 **TwinBee:**") {
t.Errorf("Underforge elite line missing/no prefix: %q", u)
}
if m == u {
t.Errorf("Manor and Underforge elite lines collided on same salt")
}
}
func TestZoneLorePool_Tier3PrependsZoneSpecific(t *testing.T) {
m := zoneLorePool(ZoneManorBlackspire)
if len(m) <= len(flavor.LoreLines) {
t.Errorf("Manor lore pool should be larger than generic; got %d vs generic %d",
len(m), len(flavor.LoreLines))
}
u := zoneLorePool(ZoneUnderforge)
if len(u) <= len(flavor.LoreLines) {
t.Errorf("Underforge lore pool should be larger than generic; got %d vs generic %d",
len(u), len(flavor.LoreLines))
}
}
func TestRoomEntryPool_Tier3PrependsZoneSpecific(t *testing.T) {
m := zoneRoomEntryPool(ZoneManorBlackspire)
if len(m) <= len(flavor.RoomEntryGeneric) {
t.Errorf("Manor room-entry pool should overlay zone-specific lines; got %d", len(m))
}
u := zoneRoomEntryPool(ZoneUnderforge)
if len(u) <= len(flavor.RoomEntryGeneric) {
t.Errorf("Underforge room-entry pool should overlay zone-specific lines; got %d", len(u))
}
}
func TestZoneCmd_AdvanceFinalRoomBumpsMood(t *testing.T) {
setupAuditTestDB(t)
uid := id.UserID("@zone-mood-complete:example")
zoneCmdTestCharacter(t, uid, 1)
defer cleanupZoneRuns(uid)
// D1e wired real combat into !zone advance, so the cmd-level path
// is no longer deterministic for an L1 fighter against the Goblin
// Warrens roster. Drive the persistence layer directly to assert
// the mood-on-completion contract.
run, err := startZoneRun(uid, ZoneGoblinWarrens, 1, nil)
if err != nil {
t.Fatal(err)
}
// Phase G: branching paths through a diamond may complete the run in
// fewer steps than TotalRooms. Drive markRoomCleared until it returns
// the empty room type (boss kill / dead-end) or we hit a safety cap.
for i := 0; i < run.TotalRooms+2; i++ {
next, err := markRoomCleared(run.RunID)
if err != nil {
t.Fatalf("markRoomCleared %d: %v", i, err)
}
if next == "" {
break
}
}
if _, err := applyMoodEvent(run.RunID, MoodEventZoneComplete); err != nil {
t.Fatal(err)
}
final, err := getZoneRun(run.RunID)
if err != nil || final == nil {
t.Fatal("could not fetch completed run")
}
if !final.BossDefeated {
t.Error("boss should be defeated after final markRoomCleared")
}
if final.DMMood != 60 {
t.Errorf("post-completion mood = %d, want 60 (50 + zone_complete +10)", final.DMMood)
}
}
// ── Phase 11 D5b — Tier 4-5 zone flavor file routing ────────────────────────
func TestComposeBossEntry_AppendsAbilityCalloutForTier4(t *testing.T) {
got := composeBossEntry(ZoneUnderdark, "run-d5b", 5)
if !strings.Contains(got, "Ilvaras") {
t.Errorf("Underdark boss-entry should mention Ilvaras: %q", got)
}
if n := strings.Count(got, "🎭 **TwinBee:**"); n != 2 {
t.Errorf("Underdark boss-entry expected 2 TwinBee blocks (entry+callout), got %d:\n%s", n, got)
}
got = composeBossEntry(ZoneFeywildCrossing, "run-d5b", 5)
if !strings.Contains(got, "Thornmother") {
t.Errorf("Feywild boss-entry should mention Thornmother: %q", got)
}
if n := strings.Count(got, "🎭 **TwinBee:**"); n != 2 {
t.Errorf("Feywild boss-entry expected 2 TwinBee blocks (entry+callout), got %d:\n%s", n, got)
}
}
func TestComposeBossEntry_AppendsAbilityCalloutForTier5(t *testing.T) {
got := composeBossEntry(ZoneDragonsLair, "run-d5b", 5)
if !strings.Contains(got, "Infernax") {
t.Errorf("Dragon's Lair boss-entry should mention Infernax: %q", got)
}
if n := strings.Count(got, "🎭 **TwinBee:**"); n != 2 {
t.Errorf("Dragon's Lair boss-entry expected 2 TwinBee blocks (entry+callout), got %d:\n%s", n, got)
}
got = composeBossEntry(ZoneAbyssPortal, "run-d5b", 5)
if !strings.Contains(got, "Belaxath") {
t.Errorf("Abyss boss-entry should mention Belaxath: %q", got)
}
if n := strings.Count(got, "🎭 **TwinBee:**"); n != 2 {
t.Errorf("Abyss boss-entry expected 2 TwinBee blocks (entry+callout), got %d:\n%s", n, got)
}
}
func TestEliteRoomEntryLine_Tier4And5Routes(t *testing.T) {
u := eliteRoomEntryLine(ZoneUnderdark, "run-elite-t45", 3)
if u == "" || !strings.HasPrefix(u, "🎭 **TwinBee:**") {
t.Errorf("Underdark elite line missing/no prefix: %q", u)
}
f := eliteRoomEntryLine(ZoneFeywildCrossing, "run-elite-t45", 3)
if f == "" || !strings.HasPrefix(f, "🎭 **TwinBee:**") {
t.Errorf("Feywild elite line missing/no prefix: %q", f)
}
d := eliteRoomEntryLine(ZoneDragonsLair, "run-elite-t45", 3)
if d == "" || !strings.HasPrefix(d, "🎭 **TwinBee:**") {
t.Errorf("Dragon's Lair elite line missing/no prefix: %q", d)
}
a := eliteRoomEntryLine(ZoneAbyssPortal, "run-elite-t45", 3)
if a == "" || !strings.HasPrefix(a, "🎭 **TwinBee:**") {
t.Errorf("Abyss elite line missing/no prefix: %q", a)
}
if u == f || u == d || u == a || f == d || f == a || d == a {
t.Errorf("Tier 4-5 elite lines collided on same salt — pools shouldn't overlap")
}
}
func TestZoneLorePool_Tier4And5PrependsZoneSpecific(t *testing.T) {
for _, z := range []ZoneID{ZoneUnderdark, ZoneFeywildCrossing, ZoneDragonsLair, ZoneAbyssPortal} {
if got := zoneLorePool(z); len(got) <= len(flavor.LoreLines) {
t.Errorf("%s lore pool should be larger than generic; got %d vs generic %d",
z, len(got), len(flavor.LoreLines))
}
}
}
func TestRoomEntryPool_Tier4And5PrependsZoneSpecific(t *testing.T) {
// All four T4-5 zones should now overlay zone-specific room entries.
for _, z := range []ZoneID{ZoneUnderdark, ZoneFeywildCrossing, ZoneDragonsLair, ZoneAbyssPortal} {
if got := zoneRoomEntryPool(z); len(got) <= len(flavor.RoomEntryGeneric) {
t.Errorf("%s room-entry pool should overlay zone-specific lines; got %d", z, len(got))
}
}
}
// ── Phase 11 D6 — boss phase-two narration ──────────────────────────────────
func TestBossPhaseTwoPool_RoutesEightZones(t *testing.T) {
cases := map[ZoneID]int{
ZoneCryptValdris: len(flavor.ValdrisPhaseTwoLines),
ZoneForestShadows: len(flavor.HollowKingPhaseTwoLines),
ZoneManorBlackspire: len(flavor.AldricPhaseTwoLines),
ZoneUnderforge: len(flavor.ThyrakPhaseTwoLines),
ZoneUnderdark: len(flavor.IlvarasPhaseTwoLines),
ZoneFeywildCrossing: len(flavor.ThornmotherPhaseTwoLines),
ZoneDragonsLair: len(flavor.InfernaxPhaseTwoLines),
ZoneAbyssPortal: len(flavor.BelaxathPhaseTwoLines),
}
for z, want := range cases {
got := bossPhaseTwoPool(z)
if len(got) == 0 || len(got) != want {
t.Errorf("%s phase-two pool len mismatch: got %d, want %d", z, len(got), want)
}
}
// Bosses without a phase-two transition return nil.
for _, z := range []ZoneID{ZoneGoblinWarrens, ZoneSunkenTemple} {
if got := bossPhaseTwoPool(z); got != nil {
t.Errorf("%s should not have a phase-two pool (no PhaseTwoAt)", z)
}
}
}
func TestBossPhaseTwoLine_PrependsTwinBee(t *testing.T) {
line := bossPhaseTwoLine(ZoneCryptValdris, "run-d6", 4)
if !strings.HasPrefix(line, "🎭 **TwinBee:**") {
t.Errorf("phase-two line missing TwinBee prefix: %q", line)
}
if bossPhaseTwoLine(ZoneGoblinWarrens, "run-d6", 4) != "" {
t.Errorf("Grol has no phase-two pool — expected empty string")
}
}
func TestPhaseTwoCrossedInEvents(t *testing.T) {
startHP := 100
threshold := 0.5
events := []CombatEvent{{EnemyHP: 80}, {EnemyHP: 60}, {EnemyHP: 40}}
if !phaseTwoCrossedInEvents(events, startHP, threshold) {
t.Errorf("expected crossing at EnemyHP=40 (cutoff=50)")
}
if phaseTwoCrossedInEvents(events[:2], startHP, threshold) {
t.Errorf("did not cross 50%% by event 2 (EnemyHP=60); should be false")
}
if phaseTwoCrossedInEvents(events, startHP, 0) {
t.Errorf("threshold 0 means no phase 2 — should be false")
}
if phaseTwoCrossedInEvents(events, 0, threshold) {
t.Errorf("startHP 0 — should be false (degenerate)")
}
}
// ── Phase 11 D8 — mood-banded TwinBee aside ─────────────────────────────────
func TestMoodAsideLine_AllBandsExceptNeutral(t *testing.T) {
// Strict Neutral (4059) stays silent — TwinBee has nothing
// notable to say. Every other band produces an aside.
for _, mood := range []int{40, 50, 59} {
if got := moodAsideLine(mood, "run-aside", 3); got != "" {
t.Errorf("mood=%d (neutral) should produce no aside; got %q", mood, got)
}
}
for label, moods := range map[string][]int{
"hostile": {0, 19},
"grumpy": {20, 39},
"friendly": {60, 79},
"effusive": {80, 100},
} {
for _, mood := range moods {
got := moodAsideLine(mood, "run-aside", 3)
if got == "" || !strings.HasPrefix(got, "🎭 **TwinBee:**") {
t.Errorf("%s mood=%d should produce aside; got %q", label, mood, got)
}
}
}
}
func TestMoodAsideLine_DrawsFromCorrectPool(t *testing.T) {
hostile := moodAsideLine(5, "run-aside", 1)
effusive := moodAsideLine(95, "run-aside", 1)
// Each line must come from its band's pool.
if !lineInPool(hostile, flavor.MoodAsidesHostile) {
t.Errorf("hostile aside %q not in MoodAsidesHostile", hostile)
}
if !lineInPool(effusive, flavor.MoodAsidesEffusive) {
t.Errorf("effusive aside %q not in MoodAsidesEffusive", effusive)
}
}
func TestMoodAsideLine_DeterministicPerRoom(t *testing.T) {
a := moodAsideLine(10, "run-D8", 5)
b := moodAsideLine(10, "run-D8", 5)
if a != b {
t.Errorf("same (mood, runID, roomIdx) should produce same aside: %q vs %q", a, b)
}
}
// lineInPool returns true if rendered (with TwinBee prefix) corresponds to
// any line in pool.
func lineInPool(rendered string, pool []string) bool {
const prefix = "🎭 **TwinBee:** "
if !strings.HasPrefix(rendered, prefix) {
return false
}
body := strings.TrimPrefix(rendered, prefix)
for _, line := range pool {
if line == body {
return true
}
}
return false
}