This commit is contained in:
Richard
2026-03-26 18:43:01 +01:00
commit 9616a8236b
25 changed files with 2773 additions and 0 deletions

7
.gitignore vendored Normal file
View File

@@ -0,0 +1,7 @@
node_modules
data
reference/dev.moment.com
farm/core*
farm/build
farm/bin
farm/farm

60
analyze/trace_parser.py Normal file
View File

@@ -0,0 +1,60 @@
#!/usr/bin/env python3
import os
import sys
import argparse
import logging
import re
class TraceparserError(Exception):
pass
class Traceparser:
def __init__(self, file):
self.logger = logging.getLogger("Traceparser")
self.file = file
def run(self):
current_map = None
pos = (0, 0)
lid = 0
for line in self.file:
lid += 1
line = line.strip()
if line.startswith("MAP"):
current_map = line.split()[1]
print(f"Current map: {current_map}")
elif line.startswith("A"):
m = re.match(r'A(\d+) @(\d+):(\d+) R:([0-9a-f]+) ([0-9a-f]+) ([0-9a-f]+) ([0-9a-f]+) ([0-9a-f]+) ([0-9a-f]+) ([0-9a-f]+) ([0-9a-f]+)', line)
if m:
new_pos = (int(m.group(2)), int(m.group(3)))
if new_pos != pos:
pos = new_pos
print(f"{lid} POS: {pos}")
def main(args):
traceparser = Traceparser(args.file)
traceparser.run()
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument("--debug", "-d", action="store_true")
parser.add_argument("file", type=argparse.FileType('r'))
args = parser.parse_args()
log_format = "%(asctime)s [%(levelname)s] %(message)s"
log_date_format = "%Y-%m-%dT%H%M%S%z"
log_level = logging.DEBUG if args.debug else logging.INFO
logging.basicConfig(format=log_format, level=log_level, datefmt=log_date_format)
try:
main(args)
except TraceparserError as e:
logging.error(e)
sys.exit(1)
except KeyboardInterrupt:
logging.error("Interrupted")
sys.exit(2)
except Exception as e:
logging.error(e, exc_info=True)
sys.exit(3)

239
asm/asm.py Executable file
View File

@@ -0,0 +1,239 @@
#!/usr/bin/env python3
import os
import sys
import argparse
import logging
from typing import List
from dataclasses import dataclass
from struct import pack
class AssemblerError(Exception):
pass
@dataclass
class Instruction:
name: str
number: int
load_jr: bool = False
class Assembler:
def __init__(self):
self.prog = bytearray()
self.lineno = 0
self.labels = {}
self.label_references = []
self.logger = logging.getLogger("asm")
instructions = [
Instruction("NOOP", 0), # Come on, we just need one
Instruction("ID", 1),
Instruction("TAG", 2),
Instruction("MOVE", 3),
Instruction("SENSE", 4),
Instruction("PROBE", 5),
Instruction("PICKUP", 6),
Instruction("CARRYING", 7),
Instruction("DROP", 8),
Instruction("SMELL", 9),
Instruction("SNIFF", 0xa),
Instruction("MARK", 0xb),
Instruction("SET", 0x10),
Instruction("AND", 0x11),
Instruction("OR", 0x12),
Instruction("XOR", 0x13),
Instruction("LSHIFT", 0x14),
Instruction("RSHIFT", 0x15),
Instruction("ADD", 0x16),
Instruction("SUB", 0x17),
Instruction("MUL", 0x18),
Instruction("DIV", 0x19),
Instruction("MOD", 0x1a),
Instruction("RANDOM", 0x1b),
Instruction("SJR", 0x20), # New 'hidden' instruction to support jmp instructions
Instruction("MJR", 0x21), # New 'hidden' instruction to support jmp instruction from reg
Instruction("JMP", 0x22, True),
Instruction("CALL", 0x23, True),
Instruction("JEQ", 0x24, True),
Instruction("JNE", 0x25, True),
Instruction("JGT", 0x26, True),
Instruction("JLT", 0x27, True)
]
self.instructions = {i.name:i for i in instructions}
self.constants = {
"r0": 0x80,
"r1": 0x81,
"r2": 0x82,
"r3": 0x83,
"r4": 0x84,
"r5": 0x85,
"r6": 0x86,
"r7": 0x87,
"jr": 0x88,
"empty": 0,
"wall": 1,
"food": 2,
"nest": 3,
"ant": 4,
"here": 0,
"n": 1,
"e": 2,
"s": 3,
"w": 4,
"north": 1,
"east": 2,
"south": 3,
"west": 4,
"random": 5,
"ch_red": 0,
"ch_blue": 1,
"ch_green": 2,
"ch_yellow": 3,
}
def write(self, bt):
if type(bt) == int:
self.prog += bytes([bt])
else:
self.prog += bt
#print(self.prog.hex())
def handle_label(self, iname):
lblname = iname[:-1].lower()
if lblname in self.labels:
raise AssemblerError(f"Label {lblname} defined twice")
self.labels[lblname] = len(self.prog) // 4
def handle_directive(self, iname, args):
if iname in ['.alias', '.const', '.tag']:
source = args[0] if iname == '.tag' else args[1]
dest = args[1].lower() if iname == '.tag' else args[0].lower()
self.logger.debug(f"{iname} {dest} = {self.resolve_arg(source)}")
if dest in self.constants:
raise AssemblerError(f"Overwriting const {dest}")
self.constants[dest] = self.resolve_arg(source)
else:
raise AssemblerError(f"Invalid directive {iname}")
def resolve_arg(self, arg, raise_if_fail=True):
arg = arg.lower()
if arg in self.constants:
result = self.constants[arg]
elif arg.isnumeric():
result = int(arg)
elif arg.startswith("0x"):
result = int(arg, 16)
elif raise_if_fail:
raise AssemblerError(f"Cant parse argument {arg}")
else:
result = 0
if result > 255:
raise AssemblerError(f"Arg {arg} value bigger than 255")
return result
def handle_load_jr(self, instruction, target):
target_reg = self.resolve_arg(target, raise_if_fail=False)
if instruction.name == "JMP" and target_reg in range(0x80, 0x88):
mjr = self.instructions["MJR"]
data = [mjr.number, target_reg, 0, 0]
else:
sjr = self.instructions["SJR"]
data = [sjr.number, 0, 0, 0]
offset = len(self.prog) + 1
self.label_references.append((target, offset))
self.write(bytes(data))
def handle_instruction(self, iname, args):
instruction = self.instructions[iname.upper()]
if len(args) > 3:
raise AssemblerError("Too many arguments")
data = [instruction.number]
if instruction.load_jr:
target = args[-1]
self.handle_load_jr(instruction, target)
args = args[:-1]
for i in range(3):
if i >= len(args):
data.append(0)
else:
data.append(self.resolve_arg(args[i]))
self.write(bytes(data))
def assemble_line(self, line):
pre_offset = len(self.prog)
line = line.strip()
# remove comments
parts = line.split(";", 1)
if len(parts) == 2:
line = parts[0]
# split into parts
parts = line.split()
if len(parts) == 0:
return
iname = parts[0]
if iname.endswith(":"):
self.handle_label(iname)
elif iname.startswith("."):
self.handle_directive(iname, parts[1:])
elif iname.upper() in self.instructions:
self.handle_instruction(iname, parts[1:])
else:
raise AssemblerError(f"Instruction {iname} not found")
new_data = self.prog[pre_offset:]
self.logger.debug(f"{line.strip()} : {new_data.hex()}")
def resolve_references(self):
for reflabel, refoffset in self.label_references:
if not reflabel in self.labels:
raise AssemblerError(f"Could not resolve label {reflabel}")
lblloc = self.labels[reflabel]
self.prog[refoffset] = lblloc & 0xff
self.prog[refoffset + 1] = lblloc >> 8 & 0xff
self.prog[refoffset + 2] = lblloc >> 16 & 0xff
def assemble_text(self, data):
for line in data.split("\n"):
self.lineno += 1
self.assemble_line(line)
self.resolve_references()
return self.prog
def assemble_file(self, inf, outf):
with open(inf) as asm:
assy = asm.read()
assy = "JMP main\n" + assy
prog = self.assemble_text(assy)
output = sys.stdout.buffer if outf is None else open(outf, 'wb')
output.write(pack("<I", len(prog) >> 2)) # This is the header of the file: num_instructions
output.write(prog)
if output != sys.stdout:
output.close()
if __name__ == '__main__':
assembler = Assembler()
parser = argparse.ArgumentParser(description='Assemble a program.')
parser.add_argument('file', help='File to assemble')
parser.add_argument('--output', metavar='FILE',
help='Output file (stdout if ommitted)')
parser.add_argument('--debug', '-d', action='store_true')
args = parser.parse_args()
logging.basicConfig(level=logging.DEBUG if args.debug else logging.INFO)
try:
assembler.assemble_file(args.file, args.output)
except AssemblerError as e:
sys.stderr.write(f"Error at line: {assembler.lineno}: {e}\n")
sys.exit(1)

11
asm/test.ant Normal file
View File

@@ -0,0 +1,11 @@
SET r5 0xaa
main:
ID r0
SET r5 0x55
ADD r5 0x5
RSHIFT r5 4
LSHIFT r5 4
RANDOM r1 0x1f
RANDOM r2 10
SNIFF CH_RED
MOVE RANDOM

135
asm/twotrail.ant Normal file
View File

@@ -0,0 +1,135 @@
; ── SWARM FORAGER ────────────────────────────
; Random walk to find food, dead reckoning to
; navigate home. Pheromone marks food for others.
; Hit play, then try improving it!
; ── Registers ──
.alias dx r1 ; x displacement from nest
.alias dy r2 ; y displacement from nest
.alias ret r3 ; return address for CALL
; ── Pheromone ──
.const PH_FOOD CH_RED ; marks food locations
.const PH_NEST CH_BLUE
.const PHER_INIT 255
.const PHER_DECAY 2
; ── Roles (visible in simulation viewer) ──
.tag 0 foraging
.tag 1 homing
; ──────────────────────────────────────────────
main:
set r7 PHER_INIT
loop:
CARRYING
JNE r0 0 go_home
TAG foraging
; ── SEARCH: find food ─────────────────────────
search:
SENSE FOOD
JNE r0 0 grab
; if we are too far from the food, dont mark
JLT r7 PHER_DECAY smell_trail
SUB r7 PHER_DECAY
; if there is already a trail here, dont mark
SNIFF PH_NEST HERE r0
JNE r0 0 smell_trail
MARK PH_NEST r7
smell_trail:
SMELL PH_FOOD
JNE r0 0 step
SENSE EMPTY
step:
CALL ret move_track
JMP loop
grab:
CALL ret move_track
PICKUP
MARK PH_FOOD 255
SET r6 PHER_INIT
JMP loop
; ── GO HOME: dead reckon toward nest ──────────
go_home:
TAG homing
; if we are too far from the food, dont mark
JLT r6 PHER_DECAY sense_nest
SUB r6 PHER_DECAY
; if there is already a trail here, dont mark
SNIFF PH_FOOD HERE r0
JNE r0 0 sense_nest
MARK PH_FOOD r6
sense_nest:
SENSE NEST
JNE r0 0 deliver
smell_nest:
SMELL PH_NEST
JNE r0 0 move_track
RANDOM r0 4
JEQ r0 0 wander ; 25% random to unstick walls
CALL ret home_dir
CALL ret move_track
JMP main
wander:
SENSE EMPTY
CALL ret move_track
JMP main
deliver:
CALL ret move_track
DROP
SET r7 PHER_INIT
JMP main
; ══════════════════════════════════════════════
; SUBROUTINES — CALL ret <label>, return JMP ret
; ══════════════════════════════════════════════
; home_dir: set r0 = direction toward nest
home_dir:
SET r4 dx
JGT dx 0 hd_dxp
SET r4 0
SUB r4 dx
hd_dxp:
SET r5 dy
JGT dy 0 hd_dyp
SET r5 0
SUB r5 dy
hd_dyp:
JGT r5 r4 hd_y
JGT dx 0 hd_w
SET r0 2
JMP ret
hd_w:
SET r0 4
JMP ret
hd_y:
JGT dy 0 hd_n
SET r0 3
JMP ret
hd_n:
SET r0 1
JMP ret
; move_track: MOVE r0, then update dx/dy
move_track:
MOVE r0
JEQ r0 1 mt_n
JEQ r0 2 mt_e
JEQ r0 3 mt_s
SUB dx 1
JMP ret
mt_n:
SUB dy 1
JMP ret
mt_e:
ADD dx 1
JMP ret
mt_s:
ADD dy 1
JMP ret

7
dev.sh Executable file
View File

@@ -0,0 +1,7 @@
#!/bin/bash
echo "Building image..."
pushd farm
docker build -qt ants-dev .
popd
echo "Dropping into shell..."
docker run --rm -ti -v $PWD/data:/data -v $PWD/farm:/home/dev/farm ants-dev

6
farm/Dockerfile Normal file
View File

@@ -0,0 +1,6 @@
FROM debian
RUN apt update && apt install -y curl vim wget xz-utils build-essential less
RUN useradd -m dev
USER dev
WORKDIR /home/dev/farm
ADD . /home/dev/farm

194
farm/Makefile Normal file
View File

@@ -0,0 +1,194 @@
#### PROJECT SETTINGS ####
# The name of the executable to be created
BIN_NAME := farm
# Compiler used
CC ?= gcc
# Extension of source files used in the project
SRC_EXT = c
# Path to the source directory, relative to the makefile
SRC_PATH = .
# Space-separated pkg-config libraries used by this project
LIBS =
# General compiler flags
COMPILE_FLAGS = -std=c99 -Wall -Wextra -g
# Additional release-specific flags
RCOMPILE_FLAGS = -D NDEBUG
# Additional debug-specific flags
DCOMPILE_FLAGS = -D DEBUG
# Add additional include paths
INCLUDES = -I $(SRC_PATH)
# General linker settings
LINK_FLAGS =
# Additional release-specific linker settings
RLINK_FLAGS =
# Additional debug-specific linker settings
DLINK_FLAGS =
# Destination directory, like a jail or mounted system
DESTDIR = /
# Install path (bin/ is appended automatically)
INSTALL_PREFIX = usr/local
#### END PROJECT SETTINGS ####
# Optionally you may move the section above to a separate config.mk file, and
# uncomment the line below
# include config.mk
# Generally should not need to edit below this line
# Obtains the OS type, either 'Darwin' (OS X) or 'Linux'
UNAME_S:=$(shell uname -s)
# Function used to check variables. Use on the command line:
# make print-VARNAME
# Useful for debugging and adding features
print-%: ; @echo $*=$($*)
# Shell used in this makefile
# bash is used for 'echo -en'
SHELL = /bin/bash
# Clear built-in rules
.SUFFIXES:
# Programs for installation
INSTALL = install
INSTALL_PROGRAM = $(INSTALL)
INSTALL_DATA = $(INSTALL) -m 644
# Append pkg-config specific libraries if need be
ifneq ($(LIBS),)
COMPILE_FLAGS += $(shell pkg-config --cflags $(LIBS))
LINK_FLAGS += $(shell pkg-config --libs $(LIBS))
endif
# Verbose option, to output compile and link commands
export V := false
export CMD_PREFIX := @
ifeq ($(V),true)
CMD_PREFIX :=
endif
# Combine compiler and linker flags
release: export CFLAGS := $(CFLAGS) $(COMPILE_FLAGS) $(RCOMPILE_FLAGS)
release: export LDFLAGS := $(LDFLAGS) $(LINK_FLAGS) $(RLINK_FLAGS)
debug: export CFLAGS := $(CFLAGS) $(COMPILE_FLAGS) $(DCOMPILE_FLAGS)
debug: export LDFLAGS := $(LDFLAGS) $(LINK_FLAGS) $(DLINK_FLAGS)
# Build and output paths
release: export BUILD_PATH := build/release
release: export BIN_PATH := bin/release
debug: export BUILD_PATH := build/debug
debug: export BIN_PATH := bin/debug
install: export BIN_PATH := bin/release
# Find all source files in the source directory, sorted by most
# recently modified
ifeq ($(UNAME_S),Darwin)
SOURCES = $(shell find $(SRC_PATH) -name '*.$(SRC_EXT)' | sort -k 1nr | cut -f2-)
else
SOURCES = $(shell find $(SRC_PATH) -name '*.$(SRC_EXT)' -printf '%T@\t%p\n' \
| sort -k 1nr | cut -f2-)
endif
# fallback in case the above fails
rwildcard = $(foreach d, $(wildcard $1*), $(call rwildcard,$d/,$2) \
$(filter $(subst *,%,$2), $d))
ifeq ($(SOURCES),)
SOURCES := $(call rwildcard, $(SRC_PATH), *.$(SRC_EXT))
endif
# Set the object file names, with the source directory stripped
# from the path, and the build path prepended in its place
OBJECTS = $(SOURCES:$(SRC_PATH)/%.$(SRC_EXT)=$(BUILD_PATH)/%.o)
# Set the dependency files that will be used to add header dependencies
DEPS = $(OBJECTS:.o=.d)
# Version macros
# Comment/remove this section to remove versioning
USE_VERSION := false
# If this isn't a git repo or the repo has no tags, git describe will return non-zero
ifeq ($(shell git describe > /dev/null 2>&1 ; echo $$?), 0)
USE_VERSION := true
VERSION := $(shell git describe --tags --long --dirty --always | \
sed 's/v\([0-9]*\)\.\([0-9]*\)\.\([0-9]*\)-\?.*-\([0-9]*\)-\(.*\)/\1 \2 \3 \4 \5/g')
VERSION_MAJOR := $(word 1, $(VERSION))
VERSION_MINOR := $(word 2, $(VERSION))
VERSION_PATCH := $(word 3, $(VERSION))
VERSION_REVISION := $(word 4, $(VERSION))
VERSION_HASH := $(word 5, $(VERSION))
VERSION_STRING := \
"$(VERSION_MAJOR).$(VERSION_MINOR).$(VERSION_PATCH).$(VERSION_REVISION)-$(VERSION_HASH)"
override CFLAGS := $(CFLAGS) \
-D VERSION_MAJOR=$(VERSION_MAJOR) \
-D VERSION_MINOR=$(VERSION_MINOR) \
-D VERSION_PATCH=$(VERSION_PATCH) \
-D VERSION_REVISION=$(VERSION_REVISION) \
-D VERSION_HASH=\"$(VERSION_HASH)\"
endif
# Standard, non-optimized release build
.PHONY: release
release: dirs
ifeq ($(USE_VERSION), true)
@echo "Beginning release build v$(VERSION_STRING)"
else
@echo "Beginning release build"
endif
@$(MAKE) all --no-print-directory
# Debug build for gdb debugging
.PHONY: debug
debug: dirs
ifeq ($(USE_VERSION), true)
@echo "Beginning debug build v$(VERSION_STRING)"
else
@echo "Beginning debug build"
endif
@$(MAKE) all --no-print-directory
# Create the directories used in the build
.PHONY: dirs
dirs:
@echo "Creating directories"
@mkdir -p $(dir $(OBJECTS))
@mkdir -p $(BIN_PATH)
# Installs to the set path
.PHONY: install
install:
@echo "Installing to $(DESTDIR)$(INSTALL_PREFIX)/bin"
@$(INSTALL_PROGRAM) $(BIN_PATH)/$(BIN_NAME) $(DESTDIR)$(INSTALL_PREFIX)/bin
# Uninstalls the program
.PHONY: uninstall
uninstall:
@echo "Removing $(DESTDIR)$(INSTALL_PREFIX)/bin/$(BIN_NAME)"
@$(RM) $(DESTDIR)$(INSTALL_PREFIX)/bin/$(BIN_NAME)
# Removes all build files
.PHONY: clean
clean:
@echo "Deleting $(BIN_NAME) symlink"
@$(RM) $(BIN_NAME)
@echo "Deleting directories"
@$(RM) -r build
@$(RM) -r bin
# Main rule, checks the executable and symlinks to the output
all: $(BIN_PATH)/$(BIN_NAME)
@echo "Making symlink: $(BIN_NAME) -> $<"
@$(RM) $(BIN_NAME)
@ln -s $(BIN_PATH)/$(BIN_NAME) $(BIN_NAME)
# Link the executable
$(BIN_PATH)/$(BIN_NAME): $(OBJECTS)
@echo "Linking: $@"
$(CMD_PREFIX)$(CC) $(OBJECTS) $(LDFLAGS) -o $@
# Add dependency files, if they exist
-include $(DEPS)
# Source file rules
# After the first compilation they will be joined with the rules from the
# dependency files to provide header dependencies
$(BUILD_PATH)/%.o: $(SRC_PATH)/%.$(SRC_EXT)
@echo "Compiling: $< -> $@"
$(CMD_PREFIX)$(CC) $(CFLAGS) $(INCLUDES) -MP -MMD -c $< -o $@

19
farm/ant.c Normal file
View File

@@ -0,0 +1,19 @@
#include "ant.h"
#include <stddef.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
void reset_ants(struct Ant* ants, unsigned short nest) {
memset(ants, 0, sizeof(struct Ant) * NUM_ANTS);
for (int i = 0; i < NUM_ANTS; i++) {
ants[i].id = i;
ants[i].cell = nest;
}
}
struct Ant* create_ants() {
int ants_size = sizeof(struct Ant) * NUM_ANTS;
struct Ant *ants = (struct Ant *)malloc(ants_size);
return ants;
}

25
farm/ant.h Normal file
View File

@@ -0,0 +1,25 @@
#ifdef DEBUG
#define NUM_ANTS 1
#else
#define NUM_ANTS 200
#endif
struct Ant {
unsigned char id;
unsigned int pc;
unsigned short cell;
unsigned char carrying;
int r0;
int r1;
int r2;
int r3;
int r4;
int r5;
int r6;
int r7;
int jr;
};
struct Ant* create_ants();
void reset_ants(struct Ant* ants, unsigned short nest);

392
farm/farm.c Normal file
View File

@@ -0,0 +1,392 @@
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "maps.h"
#include "program.h"
#include "ant.h"
#define TICKS 2000
#define MAX_OPS 64
#define NUM_CHANNELS 4
const char map_offsets[] = { 0, -MAP_WIDTH , 1, MAP_WIDTH, -1 };
#define MIN(X,Y) ((X) < (Y) ? (X) : (Y))
#define MAX(X,Y) ((X) > (Y) ? (X) : (Y))
#define cell_from(org, dir) (org) + map_offsets[(dir)]
#define reg_id(rid) (rid) < 8 ? (rid) : (rid) - 0x80
#define get_value(ant, v) (v) < 0x80 ? (v) : get_reg((ant), (v))
static inline unsigned int rnum(unsigned int* rstate) {
unsigned int e = *rstate + 0x9e3779b9;
*rstate = e;
e = (e ^ (e >> 16)) * 0x85ebca6b;
e = (e ^ (e >> 13)) * 0xc2b2ae35;
e = e ^ (e >> 16);
//e = e / 0x100000000;
return e;
}
// static inline unsigned int rnum(unsigned int* rstate) {
// return rand();
// }
static inline void set_reg(struct Ant* ant, unsigned char rid, unsigned int v) {
switch(reg_id(rid)) {
case 0:
ant->r0 = v;
break;
case 1:
ant->r1 = v;
break;
case 2:
ant->r2 = v;
break;
case 3:
ant->r3 = v;
break;
case 4:
ant->r4 = v;
break;
case 5:
ant->r5 = v;
break;
case 6:
ant->r6 = v;
break;
case 7:
ant->r7 = v;
break;
}
}
static inline unsigned int get_reg(struct Ant* ant, unsigned char rid) {
switch(reg_id(rid)) {
case 0:
return ant->r0;
case 1:
return ant->r1;
case 2:
return ant->r2;
case 3:
return ant->r3;
case 4:
return ant->r4;
case 5:
return ant->r5;
case 6:
return ant->r6;
case 7:
return ant->r7;
default:
return 0;
}
}
#ifdef DEBUG
#define debug(msg, ...) printf((msg), ##__VA_ARGS__)
#else
#define debug(msg, ...)
#endif
#define debug_ant(ant) debug("A%d @%d:%d R:%02x %02x %02x %02x %02x %02x %02x %02x\n", (ant)->id, (ant)->cell % MAP_WIDTH, (ant)->cell / MAP_WIDTH, (ant)->r0, (ant)->r1, (ant)->r2, (ant)->r3, (ant)->r4, (ant)->r5, (ant)->r6, (ant)->r7)
#define debug_ins(pc, ins) debug("I%02d %s %02x %02x %02x\n", pc, instruction_names[ins->op], ins->arg1, ins->arg2, ins->arg3)
int run_simulations(struct Maps* maps, struct Program* program) {
int ops;
int cost;
int score;
int dirs[4];
unsigned int rstate;
int cell;
int found_dirs = 0;
struct Map* map;
int v1, v2, v3, res;
struct Ant* ant;
unsigned int pc;
struct Instruction* ins;
struct Ant* ants = create_ants();
unsigned char food[NUM_CELLS];
unsigned char smells[NUM_CHANNELS][NUM_CELLS];
for (unsigned int mi = 0; mi < maps->num_maps; mi++) {
map = &maps->maps + mi;
debug("MAP %s\n", map->name);
memset(&smells, 0, NUM_CHANNELS * NUM_CELLS);
reset_ants(ants, map->nest);
score = 0;
rstate = 2;
for (int i = 0; i < NUM_CELLS; i++ ) {
food[i] = map->cells[i] == CELL_FOOD ? 1 : 0;
}
for (int t=0; t < TICKS; t++) {
for (int ai=0; ai < NUM_ANTS; ai++) {
ops = 0;
while (ops < MAX_OPS) {
cost = 1;
ant = &ants[ai];
pc = ant->pc;
ins = &program->instructions + pc;
debug_ins(pc, ins);
ant->pc = (pc + 1) % program->num_instructions;
switch(ins->op) {
case INS_ID:
set_reg(ant, ins->arg1, ant->id);
break;
case INS_TAG:
//noop
break;
case INS_MOVE:
v1 = get_value(ant, ins->arg1);
if (v1 == 5) {
v1 = rnum(&rstate) % 4 + 1;
}
cell = cell_from(ant->cell, v1);
if (cell > 0 && cell < NUM_CELLS && map->cells[cell] != CELL_WALL) {
ant->cell = cell;
}
cost += MAX_OPS;
break;
case INS_SENSE:
v1 = get_value(ant, ins->arg1);
v2 = ins->arg2;
found_dirs = 0;
if (map->cells[cell_from(ant->cell, 1)] == v1) dirs[found_dirs++] = 1;
if (map->cells[cell_from(ant->cell, 2)] == v1) dirs[found_dirs++] = 2;
if (map->cells[cell_from(ant->cell, 3)] == v1) dirs[found_dirs++] = 3;
if (map->cells[cell_from(ant->cell, 4)] == v1) dirs[found_dirs++] = 4;
if (found_dirs == 0) {
res = 0;
} else if (found_dirs == 1) {
res = dirs[0];
} else {
res = dirs[rnum(&rstate) % found_dirs];
}
set_reg(ant, v2, res);
break;
case INS_PROBE:
v1 = get_value(ant, ins->arg1);
v1 = cell_from(ant->cell, v1);
v1 = map->cells[v1];
set_reg(ant, ins->arg2, v1);
break;
case INS_PICKUP:
debug("PICKUP?\n");
if (!ant->carrying && food[ant->cell] > 0) {
ant->carrying = food[ant->cell]--;
debug("PICKUP!\n");
}
cost += MAX_OPS;
break;
case INS_CARRYING:
set_reg(ant, ins->arg1, ant->carrying);
break;
case INS_DROP:
debug("DROP?\n");
if (ant->carrying) {
ant -> carrying = 0;
if (map->nest == ant->cell){
debug("DROP SCORE\n");
score += 1;
} else {
food[ant->cell] += 1;
debug("DROP!\n");
}
}
cost += MAX_OPS;
break;
case INS_SMELL:
v1 = get_value(ant, ins->arg1);
v2 = ins->arg2;
res = 0;
v3 = smells[v1][cell_from(ant->cell, 1)];
if (v3 > res) found_dirs = 0;
if (v3 >= res) dirs[found_dirs++] = 1;
v3 = smells[v1][cell_from(ant->cell, 2)];
if (v3 > res) found_dirs = 0;
if (v3 >= res) dirs[found_dirs++] = 2;
v3 = smells[v1][cell_from(ant->cell, 3)];
if (v3 > res) found_dirs = 0;
if (v3 >= res) dirs[found_dirs++] = 3;
v3 = smells[v1][cell_from(ant->cell, 4)];
if (v3 > res) found_dirs = 0;
if (v3 >= res) dirs[found_dirs++] = 4;
if (res == 0) {
res = (rnum(&rstate) % 4) + 1;
}else if (found_dirs == 1) {
res = dirs[0];
} else {
res = dirs[rnum(&rstate) % found_dirs];
}
set_reg(ant, v2, res);
break;
case INS_SNIFF:
v1 = ins->arg1;
v2 = get_value(ant, ins->arg2);
v2 = cell_from(ant->cell, v2);
v1 = smells[v1][v2];
set_reg(ant, ins->arg3, v1);
break;
case INS_MARK:
v1 = get_value(ant, ins->arg1);
v2 = get_value(ant, ins->arg2);
v3 = smells[v1][ant->cell];
smells[v1][ant->cell] = MAX(v3+v2, 255);
break;
case INS_SET:
set_reg(ant, ins->arg1, ins->arg2);
break;
case INS_AND:
v1 = get_value(ant, ins->arg1);
v2 = get_value(ant, ins->arg2);
set_reg(ant, ins->arg1, v1 & v2);
break;
case INS_OR:
v1 = get_value(ant, ins->arg1);
v2 = get_value(ant, ins->arg2);
set_reg(ant, ins->arg1, v1 | v2);
break;
case INS_XOR:
v1 = get_value(ant, ins->arg1);
v2 = get_value(ant, ins->arg2);
set_reg(ant, ins->arg1, v1 | v2);
break;
case INS_LSHIFT:
v1 = get_value(ant, ins->arg1);
v2 = get_value(ant, ins->arg2);
set_reg(ant, ins->arg1, v1 << v2);
break;
case INS_RSHIFT:
v1 = get_value(ant, ins->arg1);
v2 = get_value(ant, ins->arg2);
set_reg(ant, ins->arg1, v1 >> v2);
break;
case INS_ADD:
v1 = get_value(ant, ins->arg1);
v2 = get_value(ant, ins->arg2);
set_reg(ant, ins->arg1, v1 + v2);
break;
case INS_SUB:
v1 = get_value(ant, ins->arg1);
v2 = get_value(ant, ins->arg2);
set_reg(ant, ins->arg1, v1 - v2);
break;
case INS_MUL:
v1 = get_value(ant, ins->arg1);
v2 = get_value(ant, ins->arg2);
set_reg(ant, ins->arg1, v1 * v2);
break;
case INS_DIV:
v1 = get_value(ant, ins->arg1);
v2 = get_value(ant, ins->arg2);
if (v2 != 0) {
set_reg(ant, ins->arg1, v1 / v2);
}
break;
case INS_MOD:
v1 = get_value(ant, ins->arg1);
v2 = get_value(ant, ins->arg2);
set_reg(ant, ins->arg1, v1 % v2);
break;
case INS_RANDOM:
v2 = get_value(ant, ins->arg2);
if (v2 != 0) {
v1 = rnum(&rstate) % v2;
set_reg(ant, ins->arg1, v1);
}
break;
case INS_SJR:
ant->jr = *((int*)ins) >> 8;
break;
case INS_MJR:
ant->jr = get_value(ant, ins->arg1);
break;
case INS_JMP:
ant->pc = ant->jr;
break;
case INS_CALL:
set_reg(ant, ins->arg1, ant->pc);
ant->pc = ant->jr;
break;
case INS_JEQ:
v1 = get_value(ant, ins->arg1);
v2 = get_value(ant, ins->arg2);
if (v1 == v2) {
ant->pc = ant->jr;
}
break;
case INS_JNE:
v1 = get_value(ant, ins->arg1);
v2 = get_value(ant, ins->arg2);
if (v1 != v2) {
ant->pc = ant->jr;
}
break;
case INS_JGT:
v1 = get_value(ant, ins->arg1);
v2 = get_value(ant, ins->arg2);
if (v1 > v2) {
ant->pc = ant->jr;
}
break;
case INS_JLT:
v1 = get_value(ant, ins->arg1);
v2 = get_value(ant, ins->arg2);
if (v1 < v2) {
ant->pc = ant->jr;
}
break;
};
debug_ant(ant);
ops += cost;
}
}
for (int pi = 0; pi < NUM_CHANNELS; pi++) {
for (int ci = 0; ci < NUM_CELLS; ci++) {
if (smells[pi][ci] > 0) smells[pi][ci]--;
}
}
}
}
return score;
}
int main(int argc, char* argv[])
{
struct Program* program = NULL;
struct Maps* maps = NULL;
int score = 0;
if (argc < 3) {
printf("Usage: farm <maps file> <program file>\n");
return 1;
}
char* maps_filename = argv[1];
char* program_filename = argv[2];
maps = load_maps(maps_filename);
program = load_program(program_filename);
int result = 0;
if (maps == NULL) {
printf("Could not load maps\n");
result = 2;
}else if (program == NULL) {
printf("Could not load program\n");
result = 3;
} else {
score = run_simulations(maps, program);
}
free(program);
free(maps);
printf("Score: %d\n", score);
return result;
}

26
farm/maps.c Normal file
View File

@@ -0,0 +1,26 @@
#include <stddef.h>
#include <stdlib.h>
#include <stdio.h>
#include "maps.h"
struct Maps* load_maps(char* filename) {
FILE* f = fopen(filename, "r");
if (f == NULL) {
return NULL;
}
int num_maps;
int num_read = fread(&num_maps, 4, 1, f);
if (num_read != 1 || num_maps > 10000) {
return NULL;
}
int mapfile_size = sizeof(struct Map) * num_maps;
struct Maps *maps = (struct Maps *)malloc(mapfile_size + 4);
maps->num_maps = num_maps;
num_read = fread(&maps->maps, 1, mapfile_size, f);
if (num_read != mapfile_size) {
return NULL;
}
fclose(f);
return maps;
}

23
farm/maps.h Normal file
View File

@@ -0,0 +1,23 @@
#define MAP_WIDTH 128
#define MAP_HEIGHT 128
#define NUM_CELLS MAP_WIDTH * MAP_HEIGHT
#define MAP_NAME_LENGTH 32
#define CELL_EMPTY 0
#define CELL_WALL 1
#define CELL_FOOD 2
#define CELL_NEST 3
struct Map {
char name[MAP_NAME_LENGTH];
unsigned short nest;
unsigned char cells[MAP_WIDTH * MAP_HEIGHT];
};
struct Maps {
unsigned int num_maps;
struct Map maps;
};
struct Maps* load_maps(char* filename);

24
farm/program.c Normal file
View File

@@ -0,0 +1,24 @@
#include "program.h"
#include <stddef.h>
#include <stdlib.h>
#include <stdio.h>
struct Program* load_program(char* filename) {
FILE* f = fopen(filename, "r");
if (f == NULL) {
return NULL;
}
int num_instructions;
int num_read = fread(&num_instructions, 4, 1, f);
if (num_read != 1) {
return NULL;
}
int program_size = 4 + num_instructions * 4;
struct Program* program = (struct Program *)malloc(program_size);
program->num_instructions = num_instructions;
num_read = fread(&program->instructions, 1, program_size-4, f);
if (num_read != program_size-4) {
return NULL;
}
fclose(f);
return program;
}

70
farm/program.h Normal file
View File

@@ -0,0 +1,70 @@
#define INS_NOOP 0
#define INS_ID 1
#define INS_TAG 2
#define INS_MOVE 3
#define INS_SENSE 4
#define INS_PROBE 5
#define INS_PICKUP 6
#define INS_CARRYING 7
#define INS_DROP 8
#define INS_SMELL 9
#define INS_SNIFF 0xa
#define INS_MARK 0xb
#define INS_SET 0x10
#define INS_AND 0x11
#define INS_OR 0x12
#define INS_XOR 0x13
#define INS_LSHIFT 0x14
#define INS_RSHIFT 0x15
#define INS_ADD 0x16
#define INS_SUB 0x17
#define INS_MUL 0x18
#define INS_DIV 0x19
#define INS_MOD 0x1a
#define INS_RANDOM 0x1b
#define INS_SJR 0x20
#define INS_MJR 0x21
#define INS_JMP 0x22
#define INS_CALL 0x23
#define INS_JEQ 0x24
#define INS_JNE 0x25
#define INS_JGT 0x26
#define INS_JLT 0x27
static char* instruction_names[256] = {
/* 0x00 */ "NOOP", "ID ", "TAG ", "MOVE", "SENS", "PROB", "PICK", "CARY", "DROP", "SMEL", "SNIF", "MARK", "?", "?", "?", "?",
/* 0x10 */ "SET ", "AND ", "OR ", "XOR ", "SHL ", "SHR ", "ADD ", "SUB ", "MUL ", "DIV ", "MOD", "RAND", "?", "?", "?", "?",
/* 0x20 */ "SJR ", "MJR ", "JMP ", "CALL", "JEQ", "JNE ", "JGT ", "JLT ", "?", "?", "?", "?", "?", "?", "?", "?",
/* 0x30 */ "?", "?", "?", "?", "?", "?", "?", "?", "?", "?", "?", "?", "?", "?", "?", "?",
/* 0x40 */ "?", "?", "?", "?", "?", "?", "?", "?", "?", "?", "?", "?", "?", "?", "?", "?",
/* 0x50 */ "?", "?", "?", "?", "?", "?", "?", "?", "?", "?", "?", "?", "?", "?", "?", "?",
/* 0x60 */ "?", "?", "?", "?", "?", "?", "?", "?", "?", "?", "?", "?", "?", "?", "?", "?",
/* 0x70 */ "?", "?", "?", "?", "?", "?", "?", "?", "?", "?", "?", "?", "?", "?", "?", "?",
/* 0x80 */ "?", "?", "?", "?", "?", "?", "?", "?", "?", "?", "?", "?", "?", "?", "?", "?",
/* 0x90 */ "?", "?", "?", "?", "?", "?", "?", "?", "?", "?", "?", "?", "?", "?", "?", "?",
/* 0xa0 */ "?", "?", "?", "?", "?", "?", "?", "?", "?", "?", "?", "?", "?", "?", "?", "?",
/* 0xb0 */ "?", "?", "?", "?", "?", "?", "?", "?", "?", "?", "?", "?", "?", "?", "?", "?",
/* 0xc0 */ "?", "?", "?", "?", "?", "?", "?", "?", "?", "?", "?", "?", "?", "?", "?", "?",
/* 0xd0 */ "?", "?", "?", "?", "?", "?", "?", "?", "?", "?", "?", "?", "?", "?", "?", "?",
/* 0xe0 */ "?", "?", "?", "?", "?", "?", "?", "?", "?", "?", "?", "?", "?", "?", "?", "?",
/* 0xf0 */ "?", "?", "?", "?", "?", "?", "?", "?", "?", "?", "?", "?", "?", "?", "?", "?",
};
struct Instruction {
unsigned char op;
unsigned char arg1;
unsigned char arg2;
unsigned char arg3;
};
struct Program {
unsigned int num_instructions;
struct Instruction instructions;
};
struct Program* load_program(char* filename);

9
generate_maps.sh Executable file
View File

@@ -0,0 +1,9 @@
#!/bin/bash
echo "Building image..."
pushd maps
docker build -qt ants-maps .
popd
echo "Generating maps..."
docker run --rm -ti -v $PWD/data:/maps ants-maps generate -n 120 -t open -o /maps/open.maps
docker run --rm -ti -v $PWD/data:/maps ants-maps generate -n 120 -t random -o /maps/random.maps
echo "DONE"

7
maps/Dockerfile Normal file
View File

@@ -0,0 +1,7 @@
FROM node
WORKDIR /app
ADD package.json* .
RUN npm install
ADD *.js /app
VOLUME /maps
ENTRYPOINT ["./maps.js"]

1065
maps/maps.js Executable file

File diff suppressed because it is too large Load Diff

20
maps/package-lock.json generated Normal file
View File

@@ -0,0 +1,20 @@
{
"name": "ants",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"dependencies": {
"commander": "^14.0.3"
}
},
"node_modules/commander": {
"version": "14.0.3",
"resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz",
"integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==",
"engines": {
"node": ">=20"
}
}
}
}

5
maps/package.json Normal file
View File

@@ -0,0 +1,5 @@
{
"dependencies": {
"commander": "^14.0.3"
}
}

63
readme.md Normal file
View File

@@ -0,0 +1,63 @@
# SWARM challenge
## The plan:
* Reproduce the maps on the website
* Create an assembler to generate bytecode. Add special instructions to define tweakable params and optional code blocks?
* Create an antfarm: a high-performance program that can score ant programs inside those maps
* Create a highly modular and customizable ant program
* Create a program that makes small random changes to ant programs
* BREED SUPERANTS
Contents of this repo:
* `/reference` contains reference materials describing and defining the challenge
* `/maps` contains scripts to generate and view maps (nodejs)
* `/asm` Assembles antssembly into my custom antbytecode
* `/farm` This runs the **ALIEN ANT FARM**
## Reference
The reference folder contains
* A sample ant program
* The reference found on the website
* A wget download of the relevant SPA
## Maps
As mentioned in the reference, there are 12 map generator functions.
* chambers
* bridge
* gauntlet
* islands
* open
* brush
* prairie
* field
* pockets
* fortress
* maze
* spiral
When looking in the browser, the generated maps have names like 'chambers-3lc8x4' or 'bridge-1u7xlw' where the string after the dash is the seed used, encoded in base36.
* `nodejs maps.js generate -n 120 -o mymaps.maps` generates 120 maps and stores them in mymaps.maps
* `nodejs maps.js view bridge-1u7xlw` renders the indicated map to ascii
## Asm
The assembler is not copied from the challenge, but a new implementation.
It has a couple of deviations from the spec:
* Literals can only be 0-255, non negative. This is done to simplify the bytecode somewhat. I do not believe i will need literals outside this range (famous last words). This is only the literals in the assembly. The actual registers are full 32bit.
* Constants, tags and aliases all end up in the same pool, and are case insensitive
* JMP main is always prepended to every program
The bytecode is quite simple. Every instruction is 4 bytes. The first byte is the instruction number, the other 3 are single-byte arguments.
Jump instructions are a special case. They need to specify a target instruction offset, and the conditional jumps also take two normal arguments. This does not fit into 4 bytes. To accommodate this, our cpu introduces a 9th register called JR and 2 'hidden instructions' called SJR and MJR. Any jump instruction is assembled into 2 bytecode instructions: first SJR or MJR and then the original instruction. SJR (Set JR) uses all 3 argument bytes as a single unsigned little endian int to describe the offset. MJR (Move JR) copies the value from a register into JR. The jump instructions simply use JR as the destination address
## FARM
Some would say this is where the magic happens.
This is a program that reads the maps file (generated by the maps script), and an apm file (ant program, generated by the assembler) and starts breeding ants!

110
reference/example.ant Normal file
View File

@@ -0,0 +1,110 @@
; ── SWARM FORAGER ────────────────────────────
; Random walk to find food, dead reckoning to
; navigate home. Pheromone marks food for others.
; Hit play, then try improving it!
; ── Registers ──
.alias dx r1 ; x displacement from nest
.alias dy r2 ; y displacement from nest
.alias ret r3 ; return address for CALL
; ── Pheromone ──
.const TRAIL CH_RED ; marks food locations
; ── Roles (visible in simulation viewer) ──
.tag 0 foraging
.tag 1 homing
; ──────────────────────────────────────────────
main:
CARRYING
JNE r0 0 go_home
TAG foraging
; ── SEARCH: find food ─────────────────────────
search:
SENSE FOOD
JNE r0 0 grab
SMELL TRAIL
JNE r0 0 step
RANDOM r0 4
ADD r0 1
step:
CALL ret move_track
JMP main
grab:
CALL ret move_track
PICKUP
MARK TRAIL 100
JMP main
; ── GO HOME: dead reckon toward nest ──────────
go_home:
TAG homing
SENSE NEST
JNE r0 0 deliver
RANDOM r0 4
JEQ r0 0 wander ; 25% random to unstick walls
CALL ret home_dir
CALL ret move_track
JMP main
wander:
RANDOM r0 4
ADD r0 1
CALL ret move_track
JMP main
deliver:
CALL ret move_track
DROP
JMP main
; ══════════════════════════════════════════════
; SUBROUTINES — CALL ret <label>, return JMP ret
; ══════════════════════════════════════════════
; home_dir: set r0 = direction toward nest
home_dir:
SET r4 dx
JGT dx 0 hd_dxp
SET r4 0
SUB r4 dx
hd_dxp:
SET r5 dy
JGT dy 0 hd_dyp
SET r5 0
SUB r5 dy
hd_dyp:
JGT r5 r4 hd_y
JGT dx 0 hd_w
SET r0 2
JMP ret
hd_w:
SET r0 4
JMP ret
hd_y:
JGT dy 0 hd_n
SET r0 3
JMP ret
hd_n:
SET r0 1
JMP ret
; move_track: MOVE r0, then update dx/dy
move_track:
MOVE r0
JEQ r0 1 mt_n
JEQ r0 2 mt_e
JEQ r0 3 mt_s
SUB dx 1
JMP ret
mt_n:
SUB dy 1
JMP ret
mt_e:
ADD dx 1
JMP ret
mt_s:
ADD dy 1
JMP ret

2
reference/fetch_site.sh Normal file
View File

@@ -0,0 +1,2 @@
#!/bin/bash
wget -k -np -p -H --adjust-extension https://dev.moment.com/swarm

19
reference/random.js Normal file
View File

@@ -0,0 +1,19 @@
next() {
let e = 0 | (this.state += 0x9e3779b9);
return (
(
(
(
e = Math.imul(
(
e = Math.imul(
e ^ (e >>> 16), 0x85ebca6b
)
) ^ (e >>> 13),
0xc2b2ae35,
)
) ^ (e >>> 16)
) >>> 0
) / 0x100000000
);
}

235
reference/reference.txt Normal file
View File

@@ -0,0 +1,235 @@
SWARM — Ant Colony Optimization Challenge
200 ants share one program on a 128x128 grid.
Find food, bring it to the nest. Communicate
only through pheromone trails. Score across 120
maps. Hit play to see the default code run,
then improve it.
─────────────────────────────────────────────
QUICK START
SENSE FOOD r1 ; scan 4 adjacent cells for food
MOVE r1 ; walk toward it
PICKUP ; grab one unit
SENSE NEST r1 ; find the way home
MOVE r1 ; walk toward nest
DROP ; deliver (scores if at nest)
─────────────────────────────────────────────
LANGUAGE
Antssembly is like assembly. The program
counter persists across ticks — your ant's
"brain" is a continuous loop, not restarted
each tick.
DIRECTIVES
.alias dir r1 ; name a register
.alias trail r2
.const FOOD CH_RED ; named constant
.tag 0 forager ; name a tag (see TAGS)
.tag 1 scout
.tag names double as constants, so you can
write TAG forager instead of TAG 0.
LABELS
search:
SENSE FOOD dir
JEQ dir 0 wander
MOVE dir
PICKUP
JMP search
wander:
MOVE RANDOM
ENTRYPOINT
If a main: label exists and isn't at the
top, a JMP main is auto-prepended.
REGISTERS r0-r7, 8 general purpose.
Signed 32-bit integers, init to 0.
Overflow wraps (32-bit signed).
COMMENTS ; everything after semicolon.
─────────────────────────────────────────────
SENSING
All sensing ops take an optional dest register
(defaults to r0 if omitted).
SENSE <target> [reg] scan 4 adjacent cells
targets: EMPTY=0 WALL=1 FOOD=2 NEST=3
ANT=4 (or register)
returns direction (N=1 E=2 S=3 W=4), 0=none
ties broken randomly
SMELL <ch> [reg] strongest pheromone dir
ties broken randomly
SNIFF <ch> <dir> [reg] intensity 0-255
dir: HERE N E S W RANDOM (or register)
PROBE <dir> [reg] cell type at direction
returns 0=EMPTY 1=WALL 2=FOOD 3=NEST
CARRYING [reg] 1 if holding food
ID [reg] ant index (0-199)
─────────────────────────────────────────────
ACTIONS — end the ant's tick
MOVE <dir> N/E/S/W, RANDOM, or register
PICKUP pick up 1 food from current cell
DROP drop food (scores if at nest)
Each ant carries at most 1 food. Multiple ants
can share a cell (no collisions). Each tick runs
until an action or the 64-op limit. Then the next
ant goes. After all 200 ants act, pheromones decay.
─────────────────────────────────────────────
PHEROMONES — 4 channels, intensity 0-255
CH_RED CH_BLUE CH_GREEN CH_YELLOW
Toggle R/B/G/Y in the viewer to see them.
MARK <ch> <amount> add to pheromone on this cell
(additive, capped at 255)
Trail pattern:
MARK CH_RED 100 ; add to breadcrumb
SMELL CH_RED r2 ; follow gradient
MOVE r2
─────────────────────────────────────────────
ARITHMETIC
SET r1 5 ADD r1 1 SUB r1 1
MOD r1 4 MUL r1 3 DIV r1 2
AND r1 0xFF OR r1 1 XOR r1 r2
LSHIFT r1 4 RSHIFT r1 4 RANDOM r1 4
Second operand can be a register or literal.
DIV truncates toward zero. MOD is always
non-negative. Both are no-ops if divisor=0.
SHR is arithmetic (sign-preserving).
RANDOM r1 N sets r1 to a value in [0, N).
─────────────────────────────────────────────
CONTROL FLOW
JMP <label> unconditional
JMP <reg> indirect (jump to address in reg)
CALL <reg> <label> save return addr in reg, jump
JEQ <a> <b> <label> jump if a == b
JNE <a> <b> <label> jump if a != b
JGT <a> <b> <label> jump if a > b
JLT <a> <b> <label> jump if a < b
Function call pattern:
CALL r7 my_func ; save return addr, jump
; ...returns here...
my_func:
; do work
JMP r7 ; return to caller
─────────────────────────────────────────────
TAGS — role heatmaps for debugging
TAG <value> set ant tag 0-7 (ZERO COST)
.tag 0 forager name tags for viewer toggles
.tag 1 scout then use: TAG forager
Toggle tag heatmaps in the viewer to see
where each role's ants have walked. Useful
for visualizing role specialization:
ID r3
MOD r3 4
TAG r3 ; 4 roles, 4 heatmaps
─────────────────────────────────────────────
THE GRID
EMPTY passable WALL impassable
FOOD 1-8 units NEST deliver here
Directions: N E S W (or NORTH EAST SOUTH WEST)
RANDOM = random cardinal direction each call
─────────────────────────────────────────────
MAP TYPES — 12 procedural generators
Your score is averaged across 120 maps drawn
from these types, seeded deterministically.
open No internal walls. Random food
clusters scattered around the nest.
maze Wide-corridor maze. 2-wide passages,
2-wide walls.
spiral Concentric wavy ring walls with wide
random gaps.
field Nearly open, a few lazy curvy walls.
bridge A vertical wall splits the map with
2-4 narrow crossings. All food is
on the far side from the nest.
gauntlet Nest far left, food far right.
Staggered vertical walls with gaps.
pockets Circular walled cells with narrow
entrances and food inside.
fortress Nest cornered behind wavy concentric
walls with gates. Food is deep
inside the fortress.
islands Rooms separated by walls with one
doorway between adjacent rooms.
chambers Rooms carved in rock connected by
narrow corridors.
prairie Food everywhere at varying density,
no blobs.
brush Dense random wall clutter throughout.
Food in medium clusters.
─────────────────────────────────────────────
SCORING
Each map: food delivered / food available.
Score = average ratio across 120 maps * 1000.
Deterministic — same code, same score.
─────────────────────────────────────────────
TIPS
1. Read the default code — it's a complete
two-trail forager with comments.
2. SENSE/SMELL return directions — pass to MOVE
3. MARK when carrying, SMELL when searching
4. ID + MOD for role specialization
5. SNIFF reads exact intensity for smarter
routing (e.g. pick strongest neighbor)
6. .alias and .tag cost nothing — use them