Skip to main content

Command Palette

Search for a command to run...

Tile-based Simulation in Machi

Updated
3 min read
Tile-based Simulation in Machi

Bootstrapped the tile map logic in Machi. The architecture is dead simple.

#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
pub enum TileType {
    Air,
    Dirt,
    Stone,
    Water,
}

#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Tile {
    pub tile_type: TileType,
    pub water_amount: u16, // 0 = dry, 1024 = full
}

#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct TileMap {
    pub width: usize,
    pub height: usize,
    pub tiles: Vec<Tile>,
}
impl TileMap {
    pub fn new(width: usize, height: usize) -> Self {
        let tiles = vec![Tile {
            tile_type: TileType::Air,
            water_amount: 0,
        }; width * height];
        TileMap { width, height, tiles }
    }
    pub fn get_tile(&self, x: usize, y: usize) -> Option<&Tile> {
        if x < self.width && y < self.height {
            Some(&self.tiles[y * self.width + x])
        } else {
            None
        }
    }
    pub fn set_tile(&mut self, x: usize, y: usize, tile: Tile) {
        if x < self.width && y < self.height {
            self.tiles[y * self.width + x] = tile;
        }
    }
}

There is TileType which are of Ari, Dirt, Stone or Water.

Then there is water_amount which specify the amount of water in water tile.

Here is full water simulation code from Claude 4.0.

    pub fn simulate_water(&mut self) {
        let w  = self.tile_map.width;
        let h  = self.tile_map.height;
        let len = w * h;

        // Signed changes for each tile (outflow = negative, inflow = positive)
        let mut delta: Vec<i32> = vec![0; len];

        // --- 1 ░ Gather phase -------------------------------------------------
        for y in 0..h {
            for x in 0..w {
                let i = y * w + x;
                let tile = &self.tile_map.tiles[i];

                // Only flowing water can move
                if tile.tile_type != TileType::Water || tile.water_amount == 0 {
                    continue;
                }

                let mut remaining = tile.water_amount;

                // helper to register a flow
                let mut push = |from_idx: usize, to_idx: usize, amount: u16| {
                    if amount == 0 { return; }
                    delta[from_idx] -= amount as i32;
                    delta[to_idx]   += amount as i32;
                };

                // ── a) Vertical – gravity first (toward smaller world-y)
                if y > 0 {
                    let j = (y - 1) * w + x;
                    let below = &self.tile_map.tiles[j];

                    if below.tile_type == TileType::Air ||
                       (below.tile_type == TileType::Water &&
                        below.water_amount < MAX_WATER_AMOUNT)
                    {
                        let room   = MAX_WATER_AMOUNT - below.water_amount;
                        let flow   = remaining.min(room);
                        remaining -= flow;
                        push(i, j, flow);
                    }
                }

                // ── b) Horizontal – equalise with neighbours
                // Only move half the height difference to avoid “teleporting”
                let neighbours = [
                    (x.wrapping_sub(1), y),      // left  (wraps harmlessly for x=0)
                    (x + 1,             y),      // right
                ];

                for (nx, ny) in neighbours {
                    if nx >= w { continue; }
                    let j = ny * w + nx;
                    let n_tile = &self.tile_map.tiles[j];

                    if n_tile.tile_type == TileType::Stone || n_tile.tile_type == TileType::Dirt {
                        continue; // solid wall
                    }

                    let target = (remaining as i32 + n_tile.water_amount as i32) / 2;
                    if remaining as i32 > target {
                        let flow = (remaining as i32 - target) as u16;
                        remaining -= flow;
                        push(i, j, flow);
                    }
                }

                // ── c) Optional small upflow (pressure equalisation) -------------
                // Not strictly needed – comment out if you want one-way gravity.
            }
        }

        // --- 2 ░ Apply phase ---------------------------------------------------
        for idx in 0..len {
            let change = delta[idx];
            if change == 0 { continue; }

            let t = &mut self.tile_map.tiles[idx];
            let new_amt = (t.water_amount as i32 + change)
                .clamp(0, MAX_WATER_AMOUNT as i32) as u16;

            // Flip tile_type depending on new water level
            if new_amt == 0 {
                if t.tile_type == TileType::Water {
                    t.tile_type = TileType::Air;
                }
            } else {
                t.tile_type = TileType::Water;
            }

            t.water_amount = new_amt;
        }
    }

It’s good enough for my use-case. Had to iterate a few times with the agent because the processing order was impacting the water simulation and had directional bias.

Machi

Part 31 of 37

Follow the development of Machi, a side-scrolling simulation world built to test AI agents, tile-based emergence, and the future of embodied intelligence. Coming Soon: https://machi.sprited.app

Up next

Day 2 of Sprited

Today was little bit hectic. Registered for AI Conference happening on September. Built JS port of Ant’s Art project and posted here. Built Rust port of Ant’s Art project and posted here. Started a design document for implementing gravity here. ...