↓ Skip to main content
  1. posts/

Redcode, instruction by instruction

··Index: 9·

Redcode is the assembly language of Core War, and it’s small, with 16 opcodes, 7 modifiers and 8 addressing modes. Every warrior, from the one-line Imp to a tournament winner, is built out of those pieces, and this page goes through what they do, one animation at a time. At the end there’s a debugger where you can run any warrior step by step and go back in time.

All the animations run on the same simulator as the arena (ICWS'94 rules, an 8000-cell core), and what you see here is exactly how a battle plays out.

An instruction
#

Every memory cell holds exactly one instruction. Redcode has no bytes, no registers and no separate data memory, and numbers live in the two fields of an instruction, most of the time a DAT.

Addresses are relative, and the core is a ring
#

Redcode has no address 0. Every number is a distance from the instruction that is running, which means 1 is the next cell and -1 the previous one, and since the core is circular, counting past the last cell brings you back to the first. Warriors never refer to fixed places, which is why they work wherever they get loaded.

How one instruction runs
#

Every instruction goes through the same five phases, and the animations use the same colours for them:

  1. Fetch. The warrior whose turn it is takes the first process from its queue and reads the instruction at that address.
  2. A-operand (amber). Work out which cell the A-operand points to, following pointers if the mode is indirect.
  3. B-operand (violet). The same for B.
  4. Effect. Do the work: copy, add, compare, jump… Green shows a write, white a jump, red a dying process.
  5. Re-queue. Put the process back at the end of the queue with its next address. A process that ran a DAT doesn’t go back, because it’s dead.

In every demo, Step ▸ runs one instruction and animates its phases, and if you tick pause at each phase you can go through them at your own pace. Coloured bars to the left of an address show which warrior wrote to that cell, and round markers on the right are the processes waiting there.

Addressing modes
#

The mode is the character in front of a number, and it decides how that number turns into an address.

ModeNameWhere the operand points
#immediateat the instruction itself; the number is just a value
$directthe cell number cells away (the default)
*A-indirectgo to that cell, then go on by the value in its A-field
@B-indirectthe same, using the B-field
{A-predecrementlike *, but first subtract 1 from that A-field
<B-predecrementlike @, but first subtract 1 from that B-field
}A-postincrementlike *, and add 1 to that A-field afterwards
>B-postincrementlike @, and add 1 to that B-field afterwards

In this demo MOV.I copies something into dest. Only the A-mode changes, and every mode picks a different source. The pointer cell ptr has #3 in the A-field and #1 in the B-field. Pick a mode and press Step ▸:

Indirect modes are what keeps Redcode programs short. MOV bomb, @bomb drops a bomb wherever the B-field of bomb points, and the increment modes turn cells into moving pointers, and with them MOV >-1, }-1 manages to copy a line and advance two pointers in a single instruction.

Modifiers
#

The modifier after the dot says which fields of the source (A) instruction get used and which fields of the destination (B) instruction get changed.

ModifierFrom ATo B
.AA-fieldA-field
.BB-fieldB-field
.ABA-fieldB-field
.BAB-fieldA-field
.Fboth fieldsboth fields
.Xboth fieldsboth fields, crossed (A→B, B→A)
.Ithe whole instructionthe whole instruction (for arithmetic, the same as .F)

Here the source is SPL #7, #9 and the destination is DAT #10, #20, so you can follow which number goes where. Try MOV.X, ADD.F and SEQ.I:

If you leave the modifier out, the assembler picks one based on the opcode and the modes, and MOV 0, 1 becomes MOV.I and ADD #4, 3 becomes ADD.AB. I write modifiers explicitly anyway, because I find them easier to read that way.

The instructions
#

DAT: data, and death
#

DAT holds numbers, and processes that try to execute one get removed. Once a warrior has no processes left, it loses, which is why most bombs in Core War are DATs.

MOV: copy
#

MOV copies from the A-address to the B-address. With .I it copies the whole instruction, and that’s how warriors copy themselves and drop bombs. The Imp is a single MOV.I 0, 1 that copies itself to the next cell and then runs that copy. Press Run to watch it walk.

ADD, SUB, MUL, DIV, MOD: arithmetic
#

Arithmetic changes fields of the B-instruction using the A-values. Results wrap around the core size (8000), which leaves no negative numbers inside the core, and -1 gets stored as 7999. Dividing by zero kills the process.

JMP: jump
#

The process continues at the A-address. JMP never uses the B-operand’s value, yet it still evaluates the operand, which lets JMP 0, <5 jump and decrement a pointer at the same time.

JMZ and JMN: conditional jumps
#

JMZ jumps to A when the B-value is zero, and JMN jumps when it’s not zero. With .F or .I both fields get tested.

DJN: decrement and jump
#

DJN subtracts 1 from the B-target and jumps to A if the result isn’t zero, which makes it the classic loop counter. The decrement is a write, and DJN can use it to damage enemy code too.

SEQ, SNE, SLT: compare and skip
#

When the test holds, a comparison skips the next instruction. SEQ (also written CMP) tests for equality, SNE for inequality and SLT for “A is less than B”, and scanners use them to tell empty cells from enemy code.

SPL: a new process
#

SPL starts a new process at the A-address. The new process goes to the end of the warrior’s queue, after the current one, which carries on with the next instruction, and a warrior can have up to 8000 processes.

NOP
#

NOP does nothing, but the operands still get evaluated, which means the increment and decrement modes work as usual.

Processes and turns
#

Each warrior has a queue of processes, and the simulator alternates between warriors: one instruction of warrior 1, one of warrior 2, and so on. A warrior with more processes does not get more turns. Those turns get shared among all of its processes, and each of them has to run more slowly. In this demo warrior 1 splits into four processes and warrior 2 has one:

Putting it together: the Dwarf
#

dwarf   ADD.AB  #4, bomb      ; move the target 4 cells further
        MOV.I   bomb, @bomb   ; drop the bomb where its B-field points
        JMP     dwarf
bomb    DAT     #0, #0

Three instructions in a loop. ADD.AB adds 4 to the B-field of bomb, MOV.I bomb, @bomb follows that B-field (B-indirect) and copies the DAT there, and JMP goes back to the start. Press Run to see the bombs land 4, 8, 12… cells away, and once the target leaves the window, the arrow points off the edge and shows the address.

The debugger
#

Load any warrior, with or without an opponent:

  • Step ▸ (→) runs one instruction with the full animation. At the phase by phase speed, each click shows one phase.
  • ◂ Back (←) goes back one instruction. The debugger saves the core every 1000 steps and replays the rest, so you can rewind a long way.
  • Run ▶ (Space) animates at the slow speeds and runs without animation at the fast ones. It stops at breakpoints: click the dot column next to an address.
  • Process queues show every warrior’s processes in turn order. The highlighted one runs next. Click an address to jump there.
  • Core map shows all 8000 cells. Hover to read a cell, click to open it in the listing. The white frame is the part shown in the listing.
  • Recent steps lists the last instructions of both warriors and what they did.
  • Edit the source to change a warrior and load it again.

Try:

  • Load Mice and step through its copy loop at the phase by phase speed. Watch <dest move the pointer backwards before each copy.
  • Load Paper, run it at 2 000 / s for a moment, then step back and find the line where the first copy starts running.
  • Load Scanner against Dwarf, set a breakpoint on the MOV.I trap, @ptr line and see what the scanner found when it stops.

Related