// Copyright (c) 2026 Storm Dragon // SPDX-License-Identifier: GPL-3.0-or-later package rogue_test import ( "slices" "strings" "testing" "git.stormux.org/storm/fenrir/internal/applicationoverrides/rogue" "git.stormux.org/storm/fenrir/internal/command" "git.stormux.org/storm/fenrir/internal/presentation" "git.stormux.org/storm/fenrir/internal/reader" "git.stormux.org/storm/fenrir/internal/screen" ) const initialStatus = "Level: 1 Gold: 0 Hp: 12(12) Str: 16(16) Arm: 4 Exp: 1/0" func game(rows, cols int, message, status string) screen.Snapshot { s := screen.Snapshot{Terminal: 1, Active: true, Rows: rows, Cols: cols, X: 10, Y: 5, Cells: make([]rune, rows*cols)} for index := range s.Cells { s.Cells[index] = ' ' } copy(s.Line(0), []rune(message)) copy(s.Line(4)[8:], []rune("-----")) copy(s.Line(5)[8:], []rune("|.@.+")) copy(s.Line(6)[8:], []rune("----|")) copy(s.Line(23), []rune(status)) return s } func texts(messages []presentation.Message) []string { var result []string for _, message := range messages { if message.Text != "" { result = append(result, message.Text) } } return result } func TestNamedRegionsAndVisibleMap(t *testing.T) { for _, size := range [][2]int{{24, 80}, {32, 100}} { a := &rogue.Adapter{} s := game(size[0], size[1], "Welcome", initialStatus) s.Line(5)[14], s.Line(8)[10], s.Line(9)[10] = 'B', '%', '!' frame := a.Observe(s) if !frame.Recognized || !frame.OwnsPresentation { t.Fatal("game not recognized") } for _, trial := range []struct { id command.ID want string }{ {command.ReadLiveRegion1, "Health: 12 of 12"}, {command.ReadLiveRegion2, "Status: " + initialStatus}, {command.ReadLiveRegion3, "Latest message: Welcome"}, {command.ReadLiveRegion4, "Wall, north 1"}, {command.ReadLiveRegion5, "Visible monsters: bat, east 4"}, {command.ReadLiveRegion6, "Stairs, south 3"}, } { got, handled := a.Command(trial.id) if !handled || !strings.Contains(got.Text, trial.want) { t.Fatalf("%s: %+v", trial.id, got) } } if got, _ := a.Command(command.ListReadingRegions); !strings.Contains(got.Text, "Live region 1: Health") || !strings.Contains(got.Text, "Live region 6: Visible objects and landmarks") { t.Fatal(got) } if _, handled := a.Command(command.ReadLiveRegion7); handled { t.Fatal("unassigned slot captured") } if got, _ := a.Command(command.ReadLiveRegion5); strings.Contains(got.Text, "dragon") { t.Fatal("invented hidden monster") } } } func TestMessagesHealthAndHungerWithoutMapChatter(t *testing.T) { a := &rogue.Adapter{} r := reader.New() update := func(s screen.Snapshot) []string { return texts(r.Update_application(s, a.Observe(s))) } update(game(24, 80, "", initialStatus)) if got := update(game(24, 80, "", initialStatus)); len(got) != 0 { t.Fatal("idle chatter", got) } damaged := strings.Replace(initialStatus, "12(12)", "9(12)", 1) + " Hungry" got := update(game(24, 80, "The bat hits you", damaged)) if !slices.Equal(got, []string{"The bat hits you", "Health: 9 of 12", "Hungry"}) { t.Fatal(got) } if got := update(game(24, 80, "The bat hits you", damaged)); len(got) != 0 { t.Fatal("repeated snapshot", got) } update(game(24, 80, "", damaged)) if got := update(game(24, 80, "The bat hits you", damaged)); !slices.Equal(got, []string{"The bat hits you"}) { t.Fatal("repeated combat lost", got) } if got := update(game(24, 80, "The bat hits you--More--", damaged)); len(got) != 1 || !strings.Contains(got[0], "More") { t.Fatal("paging prompt lost", got) } if got := update(game(24, 80, "", initialStatus)); !slices.Contains(got, "Not hungry") { t.Fatal("hunger recovery", got) } r.Configure(false, true) if got := update(game(24, 80, "A new message", damaged)); len(got) != 0 { t.Fatal("automatic disabled", got) } if got, _ := a.Command(command.ReadLiveRegion3); got.Text != "Latest message: A new message" { t.Fatal("explicit read disabled", got) } } func TestNewMonstersAndLandmarks(t *testing.T) { a := &rogue.Adapter{} a.Observe(game(24, 80, "", initialStatus)) s := game(24, 80, "", initialStatus) s.Line(5)[14], s.Line(8)[10] = 'B', '%' if got := texts(a.Observe(s).Updates); !slices.Equal(got, []string{"bat, east 4", "Stairs, south 3"}) { t.Fatal(got) } s = game(24, 80, "", initialStatus) s.Line(5)[13], s.Line(8)[10] = 'B', '%' if got := texts(a.Observe(s).Updates); len(got) != 0 { t.Fatal("moving monster chatter", got) } s = game(24, 80, "", initialStatus) s.Line(5)[13], s.Line(8)[10], s.Line(6)[15] = 'B', '%', 'B' if got := texts(a.Observe(s).Updates); !slices.Equal(got, []string{"bat, south 1, east 5"}) { t.Fatal("second monster", got) } } func TestStatusChangesAndNewLevelBaseline(t *testing.T) { a := &rogue.Adapter{} a.Observe(game(24, 80, "", initialStatus)) s := game(24, 80, "", "Level: 2 Gold: 45 Hp: 9(15) Str: 15(16) Arm: 3 Exp: 2/10 Weak") s.Line(10)[10] = 'D' want := []string{"Health: 9 of 15", "Dungeon level: 2", "Strength: 15", "Armor: 3", "Experience level: 2", "Weak"} if got := texts(a.Observe(s).Updates); !slices.Equal(got, want) { t.Fatal("status or level baseline", got) } } func TestDialogsAndContextBoundaries(t *testing.T) { a := &rogue.Adapter{} a.Observe(game(24, 80, "Welcome", initialStatus)) s := game(24, 80, "", initialStatus) for index := range s.Cells { s.Cells[index] = ' ' } copy(s.Line(0), []rune("a) A potion of healing")) copy(s.Line(23), []rune("--Press space to continue--")) frame := a.Observe(s) if !frame.Recognized || frame.OwnsPresentation { t.Fatal("inventory did not use ordinary reading") } if got, _ := a.Command(command.ReadLiveRegion4); got.Text != "Surroundings: Information unavailable" { t.Fatal("stale surroundings", got) } if got := texts(a.Observe(game(24, 80, "", initialStatus)).Updates); len(got) != 0 { t.Fatal("return replayed game", got) } for _, boundary := range []string{"resize", "terminal", "buffer", "inactive", "shell", "malformed"} { t.Run(boundary, func(t *testing.T) { a := &rogue.Adapter{} a.Observe(game(24, 80, "Old message", initialStatus)) s := game(24, 80, "", initialStatus) switch boundary { case "resize": s = game(32, 100, "", initialStatus) case "terminal": s.Terminal++ case "buffer": s.Generation++ case "inactive": s.Active = false case "shell": for index := range s.Cells { s.Cells[index] = ' ' } copy(s.Line(23), []rune("user@host $ ")) case "malformed": s.Cells = s.Cells[:10] } frame := a.Observe(s) if boundary == "inactive" || boundary == "shell" || boundary == "malformed" { if frame.Recognized { t.Fatal("unrelated screen recognized") } } if got, _ := a.Command(command.ReadLiveRegion3); strings.Contains(got.Text, "Old message") { t.Fatal("stale context", got) } }) } } func TestRawReviewStillReadsActualMap(t *testing.T) { a, r := &rogue.Adapter{}, reader.New() s := game(24, 80, "", initialStatus) r.Update_application(s, a.Observe(s)) if got := r.Command(command.CurrentLine); !strings.Contains(got.Text, "@") { t.Fatal(got) } r.Note_input(true, false, false) s = game(24, 80, "", initialStatus) s.Line(5)[10], s.Line(5)[11], s.X = '.', '@', 11 if got := texts(r.Update_application(s, a.Observe(s))); len(got) != 0 { t.Fatal("movement spoke map", got) } }