From 9616a8236bded5d687985ff4b6b06ef041b94a08 Mon Sep 17 00:00:00 2001 From: Richard Date: Thu, 26 Mar 2026 18:43:01 +0100 Subject: [PATCH] initial --- .gitignore | 7 + analyze/trace_parser.py | 60 +++ asm/asm.py | 239 +++++++++ asm/test.ant | 11 + asm/twotrail.ant | 135 +++++ dev.sh | 7 + farm/Dockerfile | 6 + farm/Makefile | 194 +++++++ farm/ant.c | 19 + farm/ant.h | 25 + farm/farm.c | 392 ++++++++++++++ farm/maps.c | 26 + farm/maps.h | 23 + farm/program.c | 24 + farm/program.h | 70 +++ generate_maps.sh | 9 + maps/Dockerfile | 7 + maps/maps.js | 1065 +++++++++++++++++++++++++++++++++++++++ maps/package-lock.json | 20 + maps/package.json | 5 + readme.md | 63 +++ reference/example.ant | 110 ++++ reference/fetch_site.sh | 2 + reference/random.js | 19 + reference/reference.txt | 235 +++++++++ 25 files changed, 2773 insertions(+) create mode 100644 .gitignore create mode 100644 analyze/trace_parser.py create mode 100755 asm/asm.py create mode 100644 asm/test.ant create mode 100644 asm/twotrail.ant create mode 100755 dev.sh create mode 100644 farm/Dockerfile create mode 100644 farm/Makefile create mode 100644 farm/ant.c create mode 100644 farm/ant.h create mode 100644 farm/farm.c create mode 100644 farm/maps.c create mode 100644 farm/maps.h create mode 100644 farm/program.c create mode 100644 farm/program.h create mode 100755 generate_maps.sh create mode 100644 maps/Dockerfile create mode 100755 maps/maps.js create mode 100644 maps/package-lock.json create mode 100644 maps/package.json create mode 100644 readme.md create mode 100644 reference/example.ant create mode 100644 reference/fetch_site.sh create mode 100644 reference/random.js create mode 100644 reference/reference.txt diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ed37a16 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +node_modules +data +reference/dev.moment.com +farm/core* +farm/build +farm/bin +farm/farm diff --git a/analyze/trace_parser.py b/analyze/trace_parser.py new file mode 100644 index 0000000..1cc4130 --- /dev/null +++ b/analyze/trace_parser.py @@ -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) diff --git a/asm/asm.py b/asm/asm.py new file mode 100755 index 0000000..9259bb6 --- /dev/null +++ b/asm/asm.py @@ -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("> 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) diff --git a/asm/test.ant b/asm/test.ant new file mode 100644 index 0000000..a436573 --- /dev/null +++ b/asm/test.ant @@ -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 diff --git a/asm/twotrail.ant b/asm/twotrail.ant new file mode 100644 index 0000000..2028daf --- /dev/null +++ b/asm/twotrail.ant @@ -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