aboutsummaryrefslogtreecommitdiff
path: root/internal/game/cmd_room_remove_test.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/game/cmd_room_remove_test.go')
-rw-r--r--internal/game/cmd_room_remove_test.go331
1 files changed, 331 insertions, 0 deletions
diff --git a/internal/game/cmd_room_remove_test.go b/internal/game/cmd_room_remove_test.go
new file mode 100644
index 0000000..1868c30
--- /dev/null
+++ b/internal/game/cmd_room_remove_test.go
@@ -0,0 +1,331 @@
+package game
+
+import (
+ "os"
+ "path/filepath"
+ "strconv"
+ "strings"
+ "testing"
+
+ "thehouseoficarus/internal/combat"
+ "thehouseoficarus/internal/net"
+ "thehouseoficarus/internal/player"
+ "thehouseoficarus/internal/world"
+)
+
+// removeTestConn is a minimal Conn that records writes for assertions.
+type removeTestConn struct{ buf []byte }
+
+func (c *removeTestConn) ReadMessage() (string, error) { return "", nil }
+func (c *removeTestConn) Write(b []byte) (int, error) { c.buf = append(c.buf, b...); return len(b), nil }
+func (c *removeTestConn) Close() error { return nil }
+func (c *removeTestConn) SetEcho(bool) error { return nil }
+
+func (c *removeTestConn) output() string { return string(c.buf) }
+
+// newRemoveGame builds a Game with the data stores roomRemove touches, a Hub
+// (so relocate logic is exercised), and no telephony. Rooms are written into
+// dir by the caller.
+func newRemoveGame(t *testing.T, dir string) *Game {
+ t.Helper()
+ g := &Game{
+ Deps: Deps{
+ World: world.New(dir),
+ MobStore: world.NewMobStore(dir),
+ AccountStore: player.NewAccountStore(dir),
+ DataDir: dir,
+ },
+ Hub: net.NewHub(),
+ Combat: combat.NewTracker(),
+ safespot: NewSafespotManager(),
+ }
+ return g
+}
+
+func newRemoveSession(p *player.Player) *net.Session {
+ return &net.Session{Conn: &removeTestConn{}, Player: p, State: net.StateGame}
+}
+
+func writeRemoveRoomFile(t *testing.T, dir string, id int, body string) {
+ t.Helper()
+ roomsDir := filepath.Join(dir, "rooms")
+ if err := os.MkdirAll(roomsDir, 0755); err != nil {
+ t.Fatal(err)
+ }
+ if err := os.WriteFile(filepath.Join(roomsDir, strconv.Itoa(id)+".yaml"), []byte(body), 0644); err != nil {
+ t.Fatal(err)
+ }
+}
+
+func assertOutContains(t *testing.T, sess *net.Session, sub string) {
+ t.Helper()
+ out := sess.Conn.(*removeTestConn).output()
+ if !strings.Contains(out, sub) {
+ t.Errorf("expected session output to contain %q, got:\n%s", sub, out)
+ }
+}
+
+func assertOutNotContains(t *testing.T, sess *net.Session, sub string) {
+ t.Helper()
+ out := sess.Conn.(*removeTestConn).output()
+ if strings.Contains(out, sub) {
+ t.Errorf("expected session output to NOT contain %q, got:\n%s", sub, out)
+ }
+}
+
+// reloadAfterCmd re-reads a room from disk after a command has mutated it.
+func reloadRoom(t *testing.T, g *Game, id int) *world.Room {
+ t.Helper()
+ r, err := g.World.LoadRoom(id)
+ if err != nil {
+ t.Fatalf("reload room %d: %v", id, err)
+ }
+ return r
+}
+
+func TestRoomRemoveSuccessPull(t *testing.T) {
+ dir := t.TempDir()
+ writeRemoveRoomFile(t, dir, 1, "name: A\nexits:\n east: 2\n")
+ writeRemoveRoomFile(t, dir, 2, "name: B\nexits:\n east: 3\n west: 1\n")
+ writeRemoveRoomFile(t, dir, 3, "name: C\nexits:\n west: 2\n")
+ g := newRemoveGame(t, dir)
+ sess := newRemoveSession(&player.Player{Name: "tester", RoomID: 1})
+
+ g.roomRemove(sess, []string{"east"})
+
+ assertOutContains(t, sess, "You remove #2 (B) and pull #3 (C) back to the east.")
+
+ // A's east exit now leads to C (3), not B (2).
+ a := reloadRoom(t, g, 1)
+ if a.Exits[world.East].Room != 3 {
+ t.Errorf("A.Exits[east] = %d, want 3", a.Exits[world.East].Room)
+ }
+ // C's west exit now leads to A (1), not B (2).
+ c := reloadRoom(t, g, 3)
+ if c.Exits[world.West].Room != 1 {
+ t.Errorf("C.Exits[west] = %d, want 1", c.Exits[world.West].Room)
+ }
+ // B's file is gone.
+ if _, err := os.Stat(filepath.Join(dir, "rooms", "2.yaml")); !os.IsNotExist(err) {
+ t.Errorf("expected 2.yaml to be deleted, got err=%v", err)
+ }
+}
+
+func TestRoomRemoveDiagonalPull(t *testing.T) {
+ dir := t.TempDir()
+ writeRemoveRoomFile(t, dir, 1, "name: A\nexits:\n northeast: 2\n")
+ writeRemoveRoomFile(t, dir, 2, "name: B\nexits:\n northeast: 3\n southwest: 1\n")
+ writeRemoveRoomFile(t, dir, 3, "name: C\nexits:\n southwest: 2\n")
+ g := newRemoveGame(t, dir)
+ sess := newRemoveSession(&player.Player{Name: "tester", RoomID: 1})
+
+ g.roomRemove(sess, []string{"northeast"})
+
+ assertOutContains(t, sess, "You remove #2 (B) and pull #3 (C) back to the northeast.")
+ a := reloadRoom(t, g, 1)
+ if a.Exits[world.Northeast].Room != 3 {
+ t.Errorf("A.Exits[ne] = %d, want 3", a.Exits[world.Northeast].Room)
+ }
+ c := reloadRoom(t, g, 3)
+ if c.Exits[world.Southwest].Room != 1 {
+ t.Errorf("C.Exits[sw] = %d, want 1", c.Exits[world.Southwest].Room)
+ }
+}
+
+func TestRoomRemoveNoExit(t *testing.T) {
+ dir := t.TempDir()
+ writeRemoveRoomFile(t, dir, 1, "name: A\nexits:\n")
+ g := newRemoveGame(t, dir)
+ sess := newRemoveSession(&player.Player{Name: "tester", RoomID: 1})
+
+ g.roomRemove(sess, []string{"east"})
+ assertOutContains(t, sess, "There is no exit to the east from here.")
+}
+
+func TestRoomRemoveNotInsertChain(t *testing.T) {
+ dir := t.TempDir()
+ // B does not lead back to A via west.
+ writeRemoveRoomFile(t, dir, 1, "name: A\nexits:\n east: 2\n")
+ writeRemoveRoomFile(t, dir, 2, "name: B\nexits:\n east: 3\n")
+ writeRemoveRoomFile(t, dir, 3, "name: C\nexits:\n west: 2\n")
+ g := newRemoveGame(t, dir)
+ sess := newRemoveSession(&player.Player{Name: "tester", RoomID: 1})
+
+ g.roomRemove(sess, []string{"east"})
+ assertOutContains(t, sess, "does not lead back here via west")
+ // Nothing was deleted.
+ if _, err := os.Stat(filepath.Join(dir, "rooms", "2.yaml")); err != nil {
+ t.Errorf("2.yaml should still exist, got err=%v", err)
+ }
+}
+
+func TestRoomRemoveDeadEndFarRoom(t *testing.T) {
+ dir := t.TempDir()
+ // B leads back to A but has no forward east exit.
+ writeRemoveRoomFile(t, dir, 1, "name: A\nexits:\n east: 2\n")
+ writeRemoveRoomFile(t, dir, 2, "name: B\nexits:\n west: 1\n")
+ g := newRemoveGame(t, dir)
+ sess := newRemoveSession(&player.Player{Name: "tester", RoomID: 1})
+
+ g.roomRemove(sess, []string{"east"})
+ assertOutContains(t, sess, "no room beyond to pull")
+ if _, err := os.Stat(filepath.Join(dir, "rooms", "2.yaml")); err != nil {
+ t.Errorf("2.yaml should still exist, got err=%v", err)
+ }
+}
+
+func TestRoomRemoveOtherExitsInB(t *testing.T) {
+ dir := t.TempDir()
+ // B has east (fwd), west (back), AND up (extra).
+ writeRemoveRoomFile(t, dir, 1, "name: A\nexits:\n east: 2\n")
+ writeRemoveRoomFile(t, dir, 2, "name: B\nexits:\n east: 3\n west: 1\n up: 4\n")
+ writeRemoveRoomFile(t, dir, 3, "name: C\nexits:\n west: 2\n")
+ writeRemoveRoomFile(t, dir, 4, "name: D\nexits:\n down: 2\n")
+ g := newRemoveGame(t, dir)
+ sess := newRemoveSession(&player.Player{Name: "tester", RoomID: 1})
+
+ g.roomRemove(sess, []string{"east"})
+ assertOutContains(t, sess, "has other exits besides west and east")
+ if _, err := os.Stat(filepath.Join(dir, "rooms", "2.yaml")); err != nil {
+ t.Errorf("2.yaml should still exist, got err=%v", err)
+ }
+}
+
+func TestRoomRemoveFarReciprocityBroken(t *testing.T) {
+ dir := t.TempDir()
+ // C's west does not point back to B.
+ writeRemoveRoomFile(t, dir, 1, "name: A\nexits:\n east: 2\n")
+ writeRemoveRoomFile(t, dir, 2, "name: B\nexits:\n east: 3\n west: 1\n")
+ writeRemoveRoomFile(t, dir, 3, "name: C\nexits:\n")
+ g := newRemoveGame(t, dir)
+ sess := newRemoveSession(&player.Player{Name: "tester", RoomID: 1})
+
+ g.roomRemove(sess, []string{"east"})
+ assertOutContains(t, sess, "does not lead back to #2")
+ if _, err := os.Stat(filepath.Join(dir, "rooms", "2.yaml")); err != nil {
+ t.Errorf("2.yaml should still exist, got err=%v", err)
+ }
+}
+
+func TestRoomRemoveExtraInboundEdge(t *testing.T) {
+ dir := t.TempDir()
+ // A clean chain A-east->B-east->C, but an unrelated room 5 also points to B.
+ writeRemoveRoomFile(t, dir, 1, "name: A\nexits:\n east: 2\n")
+ writeRemoveRoomFile(t, dir, 2, "name: B\nexits:\n east: 3\n west: 1\n")
+ writeRemoveRoomFile(t, dir, 3, "name: C\nexits:\n west: 2\n")
+ writeRemoveRoomFile(t, dir, 5, "name: E\nexits:\n north: 2\n")
+ g := newRemoveGame(t, dir)
+ sess := newRemoveSession(&player.Player{Name: "tester", RoomID: 1})
+
+ g.roomRemove(sess, []string{"east"})
+ assertOutContains(t, sess, "other exit(s) point into #2")
+ if _, err := os.Stat(filepath.Join(dir, "rooms", "2.yaml")); err != nil {
+ t.Errorf("2.yaml should still exist, got err=%v", err)
+ }
+}
+
+// TestRoomRemoveOverlap mirrors TestRemoveGridConflictsOverlap: removing B
+// pulls room 6 onto room 5's cell, which must be rejected.
+func TestRoomRemoveOverlap(t *testing.T) {
+ dir := t.TempDir()
+ writeRemoveRoomFile(t, dir, 1, "name: A\nexits:\n east: 2\n south: 4\n")
+ writeRemoveRoomFile(t, dir, 2, "name: B\nexits:\n east: 3\n west: 1\n")
+ writeRemoveRoomFile(t, dir, 3, "name: C\nexits:\n south: 6\n west: 2\n")
+ writeRemoveRoomFile(t, dir, 4, "name: D\nexits:\n east: 5\n north: 1\n")
+ writeRemoveRoomFile(t, dir, 5, "name: E\nexits:\n west: 4\n")
+ writeRemoveRoomFile(t, dir, 6, "name: F\nexits:\n north: 3\n")
+ g := newRemoveGame(t, dir)
+ sess := newRemoveSession(&player.Player{Name: "tester", RoomID: 1})
+
+ g.roomRemove(sess, []string{"east"})
+ assertOutContains(t, sess, "pulling the far end would cause grid collision")
+ if _, err := os.Stat(filepath.Join(dir, "rooms", "2.yaml")); err != nil {
+ t.Errorf("2.yaml should still exist, got err=%v", err)
+ }
+}
+
+func TestRoomRemoveInvalidDirection(t *testing.T) {
+ dir := t.TempDir()
+ writeRemoveRoomFile(t, dir, 1, "name: A\nexits:\n")
+ g := newRemoveGame(t, dir)
+ sess := newRemoveSession(&player.Player{Name: "tester", RoomID: 1})
+
+ g.roomRemove(sess, []string{"sideways"})
+ assertOutContains(t, sess, "Invalid direction")
+}
+
+func TestRoomRemoveUsage(t *testing.T) {
+ dir := t.TempDir()
+ writeRemoveRoomFile(t, dir, 1, "name: A\nexits:\n")
+ g := newRemoveGame(t, dir)
+ sess := newRemoveSession(&player.Player{Name: "tester", RoomID: 1})
+
+ g.roomRemove(sess, nil)
+ assertOutContains(t, sess, "Usage: room remove <direction>")
+}
+
+// TestRoomRemovePreservesExitProps confirms the near-side exit's flags travel
+// onto the repointed A→dir→C edge (and the far side's onto C→oppositeDir→A).
+func TestRoomRemovePreservesExitProps(t *testing.T) {
+ dir := t.TempDir()
+ writeRemoveRoomFile(t, dir, 1, `name: A
+exits:
+ east:
+ room: 2
+ blocked_message: "A rock blocks the way."
+ hidden: true
+`)
+ writeRemoveRoomFile(t, dir, 2, "name: B\nexits:\n east: 3\n west: 1\n")
+ writeRemoveRoomFile(t, dir, 3, `name: C
+exits:
+ west:
+ room: 2
+ blocked_message: "The far side is sealed."
+`)
+ g := newRemoveGame(t, dir)
+ sess := newRemoveSession(&player.Player{Name: "tester", RoomID: 1})
+
+ g.roomRemove(sess, []string{"east"})
+
+ a := reloadRoom(t, g, 1)
+ if a.Exits[world.East].Room != 3 {
+ t.Errorf("A.Exits[east].Room = %d, want 3", a.Exits[world.East].Room)
+ }
+ if a.Exits[world.East].BlockedMessage != "A rock blocks the way." {
+ t.Errorf("A.Exits[east].BlockedMessage = %q, want preserved", a.Exits[world.East].BlockedMessage)
+ }
+ if !a.Exits[world.East].Hidden {
+ t.Error("A.Exits[east].Hidden should be preserved true")
+ }
+ c := reloadRoom(t, g, 3)
+ if c.Exits[world.West].Room != 1 {
+ t.Errorf("C.Exits[west].Room = %d, want 1", c.Exits[world.West].Room)
+ }
+ if c.Exits[world.West].BlockedMessage != "The far side is sealed." {
+ t.Errorf("C.Exits[west].BlockedMessage = %q, want preserved", c.Exits[world.West].BlockedMessage)
+ }
+}
+
+// TestRoomRemoveRelocatesPlayersInB verifies another session standing in B is
+// moved to A and notified. The caller stays in A.
+func TestRoomRemoveRelocatesPlayersInB(t *testing.T) {
+ dir := t.TempDir()
+ writeRemoveRoomFile(t, dir, 1, "name: A\nexits:\n east: 2\n")
+ writeRemoveRoomFile(t, dir, 2, "name: B\nexits:\n east: 3\n west: 1\n")
+ writeRemoveRoomFile(t, dir, 3, "name: C\nexits:\n west: 2\n")
+ g := newRemoveGame(t, dir)
+
+ caller := newRemoveSession(&player.Player{Name: "builder", RoomID: 1})
+ bystander := newRemoveSession(&player.Player{Name: "wanderer", RoomID: 2})
+ g.Hub.Add(caller)
+ g.Hub.Add(bystander)
+ g.Hub.EnterRoom(caller, 1)
+ g.Hub.EnterRoom(bystander, 2)
+
+ g.roomRemove(caller, []string{"east"})
+
+ if bystander.Player.RoomID != 1 {
+ t.Errorf("bystander RoomID = %d, want 1 (relocated to A)", bystander.Player.RoomID)
+ }
+ assertOutContains(t, bystander, "The room around you collapses")
+}