corewar.dev

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.

Every address is relative0 is the current instruction, 1 is the next one, -1 the previous one. There are no absolute addresses.
The core wraps aroundIn an 8000-cell core, address 8000 is address 0 again. Every number is stored as a value from 0 to 7999, so -1 is stored as 7999.
Empty core is DAT 0, 0All cells that do not belong to a warrior start as DAT.F $0, $0. Jumping into empty core kills the process.
ScoringIn a two-warrior match, a win is worth 3 points, a tie 1, and a loss 0. Matches run many rounds from random starting positions.

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
LineMeaning
;redcode-94Marks the start of the code. Anything before it is ignored, so you can paste a warrior into an email or notes.
;name, ;authorShown in results and on hills. Optional, but please use them.
;strategyA 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.
;assertAn 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 4A named constant. The assembler replaces step with 4.
org loopWhere execution starts. Without it, execution starts at the first instruction. end loop does the same.
loop, bombLabels. 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.
endEnd 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

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.

ModeNamePoints toTypical use
#ImmediateThe number itself. The address is the current instruction.Constants: add #4, ptr
$DirectThe cell that many steps away. The default.jmp loop
@B-indirectGo to the cell, then add its B-field.Use a B-field as a pointer: mov bomb, @ptr
*A-indirectGo to the cell, then add its A-field.Use an A-field as a pointer
<B pre-decrementSubtract 1 from the cell’s B-field, then use it like @.Walking a pointer backwards
>B post-incrementUse the B-field like @, then add 1 to it.Walking a pointer forwards
{A pre-decrementLike <, using the A-field.Second pointer in the same cell
}A post-incrementLike >, 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.

0mov.i 2, @21…2dat 0, 53…4…5…6…7← copy

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

OpcodeWhat it doesDefault modifier
DATKills the process that executes it. Also used to hold data, pointers and bombs..F
MOVCopies A to B..AB if A is #, else .B if B is #, else .I
ADDB = B + A.AB if A is #, else .B if B is #, else .F
SUBB = B − A
MULB = B × A
DIVB = B ÷ A, rounded down. Dividing by zero kills the process.same as ADD
MODB = 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

OpcodeWhat it doesDefault modifier
JMPContinue at A..B
JMZContinue at A if B is zero, otherwise go to the next instruction.
JMNContinue at A if B is not zero.
DJNDecrement B, then continue at A if B is not zero. A loop counter in one instruction.
SPLStart 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.

OpcodeSkips the next instruction ifDefault modifier
SEQ / CMPA equals B. CMP is the old name.same as MOV
SNEA does not equal B.
SLTA 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

OpcodeWhat it doesDefault modifier
NOPNothing. Operands are still evaluated, so nop >a, <b still changes a and b..F
LDPLoads P-space cell number A into B. See P-space..AB if A is #, else .B
STPStores 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”.

ModifierFrom ATo BExample
.AA-fieldA-fieldadd.a #1, ptr moves an A-field pointer
.BB-fieldB-fieldmov.b ptr, save copies a pointer
.ABA-fieldB-fieldadd.ab #4, bomb (the Dwarf)
.BAB-fieldA-fieldMove a B-pointer into an A-field
.Fboth fieldsboth fields, A→A and B→Badd.f step, ptrs moves two pointers at once
.Xboth fieldsboth fields, swappedmov.x ptr, ptr swaps a cell’s two fields
.Ithe whole instruction, including opcode, modifier and modesmov.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.

        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.

NameMeaning
CORESIZENumber of cells in the core
MAXPROCESSESProcess limit per warrior
MAXCYCLESCycles before the round is a tie
MAXLENGTHMaximum warrior length in instructions
MINDISTANCEMinimum distance between warriors when loaded
PSPACESIZENumber of P-space cells
WARRIORSNumber of warriors in the battle
ROUNDSNumber of rounds in the match
CURLINENumber 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.

        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

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

Hill settings

The match preset in the app sets these values. Warriors written for one hill can behave very differently on another.

PresetCore sizeProcessesCyclesMax lengthNotes
Standard ’948000800080000100The usual starting point
’94 No P-space8000800080000100P-space size 1
Standard ’888000800080000100’88 rules (see below)
’94 Experimental5544010000500000200Large core
Limited Process8000880000200Few processes
Medium Process80006480000100
Tiny800800800020
Nano80808005Five instructions

’88 vs ’94

ICWS ’88 is the older, smaller dialect. Under ’88 rules (the Standard ’88 and ICWS ’88 Tournament presets):

Most ’88 warriors run unchanged under ’94 rules. The reverse is usually not true.

Further reading