Skip to content
Venessa Tinch
All projects

Village simulation

A zero-player village survival game in Rust. Villagers work, eat, and starve on their own; you can only nudge them.

  • Rust
  • Cargo
  • Terminal

A terminal game with no player character. You start with three villagers, a house, and a store of food, water, and stone, and then you mostly watch. Each day every villager acts according to their job, the storage goes up or down, and the run ends when the last one dies — the game tells you how long the longest-lived villager lasted.

The design rule I set myself, written into the TODO list at the top of main.rs, was subtle influences, not direct control. Once you cannot simply order a villager to go get water, the interesting question stops being "what do I do" and becomes "what did I build that makes them do the right thing on their own".

How it is put together

A Village owns three vectors — the people, the buildings, and the shared Storage — and a day() method drives one tick of the simulation. Each villager is dispatched on their job:

pub fn day(&mut self) {
    events(self);
    for ped in &mut self.people {
        match ped.job() {
            Jobs::Gatherer => people::gatherer::daily(ped, &mut self.storage),
            Jobs::Hunter   => people::hunter::daily(ped, &mut self.storage),
            Jobs::Jobless  => people::daily(ped, &mut self.storage),
            Jobs::Child    => people::daily(ped, &mut self.storage),
        }
    }
}

Jobs are an enum rather than a field on a person, so adding a role means adding a variant and a daily function, and the compiler tells me every place I still have to handle it.

The code is split into village (with building, people, and storage beneath it), events, and interaction, so the simulation, the randomness, and the interface stay separate.

What Rust made me think about

Every villager needs to read and write the same storage on the same day, which is exactly the situation Rust refuses to let you be careless about. Passing &mut self.storage into each villager's daily function while iterating &mut self.people forced me to be explicit about who owns what and for how long — a bug I would have shipped without noticing in a language that let me hold two mutable handles at once.

The only dependency is rand. Random events fire before the day runs: a wandering villager can join, and a child can be born, but only if at least two villagers are in a Fine state and have survived ten days. Gating events on the simulation's own state, instead of on a dice roll alone, is what stops it feeling arbitrary.

Where it stands

It is a learning project and still an unfinished one. The TODO list is honest about that — more random events, a way to create new people, and a decision I have not settled between a compositional and an inheritance-shaped approach to villagers. It did what I wanted it to, which was to make me use Rust's ownership rules on something with enough moving parts to actually push back.