# Tiny Colony (part 4)

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**.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1770509821106/f811669d-d4c6-413b-9786-0cdb1741c1c0.png align="center")

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:**

```rust
#[derive(Resource)]
pub struct Reservations {
    pub reserved_tiles: HashMap<IVec2, Entity>
}
```

**Claim on idle:**

```rust
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:**

```rust
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
    

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1770509954158/260bd9c1-3a4a-413e-8e9d-f28f4ab44208.png align="center")

---

## 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.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1770510048690/22656afc-2760-4d6e-81b2-a4881ed04f46.png align="center")

---

## “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.

```rust
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.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1770510163134/9df45c70-da40-4c52-a6bd-516be5f2012d.png align="center")

---

## WorldTrees: stop scanning the universe

The fix was simple and very unglamorous:

* maintain a `WorldTrees` index (`HashSet<IVec2>`)
    
* iterate *only existing trees*
    
* remove trees from the index when chopped
    

**Index:**

```rust
#[derive(Resource)]
pub struct WorldTrees(pub HashSet<IVec2>);
```

**Iterate only trees instead of the full grid:**

```rust
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:**

```rust
world_trees.0.remove(&at);
```

Suddenly:

* late-game performance stabilized
    
* search cost scaled with remaining trees, not map size
    
* the simulation stopped punishing success
    

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1770510308972/f6d1b37b-9bb8-482b-9fe6-02b5e1be5e64.png align="center")

---

## 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](https://github.com/GuzzoLM/tiny-colony/tree/post-4/tiny-colony)
