A type-safe 2D grid simulation engine built with TypeScript.
This project simulates how contamination spreads across a rectangular grid. Carriers move through the grid using direction commands. When a carrier enters a contaminated cell, the carrier becomes infected. Once infected, every cell visited by that carrier becomes contaminated.
The project focuses on clear responsibility separation, 2D array handling, deterministic simulation logic, edge-case coverage, and automated testing.
- TypeScript
- Node.js
- Vitest
- Docker
- Prettier
npm installnpm run devnpm testnpm run buildnpm run format:checknpm run formatdocker run --rm -v ${PWD}:/app -w /app node:22-alpine npm installdocker run --rm -v ${PWD}:/app -w /app node:22-alpine npm run devdocker run --rm -v ${PWD}:/app -w /app node:22-alpine npm testdocker run --rm -v ${PWD}:/app -w /app node:22-alpine npm run builddocker run --rm -v ${PWD}:/app -w /app node:22-alpine npm run format:checkIf using WSL or Git Bash, $(pwd) may be more reliable:
docker run --rm -v "$(pwd)":/app -w /app node:22-alpine npm testThe sample input is stored in:
examples/sample-input.json
Example:
{
"grid": {
"width": 4,
"height": 4
},
"initialContaminatedPositions": [
[1, 1]
],
"carriers": [
{
"id": 1,
"position": [0, 1]
},
{
"id": 2,
"position": [3, 3]
}
],
"moves": "RDR"
}The expected sample output is stored in:
examples/sample-output.txt
Example:
Infected carriers: 1
Contaminated positions: (1,1), (1,2), (2,2)
Final carrier positions:
- Carrier 1: (2,2), infected
- Carrier 2: (1,0), clean
TypeScript-Grid-Simulation-Engine/
├── README.md
├── package.json
├── tsconfig.json
├── vitest.config.ts
├── Dockerfile
├── docker-compose.yml
├── .gitignore
├── .prettierrc.json
├── .prettierignore
│
├── docs/
│ ├── 01-problem-definition.md
│ ├── 02-requirement-analysis.md
│ ├── 03-assumptions-and-edge-cases.md
│ ├── 04-design-notes.md
│ ├── 05-test-strategy.md
│ └── ai-usage.md
│
├── examples/
│ ├── sample-input.json
│ └── sample-output.txt
│
├── src/
│ ├── domain/
│ │ ├── Position.ts
│ │ ├── Direction.ts
│ │ ├── CellState.ts
│ │ ├── Carrier.ts
│ │ ├── SimulationInput.ts
│ │ └── SimulationResult.ts
│ │
│ ├── grid/
│ │ └── ContaminationGrid.ts
│ │
│ ├── parser/
│ │ ├── InputParser.ts
│ │ └── ValidationError.ts
│ │
│ ├── engine/
│ │ └── SimulationEngine.ts
│ │
│ ├── output/
│ │ └── ResultFormatter.ts
│ │
│ └── index.ts
│
└── tests/
├── domain/
├── grid/
├── parser/
├── engine/
└── output/
- Coordinates use zero-based indexing.
- The top-left cell is
(0, 0). - The grid is stored internally as
grid[y][x]. xrepresents the column.yrepresents the row.- Movement commands are:
U: move upD: move downL: move leftR: move right
- Movement wraps around grid boundaries.
- A carrier becomes infected when it enters a contaminated cell.
- A carrier that starts on a contaminated cell is infected before movement.
- Once infected, every cell visited by that carrier becomes contaminated.
- Duplicate contaminated cells appear only once in the final output.
Wrapping depends on grid width and height, so this logic belongs to ContaminationGrid, not Position.
Position only stores coordinates.
The 2D array stores whether a cell is clean or contaminated.
Carriers are stored separately because they are moving entities with their own identity, position, and infection status.
SimulationEngine receives structured input and returns structured output.
It does not read files, parse JSON, or print console output.
InputParser converts raw JSON or objects into SimulationInput.
ResultFormatter converts SimulationResult into readable text output.
Tests are organised by domain, grid, parser, engine, and output behaviour instead of being placed in one large test file.
- The grid must have positive integer width and height.
- Coordinates must be integers.
- Coordinates must be inside the grid before simulation starts.
- Movement commands must be one of
U,D,L, orR. - Multiple carriers may occupy the same cell.
- Duplicate contaminated positions are treated as one contaminated cell.
- Once a carrier becomes infected, it remains infected.
- Empty carrier lists and empty movement sequences are valid inputs.
The test suite covers:
- position equality
- direction validation
- carrier movement and infection status
- 2D grid creation
- contamination checks
- invalid grid positions
- wraparound movement in all four directions
- parser validation
- initial contaminated-cell infection
- infected carriers contaminating visited cells
- multiple carrier behaviour
- output formatting
Run:
npm testBefore committing changes, run:
npm run format:check
npm test
npm run build
npm run devWith Docker:
docker run --rm -v ${PWD}:/app -w /app node:22-alpine npm run format:check
docker run --rm -v ${PWD}:/app -w /app node:22-alpine npm test
docker run --rm -v ${PWD}:/app -w /app node:22-alpine npm run build
docker run --rm -v ${PWD}:/app -w /app node:22-alpine npm run devDetailed engineering notes are stored in the docs/ folder:
01-problem-definition.md02-requirement-analysis.md03-assumptions-and-edge-cases.md04-design-notes.md05-test-strategy.mdai-usage.md
The README is kept concise so reviewers can quickly understand how to run, test, and evaluate the project.
AI tools were used to assist with:
- brainstorming project structure
- identifying possible edge cases
- reviewing responsibility separation
- suggesting test scenarios
- improving documentation clarity
The final design decisions, implementation, testing, debugging, and validation were completed and reviewed by me.
A more detailed AI usage summary is available in:
docs/ai-usage.md
- Add CLI support for custom input file paths.
- Add JSON output for easier automated verification.
- Add a small visual grid renderer for demonstration purposes.