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
- A rectangular grid (default 64×64). Coordinates are
(x, y) with (0, 0) top-left.
- Four colonies start one square in from each corner: colony 0 top-left, 1 top-right,
2 bottom-left, 3 bottom-right. Each starts with one cell with 100 energy.
- Squares outside the grid are walls; nothing can move into or split into a wall.
Ticks
The game runs in ticks (default 2000). Every tick, in order:
- 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.
- 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.
- 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.
- 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).
- 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:
- Living colonies first, by population, then by total energy.
- Dead colonies after them, by how long they survived.
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
- Resting is the only way to gain energy. A lone cell that just rests nets +3/tick.
- A split costs 4 and halves your energy. Splitting early builds population; splitting
late builds strong cells. Both lose to a colony that knows when to do which.
- Attacking adjacent enemies with low energy kills them outright and is cheap.
- Memory is your only state. Use it: a few bytes for "what am I doing" and "which way
was I going" is enough to stop your cells wobbling in place.
- The random byte in the view is the only randomness you get;
rdtsc returns the tick
number and rdrand faults. Matches are fully deterministic and replayable.
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
- Instruction budget:
INSN_BUDGET (5000) instructions per call. Over budget → fault.
- Program size: at most
CODE_MAX (32 KB) of assembled output, source at most 64 KB.
- Forbidden:
syscall, sysenter, int n, int3, ud2, in/out, rdrand.
The CPU model is a plain qemu64: SSE2/SSE3 are available, anything newer is not
guaranteed. rdtsc works but returns the tick number (and clobbers rdx as usual).
- Memory faults: reading/writing/executing unmapped addresses, writing to the code
or view region, overflowing the stack — all faults.
- A fault means no action this tick (upkeep is still paid). The first faults of each
match are logged with tick, position, kind and
rip, visible on the match page for
your own colony.
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