A recreation of the legendary 1984 programming game by D. G. Jones, where programs battle for survival in a virtual machine's memory arena.
- Overview
- Game Mechanics
- Project Structure
- Building
- Usage
- Assembly Language
- Instruction Set
- File Format
- Examples
- Development
Corewar is a programming game where players write programs (called "champions" or "players") in a custom assembly language. These programs are then compiled to bytecode and executed in a virtual machine where they compete for survival. The goal is to be the last program executing a live instruction before the game ends.
- Virtual Machine (VM): Executes player bytecode in a circular memory arena
- Assembler: Compiles
.sassembly files into.corbytecode files - Arena: The shared memory space where programs battle
- Processes: Each program can spawn multiple execution threads
- Registers: Each process has 16 private 32-bit registers (r1-r16)
- Carry Flag: A boolean flag affected by certain operations
The last player to execute a live instruction before all processes die wins the match. The VM periodically checks which processes have executed live instructions and kills those that haven't.
- Circular Memory: Moving past the last address wraps to address 0
- Relative Addressing: All addresses are relative to the current Program Counter (PC)
- IDX_MOD: Some instructions apply modulo IDX_MOD to limit memory reach
- Each player starts with one process at their spawn position
- Processes can fork to create copies
- Processes must execute
liveperiodically to survive - Dead processes are removed during periodic checks
- Game ends when no processes remain or CYCLE_TO_DIE reaches zero
- CYCLE_TO_DIE: Time between liveness checks
- CYCLE_DELTA: Amount CYCLE_TO_DIE decreases
- NBR_LIVE: Threshold of live calls to trigger decrease
- MAX_CHECKS: Maximum checks before forced decrease
corewar/
├── src/
│ ├── assembler/ # Assembly compiler
│ ├── vm/ # Virtual machine
│ └── disassembler/ # Bytecode decompiler (bonus)
├── players/ # Champion .s files
├── config/ # Configuration constants
└── Makefile
This project uses Rust and Cargo:
# Build all components in release mode
make build
# Or use cargo directly
cargo build --releaseExecutables will be in target/release/:
assembler- Compiles .s to .corvm- Executes .cor filesdisassembler- Decompiles .cor to .s (bonus)
Compile assembly files to bytecode:
# Using make
make asm ARGS="player.s"
# Direct execution
./target/release/assembler player.sOutput: Creates player.cor in the same directory
Error Handling: Exits with error code and descriptive message if compilation fails
Execute compiled champions:
# Using make
make run ARGS="player1.cor player2.cor"
# Direct execution
./target/release/vm player1.cor player2.cor
# With memory dump at cycle 100
./target/release/vm -d 100 player1.cor player2.corFlags:
-d [NB_CYCLES]: Dump memory state and exit at specified cycle-v: Verbose mode showing VM state each cycle (reference implementation)
Player Limit: Maximum 4 players per match
Decompile bytecode back to assembly:
make disasm ARGS="player.cor"
./target/release/disassembler player.corEvery .s file must contain:
.name "Champion Name"
.description "Champion description"
# Your code here- One instruction per line
- Optional label before instruction
- Instruction and parameters separated by whitespace
- Parameters separated by commas
- Comments use
#
| Type | Format | Description | Size |
|---|---|---|---|
| Register | r1 to r16 |
Process-local storage | 1 byte |
| Direct | %42 |
Literal value | 2 or 4 bytes* |
| Indirect | 42 |
Relative memory address | 2 bytes |
*Direct parameters are 2 bytes for instructions with Has Idx = true, otherwise 4 bytes
Labels mark positions in code for jumps:
loop: # Label on own line
live %1
zjmp %:loop # Reference to label
start: sti r1, %:target, %1 # Label before instructionLabels are replaced with relative byte offsets during compilation.
| Instruction | Params | Opcode | Cycles | Description |
|---|---|---|---|---|
live |
1 | 01 | 10 | Notify VM player is alive |
ld |
2 | 02 | 5 | Load value into register |
st |
2 | 03 | 5 | Store register to memory |
add |
3 | 04 | 10 | Add two registers |
sub |
3 | 05 | 10 | Subtract two registers |
and |
3 | 06 | 6 | Bitwise AND |
or |
3 | 07 | 6 | Bitwise OR |
xor |
3 | 08 | 6 | Bitwise XOR |
zjmp |
1 | 09 | 20 | Jump if carry is true |
ldi |
3 | 10 | 25 | Load indirect |
sti |
3 | 11 | 25 | Store indirect |
fork |
1 | 12 | 800 | Create new process |
lld |
2 | 13 | 10 | Long load (no IDX_MOD) |
lldi |
3 | 14 | 50 | Long load indirect |
lfork |
1 | 15 | 1000 | Long fork |
nop |
0-1 | 16 | 2 | No operation |
Instructions that modify the carry flag: ld, add, sub, and, or, xor
- Set to
trueif result is zero - Set to
falseotherwise
Only zjmp reads the carry flag (jumps if true).
[4 bytes] Magic/Signature (0x00EA83F3)
[128 bytes] Program name (null-padded)
[4 bytes] Code size in bytes
[2048 bytes] Description (null-padded)
[N bytes] Bytecode instructions
All integers are big-endian.
.name "ameba"
.description "not doing much"
sti r1, %:hello, %1
and r1, %0, r1
hello:
live %1
zjmp %:helloCompiles to:
00 ea 83 f3 # Magic
61 6d 65 62 61 ... # "ameba" + padding
00 00 00 17 # Size: 23 bytes
6e 6f 74 20 64 6f 69 6e 67... # "not doing much" + padding
0b 68 01 00 0f 00 01 # sti r1, %15, %1
06 64 01 00 00 00 00 01 # and r1, %0, r1
01 00 00 00 01 # live %1
09 ff fb # zjmp %-5
.name "Replicator"
.description "Spawns copies"
start:
sti r1, %:live_call, %1
fork %:start
live_call:
live %1
zjmp %:live_call.name "Bomber"
.description "Overwrites opponent memory"
sti r1, %:live_stmt, %1
loop:
st r1, 510 # Write to distant memory
st r1, 515
live_stmt: live %1
zjmp %:loop# Clean compiled players
make clean
# Assemble a player
make asm ARGS="players/ameba.s"
# Run match
make run ARGS="players/ameba.cor players/other.cor"- Use
-vflag on reference VM to see execution details - Use
-d Nto dump memory at specific cycle - Use
hexdump -C player.corto inspect bytecode - Test against provided reference players
The project includes several test champions in players/:
ameba.s- Basic survival example- Additional test players for validation
All VM and assembler constants are centralized in the config file:
MEM_SIZE: Arena sizeIDX_MOD: Address modulo valueMAX_PLAYER_SIZE: Maximum bytecode sizeREG_NUMBER: Number of registers (16)CYCLE_TO_DIE: Initial liveness check intervalCYCLE_DELTA: Decrease amountNBR_LIVE: Live thresholdMAX_CHECKS: Check limit
- No external libraries for core functionality (parsing, execution)
- Standard library only for Assembler and VM
- No memory leaks
- No crashes under any input
- Deterministic execution - same inputs always produce same outputs
- Big-endian binary format
- ✅ Disassembler (.cor → .s)
- ✅ Real-time visualizer
- ✅ Arithmetic operations in assembly
- ✅ Macro system
Bonuses only evaluated if core Assembler and VM are perfect
- Reference VM and Assembler binaries provided in
playground/ - Dockerfile for containerized testing
- Configuration file with all constants
Educational project - implementation of D. G. Jones' 1984 Corewar concept.
Note: This is a complex systems programming project that provides deep insights into CPU architecture, virtual machines, and Von Neumann computing principles. Take time to understand the execution model before diving into implementation.