Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
54 commits
Select commit Hold shift + click to select a range
9da928c
Introduce kata and summarise the steps
vfarah-if May 17, 2025
9d79199
Add failing test for getting started
vfarah-if May 17, 2025
ee76f41
Make simple test pass
vfarah-if May 17, 2025
9db2ac8
Refactor moving the sut into its own file
vfarah-if May 17, 2025
5e57507
(RED) Create failing test for a single move in the same direction
vfarah-if May 17, 2025
542a0a6
(GREEN) make test pass for single move in the same direction
vfarah-if May 17, 2025
c7abf41
(GREEN) Refactor test and remove duplication
vfarah-if May 17, 2025
6300127
(RED) change direction
vfarah-if May 17, 2025
c2713f9
(GREEN) pass change direction
vfarah-if May 17, 2025
33e954e
(REF) Introduce basic variables used simply in interpolation strings
vfarah-if May 17, 2025
42fbfef
(RED) Failing test to expand commands in the same direction
vfarah-if May 17, 2025
29acb7d
(GREEN) Iterate through commands making smallest change
vfarah-if May 17, 2025
b71f641
(REFACTOR) Extract human readable types and enums
vfarah-if May 17, 2025
d82ca6f
(RED) Failing test for continually changing direction
vfarah-if May 17, 2025
4da7baf
(GREEN) Fix incremental direction change
vfarah-if May 17, 2025
722bd88
(REFACTOR): Remove unused variable
vfarah-if May 17, 2025
6ad42fb
(RED) Failing test for turning to face north again
vfarah-if May 17, 2025
46964fd
(GREEN) Implement simple cardinality to bring it back in the correct …
vfarah-if May 17, 2025
f3fa38c
(RED) Failing test for turning left
vfarah-if May 17, 2025
9ae336f
(GREEN) Matched existing moveRight pattern for symmmetry
vfarah-if May 17, 2025
37d53a4
(RED) Failing test for moving north than south
vfarah-if May 17, 2025
93f944a
(GREEN) Fix move to cater for direction
vfarah-if May 17, 2025
2862a79
(REFACTOR): Swicth move ifs to case
vfarah-if May 17, 2025
9037f02
(RED) Add failing test for moving east
vfarah-if May 17, 2025
44ebbf5
(GREEN) Fix east move test
vfarah-if May 17, 2025
156942d
(RED) Add failing test for moving west
vfarah-if May 17, 2025
8d440cf
(GREEN) Fix west moving test
vfarah-if May 17, 2025
4062485
(REFACTOR): Simplify test using test each
vfarah-if May 17, 2025
98e626b
(RED) Add failing test for north to bottom
vfarah-if May 17, 2025
a4ba99e
(GREEN) Fix failing test
vfarah-if May 17, 2025
5ea807f
(RED) Add failing test south bound
vfarah-if May 17, 2025
5ab0bb1
(GREEN) Fix failing test
vfarah-if May 17, 2025
8cfe657
(RED) Add failing test east bound
vfarah-if May 17, 2025
32e52dc
(GREEN) Fix failing test
vfarah-if May 17, 2025
886465b
(RED) Add failing test west bound
vfarah-if May 17, 2025
bd7b6cf
(GREEN) Fix failing test
vfarah-if May 17, 2025
698086e
(REFACTOR): Group tests so it is easier to understand
vfarah-if May 17, 2025
d6493d2
(REFACTOR): Extract Position class to rid primitive obsession & tight…
vfarah-if May 17, 2025
2908174
(REFACTOR): Move north positioning ontoPosition
vfarah-if May 17, 2025
8d5f1ff
(REFACTOR): Move south positioning onto Position
vfarah-if May 17, 2025
a99ee73
(REFACTOR): Move east positioning onto Position
vfarah-if May 17, 2025
7fe3ca5
(REFACTOR): Move west and seal position
vfarah-if May 17, 2025
708ab9a
(REFACTOR): Move position corodinates as string
vfarah-if May 17, 2025
2547e0f
(REFACTOR): Reduce switch to tuple
vfarah-if May 17, 2025
b5f55ae
(REFACTOR): Extract Direction and refactor rover
vfarah-if May 17, 2025
425dec0
(REFACTOR): Fix law of demeter violation
vfarah-if May 17, 2025
e7e5ad8
(REFACTOR): Add and refactor final full tests with no obstacles
vfarah-if May 17, 2025
ddec397
(REFACTOR): Fix case to get back into it
vfarah-if May 18, 2025
4dbb0e8
(RED) Add failing test for obstacle
vfarah-if May 18, 2025
23f9714
(GREEN) Fix failing obstacle test
vfarah-if May 18, 2025
3431ad6
(RED) Refactor to extract grid
vfarah-if May 18, 2025
1d8b39a
(GREEN/REFACTOR) Centralise grid logic
vfarah-if May 18, 2025
14832e1
(REFACTOR) Clean up test with GridBuilder
vfarah-if May 18, 2025
89a356f
Its a wrap
vfarah-if May 18, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
197 changes: 197 additions & 0 deletions test-pnpm/mars_rovers/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
## Instructions

A squad of robotic rovers are to be landed by NASA on a plateau on Mars.

This plateau, which is curiously rectangular, must be navigated by the rovers so that their onboard cameras can get a complete view of the surrounding terrain to send back to Earth.

Your task is to develop an API that moves the rovers around on the plateau.

In this API, the plateau is represented as a 10x10 grid, and a rover has state consisting of two parts:

- its position on the grid (represented by an X,Y coordinate pair)
- the compass direction it's facing (represented by a letter, one of `N`, `S`, `E`, `W`)

### Input

The input to the program is a string of one-character move commands:

- `L` and `R` rotate the direction the rover is facing
- `M` moves the rover one grid square forward in the direction it is currently facing

If a rover reaches the end of the plateau, it wraps around the end of the grid.

### Output

The program's output is the final position of the rover after all the move commands have been executed. The position is represented as a coordinate pair and a direction, joined by colons to form a string. For example: a rover whose position is `2:3:W` is at square (2,3), facing west.

### Obstacles

The grid may have obstacles. If a given sequence of commands encounters an obstacle, the rover moves up to the last possible point and reports the obstacle by prefixing `O:` to the position string that it returns. For instance, `O:1:1:N` would mean that the rover encountered an obstacle at position (1, 2).

### Examples

- given a grid with no obstacles, input `MMRMMLM` gives output `2:3:N`
- given a grid with no obstacles, input `MMMMMMMMMM` gives output `0:0:N` (due to wrap-around)
- given a grid with an obstacle at (0, 3), input `MMMM` gives output `O:0:2:N`

### Interface

There are no restrictions on the design of the public interface. In particular, much of the details of the obstacle feature's implementation are up to you.

Rules:

- The rover receives a char array of commands e.g. `RMMLM` and returns the finishing point after the moves e.g. `2:1:N`
- The rover wraps around if it reaches the end of the grid.

Credit: [Google Code Archive](https://code.google.com/archive/p/marsrovertechchallenge/)

Credit: [Google Code Archive](https://code.google.com/archive/p/marsrovertechchallenge/)

## Useful Links

### Books

- [Head First Design Patterns](https://www.oreilly.com/library/view/head-first-design/0596007124/) by Eric Freeman et al.
- [Understanding the Four Rules of Simple Design](https://leanpub.com/4rulesofsimpledesign) by Corey Haines

### Articles

- [Mars Rover Kata: Refactoring to Patterns](https://codurance.com/2019/01/22/mars-rover-kata-refactoring-to-patterns/) by [Simion Iulian Belea](https://codurance.com/publications/author/simion-iulian-belea/)

### Solutions

- [Java](https://github.com/sandromancuso/mars-rover-screencast/tree/screencast) by[ Sandro Mancuso](https://www.codurance.com/publications/author/sandro-mancuso)

## Steps

Certainly, Vincent. Here's a well-structured summary of steps to break down the Mars Rover kata into focused, testable iterations using outside-in TDD. This progression balances incremental delivery, testability, and clean design principles:

------

## ✅ Step-by-Step Breakdown for the Mars Rover Kata (TypeScript)

### 🧭 **1. Define the Domain Types and Enums**

- **Purpose**: Model the rover’s state and commands.
- **Entities**:
- `Direction` enum or type union: `'N' | 'E' | 'S' | 'W'`
- `Position` type: `{ x: number; y: number }`
- `Command` type: `'L' | 'R' | 'M'`

✅ Write unit tests for parsing and wrapping directions if you prefer utility functions early.

------

### 🧱 **2. Implement the `Grid`**

- **Responsibility**: Represent a 10x10 plateau, handle wrap-around logic, and detect obstacles.
- **Features**:
- `wrap(position: Position): Position`
- `hasObstacle(position: Position): boolean`

✅ Unit tests:

- `wrap({x:10,y:0}) => {x:0,y:0}`
- `hasObstacle({x:0,y:3}) => true`

------

### 🚙 **3. Create the `MarsRover` Class Skeleton**

- **Initial state**: Position at (0, 0), facing North

✅ Unit test:

- `new MarsRover(grid).execute('') => '0:0:N'`

------

### 🔁 **4. Implement Turning Logic**

- `L` and `R` commands modify direction
- Use direction index rotation: N → E → S → W → N

✅ Unit tests:

- `'L' from N => W`
- `'R' from N => E`

------

### ➡️ **5. Implement Forward Movement (`M`)**

- Move one step in the direction facing
- Use `Grid.wrap()` to handle edge crossing

✅ Unit tests:

- `execute('M')` updates position to `0:1:N`
- `execute('MMMMMMMMMM')` wraps to `0:0:N`

------

### ⛰️ **6. Add Obstacle Handling**

- Prevent movement into an obstacle
- Prefix output with `O:` and report last safe position

✅ Unit tests:

- Obstacle at `0:3`, `execute('MMMM') => O:0:2:N`

------

### 🧪 **7. Full Behavioural Tests**

Add scenario-based tests:

- `MMRMMLM => 2:3:N`
- `MMMMMMMMMM => 0:0:N`
- `MMMM` with obstacle at `0:3 => O:0:2:N`

------

### 🧼 **8. Refactor & Polish**

- Extract reusable logic (e.g. `rotate`, `nextPosition`)
- Add immutability if you favour a purer style
- Optionally expose state inspection for diagnostics/logging
- Create a functional approach to the same Kata

### Summary

Thanks *Danil Suits* for your lovely kata. I put a lot of empasis on dicipline with RED, GREEN, REFACTOR pattern of TDD * INFINITY. I also tried harder than usual to identify the natural anti-patterns I was creating by keeping things small and simple, adding a reference to [refactoring smells](https://refactoring.guru/refactoring/smells) so you can see the anti patterns I added and tried to fix. Keep timing tight, which I didn't inbetween doing stuff and other unrelated activities. https://cuckoo.team/ is a great tool for that. If you are going to pair with someone, make sure you discuss some [principles and etiquette](https://www.thoughtworks.com/insights/blog/seven-principles-pair-programming-etiquette) around what you need to do. Two controversial points with my solution. One is *Typescript* and the other is *functional* vs *class*. I personally use both when appropriate and am not a real javascript purest or functional purest. Functional approaches shine when the state transitions are stateless and I want to you want to compose behaviour easily. Class-based approach shines when encapsulation of state and behaviour and *Object-Orientation* Models the domain well. I also find as I am in C# these days, it just feels more natural to use and swap between it.

Verdict

> **Your class-based approach is very clean and modular — not worse than functional, just different. You're modelling the domain responsibly.**

A little bit of a break down into how you can prevent boiling the ocean :)

The hard part was breaking it up to not do too much at a time, so lots of small tests driving small changes and requirments to introduce stuff.

`''` → `0:0:N` ✅

`'M'` → `0:1:N` ✅

`'MM'` → `0:2:N` ✅

`'R'` → `0:0:E` ✅

Directional requirments seemed easier to expand by

| Input | Expected Output | Notes |
| ------ | --------------- | ------------------------------------ |
| `R` | `0:0:E` | North → East |
| `RR` | `0:0:S` | East → South |
| `RRR` | `0:0:W` | South → West |
| `RRRR` | `0:0:N` | Wrap-around to North (full rotation) |

const directions = ['N', 'E', 'S', 'W']; // indices: 0, 1, 2, 3

| Current index | +1 | Mod 4 (`% 4`) | New Direction |
| ------------- | ---- | ------------- | ------------- |
| 0 (N) | 1 | 1 | E |
| 1 (E) | 2 | 2 | S |
| 2 (S) | 3 | 3 | W |
| 3 (W) | 4 | 0 | N (wrap) |
31 changes: 31 additions & 0 deletions test-pnpm/mars_rovers/direction.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
export enum CompassDirection {
North = 'N',
East = 'E',
South = 'S',
West = 'W',
}

export class Direction {
private _directions: CompassDirection[];

constructor(public readonly value: CompassDirection) {
this._directions = new Array<CompassDirection>(
CompassDirection.North,
CompassDirection.East,
CompassDirection.South,
CompassDirection.West,
);
}

rotateLeft(): Direction {
const index = this._directions.indexOf(this.value);
const newIndex = (index - 1 + this._directions.length) % this._directions.length;
return new Direction(this._directions[newIndex]);
}

rotateRight(): Direction {
const index = this._directions.indexOf(this.value);
const newIndex = (index + 1 + this._directions.length) % this._directions.length;
return new Direction(this._directions[newIndex]);
}
}
21 changes: 21 additions & 0 deletions test-pnpm/mars_rovers/grid.builder.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { Grid } from './grid';
import { Position } from './position';

export class GridBuilder {
private _size: number = 10;
private _obstacles: Array<Position>;

withSize(size: number): this {
this._size = size;
return this;
}

withObstacles(...obstacles: Position[]): this {
this._obstacles = obstacles;
return this;
}

buid(): Grid {
return new Grid(this._obstacles, this._size);
}
}
24 changes: 24 additions & 0 deletions test-pnpm/mars_rovers/grid.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { Position } from './position';

export type Cordinate = {
x: number;
y: number;
};

export class Grid {
constructor(
private readonly _obstacles: Array<Position> | null = null,
public readonly size: number = 10,
) {}

hasObstacle(x: number, y: number): boolean {
return this._obstacles?.some((obstacle) => obstacle.x === x && obstacle.y === y);
}

wrap(x: number, y: number): Cordinate {
return {
x: (x + this.size) % this.size,
y: (y + this.size) % this.size,
};
}
}
52 changes: 52 additions & 0 deletions test-pnpm/mars_rovers/mars.rover.should.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { Grid } from './grid';
import { GridBuilder } from './grid.builder';
import { MarsRover } from './mars.rover';
import { Position } from './position';

describe('mars rover should', () => {
let grid: Grid;
let marsRover: MarsRover;

beforeEach(() => {
grid = new GridBuilder().withSize(10).buid();
marsRover = new MarsRover(grid);
});

test.each([
['report its default start position', '', '0:0:N'],

['move in the same direction with a single move command', 'M', '0:1:N'],
['move forward and then turn facing south', 'MRRM', '0:0:S'],
['move in the same direction with a double move command', 'MM', '0:2:N'],
['move forward facing east', 'RM', '1:0:E'],
['move forward facing west', 'RMLLM', '0:0:W'],

['turn right facing east', 'R', '0:0:E'],
['turn right twice facing south', 'RR', '0:0:S'],
['turn right 4 times and face north again', 'RRRR', '0:0:N'],
['turn left facing west', 'L', '0:0:W'],

['wrap around north edge to bottom', 'MMMMMMMMMM', '0:0:N'],
['wrap around south edge to top', 'RRMMMMMMMMMM', '0:0:S'],
['wrap around east edge to the left', 'RMMMMMMMMMM', '0:0:E'],
['wrap around west edge to the right', 'LMMMMMMMMMM', '0:0:W'],

['given a grid with no obstacles', 'MMRMMLM', '2:3:N'],
['given a grid with no obstacles', 'MMMMMMMMMM', '0:0:N'],
])('%s with input %s outputs %s', (_, input, expected) => {
const actual = marsRover.execute(input);

expect(actual).toBe(expected);
});

describe('when given obstacles should', () => {
test('navigate up to the obstacle and no further', () => {
grid = new GridBuilder().withSize(10).withObstacles(new Position(0, 3)).buid();
marsRover = new MarsRover(grid);

var actual = marsRover.execute('MMMM');

expect(actual).toBe('O:0:2:N');
});
});
});
53 changes: 53 additions & 0 deletions test-pnpm/mars_rovers/mars.rover.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { CompassDirection, Direction } from './direction';
import { Grid } from './grid';
import { Position } from './position';

enum CommandType {
Move = 'M',
TurnRight = 'R',
TurnLeft = 'L',
}

export class MarsRover {
private _position: Position;
private _direction: Direction;
private readonly commandHandlers: Record<CommandType, () => void>;

constructor(private readonly _grid: Grid) {
this._position = new Position(0, 0);
this._direction = new Direction(CompassDirection.North);
this.commandHandlers = {
[CommandType.Move]: () => this.moveForward(),
[CommandType.TurnRight]: () => this.moveRight(),
[CommandType.TurnLeft]: () => this.moveLeft(),
};
}

execute(commands: string): string {
for (const command of commands) {
this.commandHandlers[command]();
}
const next = this._position.move(this.currentDirection(), this._grid);
const prefix = this._grid.hasObstacle(next.x, next.y) ? 'O:' : '';
return `${prefix}${this._position.toString()}:${this._direction.value}`;
}

private currentDirection(): CompassDirection {
return this._direction.value;
}

private moveLeft(): void {
this._direction = this._direction.rotateLeft();
}

private moveRight(): void {
this._direction = this._direction.rotateRight();
}

private moveForward(): void {
const next = this._position.move(this.currentDirection(), this._grid);
if (!this._grid.hasObstacle(next.x, next.y)) {
this._position = next;
}
}
}
Loading