Redcode for warrior writers
A practical reference for writing Core War programs in Redcode. It covers what you need to write, read, and debug warriors. Implementers should read the full ICWS ’94 standard instead.
The basics
Two or more programs, called warriors, are loaded at random places in a circular memory called the core. They take turns executing one instruction each. A program that executes a DAT instruction, or divides by zero, loses that process. A warrior is dead when all its processes are dead. The last warrior still running wins the round. If several survive until the cycle limit, the round is a tie.
0 is the current instruction, 1 is the next one, -1 the previous one. There are no absolute addresses.-1 is stored as 7999.DAT 0, 0All cells that do not belong to a warrior start as DAT.F $0, $0. Jumping into empty core kills the process.A warrior file
Here is a complete warrior, the classic Dwarf. It drops a DAT bomb on every fourth cell of the core, forever.
;redcode-94
;name Dwarf
;author A. K. Dewdney
;strategy Bombs every fourth cell with a DAT.
;assert CORESIZE % 4 == 0
step equ 4
org loop
loop add.ab #step, bomb ; move the bomb pointer 4 cells on
mov.i bomb, @bomb ; copy the bomb to where it points
jmp loop ; and again
bomb dat #0, #0 ; the bomb, and the pointer
end
| Line | Meaning |
|---|---|
;redcode-94 | Marks the start of the code. Anything before it is ignored, so you can paste a warrior into an email or notes. |
;name, ;author | Shown in results and on hills. Optional, but please use them. |
;strategy | A short description of how the warrior works. Many hills show it next to the warrior, and it helps readers once you publish the warrior. Repeat the line for more text: each ;strategy line adds one line. pMARS itself ignores it. |
;assert | An expression that must be true, or the warrior refuses to assemble. Use it to say which settings the warrior was written for, for example ;assert CORESIZE == 8000. Some hills require an assert line; ;assert 1 always passes. |
step equ 4 | A named constant. The assembler replaces step with 4. |
org loop | Where execution starts. Without it, execution starts at the first instruction. end loop does the same. |
loop, bomb | Labels. A label is turned into the distance from the instruction that uses it, so jmp loop on the third line becomes jmp -2. |
; ... | A comment. Everything after a semicolon is ignored, except the directives above. |
end | End of the warrior. Anything after it is ignored. |
Opcodes, modifiers and directives are case-insensitive. Use the Validate button in the editor to see exactly what the assembler made of your code. For the Dwarf above it shows:
ADD.AB # 4, $ 3
MOV.I $ 2, @ 2
JMP.B $ -2, $ 0
DAT.F # 0, # 0
Instructions and fields
Each cell in the core holds exactly one instruction. An instruction has five parts:
opcode.modifier A-mode A-number, B-mode B-number
mov.i $2, @2
- Opcode: what to do, for example
MOV(copy) orJMP(jump). - Modifier: which parts of the instructions are used, for example
.Ifor the whole instruction. If you leave it out, the assembler picks a sensible default (see Modifiers). - A-field and B-field: two numbers, each with an addressing mode in front. Roughly, A is the source and B is the destination. If you leave out the mode,
$(direct) is used.
The fields are also plain data. Warriors use them as pointers and counters: the bomb in the Dwarf is both the thing being thrown and the pointer that says where to throw it.
If you write only one operand, DAT puts it in the B-field (dat #5 is dat #0, #5). Most other opcodes put it in the A-field and use $0 for B (jmp loop is jmp loop, $0).
Addressing modes
The mode tells the processor how to turn the number into an address. Indirect modes follow a pointer stored in another instruction: the number picks an instruction, and that instruction’s A- or B-field gives a further offset from that instruction.
| Mode | Name | Points to | Typical use |
|---|---|---|---|
# | Immediate | The number itself. The address is the current instruction. | Constants: add #4, ptr |
$ | Direct | The cell that many steps away. The default. | jmp loop |
@ | B-indirect | Go to the cell, then add its B-field. | Use a B-field as a pointer: mov bomb, @ptr |
* | A-indirect | Go to the cell, then add its A-field. | Use an A-field as a pointer |
< | B pre-decrement | Subtract 1 from the cell’s B-field, then use it like @. | Walking a pointer backwards |
> | B post-increment | Use the B-field like @, then add 1 to it. | Walking a pointer forwards |
{ | A pre-decrement | Like <, using the A-field. | Second pointer in the same cell |
} | A post-increment | Like >, using the A-field. | Copy loops: mov }src, >dst |
An indirect example
The instruction at address 0 is mov.i 2, @2. The A operand 2 is direct: the source is cell 2. The B operand @2 looks at cell 2, reads its B-field (5), and adds it: the destination is cell 2 + 5 = 7.
The order matters. Decrements (<, {) change the pointer first and then follow the new value. Increments (>, }) follow the current value first and then change the pointer. So a pointer of 5 used with < reaches offset 4, but used with > it reaches offset 5 and is 6 afterwards.
Increments and decrements change the core straight away, even if the instruction then does nothing useful. jmp loop, <ptr jumps and decrements ptr’s B-field. This is how many warriors fit two jobs into one instruction.
Opcodes
“A” and “B” below mean the instructions (or numbers) that the A and B operands point to. The default modifier is what you get if you write no modifier.
Moving and arithmetic
| Opcode | What it does | Default modifier |
|---|---|---|
DAT | Kills the process that executes it. Also used to hold data, pointers and bombs. | .F |
MOV | Copies A to B. | .AB if A is #, else .B if B is #, else .I |
ADD | B = B + A | .AB if A is #, else .B if B is #, else .F |
SUB | B = B − A | |
MUL | B = B × A | |
DIV | B = B ÷ A, rounded down. Dividing by zero kills the process. | same as ADD |
MOD | B = remainder of B ÷ A. Dividing by zero kills the process. |
All arithmetic wraps around the core size: in an 8000-cell core, 7999 + 2 is 1.
Jumps
| Opcode | What it does | Default modifier |
|---|---|---|
JMP | Continue at A. | .B |
JMZ | Continue at A if B is zero, otherwise go to the next instruction. | |
JMN | Continue at A if B is not zero. | |
DJN | Decrement B, then continue at A if B is not zero. A loop counter in one instruction. | |
SPL | Start a new process at A. The current process continues at the next instruction. See Processes. |
Jumps test the B-field by default. jmz.f and jmz.i jump only if both fields are zero; jmn.f jumps if either field is non-zero; djn.f decrements both fields and jumps if either is non-zero. jmz.f is the usual way to ask “is this cell still empty core?” It only looks at the numbers, though, so warriors can hide from it with instructions whose fields are both zero, such as spl #0, #0 or jmp #0, #0. Comparing whole instructions with seq.i or sne.i still sees them.
Comparisons
Comparisons do not jump. If the condition is true, they skip the next instruction.
| Opcode | Skips the next instruction if | Default modifier |
|---|---|---|
SEQ / CMP | A equals B. CMP is the old name. | same as MOV |
SNE | A does not equal B. | |
SLT | A is less than B. Numbers are compared as 0 to CORESIZE−1, so -1 counts as the largest value. | .AB if A is #, else .B |
A common scanner idiom compares two cells some distance apart, and only drops through to the attack when they differ:
seq.i ptr1, ptr2 ; both cells look the same?
jmp attack ; no: something is there
add.f step, ptr1 ; yes: look further on
Other
| Opcode | What it does | Default modifier |
|---|---|---|
NOP | Nothing. Operands are still evaluated, so nop >a, <b still changes a and b. | .F |
LDP | Loads P-space cell number A into B. See P-space. | .AB if A is #, else .B |
STP | Stores A into P-space cell number B. |
Modifiers
A modifier says which fields an instruction reads and writes. Think of it as “from which field of A, to which field of B”.
| Modifier | From A | To B | Example |
|---|---|---|---|
.A | A-field | A-field | add.a #1, ptr moves an A-field pointer |
.B | B-field | B-field | mov.b ptr, save copies a pointer |
.AB | A-field | B-field | add.ab #4, bomb (the Dwarf) |
.BA | B-field | A-field | Move a B-pointer into an A-field |
.F | both fields | both fields, A→A and B→B | add.f step, ptrs moves two pointers at once |
.X | both fields | both fields, swapped | mov.x ptr, ptr swaps a cell’s two fields |
.I | the whole instruction, including opcode, modifier and modes | mov.i bomb, @ptr copies a complete bomb | |
.I only makes a difference for MOV, SEQ and SNE. Arithmetic and jumps treat .I like .F.
How # works with modifiers
An immediate operand points at the current instruction. So in add.ab #4, bomb, “the A-field of A” is the A-field of the ADD itself, which holds 4. That is why MOV and ADD default to .AB when A is immediate: it takes the number you wrote and applies it to B’s B-field.
Watch out: mov.i #0, 5 copies the whole MOV instruction to cell 5, not a zero. If you want to put a number into a field, use .AB (the default for mov #0, 5), .A or .B.
Processes and timing
A warrior starts with one process. SPL creates more. Each warrior keeps its processes in a queue, and the warriors take turns: warrior 1 runs one instruction of its next process, then warrior 2 does the same, and so on.
- A warrior with one process runs one instruction per turn. With ten processes, each process runs only every tenth turn of that warrior. More processes do not make a warrior faster; they make it harder to kill.
- After
SPL, the current process is queued first (at the next instruction), then the new process (at A). - When the warrior already has the maximum number of processes (
MAXPROCESSES),SPLdoes not create a new one. Only the current process is queued again, at the next instruction, soSPLthen behaves likeNOP. - A process that executes
DAT, or divides by zero, is removed. The warrior lives on as long as it has at least one process.
spl 1 ; 1 process becomes 2, both continue below
spl 1 ; 4
spl 1 ; 8 processes now run the next line
spl 0 on its own line creates a new process at the same instruction every time it runs, which quickly fills the process queue. Combined with a DAT bomb, opponents hit by spl 0 bombs waste their time creating useless processes.
Assembler features
Labels and expressions
Labels and numbers can be combined in expressions: bomb+1, step*2, (CORESIZE/3)+1. The assembler supports + - * / %, comparisons (== != < > <= >=) and logic (&& || !). Comparisons give 1 for true and 0 for false.
Use brackets. EQU replaces a name with its text, not with a computed value. After x equ 2+3, the expression x*2 becomes 2+3*2, which is 8, not 10. Put brackets around combined expressions, both in the EQU (x equ (2+3)) and where you use them, to avoid surprises.
Predefined constants
These come from the match settings, so a warrior can adapt to them or assert them.
| Name | Meaning |
|---|---|
CORESIZE | Number of cells in the core |
MAXPROCESSES | Process limit per warrior |
MAXCYCLES | Cycles before the round is a tie |
MAXLENGTH | Maximum warrior length in instructions |
MINDISTANCE | Minimum distance between warriors when loaded |
PSPACESIZE | Number of P-space cells |
WARRIORS | Number of warriors in the battle |
ROUNDS | Number of rounds in the match |
CURLINE | Number of the current instruction, starting at 0 |
Repeating code with FOR
FOR n … ROF repeats the lines between them n times. A label in front of FOR becomes a counter (01, 02, …). This is handy for decoys, tables and unrolled loops.
for 10
dat #0, #0 ; ten empty cells
rof
i for 3
dat #i, #i*100 ; dat #1,#100 dat #2,#200 dat #3,#300
rof
FOR blocks can be nested. Each ROF closes the nearest open FOR, and each block can have its own counter. A label in front of a line inside the block can include the counter with &, so each copy gets its own name.
i for 2
j for 3
dat #i, #j ; i = 01..02, j = 01..03: six lines
rof ; end of j
rof ; end of i
n for 3
slot&n dat #0, #n ; labels slot01, slot02, slot03
rof
ROF cannot take a label: pMARS warns and ignores it. Use a comment after rof, as above, to show which loop it closes.
The count can be any expression. A FOR 0 block is skipped, which makes it a convenient way to comment out several lines, and a condition gives conditional assembly:
for CORESIZE == 8000
; code for the standard core size
rof
for MAXLENGTH-CURLINE
dat #1, #1 ; fill the rest of the warrior with decoys
rof
Debugging directives
In this app, ;break sets a breakpoint on the next instruction, and ;trace / ;trace off mark a range of instructions. Turn on Stop at breakpoints and debug markers in the debugger to stop there.
P-space
P-space is a small private memory that survives from one round to the next. Warriors use it to remember which strategy worked and switch if they keep losing. Only LDP and STP can access it.
- Cell 0 is filled in for you: it holds −1 in the first round, 0 if the warrior lost the previous round, and otherwise the number of warriors that survived the previous round.
- The other cells start at 0 and keep whatever you store in them. Cell numbers wrap around
PSPACESIZE. PIN ngives a warrior a P-space ID. Warriors with the same PIN share P-space.
ldp.ab #0, result ; how did the last round go?
jmz switch, result ; lost: switch strategy
...
result dat #0, #0
Classic warriors
Most warriors are built from a few basic strategies, often combined. The examples below are short on purpose: they work, but real warriors on a hill are far more refined. Try them in the app.
Imp
Copies itself one cell ahead and then executes the copy. It is very hard to kill, but it cannot kill anything either: an opponent that runs into an imp turns into an imp. Imps mostly secure ties.
mov.i 0, 1
Stone (bomber)
Throws bombs at regular intervals and hopes to hit the opponent before it is hit. The Dwarf above is the simplest stone. Good stones bomb with a step that does not hit themselves, and run fast in a tight loop.
Scanner
Looks for non-empty cells and attacks only where it finds something. It spends time looking, but its attacks rarely miss. This simple version also finds itself eventually; real scanners avoid that.
gap equ 7
org scan
scan add.ab #gap, ptr ; look 7 cells further on
ptr jmz.f scan, 20 ; empty cell? keep looking
mov.i bomb, @ptr ; found something: bomb it
jmp scan
bomb dat #0, #0
Paper (replicator)
Makes copies of itself all over the core, each running in parallel with many processes. Killing one copy does not stop the rest. Papers are hard to kill, but bombs that only slow them down can still lead to a tie.
org paper
paper spl 1 ; 1 process becomes 2
spl 1 ; ... 4
spl 1 ; ... 8
silk spl @0, 2667 ; start a process in the new copy
mov.i }silk, >silk ; copy one instruction there
The eight processes each run mov.i }silk, >silk once. Each run copies one instruction to the new location and advances both pointers, so together they copy the two-line loop (plus a few empty cells). The new copy then does the same.
Other strategies
- Core clear: overwrites the whole core cell by cell, often as the final phase of a scanner.
- Imp spiral / imp ring: several imps spaced evenly round the core, so killing one doesn’t stop them.
- Quickscanner: a fast unrolled scan of the core at the very start of a round, before switching to another strategy.
- Decoys: blocks of harmless non-empty cells that waste the opponent’s scans.
Hill players often describe strategies as rock-paper-scissors: paper tends to beat stone, stone tends to beat scanners, and scanners tend to beat paper. The tiny examples here are too simple to follow that reliably.
Common pitfalls
- Your warrior bombs itself. Check that the bombing step never lands on your own code before you’ve gone round the core. Watch the first few hundred cycles in the core view.
- The default modifier isn’t what you meant.
mov 0, 1ismov.ibutmov #0, 1ismov.ab. When in doubt, write the modifier. - Negative numbers are big.
-1is stored asCORESIZE-1.slt #10, xtreatsx = -1as a large number. DATbombs are invisible tojmz.fscanners.dat 0, 0looks exactly like empty core. That is good for bombs and bad for decoys.- Operands always run.
<,>,{and}change the core even onJMP,SPL,NOPandDAT. - Labels are relative to the instruction that uses them.
bombmeans a different number on each line. Copying a line elsewhere copies the offset, not the label. - Your warrior is too long. Warriors longer than
MAXLENGTHinstructions do not assemble. Some hills allow only 100 instructions, Nano only 5.
Hill settings
The match preset in the app sets these values. Warriors written for one hill can behave very differently on another.
| Preset | Core size | Processes | Cycles | Max length | Notes |
|---|---|---|---|---|---|
| Standard ’94 | 8000 | 8000 | 80000 | 100 | The usual starting point |
| ’94 No P-space | 8000 | 8000 | 80000 | 100 | P-space size 1 |
| Standard ’88 | 8000 | 8000 | 80000 | 100 | ’88 rules (see below) |
| ’94 Experimental | 55440 | 10000 | 500000 | 200 | Large core |
| Limited Process | 8000 | 8 | 80000 | 200 | Few processes |
| Medium Process | 8000 | 64 | 80000 | 100 | |
| Tiny | 800 | 800 | 8000 | 20 | |
| Nano | 80 | 80 | 800 | 5 | Five instructions |
’88 vs ’94
ICWS ’88 is the older, smaller dialect. Under ’88 rules (the Standard ’88 and ICWS ’88 Tournament presets):
- There are no modifiers. Each opcode behaves as in ’94 with its default modifier.
- Only the modes
#,$,@and<exist. SEQ,SNE,NOP,MUL,DIV,MOD,LDPandSTPdo not exist. UseCMPforSEQ.- Some opcodes accept only certain modes; for example
DATaccepts only#and<.
Most ’88 warriors run unchanged under ’94 rules. The reverse is usually not true.
Further reading
- The corewar.dev tutorial (coming soon): run your first battle and use the debugger.
- The beginners’ guide to Redcode: a longer introduction with more examples.
- corewar.co.uk: strategy articles, history and archives of warriors.
- The ICWS ’94 draft standard: the full specification, for exact rules.