ZOREWARS

Learn ZoreWars

Write a tiny program. Drop it into a shared memory array with a rival. The last program still running wins.

What is ZoreWars?

ZoreWars is a modern take on Core War, the programming game A. K. Dewdney and D. G. Jones introduced in 1984. Two or more programs, called warriors, are loaded into a circular block of simulated memory called the core. A virtual machine takes turns running one instruction from each warrior. Warriors can read and write anywhere in the core, so they can overwrite each other's code, copy themselves, or set traps. A warrior is eliminated when it has no running processes left.

The game is a sandbox for learning how computers work underneath: memory addresses, pointers, loops, self-modifying code, and how a CPU fetches and executes instructions. Nothing escapes the simulation. The "memory" is just an array inside the VM, and warriors can only touch cells of that array.

Warriors are written in Redcode, a small assembly language that follows the ICWS'94 standard. You can also write them in JavaScript, Python or C++: those programs generate Redcode, so you get loops, functions and constants as a macro language while the battle itself stays in pure Redcode.

Quick start

npm start                 # serves the arena at http://localhost:8080
node cli/zorewars.js list # built-in warriors
node cli/zorewars.js battle @dwarf @mice --rounds 10
  1. Open the Arena. Two classics are loaded: Dwarf (a bomber) and Mice (a replicator).
  2. Press Run (or Space). Drag the speed slider to slow down or speed up.
  3. Hover over the core to read memory in the Inspector. Click to pin an address, and use Step (S) to run one instruction at a time.
  4. Edit a warrior and the change applies on the next round. + Add brings up to four warriors into the same battle.
  5. Run a Tournament to play many rounds from random starting positions. A single round can be luck; fifty rounds rarely is.
  6. Take a working warrior to the Ladder: it plays everyone on a king-of-the-hill kept in your browser, starting with the strongest library warriors. Open the Species map to see what kind of warrior yours is and who else plays that way.

Only Node.js 18+ is needed: no install, no dependencies. Python warriors run in the browser through Pyodide, which downloads about 10 MB the first time you use one.

How the machine works

The core

The core is a circle of 8000 cells by default. Every cell holds one instruction, for example MOV.I $0, $1. At the start every cell is DAT.F $0, $0. Addresses wrap around: the cell after 7999 is 0.

Relative addressing

Warriors never see absolute addresses. Every address is relative to the instruction that uses it: 0 means "this cell", 1 the next one, -1 the previous one. That is why a warrior works the same wherever it is loaded.

Instructions have two fields

Each instruction has an A-field and a B-field, each a number with an addressing mode. In ADD.AB #4, $3 the A-field is the immediate number 4 and the B-field points 3 cells ahead. Fields are often used as data: a DAT cell is a handy place to store a counter or pointer.

Processes and turns

Each warrior owns a queue of processes (program counters). It starts with one. On its turn, a warrior takes the process at the front of its queue, executes one instruction, and puts the process back on the end of the queue pointing at its next instruction. SPL adds a second process, so one warrior can run many threads, but they share that warrior's turns: with 10 processes, each one runs only every 10th turn.

Warriors alternate strictly: one instruction from warrior 1, one from warrior 2, and so on. One full lap is a cycle.

Dying and winning

A process that executes DAT (or divides by zero) is removed. When a warrior's queue is empty, it is eliminated. The last warrior with processes wins. If several warriors survive to the cycle limit (80,000 by default), the round is a tie.

Your first warriors

1. The Imp

        MOV.I   $0, $1

Copy the instruction at offset 0 (itself) to offset 1 (the next cell). Then the process moves to the next cell, which now contains... the same instruction. The Imp crawls forward through the whole core forever. It is nearly impossible to kill, but it never kills anything either. Running into an Imp turns your code into Imp, which is why Imps cause so many ties.

2. The Dwarf

step    EQU     4

        ORG     loop
loop    ADD.AB  #step, bomb     ; add 4 to bomb's B-field
        MOV.I   bomb, @bomb     ; copy bomb to where its B-field points
        JMP     loop            ; forever
bomb    DAT.F   #0, #0

Line by line:

Each pass drops a DAT 4 cells further on. Any enemy process that executes one dies. The bombs always land a multiple of 4 cells past bomb, and the Dwarf's other three instructions sit 1, 2 and 3 cells before it. Because 4 divides 8000, a multiple of 4 can never wrap around onto those cells, so the Dwarf never bombs itself.

Try it: load Dwarf in the arena and change step to 3. Run it alone (remove the other warrior). Why does it eventually die? Hint: 3 doesn't divide 8000.

3. Mice, a replicator

        ORG     start
ptr     DAT.F   #0, #0
start   MOV.AB  #12, ptr        ; cells to copy
loop    MOV.I   @ptr, <copy     ; copy one cell, last first
        DJN.B   loop, ptr       ; count down
        SPL.B   @copy, 0        ; start a process in the new copy
        ADD.AB  #653, copy      ; pick a new destination
        JMZ.B   start, ptr      ; and do it again
copy    DAT.F   #0, #833

Mice copies itself to a new place, starts a process there with SPL, and keeps going. Every copy does the same. The number of copies grows exponentially, so a bomber cannot destroy them as fast as they appear. Watch the "Processes over time" chart in the arena to see this happen.

Reading the arena

Debugging a battle: how did it win?

Every round explains itself. While the VM runs, it records who last wrote every cell and which instruction wrote it, every process death and what caused it, and the first time each warrior wrote into, or ran, another warrior's code.

In the arena

Conversions

Overwriting code doesn't kill a process; it changes what the process runs. The post-mortem reports when a warrior has been converted, for example "Imp has been converted: 7,853 of its 8,000 processes are running Mice's program." That is how Imp vs Mice usually ends in a tie: each warrior's processes end up running the other's code.

From the command line

node cli/zorewars.js battle @dwarf @scanner -r 10 --report             # a verdict for every round
node cli/zorewars.js battle @dwarf @scanner -r 10 --report --round 3   # full post-mortem of round 3
node cli/zorewars.js trace @dwarf @scanner --round 3 --last 40         # the last 40 instructions executed

trace lists every instruction executed, with its cycle, warrior, address and process count, and marks the step where a process died and why. Add --json to either command for machine-readable reports, which is useful for AI agents improving a warrior.

Instruction reference

"A" and "B" below mean the cells the A and B operands point to after the addressing modes have been applied.

OpEffect
DATData. The process that executes it is removed. Its fields are useful for storing numbers.
MOVCopy A to B: a whole instruction with .I, or individual fields.
ADDB = B + A, modulo the core size.
SUBB = B − A.
MULB = B × A.
DIVB = B ÷ A, rounded down. Dividing by zero removes the process.
MODB = B mod A. Dividing by zero removes the process.
JMPJump to A.
JMZJump to A if B is zero.
JMNJump to A if B is not zero.
DJNDecrement B, then jump to A if it is not zero. The classic loop counter.
SPLSplit: queue the next instruction, then queue a new process at A (unless the warrior already has the maximum number of processes).
SEQ / CMPSkip the next instruction if A equals B.
SNESkip the next instruction if A does not equal B.
SLTSkip the next instruction if A is less than B. Values are compared as 0…coresize−1.
NOPDo nothing.
LDPLoad from P-space: read P-space cell A into B. See P-space.
STPStore to P-space: write A into P-space cell B.

Addressing modes

The symbol in front of an operand says how to find the cell it refers to. For indirect modes the operand first points at a cell, and that cell's field is used as a second, relative offset.

ModeNameMeaning
#ImmediateThe number itself. As an address it means "this instruction".
$DirectThe cell that many steps away. This is the default when there is no symbol.
@B-indirectGo to that cell, then move by its B-field.
*A-indirectGo to that cell, then move by its A-field.
<B-predecrementLike @, but subtract 1 from the B-field first.
{A-predecrementLike *, but subtract 1 from the A-field first.
>B-postincrementLike @, then add 1 to the B-field afterwards.
}A-postincrementLike *, then add 1 to the A-field afterwards.

Pre-decrement and post-increment let one instruction both use a pointer and move it. MOV.I bomb, >ptr writes a bomb and advances ptr in a single step, which is how core clears work.

Modifiers

The modifier after the dot says which fields an instruction reads and writes.

ModUses
.AA-field of A → A-field of B
.BB-field of A → B-field of B
.ABA-field of A → B-field of B
.BAB-field of A → A-field of B
.FBoth fields, matched: A→A and B→B
.XBoth fields, crossed: A→B and B→A
.IThe whole instruction: opcode, modifier, modes and fields

If you leave the modifier out, the ICWS'94 defaults apply:

The compiled Redcode panel in the arena always shows the full, explicit form, which is a good way to check what a line really does.

Redcode syntax

;name     Shown in the arena and results
;author   Your name
;strategy One or more lines describing the plan

step    EQU     (CORESIZE/4)+1   ; named constant (an expression)
        ORG     start            ; where execution begins
start:  mov.i   0, 1             ; labels may end in ':'; case doesn't matter
        END                      ; optional; anything after it is ignored

FOR / ROF loops

i       for     4                ; repeat the body 4 times; i counts 1..4
bomb&i  DAT     #i, #i*100       ; labels bomb01 .. bomb04
        rof

x       for     0                ; FOR 0: a block comment
        ...
        rof

pad     for     MAXLENGTH-CURLINE ; fill the rest of the program
        DAT     0, 0
        rof

The label just before FOR is the counter. In the body, the counter's name becomes its value and &counter pastes it as two digits into a label. Loops can be nested. For anything more complicated, a JavaScript or Python generator is easier to read.

P-space

P-space is a small private memory (CORESIZE / 16 cells, 500 by default) that each warrior keeps for the whole match while the core is wiped between rounds. STP stores a value there and LDP reads one back. Cell 0 is special: before each round it holds the result of the previous one (0 = lost, otherwise the number of survivors; -1 on the first round). Warriors use it to switch strategy after a loss. PIN (shared P-space) is not supported; it's ignored with a warning.

Not supported: read/write distance limits, and PIN.

How generators work

Programs in JavaScript, Python and C++ don't run inside the arena. They run once, before the battle, and print Redcode. That keeps every language on equal terms: the VM only ever runs Redcode, and anyone can read the compiled result. What a general-purpose language adds is a macro language: loops to unroll code, functions to build patterns, and arithmetic to choose constants.

All three libraries share one API:

CallEmits
mov(a, b)MOV a, b with the default modifier
mov.i(a, b)MOV.I a, b. Every opcode has .a .b .ab .ba .f .x .i
label("x")Puts label x on the next instruction
org("x")ORG x
equ("n", 4)n EQU 4
imm(4), ind("p")…#4, @p. Also dir $, aind *, predec <, apredec {, postinc >, apostinc }
name/author/strategyThe metadata comments
comment(t), raw(line)A comment, or any line of Redcode verbatim
CORESIZE…The current rules, so you can compute steps that fit

Operands can be numbers (jmp(-2)), labels ("bomb") or any Redcode operand string ("#step*2").

Converting Redcode to JavaScript or Python

Any Redcode warrior, including the famous originals, can be turned into a readable JavaScript or Python generator. Numeric EQUs become real constants, FOR/ROF blocks become real loops, and labels and comments carry over. Every conversion is verified: it must compile to exactly the same instructions as the original, or a simpler form is used instead. All 50 warriors tested, 34 of them archive originals, convert to the readable form.

# Seeker, converted from Redcode by ZoreWars.
step = 12                                   # how far the scan moves each time
gap = 6
...
label("loop"); add.f("inc", "scan")         # move both scan pointers forward by step
label("scan"); seq.i("last-scan-step", "last-scan-step+gap")
label("clear"); mov.i("kill", postinc("cptr"))

Two helpers make converted loops work: curline() returns the index of the next instruction (Redcode's CURLINE), and ldp/stp are available in every language.

JavaScript

Runs directly in the arena and in the CLI. The API functions are already in scope.

// Carpet Bomber: drop N bombs in a row, then skip GAP cells.
name("Carpet Bomber");
const N = 4, GAP = 16, LENGTH = N + 4;
if (CORESIZE % GAP !== 0 || LENGTH + N > GAP) throw new Error("carpets would hit us");

org("loop");
label("ptr");  dat.f(imm(0), imm(LENGTH));
label("loop");
for (let i = 0; i < N; i++) mov.i("bomb", postinc("ptr"));   // unrolled loop
add.ab(imm(GAP - N), "ptr");
jmp("loop");
label("bomb"); dat.f(imm(0), imm(0));

Python

Import the library and call the same functions. In the browser it runs through Pyodide; with the CLI it runs on your local Python, with lang/python added to PYTHONPATH for you.

from zorewars import *

name("Dwarf (Python)")
STEP = 4
assert CORESIZE % STEP == 0

org("loop")
label("loop"); add.ab(imm(STEP), "bomb")
mov.i("bomb", ind("bomb"))
jmp("loop")
label("bomb"); dat.f(imm(0), imm(0))

When the script exits, the Redcode is printed to stdout. Run python examples/dwarf.py with PYTHONPATH=lang/python to see it.

C++

Include lang/cpp/zorewars.hpp (C++17) and call zw::emit() at the end of main. The CLI compiles the file with g++ or clang++, runs it and reads the Redcode. C++ warriors are CLI-only: browsers can't compile C++.

#include "zorewars.hpp"
using namespace zw;

int main() {
  name("Dwarf (C++)");
  org("loop");
  label("loop"); add.ab(imm(4), "bomb");
  mov.i("bomb", ind("bomb"));
  jmp("loop");
  label("bomb"); dat.f(imm(0), imm(0));
  emit();
}

If <cstdlib> is included, write zw::div to avoid clashing with std::div.

Classic strategies

Most warriors fall into a few families. Like rock-paper-scissors, each tends to beat one family and lose to another:

Stone (bomber)beats scanners: it is small and fast, and does nothing but bomb.
Paper (replicator)beats stones: it makes copies faster than they can be bombed.
Scissors (scanner)beats paper: it finds the copies and kills them one by one.

These are tendencies, not laws. The bundled Scout is a deliberately simple scanner and still loses to Mice more often than it wins; making it beat Mice is exercise 5.

Battling the originals

Thousands of real warriors from forty years of Core War are published in public archives. ZoreWars can load them directly, without copying them into the project:

Imports are fetched when you use them and kept only in your browser. Network files are always assembled as Redcode and never run as JavaScript, Python or C++. If a warrior was written for other rules, such as the '94x hill's 55,440-cell core, the arena offers to switch.

Every warrior below is in the arena's Library menu and the warriors/ folder. The Warrior Encyclopedia covers more families (imp rings, silk papers, CMP scanners, one-shots, quickscans), with each warrior's code explained line by line, its history and benchmark results.

WarriorFamilyIdea
ImpImpOne instruction that copies itself forward forever. Survives almost anything and forces ties.
DwarfStoneDrops a DAT bomb on every 4th cell.
Carpet BomberStoneUnrolled loop (generated by JavaScript) that bombs in short bursts.
MicePaperCopies itself and starts the copy with SPL. Process count grows exponentially.
ScoutScissorsTests cells with JMZ.F and only bombs what isn't empty. Uses SLT to avoid bombing itself.
VampireStone variantDrops JMP "fangs" that send enemy processes into a pit where they die.
SweeperCore clearWrites DAT over every cell in order and stops safely on its own pointer.
StunnerStun bomberDrops SPL 0 bombs that make enemies waste their turns, then sweeps the core.

Ideas to explore next

Exercises

1. Step size. Make the Dwarf bomb every 8th cell. Is it better or worse against Mice? Run a 100-round tournament to find out.
2. Beat the Imp. The Imp ties against most bombers. Write a warrior that kills it. Hint: the Imp is always one cell ahead of where it was.
3. Self-awareness. Remove the SLT check from Scout. Run it alone. When does it kill itself, and why?
4. Generator. Write a JavaScript warrior that takes STEP as a constant and refuses to compile (with throw) if the bombs would ever hit its own code.
5. Real scissors. Scout bombs one cell each time it finds something. Mice copies are 8 cells long and keep moving. Change Scout so it beats Mice in a 100-round tournament: try bombing several cells around each hit, or bombing with SPL 0 before clearing.
6. Hill climbing. Run zorewars tournament warriors and get your warrior to the top of the table.

CLI & AI agents

The CLI runs the same engine as the arena, so results match exactly. Every command takes --json, so scripts and AI agents can write a warrior, test it, read the score and iterate.

node cli/zorewars.js list                                # built-in warriors and their @refs
node cli/zorewars.js check mine.red                      # compile only
node cli/zorewars.js compile mine.js                     # print the generated Redcode
node cli/zorewars.js battle mine.red @dwarf --rounds 100 --json
node cli/zorewars.js tournament mine.red warriors --rounds 20
cat mine.red | node cli/zorewars.js battle - @mice       # Redcode on stdin
node cli/zorewars.js classics                            # famous originals you can fight
node cli/zorewars.js battle mine.red classic:thermite --rounds 50
node cli/zorewars.js battle mine.red https://asdflkj.net/COREWAR/94/HILL32/arvon.red
OptionMeaning
--rounds NRounds per match (default 1 for battle, 10 for tournament)
--seed NSeed for load positions, so results are reproducible (default 1)
--coresize NCore size (default 8000)
--cycles NCycles before a tie (default 80000)
--processes NMax processes per warrior (default 8000)
--length NMax warrior length (default 100)
--distance NMin distance between warriors at load (default 100)
--fixed NWarrior 2 starts at N in round 1 and later rounds follow pMARS's RNG, like pmars -F N
--pspace NP-space size (default CORESIZE/16)
--jsonMachine-readable output
-r -s -c -p -l -d -F -SpMARS's own short options also work, so tournament command lines can be reused

Exit status is 0 on success, 1 if a warrior failed to compile (errors come with line numbers) and 2 for bad usage.

JSON output

From battle @mice @dwarf --rounds 100 --json:

{
  "ok": true,
  "seed": 1,
  "settings": { "coreSize": 8000, "maxCycles": 80000, "rounds": 100, ... },
  "warriors": [
    { "name": "Mice", "author": "Chip Wendell (1986), ...", "wins": 83, "losses": 1, "ties": 16, "score": 265, "file": "warriors/mice.red", "length": 8 },
    { "name": "Dwarf", "author": "A. K. Dewdney (1984)", "wins": 1, "losses": 83, "ties": 16, "score": 19, "file": "warriors/dwarf.red", "length": 4 }
  ],
  "rounds": [ { "round": 1, "winner": "Mice", "winnerIndex": 0, "cycles": 22698, "survivors": [0] }, ... ]
}

An agent loop

  1. Read AGENTS.md in the repository root: a compact reference written for language models.
  2. Write a warrior file.
  3. zorewars check file --json and fix any errors.
  4. zorewars tournament file warriors --rounds 50 --json and read ranking.
  5. Change one idea at a time, keep what scores better, repeat.

To have two agents fight, each writes a warrior and you run zorewars battle a.red b.red --rounds 200 --json. Use a fixed --seed to compare versions fairly, and a fresh one for final scoring so nobody tunes against known load positions.

Developer tools

Four commands for improving warriors, all running on every CPU core:

node cli/zorewars.js hill mine.red                # score against the library, opponent by opponent
node cli/zorewars.js hill mine.red --classics     # ...and the 34 Hall of Fame originals (fetched each run)
node cli/zorewars.js species                      # the behaviour map of the library
node cli/zorewars.js tune warriors/gatekeeper.js  # search for better constants
node cli/zorewars.js evolve --generations 6       # breed new warriors

Rules & scoring

SettingDefault
Core size8000 cells
Max cycles80,000 per round
Max processes8000 per warrior
Max length100 instructions
Min distance100 cells between warriors at load

Change the rules in the arena's Rules panel or with CLI flags. Warriors that use CORESIZE and friends adapt automatically.

Glossary & history

Core War first appeared in A. K. Dewdney's "Computer Recreations" column in Scientific American (May 1984). The International Core War Society published Redcode standards in 1986, 1988 and 1994, and online hills have been running since the early 1990s. ZoreWars keeps the rules that made the game interesting and adds a visual arena, generator languages and tooling for automated play.