Colonymap is a Rust implementation of an ant-invasion simulation on a grid-like map of colonies. It parses a custom map format describing colonies and their tunnels, provides tools to query the map (neighbors and shortest paths), validates map consistency, and runs a simulation of ants moving through the colonies until they collide and destroy colonies. The program offers a command-line interface with multiple subcommands to inspect or simulate the colony map.
- Map Parsing – Reads a text file of colony definitions (each line:
Name north=... south=... east=... west=...) and builds an internal graph of colonies and tunnels. It gracefully handles format errors (e.g. duplicate definitions or invalid directions). - Neighbor Lookup – Given a colony name, it lists all neighboring colonies by direction (north, south, east, west).
- Shortest Path – Computes the shortest path (in number of hops) between two colonies using BFS, returning a sequence of colony names if a path exists.
- Map Validation – Verifies that all tunnel links are reciprocal (if
A north=Bexists, thenB south=Ashould exist). Reports any one-way or broken links. - Ant Simulation – Simulates
Nants walking randomly through the colony map. When two or more ants end up in the same colony at the same time, they fight, killing each other and destroying the colony (removing it from the map). The simulation runs until all ants are dead or a maximum number of steps is reached. It prints a log of colony destructions and outputs the final state of the colony map. - Built‑in Demo – A one‑command demo (
colonymap demo) that runs on the embedded medium example map; no input file required.
Run the demo without any files:
# Random (secure) seed by default
colonymap demo --ants 100For a deterministic demo:
colonymap demo --ants 100 --seed 42The input map is parsed into a ColonyMap structure. Each colony has a unique name and up to four neighbors (one in each direction). For example:
Kara north=Omrida south=Celles east=Chronoskis west=Larvonthi
Omrida south=Kara
Celles north=Kara
Here, Kara connects north to Omrida, south to Celles, etc. The parser ensures each colony name is unique and each line is well-formed (no duplicate directions on the same line, all directions valid). Neighbors are stored in a map keyed by direction for easy lookup. After parsing, the optional validation step (verify_bidirectional) can check for any missing reciprocal links or references to undefined colonies.
For efficient simulation, the parsed map is compiled into a compact graph representation. This assigns each colony a numeric ID (0, 1, 2, ...) by sorting colony names (to keep IDs stable and deterministic across runs). The compact graph (Compact) contains:
- A vector of colony names (for ID-to-name lookups when printing results).
- An array of
CompactColonystructures, indexed by colony ID. EachCompactColonystores:aliveflag – whether the colony is still active (not destroyed).nbrs[4]– direct neighbor IDs in fixed order (index 0 = north, 1 = south, 2 = east, 3 = west, using a sentinel value if no neighbor in that direction).deg(degree) – the number of active neighbors.dense[0..deg-1]– a dense list of neighbor IDs for quick random selection. This list contains exactly the neighbors that are currently alive, packed at the start of the array.
Using numeric colony IDs and fixed-size neighbor lists lays the groundwork for a high-performance simulation loop by enabling constant-time lookups and memory-contiguous storage (no string keys or dynamic lookups in the hot path).
The simulation follows the rules of the ant invasion scenario:
- Initialization: We spawn
Nants at random colonies. The random placement uses a uniform distribution over all colonies. Multiple ants can start in the same colony – if that happens, those ants immediately collide. Such initial collisions are resolved before the first movement step (destroying the colony and killing those ants, per the rules). - Each Iteration (Tick):
- Movement: Every remaining ant chooses a random direction to move. If the colony has neighbors, the ant moves to one of the neighboring colonies chosen uniformly at random. If a colony has no neighbors (or all its tunnels lead to destroyed colonies), the ant stays in place for that tick. (If the colony itself is destroyed at that moment, the ant cannot move out — it’s destroyed along with the colony and removed from the simulation.)
- Collision Resolution: After all ants have decided their target colonies for this tick, we check for any collisions. A collision occurs if two or more ants land in the same colony. In that case, those ants fight and die, and the colony is destroyed. When a colony is destroyed, it is removed from the map:
- We mark the colony as dead (so no future ant can occupy or pass through it).
- We remove all tunnels leading from or to that colony. In practice, for each neighbor of the destroyed colony, we update the neighbor’s corresponding direction to indicate no tunnel (and adjust its neighbor list).
- A descriptive log message is printed: e.g.,
X has been destroyed by ant 42 and ant 7!(listing all ants involved in the collision by their IDs).
- Cleanup: Ants that died in collisions are removed from the simulation. The iteration count increases.
- Termination: The simulation repeats ticks until one of the termination conditions is met: either all ants have died, or a global step limit is reached (by default 10,000 steps as a safety cap to prevent infinite runs). At that point, the simulation stops.
- Output: Once finished, the program prints the final state of the colony map – all remaining colonies and their intact tunnels in the original input format. Any destroyed colonies or tunnels are omitted from this output.
This algorithm ensures that no ant moves more than once per tick and all collisions in a tick are resolved simultaneously at the end of that tick. Notably, if two ants swap places (ant A moves from colony X to Y while ant B moves from Y to X in the same tick), they do not collide, because they never occupy the same colony at the same time – each colony in that case sees one ant leaving and one arriving, so no fight occurs. Only co-arrivals at the exact same colony trigger a fight.
The simulation was optimized through several development phases to handle large scenarios efficiently. We made careful algorithmic and implementation choices to reduce overhead, given that potentially tens of thousands of ants could be moving for thousands of iterations. Below is an in-depth look at the optimization journey and the reasoning behind each step.
In the initial implementation, correctness was the priority. A straightforward approach was used:
- Ants were represented with their current colony (by name or by a reference to a colony object).
- Each tick, to detect collisions, a structure (e.g. a hash map) mapped each target colony to the list or count of ants moving there. We would increment counts for each ant’s destination and then scan through to find colonies with count ≥ 2 to handle collisions.
- When a collision occurred, we removed the colony and those ants. Removal of a colony involved updating neighbor lists by searching for references to the colony in other colonies’ neighbor maps.
This baseline approach was simple but had performance drawbacks:
- String and Hash Overhead: Using colony names or even hash keys for colonies in the inner loop meant a lot of hashing and string comparisons each tick, especially with many ants. The use of
HashMapto count arrivals introduced significant overhead per ant move. - Memory Allocation: Building a new map of ant destinations every tick allocates and deallocates memory repeatedly. Similarly, removing a colony by updating neighbors required traversing data structures that weren’t optimized for quick removals.
- Clearing Data: If we used a fixed array for counts per colony, we would need to reset that entire array of size (number of colonies) on each tick, which is O(number_of_colonies) work per tick even if few colonies were actually visited. In a large sparse map, that could be wasteful.
In summary, while the baseline confirmed correctness, it was not scalable. Profiling indicated that most time was spent in managing hash maps and strings rather than the core simulation logic. We needed to eliminate these overheads.
To reduce overhead, the next major improvement was to replace string-based and map-based operations with index-based, array operations:
- Numeric Colony IDs: We introduced a compilation step that assigns each colony a unique integer ID. By sorting the colony names before assigning IDs, we ensured the IDs are stable across runs (for reproducibility) and also gave a convenient ordering for output or debugging. Using integer IDs means we can use simple index lookups instead of hash maps when tracking ant positions or neighbor relationships.
- CompactColony Structure: We transformed the colony neighbor data into fixed-size arrays. Each
CompactColonystores up to 4 neighbor IDs in an arraynbrs[4], corresponding to North, South, East, West. A value ofNONE(a constant sentinel, e.g.u32::MAX) indicates no neighbor in that direction. This fixed layout lets us access neighbors by index without a conditional lookup. We also store adeg(degree) for how many neighbors are present, and a separate compressed listdense[0..deg-1]of neighbor IDs. Thedenselist is essentially the active neighbors in no particular order, which is useful for iteration and random choice. - Array-Based Tracking: Instead of a hash map for counting ant destinations each tick, we prepared to use vectors/arrays indexed by colony ID. For example, we could maintain an array
arrival_count[colony_id]and increment it for each ant’s chosen destination. This drastically reduces the cost per increment (just array access) and avoids any dynamic allocation during counting. - Efficient Neighbor Updates: With the compact graph, destroying a colony and removing its tunnels became simpler. Each neighbor link can be cut by a direct index assignment in the neighbor’s
nbrsarray. Since each colony has at most 4 neighbors, updating neighbors of a destroyed colony became at most 4 constant-time operations. We can then regenerate that neighbor’sdenselist in a loop of at most 4 iterations. This is a fixed small cost per destruction, independent of the total size of the map.
Adopting the compact graph significantly improved performance. The movement of each ant became a constant-time operation involving a random neighbor choice and an array index assignment. Initial testing with moderate values of N (number of ants) and steps showed a drastic drop in CPU usage compared to the baseline. Memory access patterns also improved (contiguous arrays are cache-friendly).
However, one inefficiency remained: how we detect and handle collisions each tick. If we used a simple array arrival_count of length equal to number of colonies, we still faced the cost of resetting this entire array or scanning it to find collisions every tick. In worst-case scenarios with very large maps, this could become a bottleneck. The solution was to make collision detection more lazy and targeted.
The final optimization phase focused on how to gather and resolve collisions without touching unnecessary memory or doing needless work each iteration. We introduced a few interlocking techniques to make collision handling extremely efficient:
-
Epoch Marking for Arrays: We added an
occ_epocharray parallel to colonies, which records the last tick (epoch) in which each colony saw any ant arrivals. We also have anocc_countarray for the count of arrivals in the current tick. Instead of clearing the entireocc_countarray at the start of each tick, we use an incrementingepochcounter. At the start of a new tick, we increment the globalepoch. When processing ant movements, for each ant that moves to colonyX, we checkocc_epoch[X]:- If
occ_epoch[X] != current_epoch, it means this colony has not been touched in the current tick. We initialize it for this tick by settingocc_epoch[X] = current_epochand resettingocc_count[X] = 1. - If
occ_epoch[X]already equals the current epoch, then we’ve seen another ant heading toXin this tick, so we incrementocc_count[X]. - This way, each colony’s count is only updated when an ant actually arrives, and each colony is initialized at most once per tick (the first time an ant goes there). We never explicitly clear counts for colonies that receive no ants in a tick; their
occ_epochsimply remains outdated until they are touched again. - Benefit: If only a small fraction of colonies have ants moving into them in a given tick (which is common, especially as colonies get destroyed or ants cluster), we avoid scanning or resetting the vast majority of the
occ_countarray. This makes the cost of collision counting scale with the number of active moves rather than total number of colonies.
- If
-
Intrusive Linked Lists for Ant Grouping: To actually gather which ants are colliding in the same colony, we use an intrusive list approach with two arrays
headandlink. The arrayhead[colony_id]points to the index of the first ant (in the global ants vector) that will arrive at that colony this tick, or1if none. Each ant (identified by its index in theantsvector) has a corresponding entrylink[ant_index]which links to the next ant index arriving at the same colony, or1if it’s the last in that colony. We build these lists on the fly in the movement phase:- Initially, all
headentries are set to -1 for the new tick (we only reset those we use, again using the epoch logic). - As each ant’s move is computed, we obtain its destination colony ID
d. We then insert the ant into that colony’s list: ifhead[d]is not set for the current epoch, we sethead[d] = ant_index. Ifhead[d]is already set (meaning another ant is already heading there this tick), we push the new ant to the front of the list by settinglink[current_ant] = head[d]and updatinghead[d] = current_ant. - Meanwhile, we use
occ_count(from the epoch method above) to count how many ants have arrived atd. If an arrival makes the count reach 2, we know a collision will occur atd(at least two ants). We then adddto acolliding_colonieslist.
Using intrusive lists like this has two major advantages:
- We avoid allocating separate vectors or lists for each colony on each tick; we reuse two large arrays (
headandlink) every tick to link up ant indices. This is memory-efficient and cache-friendly. - We can gather all ants in a colliding colony by traversing this linked list, without any additional allocation. Since we stored the ant indices in the
linkchain when building moves, retrieving them is straightforward.
- Initially, all
-
Selective Collision Processing: By recording only colonies that have collisions in the
colliding_coloniesvector, we limit the second phase of each tick (resolving collisions) to only those places where fights occur. In a typical simulation, especially with many colonies, collisions at any given tick might be relatively rare compared to the number of total ants. This means we do minimal work in the resolve phase – just iterate over the handful of colliding colonies and handle those fights. We don’t need to loop over every colony or every ant to check for collisions explicitly. -
Colony Destruction and Updates: When a collision is processed for a colony
C:- We traverse the linked list of ants that were set to arrive at
C(using theheadandlinkchains) to collect all ant IDs involved. - We sort those ant IDs (by their stable identifiers) to produce a deterministic order in the log message (this deterministic output is useful for testing and debugging, ensuring that given the same random seed the events occur in a repeatable order).
- We mark all those ants as dead for this tick.
- We then destroy the colony
C: markCasalive = false, and for each neighbor ofC, removeCfrom its neighbor’s list. Thanks to the fixed neighbor slots, ifCwas north of neighborX, we directly setX.south = NONE, and similarly for east-west. We then rebuild each affected neighbor’sdenseneighbor list (a quick scan of up to 4 slots). - Destroying the colony also means in future ticks no ant can arrive or depart
C. Our simulation respects this by always checking thealiveflag and degree: ants will not consider a destroyed colony as a valid neighbor in their random moves.
- We traverse the linked list of ants that were set to arrive at
-
Memory Recycling and Compacting Ant List: After resolving collisions, some ants have died and should be removed. Rather than removing elements one by one from the
antsvector (which could be costly), we use a swap-remove strategy via scratch buffers:- We maintain a secondary vector
scratch_ants(and a parallelscratch_nextfor their next destinations) with reserved capacity equal to the maximum number of ants. We iterate through the mainantsvector and push all surviving ants intoscratch_ants. This effectively compacts the list of ants by skipping over dead ones. - We then swap the
scratch_antsbuffer with the mainantsvector. The dead ants end up being dropped whenscratch_ants(now containing them) goes out of scope, and the mainantsvector now contains only survivors. This operation is O(number_of_ants) per tick in the worst case (if many ants die or if we do it every tick), but it’s efficient in Rust because the element types are simple and we minimize reallocations by reusing capacity. In practice, massive die-offs happen early in the simulation (if at all), and afterwards the number of ants tends to shrink, so the cost diminishes over time. - We also resize our auxiliary arrays (
next_pos,dead_flags,link) to the new number of ants if needed, or reset their content. Many of these arrays are allocated with capacity at startup and only resized when the ant vector shrinks significantly, so the overhead is low.
- We maintain a secondary vector
-
Random Number Generation Optimization: We chose the Xoshiro256++ algorithm (from the
rand_xoshirocrate) for the pseudorandom number generator. This PRNG has excellent performance characteristics (very fast generation, 256-bit state for long period and good statistical quality) and is well-suited for simulations. By seeding it with a user-provided seed, we ensure the simulation is reproducible – the same seed will result in the same sequence of moves and collisions. This reproducibility was important for testing (we have tests asserting that two runs with the same seed yield identical outcomes) and for users who want deterministic behavior.- In earlier versions, one might use
rand::thread_rng()or a default RNG, but those can be slower or non-deterministic across runs. By explicitly usingXoshiro256++and seeding it, we improved both speed and determinism. - We also made a micro-optimization for selecting a random neighbor direction: since colonies have at most 4 neighbors, the possible degrees are small. If a colony has exactly 2 or 4 neighbors (which are powers of two), we generate a random 32-bit number and use a bitmask (
rand & (deg-1)) to pick a neighbor index. This is slightly faster than using a modulo operation. For 3 neighbors (not a power of two), we fall back to a modulo, and for 1 neighbor there is no choice needed. This kind of micro-optimization helps shave off a bit of time in the tight loop, given how frequently random moves are chosen.
- In earlier versions, one might use
By the end of Phase 3, the simulation loop was highly optimized. All major per-tick operations – moving ants, counting collisions, resolving fights, and updating the graph – are either O(number_of_ants) or proportional to the number of actual events (collisions or deaths) that tick, with very low constant factors. We eliminated virtually all heap allocations and complex data structure operations from the inner loop. The use of large preallocated arrays and index arithmetic keeps memory access predictable and CPU cache-friendly. The result is a simulation that can handle large N (ants and colonies) with ease, while still being written in safe Rust with no unsafe code.
To validate the optimizations, we benchmarked the solution on both synthetic scenarios and a real-world example. All benchmarks were run in release mode on a modern machine, and times are the mean of several runs (using Criterion for rigorous measurement). The environment was:
- CPU: 14-core Intel® Core™ i9 (5.1 GHz boost)
- Memory: 15 GB RAM
- OS: Ubuntu 24.04.1 LTS, Linux 6.x kernel
- Rust Compiler: rustc 1.90.0 (1159e78c4 2025-09-14) in release profile
We measured both the map compilation time and the simulation runtime. Compilation refers to parsing the text and building the compact graph, while simulation refers to the iterative ant movement loop (excluding the final printing of the map).
Synthetic grid maps: We generated square grid maps of size 64x64 and 128x128 (each cell connected in four directions to its neighbors, forming a dense grid of colonies). We then simulated a number of ants moving for a fixed number of steps:
- Compile 64×64 grid (≈4096 colonies) – ~1.09 ms
- Compile 128×128 grid (≈16k colonies) – ~6.12 ms
- Simulate on 64×64 grid with 10,000 ants for 5,000 steps – ~0.93 ms
- Simulate on 128×128 grid with 50,000 ants for 5,000 steps – ~3.78 ms
Real-world map (hiveum_map_medium): We used a provided example map hiveum_map_medium.txt (a realistic colony network). We ran the simulation with a large number of ants:
- Compile real map (medium-sized) – ~2.74 ms
- Simulate on real map with 100,000 ants for 20,000 steps – ~3.15 ms
These results demonstrate the efficiency of the solution. For instance, even with 100k ants and 20k iterations (which is up to 2 billion potential ant-moves), the simulation completes in just a few milliseconds on a single thread. The optimizations ensure that the per-step overhead is extremely low. In practice, many ants get destroyed before reaching the max step count, so the simulation often ends early, further reducing average runtime.
Note on measurement: The simulation times above were measured with output silenced (--quiet --no-output flags) to focus on the core logic performance. Printing to console or writing the final map can add significant time for very large outputs, but those are not the focus of algorithmic optimization. We also provide a built-in way to measure simulation performance: using the --metrics flag will print a line like sim_ms=XYZ at the end, giving the milliseconds elapsed in the simulation loop (excluding parsing and output).
For example, running a simulation on the medium map with 50,000 ants and 20,000 steps with metrics enabled might look like this:
$ cargo run --release -- -f examples/hiveum_map_medium.txt simulate \
--ants 50000 --seed 42 --max-steps 20000 --quiet --no-output --metrics
sim_ms=2.972
This indicates the simulation loop took ~2.972 ms. Such tooling made it easy for us to benchmark and verify that each optimization phase improved performance.
The program is invoked via the colonymap binary with a subcommand to choose the operation. Most subcommands take an optional -f <file> to specify the map file; the demo subcommand does not require a file.
-
neighbors <NAME>– Print the neighbors of the given colony NAME. It will list each direction (north, south, east, west) and the colony in that direction, if any. For example:colonymap -f examples/hiveum_map_medium.txt neighbors Karamight output:
north: Omrida south: Celles east: Chronoskis west: Larvonthi(If the colony name is not found, it returns an error.)
-
path <FROM> <TO>– Print the shortest path from colony FROM to colony TO as a sequence of colony names separated by>. This uses a BFS on the directed graph of tunnels. If no path exists, it exits with an error. Example:colonymap -f examples/hiveum_map_medium.txt path Kara Omridawould output:
Kara -> Omridaif a path is found (in this case directly connected). If either colony doesn’t exist or is unreachable, you’ll get a message like “no path from 'Kara' to 'X'”.
-
validate(with optional-strictflag) – Check that all links in the map are bidirectional and point to valid colonies. It will list any issues found:-
Missing reciprocal link (e.g., "A north='B' but 'B' south!='A'")
-
Link to a non-existent colony (e.g., "A east -> 'Z' (missing node)").
If no issues are found, it prints "OK: all links are reciprocal and targets exist." By default,
validateexits with status 0 even if issues are found (it simply prints them), but if you provide--strict, it will instead return a non-zero exit code if any issues were detected (useful for CI or validation scripts).
-
-
demo— Run a built‑in demo on the medium example map (no file needed). The map is embedded at compile‑time fromexamples/hiveum_map_medium.txt.Options (same semantics as
simulate):--ants <N>(default 100)--seed <S>(optional; if omitted, a cryptographically secure random seed is used)--max-steps <M>(default 10,000)--quiet,--no-output,--metrics
Examples:
# Quick demo with default random seed colonymap demo --ants 100 # Deterministic demo with metrics only colonymap demo --ants 100 --seed 42 --quiet --no-output --metrics
Note: For non‑demo subcommands (
neighbors,path,validate,simulate), pass a map file with-f <file>.
simulate– Run the ant simulation on the map. This subcommand has several arguments to control the simulation:-ants <N>(required): Number of ants to spawn in the simulation.-seed <S>: RNG seed (64-bit unsigned integer) for deterministic behavior. If you run with the same seed and parameters, you will get the same sequence of events. If omitted, the tool generates a fresh cryptographically secure random seed from the operating system (non‑deterministic by default).-max-steps <M>: Maximum number of steps (iterations) to simulate, defaults to 10,000 as per the problem statement. The simulation may end earlier if all ants die.-quiet: If set, suppresses the output of fight logs. Normally, each time a colony is destroyed, a line is printed (e.g.,X has been destroyed by ant ...). In quiet mode, these lines are not printed, which can significantly reduce console spam and overhead for large simulations.-no-output: If set, skip printing the final colony map at the end. This is useful if you only care about the side effects (like the destruction logs) or performance metrics. Printing the final map can be expensive for large maps, so this flag is handy for benchmarking.-metrics: If set, do not print any logs or map, and instead output a single machine-readable linesim_ms=YYY.yyyat the end indicating the milliseconds the simulation loop took. This flag automatically implies-quietand-no-output(so no other output will appear). It is designed for performance testing and benchmarking.
Examples:
-
Run a simulation with 100 ants on
examples/hiveum_map_small.txtand see the full log and final outcome:colonymap -f examples/hiveum_map_small.txt simulate --ants 100 --seed 42(This will print destruction events as they happen and the remaining map at the end.)
-
Run the same simulation but only measure how long it takes, without any output:
colonymap -f examples/hiveum_map_small.txt simulate --ants 100 --seed 42 --quiet --no-output --metrics(This will only output something like
sim_ms=0.123and nothing else.) -
Just validate a map file strictly (suitable for scripts/CI):
colonymap -f examples/hiveum_map_small.txt validate --strict(Exit code will be 0 if OK, or 1 and an error message if issues found.)
- Colony names are assumed to be unique and formatted as single tokens (no spaces in names). The parser will error out if a duplicate name is found. We also assume names won’t contain special characters that could break the format; for instance, the input format expects no spaces in a single colony entry line other than separating tokens.
- Direction names in the input are case-insensitive (
North,north,NORTHall parse as the same direction). - The input may omit some reciprocal links (one-way tunnels). Our
validatecommand catches these, but the simulation and pathfinding will still operate on whatever directed links are present. If the map is not fully bidirectional, an ant could travel in one direction but not be able to go back because the reverse link is missing – this is allowed by the simulation logic, though the real-world scenario expects tunnels to be two-way. We assume well-formed input in normal use, but provide tools to find anomalies. - When a colony is destroyed, we remove it and its links immediately. Any ant that was heading into that colony at the same tick is considered to have arrived (and will die in the collision). Ants cannot pass through a destroyed colony in subsequent ticks. If a colony’s destruction isolates other colonies (no remaining paths out), those ants in the isolated section will effectively be trapped in their area – they will continue moving around but can never reach ants outside, so eventually they may only die if colliding with each other.
- The simulation’s random choices are uniform over available directions. We do not introduce any bias or more complex movement behavior – all ants move independently and randomly.
- The maximum steps (10,000 by default) is a safety limit. In practice, simulations typically end by then because ants collide over time. If the map is extremely large and ants somehow avoid ever colliding, the simulation will stop once each ant has moved 10,000 times to prevent an endless run.
Potential enhancements or extensions that could be explored:
- Configurable or Smarter Ant Behavior: Currently ants move randomly. We could introduce different movement algorithms or behaviors (for example, biased random walks, or even path-following if some ants had goals).
- Custom Starting Positions: Allowing an input file or parameters to place ants in specific colonies rather than random distribution. This could enable scenario testing or deterministic setups (e.g., all ants start in specific locations).
- Parallel Simulation: The simulation is single-threaded. Given the nature of the problem, it might be possible to parallelize parts of it (for instance, splitting the ant list and processing moves in parallel, then merging collision results). However, handling the collision resolution in parallel would require careful synchronization. For extremely large simulations, multithreading or even GPU acceleration could be considered.
- Persistent Colony Map State: Add the ability to save and load the compiled colony state. For example, after parsing and compiling a map, we could serialize the
Compactstructure to binary, so that if we want to run multiple simulations on the same map, we skip the parse step. Similarly, a snapshot of a simulation could be saved and resumed. - Larger Map Support: The current implementation uses 32-bit colony IDs (
u32), which supports up to ~4 billion colonies, far above any practical input size. If needed, this could be switched to 64-bit, but that would only be relevant for astronomically large maps. - Extended Directions: The code is currently limited to the four cardinal directions as per the problem. In a different context, it could be extended to arbitrary graph links or more directions (e.g., adding northeast, northwest, etc.), though that would change data structures somewhat.
Throughout development, we focused on writing clean, safe Rust code while achieving high performance. All unsafe code is forbidden (there are no unsafe blocks in the code base), meaning we rely on Rust’s guarantees for memory safety even under heavy optimization. We also wrote extensive tests (unit tests and integration tests) to verify correctness: parsing, pathfinding, validation, and simulation outcomes (including edge cases like initial collisions and swap moves). This gives confidence that the optimizations did not introduce regressions and that the program behaves as expected.