A programming game combining resource management (in the tradition of the classic Hammurabi) with territorial conquest (inspired by generals.io).
Players submit WASM programs that compete against each other. Each program acts as an ensi (Sumerian for "city governor"), managing resources and expanding territory in an ancient Mesopotamian setting.
The core constraint: programs have a limited number of instructions per turn. Victory goes to those who optimize their decision-making within these computational bounds.
- 64x64 grid with three tile types:
- City - Has population, produces food, can convert population to army
- Desert - Passable terrain, no special properties
- Mountain - Impassable barrier
- Population - Grows in cities based on food availability
- Food - Produced by cities adjacent to other owned cities (adjacency bonus)
- Army - Military units converted from population, used for conquest
Each turn, bots can:
- Move army between adjacent tiles
- Convert population to army in owned cities
- Move Capital to a larger city (strategic repositioning)
- Yield to end turn early and save compute budget
- Domination - Eliminate all other players (capture their capitals)
- Turn Limit - After 1000 turns, player with most territory wins
# Clone and build
git clone https://github.com/troutwine/ensi
cd ensi
cargo build --release
# The binary is at ./target/release/ensi# Run a game between two bots
ensi run bot1.wasm bot2.wasm
# Watch a game in the terminal UI
ensi watch bot1.wasm bot2.wasm --speed 200
# Run a tournament (1000 games, parallel)
ensi tournament bot1.wasm bot2.wasm -g 1000 --progress# Start evolution (runs until Ctrl+C)
ensi evolve -v
# Run for specific generations
ensi evolve -g 100 -v
# Resume from checkpoint
ensi evolve -r evolution/gen_00050.bin -v
# Limit CPU usage
ensi evolve -j 4 -v# List evolved bots
ensi bots --list
# Examine a bot's genome
ensi bots --examine ~/.ensi/evolved/evolved_20260112.wasm
# Or use the example tool for detailed analysis
cargo run --example examine_genome -- evolution/best.wasmRun a single game between 2-8 bots.
ensi run [OPTIONS] <BOTS>...
Options:
-s, --seed <SEED> Random seed for reproducibility
-t, --turns <TURNS> Maximum turns (default: 1000)
-f, --format <FMT> Output: text, json, or llm
--save <FILE> Save replay to file
-q, --quiet Suppress turn-by-turn outputInteractive terminal UI for watching games.
ensi watch [OPTIONS] <BOTS>...
Options:
-s, --seed <SEED> Random seed
--speed <MS> Turn delay in milliseconds (default: 500)
-p, --player <N> View from player N's perspective (fog of war)Run mass parallel games for statistical analysis.
ensi tournament [OPTIONS] <BOTS>...
Options:
-g, --games <N> Number of games (default: 1000)
-j, --threads <N> Parallel threads (default: all CPUs)
-f, --format <FMT> Output: text, json, or csv
-p, --progress Show progress barEvolve bots using genetic programming.
ensi evolve [OPTIONS]
Options:
-o, --output <DIR> Output directory (default: evolution)
-p, --population <N> Population size (default: 100)
-g, --generations <N> Generations to run (0 = forever)
--games <N> Games per fitness eval (default: 20)
-s, --seed <SEED> Random seed
-r, --resume <FILE> Resume from checkpoint
-j, --jobs <N> Parallel threads
-v, --verbose Show progressBots are WASM modules that export a run_turn function. The SDK (sdk/ensi.h)
provides a C interface, but any language that compiles to WASM32 works.
#include "ensi.h"
int run_turn(int fuel_budget) {
int player_id = ensi_get_player_id();
int width = ensi_get_map_width();
int height = ensi_get_map_height();
// Scan map using push-based visibility (fast!)
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
int tile = ensi_tile_map_get(x, y);
if (!TILE_VISIBLE(tile)) continue;
if (!TILE_OWNED_BY(tile, player_id)) continue;
int army = TILE_ARMY(tile);
if (army > 1) {
// Move army to adjacent tile
ensi_move(x, y, x+1, y, army - 1);
}
}
}
ensi_yield();
return 0;
}clang --target=wasm32-unknown-unknown -O3 -nostdlib \
-Wl,--no-entry -Wl,--export=run_turn \
-o bot.wasm bot.cQuery Functions (read game state):
ensi_get_turn()- Current turn numberensi_get_player_id()- Your player ID (1-8)ensi_get_tile(x, y)- Tile info (slow syscall)ensi_tile_map_get(x, y)- Tile info (fast, ~100x faster)ensi_get_my_food()- Food balanceensi_get_my_population()- Total populationensi_get_my_army()- Total armyensi_get_my_capital()- Capital coordinates
Action Functions (issue commands):
ensi_move(fx, fy, tx, ty, count)- Move armyensi_convert(x, y, count)- Convert population to armyensi_move_capital(x, y)- Relocate capitalensi_yield()- End turn early
Tile data is packed into a 32-bit integer:
- Bits 0-7: Tile type (0=city, 1=desert, 2=mountain, 255=fog)
- Bits 8-15: Owner player ID (0=neutral, 255=fog)
- Bits 16-31: Army count
Use macros to unpack: TILE_TYPE(t), TILE_OWNER(t), TILE_ARMY(t)
The ensi evolve command uses genetic programming to evolve bot strategies.
Evolved bots use a rule-based genome that compiles to WASM.
Each genome contains:
- 16 constants - Evolved integer values
- 8 registers - In-turn persistent state
- Rules - Condition-action pairs evaluated per tile
Rule: IF <condition> THEN <action>
Conditions use expressions:
- Constants, registers, game state queries
- Arithmetic: +, -, *, /, %
- Comparison: >, <, ==
- Logic: &&, ||, !
Actions:
- Move(from, to, count)
- Convert(city, count)
- MoveCapital(city)
- Store(reg, value) # Write to register
- Repeat(count, action) # Bounded loop (max 16 iterations)
The genome supports:
- Registers - 8 persistent i32 values per turn
- Bounded loops - Repeat actions up to 16 times
- Conditionals - Rule conditions control execution
This enables evolution of planning algorithms, not just reactive strategies.
evolution/
├── gen_00000.bin # Population checkpoints
├── gen_00010.bin
├── ...
└── best.wasm # Best bot compiled to WASM
~/.ensi/evolved/
├── evolved_20260112_151124.wasm # Timestamped copies
└── ...
The game engine achieves 610-632 games/sec per core for 1000-turn, 4-player games on a 64x64 map. This is approximately 80% of the theoretical physics limit (763 games/sec), where the physics limit represents the minimum time needed to process game state updates without any bot execution overhead.
Tournament mode scales linearly across cores, achieving ~2800 games/sec on 16 cores for parallel game execution.
- Use push-based tile map -
ensi_tile_map_get()is ~100x faster thanensi_get_tile() - Yield early - Call
ensi_yield()when done to save fuel - Batch operations - Process all tiles in one pass rather than random access
- Minimize syscalls - Cache values like
player_idandmap_width
ensi/
├── src/
│ ├── main.rs # CLI entry point
│ ├── game/ # Game mechanics & physics
│ ├── wasm/ # WASM runtime (wasmtime)
│ ├── tournament/ # Parallel game execution
│ ├── replay/ # Game recording/playback
│ └── gp/ # Genetic programming
│ ├── genome.rs # Bot representation
│ ├── compiler.rs # Genome → WASM compiler
│ ├── mutation.rs # Mutation operators
│ ├── crossover.rs # Crossover operators
│ ├── selection.rs # Tournament selection
│ ├── fitness.rs # Fitness evaluation
│ └── evolution.rs # Main evolution loop
├── sdk/
│ ├── ensi.h # C SDK header
│ └── example_bot.c # Example bot implementation
├── examples/
│ └── examine_genome.rs # Genome analysis tool
└── tests/ # Integration tests
MIT License. See LICENSE for details.