CellHack 2

CellHack 2 — game rules

CellHack is a programming game. You write the brain of a single-celled organism in x86-64 assembly. The server drops one cell from each of four colonies into a 2D arena and runs the simulation. Your cells rest, move, split and attack until the ticks run out or one colony is left standing.

The arena

Ticks

The game runs in ticks (default 2000). Every tick, in order:

  1. Decide. Every living cell's program is called once with a snapshot of the world as it was at the start of the tick. Programs run in a sandbox and return one action.
  2. Attacks resolve. Each attacker pays ATTACK_COST (2) and takes up to ATTACK_TAKE (20) energy from the cell in the chosen direction, if there is one. The energy taken is added to the attacker. You can attack friends. A target that is attacked many times loses energy to each attacker in turn, until it has none left.
  3. Moves and splits resolve. A move or split targets the adjacent square in the chosen direction. It succeeds only if that square was empty at the start of the tick and is still empty now. If several cells want the same square, one is chosen at random; the others fail but still pay. A move costs MOVE_COST (2), even into a wall or an occupied square. A split needs at least SPLIT_MIN (40) energy (with less, it does nothing and costs nothing), costs SPLIT_COST (4), and then the parent keeps half (rounded up) and the child gets half (rounded down). The child gets a copy of the parent's 64 bytes of memory, pays upkeep this tick like everyone else, and acts for the first time next tick. A cell that was drained to zero by attacks this tick still occupies its square until the end of the tick, and its own action still resolves.
  4. Rest and upkeep. Resting cells gain REST_GAIN (4). Then every cell pays UPKEEP (1), whatever it did. Energy is clamped to ENERGY_MAX (255).
  5. Deaths. Any cell at 0 energy or below is removed.

Doing nothing (ACT_NOP) still pays upkeep. A program that faults (crashes, runs over the instruction budget, tries a syscall, returns an unknown action, …) does nothing that tick; the fault is logged and you can read it in the match debug log.

Winning

A match ends at the tick limit, when at most one colony is alive, or when everyone is dead. Colonies are then ranked:

Ladder matches award placement points (1st 3, 2nd 2, 3rd 1, 4th 0) which feed your rating on the leaderboard.

What your program sees

Your function receives a 128-byte view (pointer in rdi), your energy (in rsi), and a pointer to 64 bytes of memory that persists between calls (in rdx).

The view contains the tick number, your position and energy, the arena size, colony populations, a random byte, and a 5×5 neighbourhood centred on you: for each of the 25 squares, what is there (empty, friend, enemy, wall) and how much energy that cell has. See the API reference for exact offsets.

Strategy hints

CellHack 2 — program API

Writing a cell

Your submission is a NASM source file in Intel syntax. The server wraps it like this before assembling it with nasm -f bin:

bits 64
org CODE_BASE          ; 0x00100000
default rel
; ... every constant from cellhack.inc ...
; ---- your source starts here ----

So: no %include, no section directives are needed (you may use them, but only the flat binary output matters), and execution begins at the first byte of your file. Your program is a function. When it is called, rdi, rsi, rdx are set as below and a return address is on the stack. Return your action in al with ret.

The smallest valid cell:

    mov al, ACT_REST
    ret

Calling convention

System V AMD64, the same one Linux uses for C:

Register Meaning
rdi pointer to the view struct (VIEW_BASE, read-only)
rsi your energy (0–255)
rdx pointer to your 64 bytes of persistent memory (CELLMEM_BASE, read/write)
rsp 16-byte aligned minus 8, as after a call; 16 KB of zeroed stack below it
al your action (return value)

You may clobber any register. The stack, flags, and all registers are reset before every call; only the first MEM_SIZE (64) bytes at [rdx] survive from one call to the next. The rest of the 4 KB cell-memory page is scratch and is zeroed before each call.

Your code region is read-only. Keep mutable data in [rdx] or on the stack.

The view struct ([rdi])

Offset Name Type Meaning
0 VIEW_TICK u32 current tick, 0-based
4 VIEW_ENERGY u16 your energy (same as rsi)
6 VIEW_RAND u8 a random byte, fresh every call
7 VIEW_AGE u8 ticks since you were born (saturates at 255)
8 VIEW_X u16 your x
10 VIEW_Y u16 your y
12 VIEW_W u16 arena width
14 VIEW_H u16 arena height
16 VIEW_POP_SELF u16 living cells in your colony
18 VIEW_POP_ENEMY u16 living cells in all other colonies
20 VIEW_COLONY u8 your colony slot, 0–3
32 VIEW_CELLS u8[25] 5×5 neighbourhood, row-major
64 VIEW_ENERGIES u8[25] energy of the cell on each square, 0 if none

Everything else up to byte 128 is zero.

Neighbourhood indexing

VIEW_CELLS and VIEW_ENERGIES are 5×5 arrays centred on you. Index for an offset (dx, dy) with dx, dy ∈ [-2, 2]:

index = (dy + 2) * 5 + (dx + 2)

so index 12 is you, 7 is directly above (DIR_UP), 13 is to the right, 17 below, 11 to the left. Values are CELL_EMPTY 0, CELL_FRIEND 1, CELL_ENEMY 2, CELL_WALL 3.

  idx:   0  1  2  3  4      dy=-2
         5  6  7  8  9      dy=-1
        10 11 12 13 14      dy= 0     (12 = you)
        15 16 17 18 19      dy=+1
        20 21 22 23 24      dy=+2

Actions (return in al)

Value Name Effect
0x00 ACT_NOP nothing
0x01 ACT_REST +REST_GAIN energy
0x10 + dir ACT_MOVE + dir move one square; costs MOVE_COST
0x20 + dir ACT_SPLIT + dir spawn a child; needs SPLIT_MIN, costs SPLIT_COST, halves energy
0x30 + dir ACT_ATTACK + dir take up to ATTACK_TAKE from that cell; costs ATTACK_COST

Directions: DIR_UP 0, DIR_RIGHT 1, DIR_DOWN 2, DIR_LEFT 3. Any other value in al is a fault (bad_action) and the cell does nothing. Only al is read; the upper bits of rax are ignored.

Limits and faults

Constants

Every name above is defined in cellhack.inc, which is automatically prepended to your source. Download it for reference; do not %include it.

Guest memory map

Region Address Size Access Notes
CODE_BASE 0x00100000 64 KB R+X your program; padded with int3
STACK_BASE 0x00200000 16 KB RW zeroed each call, guard pages both sides
VIEW_BASE 0x00300000 4 KB R the view struct
CELLMEM_BASE 0x00310000 4 KB RW first 64 B persist
EXIT_ADDR 0x00400000 4 KB R+X return address; ending up here ends the call

Example: rest, split when fat, eat neighbours

; neighbours: up=7 right=13 down=17 left=11
    lea  r8, [rdi + VIEW_CELLS]
    cmp  byte [r8 + 7], CELL_ENEMY
    je   .up
    cmp  byte [r8 + 13], CELL_ENEMY
    je   .right
    cmp  byte [r8 + 17], CELL_ENEMY
    je   .down
    cmp  byte [r8 + 11], CELL_ENEMY
    je   .left

    cmp  rsi, SPLIT_MIN + 10
    jb   .rest
    movzx eax, byte [rdi + VIEW_RAND]
    and  al, ACT_DIR_MASK
    or   al, ACT_SPLIT
    ret
.rest:
    mov  al, ACT_REST
    ret
.up:    mov al, ACT_ATTACK + DIR_UP
        ret
.right: mov al, ACT_ATTACK + DIR_RIGHT
        ret
.down:  mov al, ACT_ATTACK + DIR_DOWN
        ret
.left:  mov al, ACT_ATTACK + DIR_LEFT
        ret

Sample bots

Read them, upload them as-is to see how they fare, then beat them.

BotWhat it does
evil_badaction.asm misbehaves on purpose returns an action that does not exist. Faults with "bad_action".
evil_loop.asm misbehaves on purpose never returns. Every call faults with "budget".
evil_selfmod.asm misbehaves on purpose tries to patch its own code. Faults with "write_prot".
evil_stack.asm misbehaves on purpose infinite recursion. Faults with "mem_write" below the stack.
evil_syscall.asm misbehaves on purpose tries exit(0). Every call faults with "syscall".
hunter.asm chases the nearest enemy in view, attacks when adjacent, splits when
idle.asm rests forever. The simplest valid cell and the default sandbox filler.
random_walker.asm attacks adjacent enemies, otherwise splits or wanders at random.
rest_split.asm eat adjacent enemies, split into free squares once fat, else rest.