diff --git a/test-pnpm/mars_rovers/README.md b/test-pnpm/mars_rovers/README.md new file mode 100644 index 0000000..4c88275 --- /dev/null +++ b/test-pnpm/mars_rovers/README.md @@ -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) | \ No newline at end of file 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/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/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.should.test.ts b/test-pnpm/mars_rovers/mars.rover.should.test.ts new file mode 100644 index 0000000..c2b72c8 --- /dev/null +++ b/test-pnpm/mars_rovers/mars.rover.should.test.ts @@ -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'); + }); + }); +}); diff --git a/test-pnpm/mars_rovers/mars.rover.ts b/test-pnpm/mars_rovers/mars.rover.ts new file mode 100644 index 0000000..c1aa90d --- /dev/null +++ b/test-pnpm/mars_rovers/mars.rover.ts @@ -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 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; + } + } +} diff --git a/test-pnpm/mars_rovers/position.ts b/test-pnpm/mars_rovers/position.ts new file mode 100644 index 0000000..b3f1566 --- /dev/null +++ b/test-pnpm/mars_rovers/position.ts @@ -0,0 +1,23 @@ +import { CompassDirection } from './direction'; +import { Grid } from './grid'; + +export class Position { + constructor( + public readonly x: number, + public readonly y: number, + ) {} + + 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); + } + + toString(): string { + return `${this.x}:${this.y}`; + } +} diff --git a/test-pnpm/morning_routine/README.md b/test-pnpm/morning_routine/README.md deleted file mode 100644 index e69de29..0000000