From 9da928c819413d2ee66a1e87c67f9f8e9ee17ac2 Mon Sep 17 00:00:00 2001 From: Vincent Farah Date: Sat, 17 May 2025 12:56:14 +0100 Subject: [PATCH 01/54] Introduce kata and summarise the steps --- test-pnpm/mars_rovers/README.md | 159 ++++++++++++++++++++++++++++ test-pnpm/morning_routine/README.md | 0 2 files changed, 159 insertions(+) create mode 100644 test-pnpm/mars_rovers/README.md delete mode 100644 test-pnpm/morning_routine/README.md diff --git a/test-pnpm/mars_rovers/README.md b/test-pnpm/mars_rovers/README.md new file mode 100644 index 0000000..01383d5 --- /dev/null +++ b/test-pnpm/mars_rovers/README.md @@ -0,0 +1,159 @@ +## 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 + diff --git a/test-pnpm/morning_routine/README.md b/test-pnpm/morning_routine/README.md deleted file mode 100644 index e69de29..0000000 From 9d79199afb804766fe93de920eb1cef8499b7970 Mon Sep 17 00:00:00 2001 From: Vincent Farah Date: Sat, 17 May 2025 13:08:52 +0100 Subject: [PATCH 02/54] Add failing test for getting started --- test-pnpm/mars_rovers/mars.rover.should.test.ts | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 test-pnpm/mars_rovers/mars.rover.should.test.ts diff --git a/test-pnpm/mars_rovers/mars.rover.should.test.ts b/test-pnpm/mars_rovers/mars.rover.should.test.ts new file mode 100644 index 0000000..58099e7 --- /dev/null +++ b/test-pnpm/mars_rovers/mars.rover.should.test.ts @@ -0,0 +1,9 @@ +describe('mars rover should', () => { + test('report its default start position', () => { + let marsRover = new MarsRover(); + + const actual = marsRover.Execute(''); + + expect(actual).toBe('0:0:N'); + }); +}); From ee76f413baae123540a79e55d0a7f31945adbbc4 Mon Sep 17 00:00:00 2001 From: Vincent Farah Date: Sat, 17 May 2025 13:11:50 +0100 Subject: [PATCH 03/54] Make simple test pass --- test-pnpm/mars_rovers/mars.rover.should.test.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/test-pnpm/mars_rovers/mars.rover.should.test.ts b/test-pnpm/mars_rovers/mars.rover.should.test.ts index 58099e7..f128ca2 100644 --- a/test-pnpm/mars_rovers/mars.rover.should.test.ts +++ b/test-pnpm/mars_rovers/mars.rover.should.test.ts @@ -2,8 +2,14 @@ describe('mars rover should', () => { test('report its default start position', () => { let marsRover = new MarsRover(); - const actual = marsRover.Execute(''); + const actual = marsRover.execute(''); expect(actual).toBe('0:0:N'); }); }); + +export class MarsRover { + execute(command: string): string { + return '0:0:N'; + } +} From 9db2ac8e3947833e09c85914cf6f3e3cf42200ce Mon Sep 17 00:00:00 2001 From: Vincent Farah Date: Sat, 17 May 2025 13:15:26 +0100 Subject: [PATCH 04/54] Refactor moving the sut into its own file --- test-pnpm/mars_rovers/mars.rover.should.test.ts | 8 ++------ test-pnpm/mars_rovers/mars.rover.ts | 5 +++++ 2 files changed, 7 insertions(+), 6 deletions(-) create mode 100644 test-pnpm/mars_rovers/mars.rover.ts diff --git a/test-pnpm/mars_rovers/mars.rover.should.test.ts b/test-pnpm/mars_rovers/mars.rover.should.test.ts index f128ca2..dcc34eb 100644 --- a/test-pnpm/mars_rovers/mars.rover.should.test.ts +++ b/test-pnpm/mars_rovers/mars.rover.should.test.ts @@ -1,3 +1,5 @@ +import { MarsRover } from './mars.rover'; + describe('mars rover should', () => { test('report its default start position', () => { let marsRover = new MarsRover(); @@ -7,9 +9,3 @@ describe('mars rover should', () => { expect(actual).toBe('0:0:N'); }); }); - -export class MarsRover { - execute(command: string): string { - return '0:0:N'; - } -} diff --git a/test-pnpm/mars_rovers/mars.rover.ts b/test-pnpm/mars_rovers/mars.rover.ts new file mode 100644 index 0000000..fa41d34 --- /dev/null +++ b/test-pnpm/mars_rovers/mars.rover.ts @@ -0,0 +1,5 @@ +export class MarsRover { + execute(command: string): string { + return '0:0:N'; + } +} From 5e57507b2dfc30071f131cfbe86557c3e70cace1 Mon Sep 17 00:00:00 2001 From: Vincent Farah Date: Sat, 17 May 2025 13:25:00 +0100 Subject: [PATCH 05/54] (RED) Create failing test for a single move in the same direction --- test-pnpm/mars_rovers/mars.rover.should.test.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/test-pnpm/mars_rovers/mars.rover.should.test.ts b/test-pnpm/mars_rovers/mars.rover.should.test.ts index dcc34eb..7b7282b 100644 --- a/test-pnpm/mars_rovers/mars.rover.should.test.ts +++ b/test-pnpm/mars_rovers/mars.rover.should.test.ts @@ -8,4 +8,12 @@ describe('mars rover should', () => { expect(actual).toBe('0:0:N'); }); + + test('move in the same direction with single move command', () => { + let marsRover = new MarsRover(); + + const actual = marsRover.execute('M'); + + expect(actual).toBe('0:1:N'); + }); }); From 542a0a67be6e39e1b75c9ab1372533dda3309a55 Mon Sep 17 00:00:00 2001 From: Vincent Farah Date: Sat, 17 May 2025 13:32:42 +0100 Subject: [PATCH 06/54] (GREEN) make test pass for single move in the same direction --- test-pnpm/mars_rovers/mars.rover.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/test-pnpm/mars_rovers/mars.rover.ts b/test-pnpm/mars_rovers/mars.rover.ts index fa41d34..046398c 100644 --- a/test-pnpm/mars_rovers/mars.rover.ts +++ b/test-pnpm/mars_rovers/mars.rover.ts @@ -1,5 +1,8 @@ export class MarsRover { execute(command: string): string { + if (command === 'M') { + return '0:1:N'; + } return '0:0:N'; } } From c7abf41d9d6dcfa505c284483d800bc19992265c Mon Sep 17 00:00:00 2001 From: Vincent Farah Date: Sat, 17 May 2025 13:42:30 +0100 Subject: [PATCH 07/54] (GREEN) Refactor test and remove duplication --- test-pnpm/mars_rovers/mars.rover.should.test.ts | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/test-pnpm/mars_rovers/mars.rover.should.test.ts b/test-pnpm/mars_rovers/mars.rover.should.test.ts index 7b7282b..5164695 100644 --- a/test-pnpm/mars_rovers/mars.rover.should.test.ts +++ b/test-pnpm/mars_rovers/mars.rover.should.test.ts @@ -1,17 +1,19 @@ import { MarsRover } from './mars.rover'; describe('mars rover should', () => { - test('report its default start position', () => { - let marsRover = new MarsRover(); + let marsRover: MarsRover; + + beforeEach(() => { + marsRover = new MarsRover(); + }); + test('report its default start position', () => { const actual = marsRover.execute(''); expect(actual).toBe('0:0:N'); }); - test('move in the same direction with single move command', () => { - let marsRover = new MarsRover(); - + test('move in the same direction with a single move command', () => { const actual = marsRover.execute('M'); expect(actual).toBe('0:1:N'); From 630012754725519e69d00d3d3b7b5cb0eb62c981 Mon Sep 17 00:00:00 2001 From: Vincent Farah Date: Sat, 17 May 2025 13:49:19 +0100 Subject: [PATCH 08/54] (RED) change direction --- test-pnpm/mars_rovers/mars.rover.should.test.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/test-pnpm/mars_rovers/mars.rover.should.test.ts b/test-pnpm/mars_rovers/mars.rover.should.test.ts index 5164695..1270182 100644 --- a/test-pnpm/mars_rovers/mars.rover.should.test.ts +++ b/test-pnpm/mars_rovers/mars.rover.should.test.ts @@ -18,4 +18,10 @@ describe('mars rover should', () => { expect(actual).toBe('0:1:N'); }); + + test('turn right facing east', () => { + const actual = marsRover.execute('R'); + + expect(actual).toBe('0:0:E'); + }); }); From c2713f9ff1069f02544d12203589c1db55e80ad9 Mon Sep 17 00:00:00 2001 From: Vincent Farah Date: Sat, 17 May 2025 13:51:56 +0100 Subject: [PATCH 09/54] (GREEN) pass change direction --- test-pnpm/mars_rovers/mars.rover.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/test-pnpm/mars_rovers/mars.rover.ts b/test-pnpm/mars_rovers/mars.rover.ts index 046398c..274c686 100644 --- a/test-pnpm/mars_rovers/mars.rover.ts +++ b/test-pnpm/mars_rovers/mars.rover.ts @@ -3,6 +3,9 @@ export class MarsRover { if (command === 'M') { return '0:1:N'; } + if (command === 'R') { + return '0:0:E'; + } return '0:0:N'; } } From 33e954e9b1660ede065b1eb316d19160d1a352cd Mon Sep 17 00:00:00 2001 From: Vincent Farah Date: Sat, 17 May 2025 14:19:08 +0100 Subject: [PATCH 10/54] (REF) Introduce basic variables used simply in interpolation strings --- test-pnpm/mars_rovers/mars.rover.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/test-pnpm/mars_rovers/mars.rover.ts b/test-pnpm/mars_rovers/mars.rover.ts index 274c686..7df82bd 100644 --- a/test-pnpm/mars_rovers/mars.rover.ts +++ b/test-pnpm/mars_rovers/mars.rover.ts @@ -1,11 +1,15 @@ export class MarsRover { + private _direction: 'N' | 'E' | 'S' | 'W' = 'N'; + private _x: number = 0; + private _y: number = 0; + execute(command: string): string { if (command === 'M') { - return '0:1:N'; + return `${this._x}:${this._y + 1}:${this._direction}`; } if (command === 'R') { - return '0:0:E'; + return `${this._x}:${this._y}:E`; } - return '0:0:N'; + return `${this._x}:${this._y}:${this._direction}`; } } From 42fbfefdbcb74623391c787303e8e8b337702125 Mon Sep 17 00:00:00 2001 From: Vincent Farah Date: Sat, 17 May 2025 14:34:49 +0100 Subject: [PATCH 11/54] (RED) Failing test to expand commands in the same direction --- test-pnpm/mars_rovers/mars.rover.should.test.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/test-pnpm/mars_rovers/mars.rover.should.test.ts b/test-pnpm/mars_rovers/mars.rover.should.test.ts index 1270182..68640f1 100644 --- a/test-pnpm/mars_rovers/mars.rover.should.test.ts +++ b/test-pnpm/mars_rovers/mars.rover.should.test.ts @@ -19,9 +19,21 @@ describe('mars rover should', () => { expect(actual).toBe('0:1:N'); }); + test('move in the same direction with a single move command', () => { + const actual = marsRover.execute('M'); + + expect(actual).toBe('0:1:N'); + }); + test('turn right facing east', () => { const actual = marsRover.execute('R'); expect(actual).toBe('0:0:E'); }); + + test('move in the same direction with a double move command', () => { + const actual = marsRover.execute('MM'); + + expect(actual).toBe('0:2:N'); + }); }); From 29acb7dd474bfa43884cf12cedacc67f74017d83 Mon Sep 17 00:00:00 2001 From: Vincent Farah Date: Sat, 17 May 2025 14:40:10 +0100 Subject: [PATCH 12/54] (GREEN) Iterate through commands making smallest change --- test-pnpm/mars_rovers/mars.rover.ts | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/test-pnpm/mars_rovers/mars.rover.ts b/test-pnpm/mars_rovers/mars.rover.ts index 7df82bd..4d03351 100644 --- a/test-pnpm/mars_rovers/mars.rover.ts +++ b/test-pnpm/mars_rovers/mars.rover.ts @@ -3,12 +3,14 @@ export class MarsRover { private _x: number = 0; private _y: number = 0; - execute(command: string): string { - if (command === 'M') { - return `${this._x}:${this._y + 1}:${this._direction}`; - } - if (command === 'R') { - return `${this._x}:${this._y}:E`; + execute(commands: string): string { + for (const command of commands) { + if (command === 'M') { + this._y = this._y + 1; + } + if (command === 'R') { + this._direction = 'E'; + } } return `${this._x}:${this._y}:${this._direction}`; } From b71f641f753f134c6626f9c1e2163960890144d3 Mon Sep 17 00:00:00 2001 From: Vincent Farah Date: Sat, 17 May 2025 14:54:25 +0100 Subject: [PATCH 13/54] (REFACTOR) Extract human readable types and enums --- .../mars_rovers/mars.rover.should.test.ts | 6 ----- test-pnpm/mars_rovers/mars.rover.ts | 26 ++++++++++++++++--- 2 files changed, 22 insertions(+), 10 deletions(-) diff --git a/test-pnpm/mars_rovers/mars.rover.should.test.ts b/test-pnpm/mars_rovers/mars.rover.should.test.ts index 68640f1..5a87371 100644 --- a/test-pnpm/mars_rovers/mars.rover.should.test.ts +++ b/test-pnpm/mars_rovers/mars.rover.should.test.ts @@ -19,12 +19,6 @@ describe('mars rover should', () => { expect(actual).toBe('0:1:N'); }); - test('move in the same direction with a single move command', () => { - const actual = marsRover.execute('M'); - - expect(actual).toBe('0:1:N'); - }); - test('turn right facing east', () => { const actual = marsRover.execute('R'); diff --git a/test-pnpm/mars_rovers/mars.rover.ts b/test-pnpm/mars_rovers/mars.rover.ts index 4d03351..8ea9201 100644 --- a/test-pnpm/mars_rovers/mars.rover.ts +++ b/test-pnpm/mars_rovers/mars.rover.ts @@ -1,15 +1,33 @@ +type Direction = + | DirectionType.North + | DirectionType.East + | DirectionType.South + | DirectionType.West; + +enum DirectionType { + North = 'N', + East = 'E', + South = 'S', + West = 'W', +} + +enum CommandType { + Move = 'M', + TurnRight = 'R', +} + export class MarsRover { - private _direction: 'N' | 'E' | 'S' | 'W' = 'N'; + private _direction: Direction = DirectionType.North; private _x: number = 0; private _y: number = 0; execute(commands: string): string { for (const command of commands) { - if (command === 'M') { + if (command === CommandType.Move) { this._y = this._y + 1; } - if (command === 'R') { - this._direction = 'E'; + if (command === CommandType.TurnRight) { + this._direction = DirectionType.East; } } return `${this._x}:${this._y}:${this._direction}`; From d82ca6fd41226e6813142d77da40ed715a09b9ed Mon Sep 17 00:00:00 2001 From: Vincent Farah Date: Sat, 17 May 2025 15:06:55 +0100 Subject: [PATCH 14/54] (RED) Failing test for continually changing direction --- test-pnpm/mars_rovers/mars.rover.should.test.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/test-pnpm/mars_rovers/mars.rover.should.test.ts b/test-pnpm/mars_rovers/mars.rover.should.test.ts index 5a87371..64a96cb 100644 --- a/test-pnpm/mars_rovers/mars.rover.should.test.ts +++ b/test-pnpm/mars_rovers/mars.rover.should.test.ts @@ -25,6 +25,12 @@ describe('mars rover should', () => { expect(actual).toBe('0:0:E'); }); + test('turn right twice facing south', () => { + const actual = marsRover.execute('RR'); + + expect(actual).toBe('0:0:S'); + }); + test('move in the same direction with a double move command', () => { const actual = marsRover.execute('MM'); From 4da7bafa81c7f424cd84feb0983fe5997bcbaf0f Mon Sep 17 00:00:00 2001 From: Vincent Farah Date: Sat, 17 May 2025 15:20:12 +0100 Subject: [PATCH 15/54] (GREEN) Fix incremental direction change --- test-pnpm/mars_rovers/mars.rover.ts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/test-pnpm/mars_rovers/mars.rover.ts b/test-pnpm/mars_rovers/mars.rover.ts index 8ea9201..a50d014 100644 --- a/test-pnpm/mars_rovers/mars.rover.ts +++ b/test-pnpm/mars_rovers/mars.rover.ts @@ -18,6 +18,13 @@ enum CommandType { export class MarsRover { private _direction: Direction = DirectionType.North; + private _directions: Direction[] = new Array( + DirectionType.North, + DirectionType.East, + DirectionType.South, + DirectionType.West, + ); + private _directionIndex = 0; private _x: number = 0; private _y: number = 0; @@ -27,9 +34,9 @@ export class MarsRover { this._y = this._y + 1; } if (command === CommandType.TurnRight) { - this._direction = DirectionType.East; + this._directionIndex = this._directionIndex + 1; } } - return `${this._x}:${this._y}:${this._direction}`; + return `${this._x}:${this._y}:${this._directions[this._directionIndex]}`; } } From 722bd884806cddd40a9d48598b2fd12085e59641 Mon Sep 17 00:00:00 2001 From: Vincent Farah Date: Sat, 17 May 2025 15:21:40 +0100 Subject: [PATCH 16/54] (REFACTOR): Remove unused variable --- test-pnpm/mars_rovers/mars.rover.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/test-pnpm/mars_rovers/mars.rover.ts b/test-pnpm/mars_rovers/mars.rover.ts index a50d014..e0dd8f8 100644 --- a/test-pnpm/mars_rovers/mars.rover.ts +++ b/test-pnpm/mars_rovers/mars.rover.ts @@ -17,7 +17,6 @@ enum CommandType { } export class MarsRover { - private _direction: Direction = DirectionType.North; private _directions: Direction[] = new Array( DirectionType.North, DirectionType.East, From 6ad42fbe63ef93e382f5745cc2c66ae8ce4851bf Mon Sep 17 00:00:00 2001 From: Vincent Farah Date: Sat, 17 May 2025 15:25:22 +0100 Subject: [PATCH 17/54] (RED) Failing test for turning to face north again --- test-pnpm/mars_rovers/mars.rover.should.test.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/test-pnpm/mars_rovers/mars.rover.should.test.ts b/test-pnpm/mars_rovers/mars.rover.should.test.ts index 64a96cb..2f4eb3b 100644 --- a/test-pnpm/mars_rovers/mars.rover.should.test.ts +++ b/test-pnpm/mars_rovers/mars.rover.should.test.ts @@ -31,6 +31,12 @@ describe('mars rover should', () => { expect(actual).toBe('0:0:S'); }); + test('turn right 4 times and face north again', () => { + const actual = marsRover.execute('RRRR'); + + expect(actual).toBe('0:0:N'); + }); + test('move in the same direction with a double move command', () => { const actual = marsRover.execute('MM'); From 46964fddc7936db0b0319d7abfd077d322f2e280 Mon Sep 17 00:00:00 2001 From: Vincent Farah Date: Sat, 17 May 2025 15:28:09 +0100 Subject: [PATCH 18/54] (GREEN) Implement simple cardinality to bring it back in the correct direction --- test-pnpm/mars_rovers/mars.rover.ts | 32 +++++++++++++++++++---------- 1 file changed, 21 insertions(+), 11 deletions(-) diff --git a/test-pnpm/mars_rovers/mars.rover.ts b/test-pnpm/mars_rovers/mars.rover.ts index e0dd8f8..8389aeb 100644 --- a/test-pnpm/mars_rovers/mars.rover.ts +++ b/test-pnpm/mars_rovers/mars.rover.ts @@ -1,10 +1,10 @@ type Direction = - | DirectionType.North - | DirectionType.East - | DirectionType.South - | DirectionType.West; + | CompassDirection.North + | CompassDirection.East + | CompassDirection.South + | CompassDirection.West; -enum DirectionType { +enum CompassDirection { North = 'N', East = 'E', South = 'S', @@ -18,10 +18,10 @@ enum CommandType { export class MarsRover { private _directions: Direction[] = new Array( - DirectionType.North, - DirectionType.East, - DirectionType.South, - DirectionType.West, + CompassDirection.North, + CompassDirection.East, + CompassDirection.South, + CompassDirection.West, ); private _directionIndex = 0; private _x: number = 0; @@ -30,12 +30,22 @@ export class MarsRover { execute(commands: string): string { for (const command of commands) { if (command === CommandType.Move) { - this._y = this._y + 1; + this.moveForward(); } + if (command === CommandType.TurnRight) { - this._directionIndex = this._directionIndex + 1; + this.moveRight(); } } + return `${this._x}:${this._y}:${this._directions[this._directionIndex]}`; } + + private moveRight() { + this._directionIndex = (this._directionIndex + 1) % this._directions.length; + } + + private moveForward() { + this._y = this._y + 1; + } } From f3fa38c3ec29827ecb666d921a92b623cee83808 Mon Sep 17 00:00:00 2001 From: Vincent Farah Date: Sat, 17 May 2025 15:47:04 +0100 Subject: [PATCH 19/54] (RED) Failing test for turning left --- test-pnpm/mars_rovers/mars.rover.should.test.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/test-pnpm/mars_rovers/mars.rover.should.test.ts b/test-pnpm/mars_rovers/mars.rover.should.test.ts index 2f4eb3b..3c64ed7 100644 --- a/test-pnpm/mars_rovers/mars.rover.should.test.ts +++ b/test-pnpm/mars_rovers/mars.rover.should.test.ts @@ -37,6 +37,12 @@ describe('mars rover should', () => { expect(actual).toBe('0:0:N'); }); + test('turn left facing west', () => { + const actual = marsRover.execute('L'); + + expect(actual).toBe('0:0:W'); + }); + test('move in the same direction with a double move command', () => { const actual = marsRover.execute('MM'); From 9ae336f9cc3bf6b791f8e9b56da7129035a048cf Mon Sep 17 00:00:00 2001 From: Vincent Farah Date: Sat, 17 May 2025 16:00:53 +0100 Subject: [PATCH 20/54] (GREEN) Matched existing moveRight pattern for symmmetry extracted currentDirection method to make it more redable --- test-pnpm/mars_rovers/mars.rover.ts | 29 ++++++++++++++++++++++------- 1 file changed, 22 insertions(+), 7 deletions(-) diff --git a/test-pnpm/mars_rovers/mars.rover.ts b/test-pnpm/mars_rovers/mars.rover.ts index 8389aeb..c51750d 100644 --- a/test-pnpm/mars_rovers/mars.rover.ts +++ b/test-pnpm/mars_rovers/mars.rover.ts @@ -14,6 +14,7 @@ enum CompassDirection { enum CommandType { Move = 'M', TurnRight = 'R', + TurnLeft = 'L', } export class MarsRover { @@ -29,16 +30,30 @@ export class MarsRover { execute(commands: string): string { for (const command of commands) { - if (command === CommandType.Move) { - this.moveForward(); - } - - if (command === CommandType.TurnRight) { - this.moveRight(); + switch (command) { + case CommandType.Move: + this.moveForward(); + break; + case CommandType.TurnRight: + this.moveRight(); + break; + case CommandType.TurnLeft: + this.moveLeft(); + break; } } - return `${this._x}:${this._y}:${this._directions[this._directionIndex]}`; + return `${this._x}:${this._y}:${this.currentDirection()}`; + } + + private currentDirection(): Direction { + return this._directions[this._directionIndex]; + } + + private moveLeft() { + // subtract 1 but wrap around if < 0 + this._directionIndex = + (this._directionIndex - 1 + this._directions.length) % this._directions.length; } private moveRight() { From 37d53a417d6c6808dc2f8acc07ce41f4cff5bb1c Mon Sep 17 00:00:00 2001 From: Vincent Farah Date: Sat, 17 May 2025 16:50:29 +0100 Subject: [PATCH 21/54] (RED) Failing test for moving north than south --- test-pnpm/mars_rovers/mars.rover.should.test.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/test-pnpm/mars_rovers/mars.rover.should.test.ts b/test-pnpm/mars_rovers/mars.rover.should.test.ts index 3c64ed7..0ae8b83 100644 --- a/test-pnpm/mars_rovers/mars.rover.should.test.ts +++ b/test-pnpm/mars_rovers/mars.rover.should.test.ts @@ -19,6 +19,12 @@ describe('mars rover should', () => { expect(actual).toBe('0:1:N'); }); + test('move forward and then turn facing south', () => { + const actual = marsRover.execute('MRRM'); + + expect(actual).toBe('0:0:S'); + }); + test('turn right facing east', () => { const actual = marsRover.execute('R'); From 93f944a8eb9f83e09cc33253e479f9ed082ed51c Mon Sep 17 00:00:00 2001 From: Vincent Farah Date: Sat, 17 May 2025 16:51:14 +0100 Subject: [PATCH 22/54] (GREEN) Fix move to cater for direction --- test-pnpm/mars_rovers/mars.rover.ts | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/test-pnpm/mars_rovers/mars.rover.ts b/test-pnpm/mars_rovers/mars.rover.ts index c51750d..86c3e6d 100644 --- a/test-pnpm/mars_rovers/mars.rover.ts +++ b/test-pnpm/mars_rovers/mars.rover.ts @@ -50,17 +50,22 @@ export class MarsRover { return this._directions[this._directionIndex]; } - private moveLeft() { + private moveLeft(): void { // subtract 1 but wrap around if < 0 this._directionIndex = (this._directionIndex - 1 + this._directions.length) % this._directions.length; } - private moveRight() { + private moveRight(): void { this._directionIndex = (this._directionIndex + 1) % this._directions.length; } - private moveForward() { - this._y = this._y + 1; + private moveForward(): void { + if (this.currentDirection() === CompassDirection.North) { + this._y = this._y + 1; + } + if (this.currentDirection() === CompassDirection.South) { + this._y = this._y - 1; + } } } From 2862a79c7962dbb5b2b7aa04ee3678d08352aa60 Mon Sep 17 00:00:00 2001 From: Vincent Farah Date: Sat, 17 May 2025 16:54:24 +0100 Subject: [PATCH 23/54] (REFACTOR): Swicth move ifs to case --- test-pnpm/mars_rovers/mars.rover.ts | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/test-pnpm/mars_rovers/mars.rover.ts b/test-pnpm/mars_rovers/mars.rover.ts index 86c3e6d..e6b14a8 100644 --- a/test-pnpm/mars_rovers/mars.rover.ts +++ b/test-pnpm/mars_rovers/mars.rover.ts @@ -61,11 +61,15 @@ export class MarsRover { } private moveForward(): void { - if (this.currentDirection() === CompassDirection.North) { - this._y = this._y + 1; - } - if (this.currentDirection() === CompassDirection.South) { - this._y = this._y - 1; + switch (this.currentDirection()) { + case CompassDirection.North: { + this._y = this._y + 1; + break; + } + case CompassDirection.South: { + this._y = this._y - 1; + break; + } } } } From 9037f023b80dc2f8a9349203e16c1f269f30298d Mon Sep 17 00:00:00 2001 From: Vincent Farah Date: Sat, 17 May 2025 16:57:45 +0100 Subject: [PATCH 24/54] (RED) Add failing test for moving east --- test-pnpm/mars_rovers/mars.rover.should.test.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/test-pnpm/mars_rovers/mars.rover.should.test.ts b/test-pnpm/mars_rovers/mars.rover.should.test.ts index 0ae8b83..00c4f80 100644 --- a/test-pnpm/mars_rovers/mars.rover.should.test.ts +++ b/test-pnpm/mars_rovers/mars.rover.should.test.ts @@ -54,4 +54,10 @@ describe('mars rover should', () => { expect(actual).toBe('0:2:N'); }); + + test('move forward facing east', () => { + const actual = marsRover.execute('RM'); + + expect(actual).toBe('1:0:E'); + }); }); From 44ebbf57a5e27e6af260c5846f790bdb97bdd990 Mon Sep 17 00:00:00 2001 From: Vincent Farah Date: Sat, 17 May 2025 16:58:07 +0100 Subject: [PATCH 25/54] (GREEN) Fix east move test --- test-pnpm/mars_rovers/mars.rover.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/test-pnpm/mars_rovers/mars.rover.ts b/test-pnpm/mars_rovers/mars.rover.ts index e6b14a8..2bba6d8 100644 --- a/test-pnpm/mars_rovers/mars.rover.ts +++ b/test-pnpm/mars_rovers/mars.rover.ts @@ -70,6 +70,10 @@ export class MarsRover { this._y = this._y - 1; break; } + case CompassDirection.East: { + this._x = this._x + 1; + break; + } } } } From 156942d7eee17fb9a284f686e186d9132a2991cc Mon Sep 17 00:00:00 2001 From: Vincent Farah Date: Sat, 17 May 2025 17:02:57 +0100 Subject: [PATCH 26/54] (RED) Add failing test for moving west --- test-pnpm/mars_rovers/mars.rover.should.test.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/test-pnpm/mars_rovers/mars.rover.should.test.ts b/test-pnpm/mars_rovers/mars.rover.should.test.ts index 00c4f80..1635136 100644 --- a/test-pnpm/mars_rovers/mars.rover.should.test.ts +++ b/test-pnpm/mars_rovers/mars.rover.should.test.ts @@ -60,4 +60,10 @@ describe('mars rover should', () => { expect(actual).toBe('1:0:E'); }); + + test('move forward facing west', () => { + const actual = marsRover.execute('RMLLM'); + + expect(actual).toBe('0:0:W'); + }); }); From 8d440cf5f9eb4c1098eff7e4e2c6018f1c800fba Mon Sep 17 00:00:00 2001 From: Vincent Farah Date: Sat, 17 May 2025 17:03:22 +0100 Subject: [PATCH 27/54] (GREEN) Fix west moving test --- test-pnpm/mars_rovers/mars.rover.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/test-pnpm/mars_rovers/mars.rover.ts b/test-pnpm/mars_rovers/mars.rover.ts index 2bba6d8..6329ee1 100644 --- a/test-pnpm/mars_rovers/mars.rover.ts +++ b/test-pnpm/mars_rovers/mars.rover.ts @@ -74,6 +74,10 @@ export class MarsRover { this._x = this._x + 1; break; } + case CompassDirection.West: { + this._x = this._x - 1; + break; + } } } } From 406248525dfae13a7627d6d5ae32625bba32a2ab Mon Sep 17 00:00:00 2001 From: Vincent Farah Date: Sat, 17 May 2025 17:06:35 +0100 Subject: [PATCH 28/54] (REFACTOR): Simplify test using test each --- .../mars_rovers/mars.rover.should.test.ts | 73 ++++--------------- 1 file changed, 15 insertions(+), 58 deletions(-) diff --git a/test-pnpm/mars_rovers/mars.rover.should.test.ts b/test-pnpm/mars_rovers/mars.rover.should.test.ts index 1635136..d4ba393 100644 --- a/test-pnpm/mars_rovers/mars.rover.should.test.ts +++ b/test-pnpm/mars_rovers/mars.rover.should.test.ts @@ -7,63 +7,20 @@ describe('mars rover should', () => { marsRover = new MarsRover(); }); - test('report its default start position', () => { - const actual = marsRover.execute(''); - - expect(actual).toBe('0:0:N'); - }); - - test('move in the same direction with a single move command', () => { - const actual = marsRover.execute('M'); - - expect(actual).toBe('0:1:N'); - }); - - test('move forward and then turn facing south', () => { - const actual = marsRover.execute('MRRM'); - - expect(actual).toBe('0:0:S'); - }); - - test('turn right facing east', () => { - const actual = marsRover.execute('R'); - - expect(actual).toBe('0:0:E'); - }); - - test('turn right twice facing south', () => { - const actual = marsRover.execute('RR'); - - expect(actual).toBe('0:0:S'); - }); - - test('turn right 4 times and face north again', () => { - const actual = marsRover.execute('RRRR'); - - expect(actual).toBe('0:0:N'); - }); - - test('turn left facing west', () => { - const actual = marsRover.execute('L'); - - expect(actual).toBe('0:0:W'); - }); - - test('move in the same direction with a double move command', () => { - const actual = marsRover.execute('MM'); - - expect(actual).toBe('0:2:N'); - }); - - test('move forward facing east', () => { - const actual = marsRover.execute('RM'); - - expect(actual).toBe('1:0:E'); - }); - - test('move forward facing west', () => { - const actual = marsRover.execute('RMLLM'); - - expect(actual).toBe('0:0:W'); + 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'], + ['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'], + ['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'], + ])('%s', (_, input, expected) => { + const actual = marsRover.execute(input); + + expect(actual).toBe(expected); }); }); From 98e626bb8a0398ab29c42191cf3b4ba40d9b6c40 Mon Sep 17 00:00:00 2001 From: Vincent Farah Date: Sat, 17 May 2025 17:17:01 +0100 Subject: [PATCH 29/54] (RED) Add failing test for north to bottom --- test-pnpm/mars_rovers/mars.rover.should.test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/test-pnpm/mars_rovers/mars.rover.should.test.ts b/test-pnpm/mars_rovers/mars.rover.should.test.ts index d4ba393..345ab90 100644 --- a/test-pnpm/mars_rovers/mars.rover.should.test.ts +++ b/test-pnpm/mars_rovers/mars.rover.should.test.ts @@ -18,6 +18,7 @@ describe('mars rover should', () => { ['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'], + ['wrap around north edge to bottom', 'MMMMMMMMMM', '0:0:N'], ])('%s', (_, input, expected) => { const actual = marsRover.execute(input); From a4ba99e3a7a4a6c3c88f1162863178028a3704d8 Mon Sep 17 00:00:00 2001 From: Vincent Farah Date: Sat, 17 May 2025 17:17:20 +0100 Subject: [PATCH 30/54] (GREEN) Fix failing test --- test-pnpm/mars_rovers/mars.rover.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test-pnpm/mars_rovers/mars.rover.ts b/test-pnpm/mars_rovers/mars.rover.ts index 6329ee1..dd56d6c 100644 --- a/test-pnpm/mars_rovers/mars.rover.ts +++ b/test-pnpm/mars_rovers/mars.rover.ts @@ -27,6 +27,7 @@ export class MarsRover { private _directionIndex = 0; private _x: number = 0; private _y: number = 0; + private _gridSize = 10; execute(commands: string): string { for (const command of commands) { @@ -63,7 +64,7 @@ export class MarsRover { private moveForward(): void { switch (this.currentDirection()) { case CompassDirection.North: { - this._y = this._y + 1; + this._y = (this._y + 1) % this._gridSize; break; } case CompassDirection.South: { From 5ea807fdf2302a06f9c935e4e48ed3b3bc2dcf93 Mon Sep 17 00:00:00 2001 From: Vincent Farah Date: Sat, 17 May 2025 17:20:48 +0100 Subject: [PATCH 31/54] (RED) Add failing test south bound --- test-pnpm/mars_rovers/mars.rover.should.test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/test-pnpm/mars_rovers/mars.rover.should.test.ts b/test-pnpm/mars_rovers/mars.rover.should.test.ts index 345ab90..5c09d99 100644 --- a/test-pnpm/mars_rovers/mars.rover.should.test.ts +++ b/test-pnpm/mars_rovers/mars.rover.should.test.ts @@ -19,6 +19,7 @@ describe('mars rover should', () => { ['move forward facing east', 'RM', '1:0:E'], ['move forward facing west', 'RMLLM', '0:0:W'], ['wrap around north edge to bottom', 'MMMMMMMMMM', '0:0:N'], + ['wrap around south edge to top', 'RRMMMMMMMMMM', '0:0:S'], ])('%s', (_, input, expected) => { const actual = marsRover.execute(input); From 5ab0bb145645ca70e818ca7ecddc600697f843ad Mon Sep 17 00:00:00 2001 From: Vincent Farah Date: Sat, 17 May 2025 17:21:00 +0100 Subject: [PATCH 32/54] (GREEN) Fix failing test --- test-pnpm/mars_rovers/mars.rover.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-pnpm/mars_rovers/mars.rover.ts b/test-pnpm/mars_rovers/mars.rover.ts index dd56d6c..beb6f49 100644 --- a/test-pnpm/mars_rovers/mars.rover.ts +++ b/test-pnpm/mars_rovers/mars.rover.ts @@ -68,7 +68,7 @@ export class MarsRover { break; } case CompassDirection.South: { - this._y = this._y - 1; + this._y = (this._y - 1) % this._gridSize; break; } case CompassDirection.East: { From 8cfe65712f990246cda69b0bb720883e917c11fb Mon Sep 17 00:00:00 2001 From: Vincent Farah Date: Sat, 17 May 2025 17:23:01 +0100 Subject: [PATCH 33/54] (RED) Add failing test east bound --- test-pnpm/mars_rovers/mars.rover.should.test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/test-pnpm/mars_rovers/mars.rover.should.test.ts b/test-pnpm/mars_rovers/mars.rover.should.test.ts index 5c09d99..ac0f736 100644 --- a/test-pnpm/mars_rovers/mars.rover.should.test.ts +++ b/test-pnpm/mars_rovers/mars.rover.should.test.ts @@ -20,6 +20,7 @@ describe('mars rover should', () => { ['move forward facing west', 'RMLLM', '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 left', 'RMMMMMMMMMM', '0:0:E'], ])('%s', (_, input, expected) => { const actual = marsRover.execute(input); From 32e52dc040d352db76b64e13b568f5e523707b07 Mon Sep 17 00:00:00 2001 From: Vincent Farah Date: Sat, 17 May 2025 17:23:10 +0100 Subject: [PATCH 34/54] (GREEN) Fix failing test --- test-pnpm/mars_rovers/mars.rover.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-pnpm/mars_rovers/mars.rover.ts b/test-pnpm/mars_rovers/mars.rover.ts index beb6f49..fe2fd3e 100644 --- a/test-pnpm/mars_rovers/mars.rover.ts +++ b/test-pnpm/mars_rovers/mars.rover.ts @@ -72,7 +72,7 @@ export class MarsRover { break; } case CompassDirection.East: { - this._x = this._x + 1; + this._x = (this._x + 1) % this._gridSize; break; } case CompassDirection.West: { From 886465bb785721ace850276310e8025e1389bac0 Mon Sep 17 00:00:00 2001 From: Vincent Farah Date: Sat, 17 May 2025 17:25:42 +0100 Subject: [PATCH 35/54] (RED) Add failing test west bound --- test-pnpm/mars_rovers/mars.rover.should.test.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test-pnpm/mars_rovers/mars.rover.should.test.ts b/test-pnpm/mars_rovers/mars.rover.should.test.ts index ac0f736..ad8551e 100644 --- a/test-pnpm/mars_rovers/mars.rover.should.test.ts +++ b/test-pnpm/mars_rovers/mars.rover.should.test.ts @@ -20,7 +20,8 @@ describe('mars rover should', () => { ['move forward facing west', 'RMLLM', '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 left', 'RMMMMMMMMMM', '0:0:E'], + ['wrap around east edge to the left', 'RMMMMMMMMMM', '0:0:E'], + ['wrap around west edge to the right', 'LMMMMMMMMMM', '0:0:W'], ])('%s', (_, input, expected) => { const actual = marsRover.execute(input); From bd7b6cf6b8b2c994526e401b8e94e9688a631381 Mon Sep 17 00:00:00 2001 From: Vincent Farah Date: Sat, 17 May 2025 17:25:55 +0100 Subject: [PATCH 36/54] (GREEN) Fix failing test --- test-pnpm/mars_rovers/mars.rover.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-pnpm/mars_rovers/mars.rover.ts b/test-pnpm/mars_rovers/mars.rover.ts index fe2fd3e..a41d844 100644 --- a/test-pnpm/mars_rovers/mars.rover.ts +++ b/test-pnpm/mars_rovers/mars.rover.ts @@ -76,7 +76,7 @@ export class MarsRover { break; } case CompassDirection.West: { - this._x = this._x - 1; + this._x = (this._x - 1) % this._gridSize; break; } } From 698086eb86eddf76ac4f82e267aaf74647510cec Mon Sep 17 00:00:00 2001 From: Vincent Farah Date: Sat, 17 May 2025 17:27:16 +0100 Subject: [PATCH 37/54] (REFACTOR): Group tests so it is easier to understand --- test-pnpm/mars_rovers/mars.rover.should.test.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/test-pnpm/mars_rovers/mars.rover.should.test.ts b/test-pnpm/mars_rovers/mars.rover.should.test.ts index ad8551e..c9c1e9b 100644 --- a/test-pnpm/mars_rovers/mars.rover.should.test.ts +++ b/test-pnpm/mars_rovers/mars.rover.should.test.ts @@ -9,15 +9,18 @@ describe('mars rover should', () => { 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'], - ['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'], + ['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'], From d6493d2c556de4590bd1a55a42c41cdf38a3b8b8 Mon Sep 17 00:00:00 2001 From: Vincent Farah Date: Sat, 17 May 2025 18:06:35 +0100 Subject: [PATCH 38/54] (REFACTOR): Extract Position class to rid primitive obsession & tight coupling --- test-pnpm/mars_rovers/mars.rover.ts | 15 ++++++++------- test-pnpm/mars_rovers/position.ts | 6 ++++++ 2 files changed, 14 insertions(+), 7 deletions(-) create mode 100644 test-pnpm/mars_rovers/position.ts diff --git a/test-pnpm/mars_rovers/mars.rover.ts b/test-pnpm/mars_rovers/mars.rover.ts index a41d844..c334e80 100644 --- a/test-pnpm/mars_rovers/mars.rover.ts +++ b/test-pnpm/mars_rovers/mars.rover.ts @@ -1,3 +1,5 @@ +import { Position } from './position'; + type Direction = | CompassDirection.North | CompassDirection.East @@ -18,6 +20,7 @@ enum CommandType { } export class MarsRover { + private _position = new Position(0, 0); private _directions: Direction[] = new Array( CompassDirection.North, CompassDirection.East, @@ -25,8 +28,6 @@ export class MarsRover { CompassDirection.West, ); private _directionIndex = 0; - private _x: number = 0; - private _y: number = 0; private _gridSize = 10; execute(commands: string): string { @@ -44,7 +45,7 @@ export class MarsRover { } } - return `${this._x}:${this._y}:${this.currentDirection()}`; + return `${this._position.x}:${this._position.y}:${this.currentDirection()}`; } private currentDirection(): Direction { @@ -64,19 +65,19 @@ export class MarsRover { private moveForward(): void { switch (this.currentDirection()) { case CompassDirection.North: { - this._y = (this._y + 1) % this._gridSize; + this._position.y = (this._position.y + 1) % this._gridSize; break; } case CompassDirection.South: { - this._y = (this._y - 1) % this._gridSize; + this._position.y = (this._position.y - 1) % this._gridSize; break; } case CompassDirection.East: { - this._x = (this._x + 1) % this._gridSize; + this._position.x = (this._position.x + 1) % this._gridSize; break; } case CompassDirection.West: { - this._x = (this._x - 1) % this._gridSize; + this._position.x = (this._position.x - 1) % this._gridSize; break; } } diff --git a/test-pnpm/mars_rovers/position.ts b/test-pnpm/mars_rovers/position.ts new file mode 100644 index 0000000..38116d8 --- /dev/null +++ b/test-pnpm/mars_rovers/position.ts @@ -0,0 +1,6 @@ +export class Position { + constructor( + public x: number, + public y: number, + ) {} +} From 2908174d988833f335f92d44215d7b24b1b5a6c8 Mon Sep 17 00:00:00 2001 From: Vincent Farah Date: Sat, 17 May 2025 18:31:48 +0100 Subject: [PATCH 39/54] (REFACTOR): Move north positioning ontoPosition --- test-pnpm/mars_rovers/mars.rover.ts | 2 +- test-pnpm/mars_rovers/position.ts | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/test-pnpm/mars_rovers/mars.rover.ts b/test-pnpm/mars_rovers/mars.rover.ts index c334e80..ef55069 100644 --- a/test-pnpm/mars_rovers/mars.rover.ts +++ b/test-pnpm/mars_rovers/mars.rover.ts @@ -65,7 +65,7 @@ export class MarsRover { private moveForward(): void { switch (this.currentDirection()) { case CompassDirection.North: { - this._position.y = (this._position.y + 1) % this._gridSize; + this._position = this._position.moveNorth(this._gridSize); break; } case CompassDirection.South: { diff --git a/test-pnpm/mars_rovers/position.ts b/test-pnpm/mars_rovers/position.ts index 38116d8..09790f9 100644 --- a/test-pnpm/mars_rovers/position.ts +++ b/test-pnpm/mars_rovers/position.ts @@ -3,4 +3,8 @@ export class Position { public x: number, public y: number, ) {} + + moveNorth(gridSize: number): Position { + return new Position(this.x, (this.y + 1) % gridSize); + } } From 8d5f1ffb9d14b701314f11d86ecd18e872f2ccf0 Mon Sep 17 00:00:00 2001 From: Vincent Farah Date: Sat, 17 May 2025 18:35:10 +0100 Subject: [PATCH 40/54] (REFACTOR): Move south positioning onto Position --- test-pnpm/mars_rovers/mars.rover.ts | 2 +- test-pnpm/mars_rovers/position.ts | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/test-pnpm/mars_rovers/mars.rover.ts b/test-pnpm/mars_rovers/mars.rover.ts index ef55069..4b17e72 100644 --- a/test-pnpm/mars_rovers/mars.rover.ts +++ b/test-pnpm/mars_rovers/mars.rover.ts @@ -69,7 +69,7 @@ export class MarsRover { break; } case CompassDirection.South: { - this._position.y = (this._position.y - 1) % this._gridSize; + this._position = this._position.moveSouth(this._gridSize); break; } case CompassDirection.East: { diff --git a/test-pnpm/mars_rovers/position.ts b/test-pnpm/mars_rovers/position.ts index 09790f9..46dae4f 100644 --- a/test-pnpm/mars_rovers/position.ts +++ b/test-pnpm/mars_rovers/position.ts @@ -7,4 +7,8 @@ export class Position { moveNorth(gridSize: number): Position { return new Position(this.x, (this.y + 1) % gridSize); } + + moveSouth(gridSize: number): Position { + return new Position(this.x, (this.y - 1) % gridSize); + } } From a99ee73f9f976becf65cc9dee4e75f730334cc6f Mon Sep 17 00:00:00 2001 From: Vincent Farah Date: Sat, 17 May 2025 18:38:17 +0100 Subject: [PATCH 41/54] (REFACTOR): Move east positioning onto Position --- test-pnpm/mars_rovers/mars.rover.ts | 2 +- test-pnpm/mars_rovers/position.ts | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/test-pnpm/mars_rovers/mars.rover.ts b/test-pnpm/mars_rovers/mars.rover.ts index 4b17e72..3dd1d83 100644 --- a/test-pnpm/mars_rovers/mars.rover.ts +++ b/test-pnpm/mars_rovers/mars.rover.ts @@ -73,7 +73,7 @@ export class MarsRover { break; } case CompassDirection.East: { - this._position.x = (this._position.x + 1) % this._gridSize; + this._position = this._position.moveEast(this._gridSize); break; } case CompassDirection.West: { diff --git a/test-pnpm/mars_rovers/position.ts b/test-pnpm/mars_rovers/position.ts index 46dae4f..f51f9f7 100644 --- a/test-pnpm/mars_rovers/position.ts +++ b/test-pnpm/mars_rovers/position.ts @@ -11,4 +11,8 @@ export class Position { moveSouth(gridSize: number): Position { return new Position(this.x, (this.y - 1) % gridSize); } + + moveEast(gridSize: number): Position { + return new Position((this.x + 1) % gridSize, this.y); + } } From 7fe3ca5276ead81863a0c8265de781d3ba359021 Mon Sep 17 00:00:00 2001 From: Vincent Farah Date: Sat, 17 May 2025 18:44:01 +0100 Subject: [PATCH 42/54] (REFACTOR): Move west and seal position --- test-pnpm/mars_rovers/mars.rover.ts | 2 +- test-pnpm/mars_rovers/position.ts | 8 ++++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/test-pnpm/mars_rovers/mars.rover.ts b/test-pnpm/mars_rovers/mars.rover.ts index 3dd1d83..bc7dc2e 100644 --- a/test-pnpm/mars_rovers/mars.rover.ts +++ b/test-pnpm/mars_rovers/mars.rover.ts @@ -77,7 +77,7 @@ export class MarsRover { break; } case CompassDirection.West: { - this._position.x = (this._position.x - 1) % this._gridSize; + this._position = this._position.moveWest(this._gridSize); break; } } diff --git a/test-pnpm/mars_rovers/position.ts b/test-pnpm/mars_rovers/position.ts index f51f9f7..820bbd8 100644 --- a/test-pnpm/mars_rovers/position.ts +++ b/test-pnpm/mars_rovers/position.ts @@ -1,7 +1,7 @@ export class Position { constructor( - public x: number, - public y: number, + public readonly x: number, + public readonly y: number, ) {} moveNorth(gridSize: number): Position { @@ -15,4 +15,8 @@ export class Position { moveEast(gridSize: number): Position { return new Position((this.x + 1) % gridSize, this.y); } + + moveWest(gridSize: number): Position { + return new Position((this.x - 1) % gridSize, this.y); + } } From 708ab9a54f47a0ddcf1bf5ad19526644e75982a2 Mon Sep 17 00:00:00 2001 From: Vincent Farah Date: Sat, 17 May 2025 18:50:01 +0100 Subject: [PATCH 43/54] (REFACTOR): Move position corodinates as string --- test-pnpm/mars_rovers/mars.rover.ts | 2 +- test-pnpm/mars_rovers/position.ts | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/test-pnpm/mars_rovers/mars.rover.ts b/test-pnpm/mars_rovers/mars.rover.ts index bc7dc2e..48c6049 100644 --- a/test-pnpm/mars_rovers/mars.rover.ts +++ b/test-pnpm/mars_rovers/mars.rover.ts @@ -45,7 +45,7 @@ export class MarsRover { } } - return `${this._position.x}:${this._position.y}:${this.currentDirection()}`; + return `${this._position.toString()}:${this.currentDirection()}`; } private currentDirection(): Direction { diff --git a/test-pnpm/mars_rovers/position.ts b/test-pnpm/mars_rovers/position.ts index 820bbd8..99be5a3 100644 --- a/test-pnpm/mars_rovers/position.ts +++ b/test-pnpm/mars_rovers/position.ts @@ -19,4 +19,8 @@ export class Position { moveWest(gridSize: number): Position { return new Position((this.x - 1) % gridSize, this.y); } + + toString(): string { + return `${this.x}:${this.y}`; + } } From 2547e0f240f1ecd0881af6198da5b8f29a3031c8 Mon Sep 17 00:00:00 2001 From: Vincent Farah Date: Sat, 17 May 2025 18:51:43 +0100 Subject: [PATCH 44/54] (REFACTOR): Reduce switch to tuple --- test-pnpm/mars_rovers/mars.rover.ts | 25 +++++++------------------ 1 file changed, 7 insertions(+), 18 deletions(-) diff --git a/test-pnpm/mars_rovers/mars.rover.ts b/test-pnpm/mars_rovers/mars.rover.ts index 48c6049..928b01e 100644 --- a/test-pnpm/mars_rovers/mars.rover.ts +++ b/test-pnpm/mars_rovers/mars.rover.ts @@ -63,23 +63,12 @@ export class MarsRover { } private moveForward(): void { - switch (this.currentDirection()) { - case CompassDirection.North: { - this._position = this._position.moveNorth(this._gridSize); - break; - } - case CompassDirection.South: { - this._position = this._position.moveSouth(this._gridSize); - break; - } - case CompassDirection.East: { - this._position = this._position.moveEast(this._gridSize); - break; - } - case CompassDirection.West: { - this._position = this._position.moveWest(this._gridSize); - break; - } - } + const positionMap: Record Position> = { + [CompassDirection.North]: () => this._position.moveNorth(this._gridSize), + [CompassDirection.South]: () => this._position.moveSouth(this._gridSize), + [CompassDirection.East]: () => this._position.moveEast(this._gridSize), + [CompassDirection.West]: () => this._position.moveWest(this._gridSize), + }; + this._position = positionMap[this.currentDirection()](); } } From b5f55aee0b6f0fa9ac71e3dfb5d71c43d049c3d6 Mon Sep 17 00:00:00 2001 From: Vincent Farah Date: Sat, 17 May 2025 19:49:35 +0100 Subject: [PATCH 45/54] (REFACTOR): Extract Direction and refactor rover --- test-pnpm/mars_rovers/direction.ts | 31 ++++++++++++++++++++++++++++ test-pnpm/mars_rovers/mars.rover.ts | 32 ++++++----------------------- 2 files changed, 37 insertions(+), 26 deletions(-) create mode 100644 test-pnpm/mars_rovers/direction.ts diff --git a/test-pnpm/mars_rovers/direction.ts b/test-pnpm/mars_rovers/direction.ts new file mode 100644 index 0000000..5b0bd27 --- /dev/null +++ b/test-pnpm/mars_rovers/direction.ts @@ -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.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]); + } +} diff --git a/test-pnpm/mars_rovers/mars.rover.ts b/test-pnpm/mars_rovers/mars.rover.ts index 928b01e..d9c8e9e 100644 --- a/test-pnpm/mars_rovers/mars.rover.ts +++ b/test-pnpm/mars_rovers/mars.rover.ts @@ -1,18 +1,6 @@ +import { CompassDirection, Direction } from './direction'; import { Position } from './position'; -type Direction = - | CompassDirection.North - | CompassDirection.East - | CompassDirection.South - | CompassDirection.West; - -enum CompassDirection { - North = 'N', - East = 'E', - South = 'S', - West = 'W', -} - enum CommandType { Move = 'M', TurnRight = 'R', @@ -21,13 +9,7 @@ enum CommandType { export class MarsRover { private _position = new Position(0, 0); - private _directions: Direction[] = new Array( - CompassDirection.North, - CompassDirection.East, - CompassDirection.South, - CompassDirection.West, - ); - private _directionIndex = 0; + private _direction = new Direction(CompassDirection.North); private _gridSize = 10; execute(commands: string): string { @@ -48,18 +30,16 @@ export class MarsRover { return `${this._position.toString()}:${this.currentDirection()}`; } - private currentDirection(): Direction { - return this._directions[this._directionIndex]; + private currentDirection(): CompassDirection { + return this._direction.value; } private moveLeft(): void { - // subtract 1 but wrap around if < 0 - this._directionIndex = - (this._directionIndex - 1 + this._directions.length) % this._directions.length; + this._direction = this._direction.rotateLeft(); } private moveRight(): void { - this._directionIndex = (this._directionIndex + 1) % this._directions.length; + this._direction = this._direction.rotateRight(); } private moveForward(): void { From 425dec038276711287cf9d0ef285ca00e9bed1c0 Mon Sep 17 00:00:00 2001 From: Vincent Farah Date: Sat, 17 May 2025 20:16:32 +0100 Subject: [PATCH 46/54] (REFACTOR): Fix law of demeter violation --- test-pnpm/mars_rovers/mars.rover.ts | 25 +++++++------------------ test-pnpm/mars_rovers/position.ts | 23 +++++++++++++++++++---- 2 files changed, 26 insertions(+), 22 deletions(-) diff --git a/test-pnpm/mars_rovers/mars.rover.ts b/test-pnpm/mars_rovers/mars.rover.ts index d9c8e9e..9ba7a5e 100644 --- a/test-pnpm/mars_rovers/mars.rover.ts +++ b/test-pnpm/mars_rovers/mars.rover.ts @@ -11,20 +11,15 @@ export class MarsRover { private _position = new Position(0, 0); private _direction = new Direction(CompassDirection.North); private _gridSize = 10; + private readonly commandHandlers: Record void> = { + [CommandType.Move]: () => this.moveForward(), + [CommandType.TurnRight]: () => this.moveRight(), + [CommandType.TurnLeft]: () => this.moveLeft(), + }; execute(commands: string): string { for (const command of commands) { - switch (command) { - case CommandType.Move: - this.moveForward(); - break; - case CommandType.TurnRight: - this.moveRight(); - break; - case CommandType.TurnLeft: - this.moveLeft(); - break; - } + this.commandHandlers[command](); } return `${this._position.toString()}:${this.currentDirection()}`; @@ -43,12 +38,6 @@ export class MarsRover { } private moveForward(): void { - const positionMap: Record Position> = { - [CompassDirection.North]: () => this._position.moveNorth(this._gridSize), - [CompassDirection.South]: () => this._position.moveSouth(this._gridSize), - [CompassDirection.East]: () => this._position.moveEast(this._gridSize), - [CompassDirection.West]: () => this._position.moveWest(this._gridSize), - }; - this._position = positionMap[this.currentDirection()](); + this._position = this._position.move(this.currentDirection(), this._gridSize); } } diff --git a/test-pnpm/mars_rovers/position.ts b/test-pnpm/mars_rovers/position.ts index 99be5a3..54fb578 100644 --- a/test-pnpm/mars_rovers/position.ts +++ b/test-pnpm/mars_rovers/position.ts @@ -1,22 +1,37 @@ +import { CompassDirection } from './direction'; + export class Position { constructor( public readonly x: number, public readonly y: number, ) {} - moveNorth(gridSize: number): Position { + move(direction: CompassDirection, gridSize: number): Position { + switch (direction) { + case CompassDirection.North: + return this.moveNorth(gridSize); + case CompassDirection.South: + return this.moveSouth(gridSize); + case CompassDirection.East: + return this.moveEast(gridSize); + case CompassDirection.West: + return this.moveWest(gridSize); + } + } + + private moveNorth(gridSize: number): Position { return new Position(this.x, (this.y + 1) % gridSize); } - moveSouth(gridSize: number): Position { + private moveSouth(gridSize: number): Position { return new Position(this.x, (this.y - 1) % gridSize); } - moveEast(gridSize: number): Position { + private moveEast(gridSize: number): Position { return new Position((this.x + 1) % gridSize, this.y); } - moveWest(gridSize: number): Position { + private moveWest(gridSize: number): Position { return new Position((this.x - 1) % gridSize, this.y); } From e7e5ad83066cdc75bea4900b913c32e4edee98f9 Mon Sep 17 00:00:00 2001 From: Vincent Farah Date: Sat, 17 May 2025 20:25:03 +0100 Subject: [PATCH 47/54] (REFACTOR): Add and refactor final full tests with no obstacles --- test-pnpm/mars_rovers/mars.rover.should.test.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/test-pnpm/mars_rovers/mars.rover.should.test.ts b/test-pnpm/mars_rovers/mars.rover.should.test.ts index c9c1e9b..8cb66a8 100644 --- a/test-pnpm/mars_rovers/mars.rover.should.test.ts +++ b/test-pnpm/mars_rovers/mars.rover.should.test.ts @@ -25,7 +25,10 @@ describe('mars rover should', () => { ['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'], - ])('%s', (_, input, expected) => { + + ['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); From ddec397915d4732438900e59e803df456e2c21ff Mon Sep 17 00:00:00 2001 From: Vincent Farah Date: Sun, 18 May 2025 12:23:32 +0100 Subject: [PATCH 48/54] (REFACTOR): Fix case to get back into it --- test-pnpm/mars_rovers/position.ts | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/test-pnpm/mars_rovers/position.ts b/test-pnpm/mars_rovers/position.ts index 54fb578..fb845a9 100644 --- a/test-pnpm/mars_rovers/position.ts +++ b/test-pnpm/mars_rovers/position.ts @@ -7,16 +7,12 @@ export class Position { ) {} move(direction: CompassDirection, gridSize: number): Position { - switch (direction) { - case CompassDirection.North: - return this.moveNorth(gridSize); - case CompassDirection.South: - return this.moveSouth(gridSize); - case CompassDirection.East: - return this.moveEast(gridSize); - case CompassDirection.West: - return this.moveWest(gridSize); - } + return { + [CompassDirection.North]: () => this.moveNorth(gridSize), + [CompassDirection.East]: () => this.moveEast(gridSize), + [CompassDirection.South]: () => this.moveSouth(gridSize), + [CompassDirection.West]: () => this.moveWest(gridSize), + }[direction](); } private moveNorth(gridSize: number): Position { From 4dbb0e811cf6bb22190cb7e47416f6561486656b Mon Sep 17 00:00:00 2001 From: Vincent Farah Date: Sun, 18 May 2025 14:30:22 +0100 Subject: [PATCH 49/54] (RED) Add failing test for obstacle --- test-pnpm/mars_rovers/mars.rover.should.test.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/test-pnpm/mars_rovers/mars.rover.should.test.ts b/test-pnpm/mars_rovers/mars.rover.should.test.ts index 8cb66a8..8109478 100644 --- a/test-pnpm/mars_rovers/mars.rover.should.test.ts +++ b/test-pnpm/mars_rovers/mars.rover.should.test.ts @@ -1,4 +1,5 @@ import { MarsRover } from './mars.rover'; +import { Position } from './position'; describe('mars rover should', () => { let marsRover: MarsRover; @@ -33,4 +34,15 @@ describe('mars rover should', () => { expect(actual).toBe(expected); }); + + describe('when given obstacles should', () => { + test('navigate up to the obstacle and no further', () => { + var obstacle = new Position(0, 3); + marsRover = new MarsRover([obstacle]); + + var actual = marsRover.execute('MMMM'); + + expect(actual).toBe('O:0:2:N'); + }); + }); }); From 23f9714e50f3fa89cf36936ef99dbce7408d0345 Mon Sep 17 00:00:00 2001 From: Vincent Farah Date: Sun, 18 May 2025 14:40:57 +0100 Subject: [PATCH 50/54] (GREEN) Fix failing obstacle test --- test-pnpm/mars_rovers/mars.rover.ts | 31 +++++++++++++++++++---------- test-pnpm/mars_rovers/position.ts | 20 +++++++++++++------ 2 files changed, 35 insertions(+), 16 deletions(-) diff --git a/test-pnpm/mars_rovers/mars.rover.ts b/test-pnpm/mars_rovers/mars.rover.ts index 9ba7a5e..efb3f16 100644 --- a/test-pnpm/mars_rovers/mars.rover.ts +++ b/test-pnpm/mars_rovers/mars.rover.ts @@ -8,21 +8,29 @@ enum CommandType { } export class MarsRover { - private _position = new Position(0, 0); - private _direction = new Direction(CompassDirection.North); - private _gridSize = 10; - private readonly commandHandlers: Record void> = { - [CommandType.Move]: () => this.moveForward(), - [CommandType.TurnRight]: () => this.moveRight(), - [CommandType.TurnLeft]: () => this.moveLeft(), - }; + private _position: Position; + private _direction: Direction; + private readonly commandHandlers: Record void>; + + constructor( + obstacles: Array | null = null, + private readonly _gridSize = 10, + ) { + this._position = new Position(0, 0, obstacles); + 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](); } - return `${this._position.toString()}:${this.currentDirection()}`; + return `${this._position.toString(this.currentDirection(), this._gridSize)}`; } private currentDirection(): CompassDirection { @@ -38,6 +46,9 @@ export class MarsRover { } private moveForward(): void { - this._position = this._position.move(this.currentDirection(), this._gridSize); + const nextPosition = this._position.move(this.currentDirection(), this._gridSize); + if (!nextPosition.hasObstacle()) { + this._position = nextPosition; + } } } diff --git a/test-pnpm/mars_rovers/position.ts b/test-pnpm/mars_rovers/position.ts index fb845a9..a493039 100644 --- a/test-pnpm/mars_rovers/position.ts +++ b/test-pnpm/mars_rovers/position.ts @@ -4,6 +4,7 @@ export class Position { constructor( public readonly x: number, public readonly y: number, + public readonly obstacles: Array = null, ) {} move(direction: CompassDirection, gridSize: number): Position { @@ -15,23 +16,30 @@ export class Position { }[direction](); } + hasObstacle(): boolean { + return this.obstacles?.some((obstacle) => obstacle.x === this.x && obstacle.y === this.y); + } + private moveNorth(gridSize: number): Position { - return new Position(this.x, (this.y + 1) % gridSize); + return new Position(this.x, (this.y + 1) % gridSize, this.obstacles); } private moveSouth(gridSize: number): Position { - return new Position(this.x, (this.y - 1) % gridSize); + return new Position(this.x, (this.y - 1) % gridSize, this.obstacles); } private moveEast(gridSize: number): Position { - return new Position((this.x + 1) % gridSize, this.y); + return new Position((this.x + 1) % gridSize, this.y, this.obstacles); } private moveWest(gridSize: number): Position { - return new Position((this.x - 1) % gridSize, this.y); + return new Position((this.x - 1 + gridSize) % gridSize, this.y, this.obstacles); } - toString(): string { - return `${this.x}:${this.y}`; + toString(currentDirection: CompassDirection, gridSize: number): string { + const nextMove = this.move(currentDirection, gridSize); + const OBSTACLE_PREFIX = 'O:'; + const NO_PREFIX = ''; + return `${nextMove.hasObstacle() ? OBSTACLE_PREFIX : NO_PREFIX}${this.x}:${this.y}:${currentDirection}`; } } From 3431ad61b01f0daf06cf51b6792cf4645754015d Mon Sep 17 00:00:00 2001 From: Vincent Farah Date: Sun, 18 May 2025 15:30:06 +0100 Subject: [PATCH 51/54] (RED) Refactor to extract grid --- test-pnpm/mars_rovers/mars.rover.should.test.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/test-pnpm/mars_rovers/mars.rover.should.test.ts b/test-pnpm/mars_rovers/mars.rover.should.test.ts index 8109478..060086c 100644 --- a/test-pnpm/mars_rovers/mars.rover.should.test.ts +++ b/test-pnpm/mars_rovers/mars.rover.should.test.ts @@ -1,11 +1,14 @@ +import { Grid } from './grid'; import { MarsRover } from './mars.rover'; import { Position } from './position'; describe('mars rover should', () => { + let grid: Grid; let marsRover: MarsRover; beforeEach(() => { - marsRover = new MarsRover(); + grid = new Grid(); + marsRover = new MarsRover(grid); }); test.each([ @@ -38,7 +41,8 @@ describe('mars rover should', () => { describe('when given obstacles should', () => { test('navigate up to the obstacle and no further', () => { var obstacle = new Position(0, 3); - marsRover = new MarsRover([obstacle]); + grid = new Grid([obstacle]); + marsRover = new MarsRover(grid); var actual = marsRover.execute('MMMM'); From 1d8b39ae9b628eb26fc8a273778c6ae408eaf48b Mon Sep 17 00:00:00 2001 From: Vincent Farah Date: Sun, 18 May 2025 15:31:53 +0100 Subject: [PATCH 52/54] (GREEN/REFACTOR) Centralise grid logic Decouple position from obstacle presence Improve Wrap logic on the grid to be more redable --- test-pnpm/mars_rovers/grid.ts | 24 +++++++++++++++++ test-pnpm/mars_rovers/mars.rover.ts | 19 +++++++------ test-pnpm/mars_rovers/position.ts | 42 +++++++---------------------- 3 files changed, 43 insertions(+), 42 deletions(-) create mode 100644 test-pnpm/mars_rovers/grid.ts diff --git a/test-pnpm/mars_rovers/grid.ts b/test-pnpm/mars_rovers/grid.ts new file mode 100644 index 0000000..383e63d --- /dev/null +++ b/test-pnpm/mars_rovers/grid.ts @@ -0,0 +1,24 @@ +import { Position } from './position'; + +export type Cordinate = { + x: number; + y: number; +}; + +export class Grid { + constructor( + private readonly _obstacles: Array | 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, + }; + } +} diff --git a/test-pnpm/mars_rovers/mars.rover.ts b/test-pnpm/mars_rovers/mars.rover.ts index efb3f16..77087b9 100644 --- a/test-pnpm/mars_rovers/mars.rover.ts +++ b/test-pnpm/mars_rovers/mars.rover.ts @@ -1,4 +1,5 @@ import { CompassDirection, Direction } from './direction'; +import { Grid } from './grid'; import { Position } from './position'; enum CommandType { @@ -12,11 +13,8 @@ export class MarsRover { private _direction: Direction; private readonly commandHandlers: Record void>; - constructor( - obstacles: Array | null = null, - private readonly _gridSize = 10, - ) { - this._position = new Position(0, 0, obstacles); + constructor(private readonly _grid: Grid) { + this._position = new Position(0, 0); this._direction = new Direction(CompassDirection.North); this.commandHandlers = { [CommandType.Move]: () => this.moveForward(), @@ -29,8 +27,9 @@ export class MarsRover { for (const command of commands) { this.commandHandlers[command](); } - - return `${this._position.toString(this.currentDirection(), this._gridSize)}`; +C 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 { @@ -46,9 +45,9 @@ export class MarsRover { } private moveForward(): void { - const nextPosition = this._position.move(this.currentDirection(), this._gridSize); - if (!nextPosition.hasObstacle()) { - this._position = nextPosition; + const next = this._position.move(this.currentDirection(), this._grid); + if (!this._grid.hasObstacle(next.x, next.y)) { + this._position = next; } } } diff --git a/test-pnpm/mars_rovers/position.ts b/test-pnpm/mars_rovers/position.ts index a493039..b3f1566 100644 --- a/test-pnpm/mars_rovers/position.ts +++ b/test-pnpm/mars_rovers/position.ts @@ -1,45 +1,23 @@ import { CompassDirection } from './direction'; +import { Grid } from './grid'; export class Position { constructor( public readonly x: number, public readonly y: number, - public readonly obstacles: Array = null, ) {} - move(direction: CompassDirection, gridSize: number): Position { - return { - [CompassDirection.North]: () => this.moveNorth(gridSize), - [CompassDirection.East]: () => this.moveEast(gridSize), - [CompassDirection.South]: () => this.moveSouth(gridSize), - [CompassDirection.West]: () => this.moveWest(gridSize), + move(direction: CompassDirection, grid: Grid): Position { + const { x, y } = { + [CompassDirection.North]: () => grid.wrap(this.x, this.y + 1), + [CompassDirection.South]: () => grid.wrap(this.x, this.y - 1), + [CompassDirection.East]: () => grid.wrap(this.x + 1, this.y), + [CompassDirection.West]: () => grid.wrap(this.x - 1, this.y), }[direction](); + return new Position(x, y); } - hasObstacle(): boolean { - return this.obstacles?.some((obstacle) => obstacle.x === this.x && obstacle.y === this.y); - } - - private moveNorth(gridSize: number): Position { - return new Position(this.x, (this.y + 1) % gridSize, this.obstacles); - } - - private moveSouth(gridSize: number): Position { - return new Position(this.x, (this.y - 1) % gridSize, this.obstacles); - } - - private moveEast(gridSize: number): Position { - return new Position((this.x + 1) % gridSize, this.y, this.obstacles); - } - - private moveWest(gridSize: number): Position { - return new Position((this.x - 1 + gridSize) % gridSize, this.y, this.obstacles); - } - - toString(currentDirection: CompassDirection, gridSize: number): string { - const nextMove = this.move(currentDirection, gridSize); - const OBSTACLE_PREFIX = 'O:'; - const NO_PREFIX = ''; - return `${nextMove.hasObstacle() ? OBSTACLE_PREFIX : NO_PREFIX}${this.x}:${this.y}:${currentDirection}`; + toString(): string { + return `${this.x}:${this.y}`; } } From 14832e1a31372b86402fc7d744bf51a2c9d6daa6 Mon Sep 17 00:00:00 2001 From: Vincent Farah Date: Sun, 18 May 2025 15:45:51 +0100 Subject: [PATCH 53/54] (REFACTOR) Clean up test with GridBuilder --- test-pnpm/mars_rovers/grid.builder.ts | 21 +++++++++++++++++++ .../mars_rovers/mars.rover.should.test.ts | 6 +++--- test-pnpm/mars_rovers/mars.rover.ts | 2 +- 3 files changed, 25 insertions(+), 4 deletions(-) create mode 100644 test-pnpm/mars_rovers/grid.builder.ts diff --git a/test-pnpm/mars_rovers/grid.builder.ts b/test-pnpm/mars_rovers/grid.builder.ts new file mode 100644 index 0000000..f2e5bd9 --- /dev/null +++ b/test-pnpm/mars_rovers/grid.builder.ts @@ -0,0 +1,21 @@ +import { Grid } from './grid'; +import { Position } from './position'; + +export class GridBuilder { + private _size: number = 10; + private _obstacles: Array; + + 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); + } +} diff --git a/test-pnpm/mars_rovers/mars.rover.should.test.ts b/test-pnpm/mars_rovers/mars.rover.should.test.ts index 060086c..c2b72c8 100644 --- a/test-pnpm/mars_rovers/mars.rover.should.test.ts +++ b/test-pnpm/mars_rovers/mars.rover.should.test.ts @@ -1,4 +1,5 @@ import { Grid } from './grid'; +import { GridBuilder } from './grid.builder'; import { MarsRover } from './mars.rover'; import { Position } from './position'; @@ -7,7 +8,7 @@ describe('mars rover should', () => { let marsRover: MarsRover; beforeEach(() => { - grid = new Grid(); + grid = new GridBuilder().withSize(10).buid(); marsRover = new MarsRover(grid); }); @@ -40,8 +41,7 @@ describe('mars rover should', () => { describe('when given obstacles should', () => { test('navigate up to the obstacle and no further', () => { - var obstacle = new Position(0, 3); - grid = new Grid([obstacle]); + grid = new GridBuilder().withSize(10).withObstacles(new Position(0, 3)).buid(); marsRover = new MarsRover(grid); var actual = marsRover.execute('MMMM'); diff --git a/test-pnpm/mars_rovers/mars.rover.ts b/test-pnpm/mars_rovers/mars.rover.ts index 77087b9..c1aa90d 100644 --- a/test-pnpm/mars_rovers/mars.rover.ts +++ b/test-pnpm/mars_rovers/mars.rover.ts @@ -27,7 +27,7 @@ export class MarsRover { for (const command of commands) { this.commandHandlers[command](); } -C const next = this._position.move(this.currentDirection(), this._grid); + 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}`; } From 89a356f6beafa6982e966f50bdbca28677ca93af Mon Sep 17 00:00:00 2001 From: Vincent Farah Date: Sun, 18 May 2025 16:59:40 +0100 Subject: [PATCH 54/54] Its a wrap --- test-pnpm/mars_rovers/README.md | 38 +++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/test-pnpm/mars_rovers/README.md b/test-pnpm/mars_rovers/README.md index 01383d5..4c88275 100644 --- a/test-pnpm/mars_rovers/README.md +++ b/test-pnpm/mars_rovers/README.md @@ -156,4 +156,42 @@ Add scenario-based tests: - 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) | \ No newline at end of file