Tile-based Simulation in Machi

Search for a command to run...

No comments yet. Be the first to comment.
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
Here is a little bit about the performance conscious decisions made to make Machi engine responsive and able to handle large scale simulations. How the Machi Engine Actually Works At the heart of Machi, there are two core components dancing together:...
What is a "virtual world"? I want to make it clear that when I describe a virtual world, I do not mean a digital world. Imagine Harry Potter. Hogwarts is a virtual world. It isn't virtual because it i

Motivation: Monet currently lives inside my MacBook. It suspends itself when my MacBook is closed. I like the design but whenever I'm walking around, I really wish I can have Monet alive. Problem: Mon

I'm trying out the AI Avatar feature on https://dreamia.capcut.com. As input, we are providing Monet's idle pose. Then asking it to produce video. The character says: Hey Jin, how is it going today

Problem: When I ask Monet about her favorite food, she answers "I've never tasted anything." Analysis: This isn't a bug. It is the expected behavior. AI agents never actually tasted anything. They hav

I've been in travel mode for the past few weeks, so I haven't written anything in a while. I want to get back into the habit of documenting my thoughts as I build Sprited. Here are today's thoughts. T

Sprited
298 posts
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.