Tiny Colony (part 4)
Scaling the simulation

Up to now, the simulation was politely lying to me.
It looked like a colony sim:
pawns moved
trees got chopped
numbers went up
UI reacted
But under the hood, it was cheating.
There was only one pawn really doing work.
This post is about what happened when I stopped cheating.
From one pawn to many
The first lie was obvious: if pawn.id == 0 { ... }
That single check kept everything simple:
one job at a time
no conflicts
no coordination problems
Removing it was the first real step toward a simulation instead of a scripted demo.
Once I removed the early break in tick_jobs, every pawn started ticking… and immediately exposed a new class of problems.
That was expected. And, honestly, kind of satisfying.
The “everyone go chop the same tree” problem
As soon as multiple pawns existed, they all did the same thing:
find the nearest tree
walk to it
start chopping
Which, of course, meant they all picked the same tree.

This is the moment where you realize: Pawns are not smart. They need to be coordinated
So I introduced a tiny but powerful concept: reservations.
Reservations: the smallest coordination system that works
I added a Reservations resource that tracks which world tiles are already claimed.
The rules are straightforward
idle pawns reserve a tree before walking to it
reserved trees are skipped by other pawns
reservations are released when:
chopping finishes
the tree disappears
the task aborts
Resource:
#[derive(Resource)]
pub struct Reservations {
pub reserved_tiles: HashMap<IVec2, Entity>
}
Claim on idle:
if let Some(tree) = find_nearest_tree(map, from, reservations) {
reservations.reserved_tiles.insert(tree, pawn_entity);
Task::GoToTree(tree)
} else {
Task::Idle
}
Release when done / invalid:
let next = progress + 1;
if next >= 10 {
world::set_with_sprite(map, &tile_entities, q_tiles, at.x, at.y, Tile::Ground);
inv.wood += 1;
if reservations.reserved_tiles.get(&at) == Some(&pawn_entity) {
reservations.reserved_tiles.remove(&at);
}
Task::GoToStockpile
} else {
Task::Chop { at, progress: next }
}
No locks. No queues. No AI magic.
Just a set of intents.
And suddenly:
pawns spread out naturally
no more dogpiling
task lifecycles became clearer

Selection is not pawn 0
Another quiet lie in the earlier version:
the UI always showed pawn id == 0.
That’s fine, until you have more than one pawn.
So I added a SelectedPawn resource and some very basic click-to-select logic:
click a pawn → it becomes selected
selected pawn gets a visual highlight
the inspector UI shows that pawn’s state
Nothing fancy, but suddenly the simulation feels interactive instead of hardcoded.

“What if I spawn 1000 pawns?”
This was the fun (and dangerous) thought.
I cranked PAWN_COUNT up to 1000 and immediately learned why most games don’t do this casually.
Spawning pawns with fixed offsets doesn’t scale, so I switched to:
a spiral search around the stockpile
skipping trees and occupied tiles
validating positions against the world map
This required reordering world initialization so pawn spawning could query the map instead of guessing.
for p in spiral_positions(stockpile, max_radius) {
if spawned >= PAWN_COUNT { break; }
if p.x < 0 || p.x >= MAP_W || p.y < 0 || p.y >= MAP_H { continue; }
if world::get(map, p.x, p.y) == Tile::Tree { continue; }
if occupied.contains(&p) { continue; }
occupied.insert(p);
commands.spawn((Pawn { id: spawned as u32, x: p.x, y: p.y }, /* sprite */, /* task */));
spawned += 1;
}
The result:
pawns spawn cleanly
no overlaps
no trees overwritten
no hardcoded magic numbers
The beautiful lag
Then came the best part.
With 1000 pawns and a large forest, everything ran fine…
until trees started getting sparse.
Frame time exploded.
Why?
Because every pawn was scanning the entire map to find the nearest tree.
That’s when the illusion fully shattered. I added a FPS indicator to illustrate the problem.

WorldTrees: stop scanning the universe
The fix was simple and very unglamorous:
maintain a
WorldTreesindex (HashSet<IVec2>)iterate only existing trees
remove trees from the index when chopped
Index:
#[derive(Resource)]
pub struct WorldTrees(pub HashSet<IVec2>);
Iterate only trees instead of the full grid:
for &target in world_trees.0.iter() {
let reserved = reservations.reserved_tiles.contains_key(&target);
if !reserved && world::get(map, target.x, target.y) == Tile::Tree {
let dist = (from.x - target.x).abs() + (from.y - target.y).abs();
// keep best...
}
}
Remove when chopped:
world_trees.0.remove(&at);
Suddenly:
late-game performance stabilized
search cost scaled with remaining trees, not map size
the simulation stopped punishing success

What’s next
At this point:
multiple pawns work concurrently
jobs are coordinated
selection and inspection are real
performance problems are visible and explainable
There’s still a lot that could be done here: collisions, better pathfinding, smarter work distribution.
But for now, I’m pretty happy watching 1000 tiny idiots politely avoid chopping the same tree.
The code at this stage is available on GitHub under the post-4 tag: https://github.com/GuzzoLM/tiny-colony/tree/post-4/tiny-colony



