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
- Open the Arena. Two classics are loaded: Dwarf (a bomber) and Mice (a replicator).
- Press Run (or Space). Drag the speed slider to slow down or speed up.
- 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.
- Edit a warrior and the change applies on the next round. + Add brings up to four warriors into the same battle.
- Run a Tournament to play many rounds from random starting positions. A single round can be luck; fifty rounds rarely is.
- 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:
ADD.AB #step, bomb: add the number 4 (A) to the B-field of the cell labelledbomb.MOV.I bomb, @bomb:@bombmeans "look atbomb's B-field and go that far frombomb". So the bomb is copied 4, 8, 12... cells ahead of itself.JMP loop: repeat.
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.
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
- Each square is one cell of the core, row by row: 100 cells per row for an 8000-cell core.
- Solid squares are code (any instruction other than
DAT), coloured by the warrior that last wrote them. - Dots are
DATcells: bombs, pointers and counters. - Squares glow when they were just written, and flash white when they were just executed.
- White squares mark the live program counters: where each process will execute next.
- The Dwarf's step of 4 bombs every 4th cell, which shows up as vertical stripes, because 100 is a multiple of 4.
Avatars
The Avatars button shows the battle as creatures in an arena. Each warrior's body is its own code: one segment per instruction, head first (the ORG instruction). The opcode decides the part, so every warrior has its own build: SPL is an arm, MOV a claw, ADD…MOD a gear, JMP/DJN a joint, the tests (SEQ, SNE, SLT, JMZ, JMN) an eye, and DAT an ammo pod. Long warriors coil into a helix.
The view is drawn in 3D with WebGL, with lights, shadows and glow. The Flat button switches to a plain canvas drawing of the same thing, which is also what you get when the browser has no WebGL.
- When a warrior writes into another's body, a shot flies from the instruction that wrote it to the part it hit. A
DATsmashes the part; aSPLorJMPover something else webs it (a stun); the same opcode with new fields chips it (decrements and arithmetic); any other instruction turns the part into the attacker's kind of part, in the attacker's colour. - Parts glow where a process is, and light up as they run. Processes running elsewhere (a paper's copies) orbit the body. Those running an enemy's code orbit in the enemy's colour, and once enough of them do, the eyes change colour too.
- Conversion: when the warrior's processes run an enemy's program, the avatar turns into that enemy: each part slides into the enemy's shape and colour.
- The bar over each avatar is how much of its body is still its own. A warrior with no processes left falls apart, in slow motion.
- Map stands the avatars on the core itself: the floor is the live core, laid out like the grid (cells in their owner's colour, glowing where just written, running processes in white). Each avatar stands over where its processes are, tethered to the cell, and moves as they do; a hit outside any body lands on the cell that was written, so you see a bomber carpet the floor. On by default on a plane or a volume; click a cell of the floor to read it.
- On a plane or a volume, the shape button beside Map (▭ Floor, ▢ Box, ◯ Torus) chooses the map. Floor is the flat grid above. Box and Torus put the avatars inside the core in 3D, as the 3D view draws it: the unwrapped box, or the doughnut the torus really is. The core floats as a cloud of cells, each avatar stands on its process's cell, and hits land on the cells they write. A volume opens in the box, a plane on the floor; your choice is remembered.
- The camera directs itself: it sways round the arena, pushes in when a process dies and swings to a knockout. Drag to turn it, and wheel or pinch to zoom; it takes over again after a few quiet seconds. Click a part to read its instruction in the inspector.
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
- Why it ended appears when a round finishes. It gives a one-sentence verdict, for example: "Dwarf won at cycle 692. Scout's last process ran DAT.F #0, #924 at 0927: a bomb Dwarf wrote at cycle 691 using MOV.I $2, @2 at 0001." Below it is a timeline of first contact, conversions and eliminations.
- Show the killing blow pins the fatal cell in the Inspector, with its neighbours and who wrote each one.
- Replay the end re-runs the exact same round and pauses 100 cycles before the finish. Rounds are deterministic, so the replay is identical. Use Step (S) to watch the last moves one instruction at a time.
- Deaths, processes and ending code shows each warrior's original code next to what's there at the end and who overwrote it. It also gives how many processes died and why (own code, opponent bombs, empty core, division by zero), and which program the surviving processes are running.
- The Inspector shows the origin of any cell you hover over: loaded, empty, or written by which warrior at which cycle, using which instruction.
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.
| Op | Effect |
|---|---|
| DAT | Data. The process that executes it is removed. Its fields are useful for storing numbers. |
| MOV | Copy A to B: a whole instruction with .I, or individual fields. |
| ADD | B = B + A, modulo the core size. |
| SUB | B = B − A. |
| MUL | B = B × A. |
| DIV | B = B ÷ A, rounded down. Dividing by zero removes the process. |
| MOD | B = B mod A. Dividing by zero removes the process. |
| JMP | Jump to A. |
| JMZ | Jump to A if B is zero. |
| JMN | Jump to A if B is not zero. |
| DJN | Decrement B, then jump to A if it is not zero. The classic loop counter. |
| SPL | Split: queue the next instruction, then queue a new process at A (unless the warrior already has the maximum number of processes). |
| SEQ / CMP | Skip the next instruction if A equals B. |
| SNE | Skip the next instruction if A does not equal B. |
| SLT | Skip the next instruction if A is less than B. Values are compared as 0…coresize−1. |
| NOP | Do nothing. |
| LDP | Load from P-space: read P-space cell A into B. See P-space. |
| STP | Store 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.
| Mode | Name | Meaning |
|---|---|---|
| # | Immediate | The number itself. As an address it means "this instruction". |
| $ | Direct | The cell that many steps away. This is the default when there is no symbol. |
| @ | B-indirect | Go to that cell, then move by its B-field. |
| * | A-indirect | Go to that cell, then move by its A-field. |
| < | B-predecrement | Like @, but subtract 1 from the B-field first. |
| { | A-predecrement | Like *, but subtract 1 from the A-field first. |
| > | B-postincrement | Like @, then add 1 to the B-field afterwards. |
| } | A-postincrement | Like *, 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.
| Mod | Uses |
|---|---|
| .A | A-field of A → A-field of B |
| .B | B-field of A → B-field of B |
| .AB | A-field of A → B-field of B |
| .BA | B-field of A → A-field of B |
| .F | Both fields, matched: A→A and B→B |
| .X | Both fields, crossed: A→B and B→A |
| .I | The whole instruction: opcode, modifier, modes and fields |
If you leave the modifier out, the ICWS'94 defaults apply:
DAT,NOP:.FMOV,SEQ,SNE:.ABif A is#;.Bif only B is#; otherwise.IADD,SUB,MUL,DIV,MOD:.ABif A is#;.Bif only B is#; otherwise.FSLT:.ABif A is#, otherwise.BJMP,JMZ,JMN,DJN,SPL:.B
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
- Labels name an instruction. Using a label in an operand gives its distance from the current line, so
JMP loopbecomesJMP -2. - EQU defines a constant. It may refer to labels and other constants.
- Expressions support
+ - * / %, parentheses, comparisons (== != < > <= >=) and&& ||. - Predefined constants:
CORESIZE,MAXPROCESSES,MAXCYCLES,MAXLENGTH,MINDISTANCE,CURLINE. - One operand:
DAT xmeansDAT #0, x; every other instruction gets$0as its B operand. END startis an alternative toORG start.- Assertions:
;assert CORESIZE == 8000warns you when a warrior is loaded under rules it wasn't written for. - Registers: inside an expression,
a=FIRST+10stores a value in a one-letter register for later lines, as in pMARS. - Anything before a
;redcodeline is ignored, so warriors pasted from old emails and newsletters work.
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:
| Call | Emits |
|---|---|
| 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/strategy | The 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.
- In the arena, below a compiled Redcode warrior, click Convert to JavaScript or Python. The conversion opens in a new slot, so you can read the two side by side or make them fight.
- From the command line:
zorewars convert classic:thermite --to pyorzorewars convert mine.red --to js --out mine.js. - Every library warrior already has converted versions in
warriors/javascript/andwarriors/python/, and in the warrior picker under "JavaScript and Python versions".
# 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:
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:
- In the arena, use Import… to pick a famous original, paste the URL of any Redcode file, or open a file. You can also drop a
.redfile onto the editor. - In the Hall of Fame, Fight opens an original against our version of the same idea.
- In the CLI, use a URL or
classic:nameanywhere a warrior goes.
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 warrior picker (the Library button: search by name, family, author or strategy, filter by family) 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.
| Warrior | Family | Idea |
|---|---|---|
| Imp | Imp | One instruction that copies itself forward forever. Survives almost anything and forces ties. |
| Dwarf | Stone | Drops a DAT bomb on every 4th cell. |
| Carpet Bomber | Stone | Unrolled loop (generated by JavaScript) that bombs in short bursts. |
| Mice | Paper | Copies itself and starts the copy with SPL. Process count grows exponentially. |
| Scout | Scissors | Tests cells with JMZ.F and only bombs what isn't empty. Uses SLT to avoid bombing itself. |
| Vampire | Stone variant | Drops JMP "fangs" that send enemy processes into a pit where they die. |
| Sweeper | Core clear | Writes DAT over every cell in order and stops safely on its own pointer. |
| Stunner | Stun bomber | Drops SPL 0 bombs that make enemies waste their turns, then sweeps the core. |
Ideas to explore next
- Imp spiral: several Imps spaced
CORESIZE/3apart, launched so they move as one unit that a single bomb can't stop. - Imp gate: a cell that is constantly decremented, so an Imp crawling over it breaks.
- Two-pass scanner: compare two cells at once with
SEQ/SNEto scan twice as fast. - Quickscan: an unrolled series of comparisons at the start of the round, ideal to generate with a JavaScript loop.
- Decoys: junk instructions that make scanners waste time.
Exercises
SLT check from Scout. Run it alone. When does it kill itself, and why?STEP as a constant and refuses to compile (with throw) if the bombs would ever hit its own code.SPL 0 before clearing.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
| Option | Meaning |
|---|---|
| --rounds N | Rounds per match (default 1 for battle, 10 for tournament) |
| --seed N | Seed for load positions, so results are reproducible (default 1) |
| --coresize N | Core size (default 8000) |
| --cycles N | Cycles before a tie (default 80000) |
| --processes N | Max processes per warrior (default 8000) |
| --length N | Max warrior length (default 100) |
| --distance N | Min distance between warriors at load (default 100) |
| --fixed N | Warrior 2 starts at N in round 1 and later rounds follow pMARS's RNG, like pmars -F N |
| --pspace N | P-space size (default CORESIZE/16) |
| --json | Machine-readable output |
| -r -s -c -p -l -d -F -S | pMARS'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
- Read
AGENTS.mdin the repository root: a compact reference written for language models. - Write a warrior file.
zorewars check file --jsonand fix any errors.zorewars tournament file warriors --rounds 50 --jsonand readranking.- 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
- hill plays your warrior against every library warrior (add
--classicsfor the originals) and lists the results from worst to best, with the smooth score. It also sorts every round it didn't win by cause: ties, being stunned or captured, being found fast, being worn down, or own goals. - species runs warriors alone in the core and places them on a map by two measures: the most processes they run at once, and how many different cells they execute. Warriors in the same cell play alike. The strongest warriors ever written are spread over many cells, so there is more than one way to win.
- tune works on JavaScript warriors that declare their constants as
const P = {...};and the ranges to try in a// SPACE {...}comment (Gatekeeper, Papercut, Blitz, Chameleon and Bandit in the library do). It screens random candidates, then re-checks the best few against the current constants on fresh seeds, opponent by opponent. A finalist only counts if its gain is bigger than the noise margin (shown as ±), so a lucky draw isn't kept;--writesaves it into the file. - evolve breeds warriors from slices of the library (and of any warriors you name). It keeps the best warrior of each species cell (MAP-Elites), and each generation's champion joins the opponents, so the next champion has to beat its ancestors too (Red Queen). Children are repaired according to how their parent loses. Start it from your best warriors with
--king(and allow longer code with--genome). Champions and the best of each species are written toevolved/. The library's Evolved Silk came out of it. The method follows Digital Red Queen by Sakana AI and MIT.
Rules & scoring
| Setting | Default |
|---|---|
| Core size | 8000 cells |
| Max cycles | 80,000 per round |
| Max processes | 8000 per warrior |
| Max length | 100 instructions |
| Min distance | 100 cells between warriors at load |
Hill rules (in Rules, or --rules in the CLI) switch to a standard hill's settings, the ones tournaments use: 94nop (the classic settings, no P-space), ICWS'88 (the same, and the editor flags anything that isn't '88 Redcode: only DAT MOV ADD SUB JMP JMZ JMN DJN CMP SPL SLT, the modes # $ @ <, no modifiers and no ORG, exactly as pmars -8 checks), limited process (8 processes, 200 cells apart), 94x (55,440 cells, 500,000 cycles, no P-space, as tournaments play it) and the 94x hill itself (the same with P-space, as most of its leaders need). For a tournament's length limit, set Max length (CLI: --length 50), or pick one of the Core War Tournament Weekend presets: the same four hills with warriors limited to 50 instructions. Under ICWS'88 rules the warrior picker and the ladder use the library's '88 warriors (warriors/88/): Imp, Dwarf, Mice and Vampire written as '88 instruction for instruction, and '88 adaptations of Gemini, Scout, Imp Ring and Stunner. zorewars evolve --rules 88 breeds '88 warriors.
zorewars pushoff predicts a tournament place: your warriors join the top of a Koenigstuhl hill, everyone plays everyone, and an iterative push-off ranks them by average score times diversity (how differently a warrior scores from its nearest look-alike). A warrior that wins its own way ranks above a strong clone. In the arena, the Ladder's Rank by: Diversity push-off ranks your warriors the same way, against the ladder instead of the Koenigstuhl hill.
- Warrior 1 is loaded at address 0; the others at random addresses at least the minimum distance apart. Two-warrior placement uses the same generator and rule as pMARS, so
--fixed N(pMARS's-F N) replays a pMARS match exactly. Games with more warriors use ZoreWars' own placement. - The warrior that moves first rotates every round.
- Each survivor of a round scores (W²−1)/S, where W is the number of warriors and S the number of survivors. With two warriors, a win scores 3 points, a tie 1 and a loss 0.
- ZoreWars is tested against pMARS, the reference simulator: identical assembly for 53 warriors and identical results for thousands of rounds. See
COMPLIANCE.md. - Tournaments are round robin: every pair plays a match and the table is ranked by total points.
- The smooth score is shown alongside: like the normal score, but a lost round is worth the share of the cycle limit the warrior survived, from 0 to just under 1. Dying at cycle 70,000 is better than dying at cycle 2,000, even though both are losses. It gives tuners and evolvers something to climb when everything loses.
- The arena's Ladder is a king-of-the-hill: a challenger plays everyone on it, and if it ranks in the top 10 it stays and the bottom warrior drops off. Warriors are ranked by points per round, then by smooth score, or, with Rank by: Diversity push-off, the way the Tournament Weekend ranks them: average score times diversity, pushing off the lowest again and again. Try it with the Tournament Weekend rules to see which of your warriors would survive.
Change the rules in the arena's Rules panel or with CLI flags. Warriors that use CORESIZE and friends adapt automatically.
The plane (experimental)
The first step towards a 3D battlefield: choose Plane 100×80 or 125×64 under Rules → Battlefield, or use --torus 100x80 in the CLI. The core becomes a torus, wrapping left-right and top-bottom, and the arena draws it as it really is.
- Addresses are vectors.
[0,1]means one row down, and a plain number is still a step along the row, soJMP -1works as before.SIZEXandSIZEYgive the plane's size; generators havevec(x, y). - Addressing and
ADD/SUB/MUL/DIV/MODwork per axis, each wrapping on its own. Counters (DJN) and pointer increments (< > { }) are plain numbers, so a>ptrclear walks every cell row by row. - Headings: every process has a heading, +x at the start, and its next instruction is one step along it.
TRN Aturns the process to face along A:TRN $[0,1]faces down,TRN $-1faces back along the row, andTRN @ptrturns to a direction computed at run time (the axis with the largest part wins). Jumps keep the heading andSPLchildren inherit it. Column Imp crawls down a column; Pillar is a stone standing in a column, only one cell wide in each row. - No single step covers 100×80: a fixed stride repeats after at most 400 cells, so stones and scanners need two loops (see Plaid). On 125×64 a single step can still reach every cell.
- A warrior can be at most half a row long on a plane (50 instructions on 100×80), so that every label is a step along its own row.
- Classic warriors mostly still run, but ones whose constants assume the ring may bomb themselves. The library's plane warriors (Plaid, Climber, Plane Silk, Raster, Column Imp, Pillar, Weave) are written for it.
3D: the volume (experimental)
Choose Volume 40×20×10 or Cube 20×20×20 under Rules → Battlefield (CLI: --torus 40x20x10). Everything above works the same with a third axis: vectors are [x,y,z], SIZEZ gives the depth, and TRN $[0,0,1] turns a process to travel through the layers. A warrior can be half a row long: 20 instructions on 40×20×10, 10 on the cube.
Two more choices are there to experiment with. Volume 41×20×9 has sizes with no common factor, so a fixed stride can reach every cell (on 40×20×10 every stride repeats within 40 cells). In our tests clears still led there. Custom size… takes any width, height and depth up to 200,000 cells (CLI: --torus 63x32x25). Library warriors written for other sizes may not survive on yours. The warrior picker's Experimental prototypes box adds the 3D ideas that didn't make the library, each with a note on how it scored and why (warriors/prototypes/README.md).
The arena shows a volume in 3D: drag to turn it, use the wheel, pinch or the +/− buttons to zoom, and ⤢ to see the whole shape again; ⟳ lets it spin. Hover or tap a cell to read it, as on the grid.
- Battle highlights: a ring splashes in the attacker's colour wherever a warrior overwrites another's code, and white sparks burst where a process dies on an enemy's bomb. They show on the 2D grid too.
- ◎ Action flies the camera to where the fighting is hottest and follows it as it moves. ⤢ or grabbing the view takes you back.
- Follow a warrior: tap its card on the scoreboard. The camera rides one of its processes (each moment, the one nearest to where it was), with a trail behind it and a ring around it; tap the card again or ✕ to stop. On a plane this tilts the grid into 3D.
- Slice shows only some layers (along z, then y, then x): move through them with the slider, and choose 1, 3 or 5 layers at a time, to see what's happening inside the volume.
- ◯ Torus shows the shape the battlefield really is. Normally it's drawn unwrapped, as a box whose opposite faces join. Torus mode joins them: x runs round the ring and y round the tube, so a plane becomes exactly a doughnut's surface and the seams disappear. A volume's third wrap can't be drawn in 3D space, so its z layers become nested tubes, from the innermost to the outermost, with the z seam between those two. Slice along z to see one tube. The circle marked
x = 0is where the last column meets the first. ▢ Box switches back. - The 2D button shows the layers side by side instead, and planes can be tilted in 3D with the same button.
To start, try Lift (an imp that travels through depth), Star (imps along all three axes) and Volume Silk.
Strategies change in a volume. On 40×20×10 a fixed step comes back to where it started within 40 cells (it repeats along each axis), so a stone or scanner with one stride covers almost nothing. The library has warriors built for that:
- Slant is a replicator that hops diagonally, across rows, columns and layers at once, then lays a chain of copies from there while scribbling stray writes along a diagonal. The second part was a bug (it steers the wrong field), but it beats the version that works as intended, so it stays.
- Scatter is Slant as it was meant to work: every copy lands somewhere new, so it spreads much wider (it runs code in about 4,400 cells alone, against Slant's 2,000), but Slant's accidental chains score a little better.
- Battleship digs instead of striding. Its pointer counts up as a plain number, so it checks every cell in turn, row by row and layer by layer. When it finds something, it fires a salvo that covers a 3D block around the hit.
- Broadside is a silk paper whose every copy bombs along its own diagonal, so many bombing lines run side by side through the volume.
- Volume Queen is Plane Queen, the evolver's best plane warrior, written as a 3D generator: a two-pass core clear run by two processes. A clear counts through the volume cell by cell, so nothing can hide from it; it led the volume until Backdraft.
- Leapfrog answers the clears. A clear's front can't be hidden from, but it can be seen coming and the clear never moves. Leapfrog watches a marker two rows ahead of the front's path, leaps behind the front when the marker changes, and runs a slow clear of its own that finds the enemy clear. It beats every clear and stone in the library but loses to papers: rock, paper, scissors again.
- Volume Chameleon uses that triangle. It plays Leapfrog, and after a round it loses (to a paper, usually) it switches to a core clear, which at least ties papers, using P-space to remember between rounds. Volume Chameleon II (below) has since improved on it.
- Anti-Chameleon shows that a switcher can be read. Volume Chameleon keeps its strategy after a win and switches after a loss, so Anti-Chameleon (a paper and Leapfrog) switches after every decided round: when it wins, it already plays what beats Volume Chameleon's next choice. It beats Volume Chameleon about 26 rounds in 30, but against warriors that never switch it walks away from a winning strategy, so it scores lower overall.
- Adapter identifies what it is fighting during the round. Its Leapfrog marker is a sensor: an instruction with both fields zero (a clear's
SPL 0orDAT 0, or a bomb) means leap; anything with non-zero fields is copied code, a paper, so it turns into a two-process clear on the spot. It remembers what it saw in P-space and opens the next round with the answer. - Gremlin was evolved from random code, with no library warriors to learn from (
zorewars evolve --scratch). It is a DJN stream: one instruction,DJN.F #10, {-10, walks back through memory taking one off both fields of every cell. That breaks the exact arithmetic clears depend on, so Volume Queen and Volume Chameleon destroy themselves within a few hundred cycles. Papers, being many copies, shrug it off. - Backdraft is a clear that runs backward (an idea from ULTIMA RATIO REGUM, a 1990s hill warrior): its pointer counts down, so its front arrives from the side Leapfrog and Adapter don't watch. One process lays
SPL 0/JMP -1traps, then DATs, each pass stopping just short of itself, over and over. It is the strongest warrior on both volumes. - Volume Chameleon II switches between Backdraft and a paper: a DJN stream such as Gremlin beats Backdraft but can't hurt a paper. Backdraft II adds a wire that moves the clear when a stream arrives; slower overall, it beats Backdraft, Tripwire and Gremlin head to head.
- Tripwire runs a DJN stream (as Gremlin does) for one lap through the volume; when the stream reaches the wire at the end of its own code, it turns into Backdraft's clear to finish off what the stream can't break (an idea from Death, 1980s).
- Fleet combines Battleship and Slant: the scanner hunts while the replicator spreads. It is 31 instructions, too long for half a 40-cell row, so it plays on Volume 80×10×10 (40-instruction warriors).
A plane one row high is exactly the classic ring, and the tests check that battles there match the classic ones round for round. Background and open questions: design/3d-battlefield.md.
Glossary & history
- Core: the circular memory array where battles happen. The name comes from "core memory", early magnetic RAM.
- Warrior: a program that competes in the core.
- Process: one thread of execution: a program counter in a warrior's queue.
- Bomb: an instruction written over enemy code, usually
DAT. - Hill: a standing tournament where new warriors challenge the current champions.
- ICWS'94: the 1994 draft standard for Redcode, which ZoreWars follows for instructions, modes and modifiers.
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. How warriors evolved traces the families through their original sources.