diff options
Diffstat (limited to 'internal/game/cmd_room_remove_test.go')
| -rw-r--r-- | internal/game/cmd_room_remove_test.go | 331 |
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") +} |
