← All Workshops

MudEngine Part 4: Single-Player Dioxus GUI

Step 8 / 13

The World model — game logic

We reuse the same Room and World structs from Part 2, but expand the map to 9 rooms arranged in a 3×3 grid.

The player starts in room 4 — Town Square, the center of the grid. Each room connects to its grid neighbours via cardinal exits (north, south, east, west).

The two methods we need are:

  • look() — formats the current room's name, description, and exit list into a String
  • go(dir) — looks for a matching exit; if found, updates player_room and returns true; otherwise returns false

This is pure game logic — no Dioxus types, no Element, no rsx!. It lives in src/game/world.rs.

Note that every type and method is now pub: the frontend components in src/components/ need to call World::new(), world.go(..), and read world.rooms / world.player_room, and Rust items are private to their module by default.

🗺️ 3×3 Grid
RowCol 0Col 1Col 2
0Forest Path (0)Hilltop (1)Abandoned Tower (2)
1Dark Forest (3)Town Square (4)Temple Courtyard (5)
2Riverbank (6)Old Bridge (7)Graveyard (8)
mud-engine/src/game/world.rs
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Direction {
    North,
    South,
    East,
    West,
}

impl std::fmt::Display for Direction {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::North => write!(f, "north"),
            Self::South => write!(f, "south"),
            Self::East => write!(f, "east"),
            Self::West => write!(f, "west"),
        }
    }
}

#[derive(Clone, PartialEq)]
pub struct Room {
    pub name: String,
    pub description: String,
    pub exits: Vec<(Direction, usize)>,
}

#[derive(Clone, PartialEq)]
pub struct World {
    pub rooms: Vec<Room>,
    pub player_room: usize,
}

impl World {
    pub fn new() -> Self {
        let rooms = vec![
            Room {
                name: "Forest Path".into(),
                description: "A winding path leads through ancient oaks. Sunlight \
                              filters through the canopy.".into(),
                exits: vec![(Direction::South, 3), (Direction::East, 1)],
            },
            Room {
                name: "Hilltop".into(),
                description: "From this vantage point you can see the entire valley. \
                              A cool breeze carries the scent of pine.".into(),
                exits: vec![(Direction::South, 4), (Direction::West, 0), (Direction::East, 2)],
            },
            Room {
                name: "Abandoned Tower".into(),
                description: "A crumbling stone tower stands alone. Vines crawl up \
                              its walls and crows nest in the windows.".into(),
                exits: vec![(Direction::South, 5), (Direction::West, 1)],
            },
            Room {
                name: "Dark Forest".into(),
                description: "Twisted trees block out most of the light. Strange \
                              sounds echo through the undergrowth.".into(),
                exits: vec![(Direction::North, 0), (Direction::South, 6), (Direction::East, 4)],
            },
            Room {
                name: "Town Square".into(),
                description: "A bustling town square with a fountain at its center. \
                              Cobblestones gleam from the morning rain.".into(),
                exits: vec![
                    (Direction::North, 1),
                    (Direction::South, 7),
                    (Direction::West, 3),
                    (Direction::East, 5),
                ],
            },
            Room {
                name: "Temple Courtyard".into(),
                description: "Ancient stone pillars surround a quiet courtyard. Moss \
                              clings to weathered statues.".into(),
                exits: vec![(Direction::North, 2), (Direction::South, 8), (Direction::West, 4)],
            },
            Room {
                name: "Riverbank".into(),
                description: "A slow-moving river borders a muddy bank. Frogs croak \
                              from the reeds.".into(),
                exits: vec![(Direction::North, 3), (Direction::East, 7)],
            },
            Room {
                name: "Old Bridge".into(),
                description: "A weathered stone bridge crosses the river. Moss covers \
                              the ancient masonry.".into(),
                exits: vec![(Direction::North, 4), (Direction::West, 6), (Direction::East, 8)],
            },
            Room {
                name: "Graveyard".into(),
                description: "Rows of moss-covered headstones stretch into the fog. \
                              An iron gate creaks in the wind.".into(),
                exits: vec![(Direction::North, 5), (Direction::West, 7)],
            },
        ];
        World {
            rooms,
            player_room: 4,
        }
    }

    pub fn look(&self) -> String {
        let room = &self.rooms[self.player_room];
        let mut out = String::new();
        out.push_str(&room.name);
        out.push_str("\n");
        out.push_str(&room.description);
        out.push_str("\n\nExits:");
        for (dir, _) in &room.exits {
            out.push(' ');
            out.push_str(&dir.to_string());
        }
        out
    }

    pub fn go(&mut self, direction: Direction) -> bool {
        for (dir, idx) in &self.rooms[self.player_room].exits {
            if *dir == direction {
                self.player_room = *idx;
                return true;
            }
        }
        false
    }
}
🔍 Why Clone + PartialEq?

In Dioxus 0.7, props must implement Clone and PartialEq. Dioxus uses PartialEq to detect whether a prop changed — if it did not change, the component skips re-rendering. Both derives are added automatically via #[derive(Clone, PartialEq)].

The PartialEq comparison on World walks every room, name, description, and exit. That is fine for 9 rooms but you would want a smarter scheme (like a version counter) for a larger world.

🔢 Grid indexing

Room index = row * 3 + col. The player starts in the center (4).

Exits mirror the grid: each room connects to its immediate neighbours. The go method checks whether an exit exists for the given direction.

Step 8 / 13