# Tiny Colony (part 3)

In the previous post, I set up the absolute basics of a tiny colony simulation in Rust using Bevy:  
a grid, some tiles, and a handful of pawns rendered as little circles.

Everything compiled. Things moved.  
And… that was about it.

At that point, pawns were basically **well-behaved pixels**. They could move, but they didn’t really *do* anything. There was no intent, no persistence, no reason for a pawn to go somewhere other than *because I told it to*.

This post is about fixing that.

---

## The real problem: movement is not behavior

Initially, I had a system that moved *pawn 0* toward a hardcoded target. It worked fine, but it immediately felt wrong:

* It didn’t scale beyond one pawn
    
* The logic was brittle
    
* Nothing was inspectable or debuggable
    
* There was no concept of *why* a pawn was moving
    

In other words:  
I was telling pawns **how** to move, not **what** they were trying to achieve.

So I stopped doing that.

---

## Tasks: making pawn intent explicit

The first big shift was introducing a `Task` component.

Instead of directly moving pawns, each pawn now carries an explicit *job state*. In simplified form, it looks like this:

```rust
#[derive(Component, Debug)]
pub enum Task {
    Idle,
    GoToTree(IVec2),
    Chop { at: IVec2, progress: f32 },
    GoToStockpile,
}
```

This enum does a lot of heavy lifting.

Instead of “a system that moves pawns”, the simulation now has **pawns with intent**. A pawn isn’t being pushed around, it’s *in a state*, and systems simply react to that state.

This has a few immediate benefits:

* Behavior becomes inspectable
    
* Refactors stop breaking everything
    
* UI becomes trivial later
    

At this point, pawns finally stopped being dots and started being… workers.

---

### A quick aside: why tasks feel so nice in Rust

One thing I really liked here is how naturally tasks with different data fit into the model.

In Rust, enums aren’t just labels. Each variant can carry its own data:

```rust
pub enum Task {
    Idle,
    GoToTree(IVec2),
    Chop { at: IVec2, progress: f32 },
    GoToStockpile,
}
```

Each task only carries *exactly* what it needs. No optional fields, no “only valid when state == X” comments.

This isn’t completely unique to Rust, but Rust makes it unusually pleasant and safe. In C# or Java this often turns into class hierarchies or nullable fields; in Python it’s easy to represent, but easy to misuse.

Here, the type system does most of the work: adding a new task forces you to handle it everywhere, and impossible states simply can’t exist.

For a simulation where behavior *is* state, that turned out to be a really good fit.

---

## The world grows up: introducing `WorldMap`

Once pawns had tasks, they needed to reason about the world:

* Is there still a tree at this position?
    
* Did another pawn already chop it?
    
* What happens to the tile when the tree is gone?
    

To support this, I introduced a `WorldMap` resource. It owns:

* The tile grid
    
* Tile types (`Ground`, `Tree`, `Stockpile`)
    
* Small helpers to query and mutate tiles
    

The important design choice here is ownership:

> Pawns don’t own the world.  
> They query it, reason about it, and sometimes modify it — but the world is shared state.

This cleared up a lot of accidental coupling early on and made the simulation logic much easier to reason about.

---

## Colony state: stockpiling without pawns knowing

Next question: where does the chopped wood go?

Instead of letting pawns “own” resources, I added a `Colony` resource:

```rust
#[derive(Resource, Default)]
pub struct Colony {
    pub wood: u32,
}
```

Pawns don’t know how much wood the colony has.  
They just deliver items into it.

This separation turned out to be surprisingly nice:

* Pawns stay simple
    
* Colony-wide logic stays centralized
    
* UI and future systems have a clean place to read from
    

It also mirrors how I want the game to feel later: pawns do work, the colony accumulates results.

---

## The job loop

At this point, the simulation loop stopped being about movement and became about *progression*.

Very roughly, a pawn now follows this loop:

```rust
Idle
 → GoToTree
 → Chop (with progress)
 → GoToStockpile
 → DropOff
 → Idle
```

No single system “knows” this whole flow.

Each task handler only knows:

* What it needs to do right now
    
* When it’s done
    
* What the next task should be
    

For example, chopping a tree looks roughly like this:

```rust
pub fn handle_chop(
    pawn: &mut Task,
    colony: &mut Colony,
    world: &mut WorldMap,
) {
    if let Task::Chop { at, progress } = pawn {
        *progress += 0.01;

        if *progress >= 1.0 {
            world.set_with_sprite(*at, Tile::Ground);
            colony.wood += 1;
            *pawn = Task::GoToStockpile;
        }
    }
}
```

No scheduler.  
No behavior tree.  
Just “are we done yet?” — and if so, move on.

And yes, watching the first pawn successfully chop a tree felt *way* better than it should have.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1769640883796/c3a18995-36ad-4165-92b5-3912dfd05bdc.png align="center")

---

## Observability: adding the first UI

At this point, the simulation worked. But I couldn’t *see* what was going on inside it.

So I added UI early. Not for gameplay, but for observability.

Two simple UI sections:

### Colony UI

* Current wood count
    

### Pawn inspector

* Pawn ID
    
* Grid position
    
* Current task
    
* Task progress (when applicable)
    

This paid off immediately.

Suddenly I could:

* See task transitions
    
* Verify progress increments
    
* Spot logic bugs instantly
    

The UI isn’t gameplay yet.  
It’s a debugging tool that happens to be on screen.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1769640951913/ef836117-7650-450a-86a6-b4de37eae3f4.png align="center")

---

## A small detour: change detection

While wiring up the UI, I ran into something new (for me): change detection.

My first version updated the UI every frame. That works, but it’s noisy, both mentally and architecturally.

Bevy lets you filter queries so systems only run when something actually changed:

```rust
Query<(&Pawn, &Task), Changed<Task>>
```

This query only returns pawns whose `Task` component changed since the last frame. If a pawn is still chopping the same tree, the UI system doesn’t even run for it.

A few clarifications:

* This is not an optimization I needed yet
    
* It’s mostly about intent: “update UI when state changes”
    
* Unexpected UI updates now usually mean unexpected state changes
    

I like this pattern because it nudges the code toward being **event-driven**, without having to introduce explicit events this early.

---

## Keeping visuals honest: updating tile sprites

One annoying issue showed up quickly:

* A tree would be chopped in the simulation…
    
* …but visually, it stayed a tree forever
    

To fix this, I added a simple mapping from logical tiles to sprite entities. Tile updates now go through a centralized helper that updates both:

* The world state
    
* The corresponding sprite color
    

Once this was in place, visuals stopped lying.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1769641039685/3261d62b-abc5-4bc1-8932-9e7686db0ec1.png align="center")

---

## Small refactors that paid off

As the systems grew, I made a few unglamorous but valuable cleanups:

* Centralized tile + sprite updates
    
* Used change detection for UI instead of per-frame updates
    
* Reduced UI marker components into a single enum
    
* Cleaned up system signatures and warnings
    

None of these are flashy, but together they made the codebase much calmer to work in.

---

## Where things stand now

At this point, the project has crossed an important threshold.

The simulation now:

* Has intentional pawn behavior
    
* Modifies shared world state
    
* Keeps visuals in sync automatically
    
* Exposes internal state via UI
    

It’s still tiny.  
It’s still very much a toy.

But it’s no longer just rendering things, it’s **simulating a system**.

In the next steps, I want to explore:

* Multiple pawns competing for work
    
* Job selection instead of hardcoded flows
    
* Basic player interaction and selection
    

For now though, this feels like a good stopping point.

The dots have learned to work. 🙂

The code at this stage is available on GitHub under the `post-`3 tag: [https://github.com/GuzzoLM/tiny-colony/tree/post-3/tiny-colony](https://github.com/GuzzoLM/tiny-colony/tree/post-3/tiny-colony)
