This project implements a deterministic zombie infection simulation on a 2-dimensional toroidal grid. Zombies move according to a predefined sequence, infect creatures they encounter, and newly infected zombies are queued for subsequent movement. Creatures within the system do not move.
The system is designed to be fully testable, extendable, deterministic, and explicit in state transitions.
The simulation consists of:
- A square grid of fixed size
- Creatures placed at specific coordinates
- Zombies placed at specific coordinates
- A movement sequence applied to each zombie
- Infection rules that convert creatures into zombies
- A FIFO queue that governs zombie movement order
The grid wraps at edges (toroidal topology). There are no boundaries.
The Grid represents the world, and is the central controller. It owns:
- Dimensions (
width,height) - All
Cellinstances - All
ZombieandCreatureinstances - A FIFO
Queue<Zombie>used to control movement order - A
Loggerfor traceability
The grid is responsible for:
- Creating zombies and creatures
- Moving zombies
- Maintaining consistency between entities and cells
- Producing a final output state
A Cell represents a single coordinate on the grid.
Each cell tracks:
- Its
(x, y)position - Zombies currently occupying the cell
- Creatures currently occupying the cell
Key behaviours:
- Coordinate wrapping via
validate() - Cardinal neighbour lookup (
toLeft,toRight,toUp,toDown) - Infection via
infectCell() - Locks cell during infection to stop circular referencing
If at least one zombie and one creature are present, all creatures in the cell are infected, turning them into zombies.
A Zombie:
- Has a unique incrementing ID
- Occupies exactly one cell at a time
- Tracks the creature it originated from (if infected)
- Moves according to a direction sequence specified by the input
Movement:
- Executed step-by-step
- Wraps at grid edges
- Triggers infection after each step
Invalid movement instructions result in a hard error.
A Creature:
- Has a unique ID
- Occupies a single cell
- Does not move
- Can be converted into a zombie when infected
Creatures are passive entities.
The Queue<T> is a FIFO structure used to ensure zombies move in a deterministic order.
Properties:
- Enqueue at tail
- Dequeue from head
- Prevents duplicate zombie movement in queue
- Periodically compacts internal storage to avoid memory growth
The Logger provides structured logging with configurable behaviour:
- Silent or console output
- In-memory history tracking
- Optional file persistence
- Controlled crash behaviour on errors
Errors logged via throw() terminate execution.
- Zombies move one at a time, in FIFO order
- Each zombie executes the full movement sequence
- During movement, newly infected zombies are added to the queue
- Zombies already queued are not re-queued
Movement directions are defined by a Direction enum and parsed from characters.
The simulation reads from input.json:
{
"gridSize": 4,
"sequence": "RDLU",
"creaturePositions": [[0,1], [2,2]],
"zombiePositions": [[3,0]]
}The original specification for the project requested a single zombie position. For the purposes of completeness, the project can support any number of zombies during input.
Note that overlapping creature and zombie positions at input will necessarily create a new zombie immediately as the creature will become infected.
gridSize: Size of the grid (NxN)sequence: Movement sequence applied to each zombiecreaturePositions: Array of[x, y]creature coordinateszombiePositions: Array of[x, y]zombie coordinates
- Input is loaded from
input.json - Grid is created
- Creatures are placed
- Zombies are placed and queued
- Zombies move until the queue is empty
- Final grid state is printed
The final output is a structured representation of the grid and its zombies and creatures, followed by a direct output of just the zombie and creature positions.
{
width: 4,
height: 4,
zombies: [
{
id: 0,
cell: (1,1),
wasCreature: false
},
{
id: 1,
cell: (1,1),
wasCreature: true
}
],
creatures: []
}
zombies’ positions:
(1,1) (1,1)
creatures’ positions:
noneThe project includes exhaustive Jest test coverage for:
GridCellZombieCreatureQueueLogger- Utility functions
Tests validate:
- Deterministic movement
- Infection rules
- Queue correctness
- Edge wrapping
- Error handling
- Logging behaviour
- Deterministic execution
- No randomness
- No side effects outside logging
- Explicit state transitions
- Defensive rejection error handling
- Fully unit-testable components
- No diagonal movement
- No creature movement
- No concurrency
- No graphical output
I wrote this project with developer extensibility in mind. Wherever possible I have made it easy for developers to work with the code, from naming conventions, to debug testability, to log management systems.
I wrote the project in just under 4 hours without the use of AI; but I did use AI to write the README.md, then checked and expanded the information. Similarly, I used AI to think through the possible edge cases within the solution to develop the tests - however the code itself is written completely by me.
I opted for an object oriented approach; not a particular favourite approach of mine, but to the best of my knowledge the correct choice for the requirements of this project. The statefulness of the project makes object-oriented particularly appropriate and gives access to a number of options that functional or procedural solutions to the problem would not offer. This solution effectively made the system a simplistic simulation engine which developers can easily modify the instructions of to generate many different scenarios.
A number of tradeoffs have been made within the project - most notably a Queue with a head pointer instead of running an array shift; and whilst observances have been made to the correct data structures to minimise time and space complexity, my focus was readability first and foremost. For the same reason, there is a level of redundancy between the Grid, the Cells, and the Zombies with each one being able to be accessed from the other.
Whilst the intent was to have the Grid be the primary controller for the logic and act as a facade layer, I didnt want to have a situation in which you were traversing multiple layers of abstraction for any given item. This tradeoff choice in particular has led to an added complexity in updating the grid's cells when they had been modified elsewhere, which if I were continuing work on this project I would seek better solution avenues for a v2 release. As it stands the updateGrid(cell) is sufficient, and I opted to keep the existing solution so as not to overabstract the solution.