Skip to content

Commit bfc20cb

Browse files
committed
Phase3: Refactor fire big method and minor type fixes
Refactored a ShotResult for an error message Refactored big start method Extract more guards
1 parent 8ebc7dd commit bfc20cb

File tree

3 files changed

+102
-76
lines changed

3 files changed

+102
-76
lines changed

test-pnpm/battleships/constants.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,8 @@
1+
import { ShotResult } from './game.types';
2+
13
export const BLANK_BOARD_CELL = ' ';
24
export const DEFAULT_BOARD_SIZE = 10;
5+
export const NO_HIT = {
6+
hit: false,
7+
message: 'Miss!',
8+
} as ShotResult;

test-pnpm/battleships/game.service.should.test.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -211,9 +211,7 @@ describe('GameService should', () => {
211211
]);
212212

213213
gameService.fire('Player1', { x: 2, y: 2 });
214-
const result = gameService.fire('Player1', { x: 2, y: 2 });
215-
expect(result.hit).toBe(false);
216-
expect(result.message).toBe('Already fired at (2, 2)');
214+
expect(() => gameService.fire('Player1', { x: 2, y: 2 })).toThrow('Already fired at (2, 2)');
217215
});
218216

219217
test('throw error when firing out of bounds', () => {

test-pnpm/battleships/game.service.ts

Lines changed: 95 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import { DEFAULT_BOARD_SIZE } from './constants';
2-
import { Ship, Coordinate, CellState, ShotResult, GameState, Game } from './game.types';
1+
import { DEFAULT_BOARD_SIZE, NO_HIT } from './constants';
2+
import { Ship, Coordinate, CellState, ShotResult, GameState, Game, ShipType } from './game.types';
33
import { EOL } from 'os';
44

55
export interface IGameService {
@@ -20,16 +20,10 @@ export class GameService implements IGameService {
2020
private readonly _boardSize: number;
2121

2222
constructor(boardSize: number = DEFAULT_BOARD_SIZE) {
23-
if (boardSize <= 1) {
24-
throw new Error('Board size must be at least 1');
25-
}
23+
this.ensureBoardSizeGreaterThanZero(boardSize);
2624
this._boardSize = boardSize;
2725
this._players = new Set<string>();
28-
this._game = {
29-
players: [],
30-
currentPlayerIndex: 0,
31-
states: new Map<string, GameState>(),
32-
};
26+
this._game = this.createNewGame();
3327
}
3428

3529
getBoardSize(): number {
@@ -45,13 +39,7 @@ export class GameService implements IGameService {
4539
hits: new Set<string>(),
4640
}));
4741

48-
for (const ship of shipsWithNoHits) {
49-
for (const coord of ship.coordinates) {
50-
this.ensureWithinBoard(coord);
51-
this.ensureNoShipOverlap(board, coord);
52-
board[coord.y][coord.x] = ship.type;
53-
}
54-
}
42+
this.placeAndValidateShipsOnBoard(shipsWithNoHits, board);
5543

5644
const gameState = this._game.states.get(playerName) || {
5745
ships: shipsWithNoHits,
@@ -69,48 +57,12 @@ export class GameService implements IGameService {
6957
this.ensurePlayerExists(playerName);
7058
this.ensureWithinBoard(target);
7159
this.ensurePlayerTurn(playerName);
72-
73-
const gameState = this._game.states.get(playerName);
74-
if (!gameState) {
75-
throw new Error(`No board found for player: ${playerName}`);
76-
}
77-
78-
const currentState = gameState.board[target.y][target.x];
79-
if (currentState === CellState.Hit || currentState === CellState.Miss) {
80-
return {
81-
hit: false,
82-
message: `Already fired at (${target.x}, ${target.y})`,
83-
};
84-
}
85-
60+
const game = this.fetchGame(playerName);
61+
const currentState = game.board[target.y][target.x];
62+
this.ensureCellNotAlreadyUsed(currentState, target);
8663
const hit = currentState !== CellState.Empty;
87-
gameState.board[target.y][target.x] = hit ? CellState.Hit : CellState.Miss;
88-
89-
if (hit) {
90-
const coordString = `${target.x},${target.y}`;
91-
const ship = this.findShipAtCoordinate(gameState.ships, target);
92-
if (ship) {
93-
ship.hits.add(coordString);
94-
const shipDestroyed = this.isShipDestroyed(ship);
95-
const gameWon = this.checkForWin(gameState.ships);
96-
97-
if (gameWon) {
98-
gameState.winner = playerName;
99-
}
100-
101-
return {
102-
hit: true,
103-
message: shipDestroyed ? 'Ship destroyed!' : 'Hit!',
104-
shipDestroyed,
105-
gameWon,
106-
};
107-
}
108-
}
109-
110-
return {
111-
hit: false,
112-
message: 'Miss!',
113-
};
64+
game.board[target.y][target.x] = hit ? CellState.Hit : CellState.Miss;
65+
return hit ? this.getShotResult(target, game, playerName) : NO_HIT;
11466
}
11567

11668
getWinner(playerName: string): string | null {
@@ -123,9 +75,7 @@ export class GameService implements IGameService {
12375
}
12476

12577
switchTurn(): string {
126-
if (this._game.players.length < 2) {
127-
throw new Error('Need at least 2 players to switch turns');
128-
}
78+
this.ensureMoreThanOnePlayerExists();
12979
this._game.currentPlayerIndex = (this._game.currentPlayerIndex + 1) % this._game.players.length;
13080
return this.getCurrentPlayer()!;
13181
}
@@ -140,36 +90,108 @@ export class GameService implements IGameService {
14090

14191
printBoard(playerName: string): string {
14292
const gameState = this._game.states.get(playerName);
93+
this.ensureGameState(gameState, playerName);
94+
const lines: string[] = [];
95+
this.addHeader(lines);
96+
this.addContent(gameState.board, lines);
97+
return lines.join(EOL);
98+
}
99+
100+
private ensureGameState(gameState: GameState, playerName: string) {
143101
if (!gameState) {
144102
throw new Error(`No board found for player: ${playerName}`);
145103
}
104+
}
146105

147-
const lines: string[] = [];
148-
this.printHeader(lines);
149-
this.printContent(gameState.board, lines);
150-
return lines.join(EOL);
106+
private placeAndValidateShipsOnBoard(ships: Array<Ship>, board: string[][]) {
107+
for (const ship of ships) {
108+
for (const coord of ship.coordinates) {
109+
this.ensureWithinBoard(coord);
110+
this.ensureNoShipOverlap(board, coord);
111+
board[coord.y][coord.x] = ship.type;
112+
}
113+
}
114+
}
115+
116+
private createNewGame(): Game {
117+
return {
118+
players: [],
119+
currentPlayerIndex: 0,
120+
states: new Map<string, GameState>(),
121+
} as Game;
122+
}
123+
124+
private ensureBoardSizeGreaterThanZero(boardSize: number) {
125+
if (boardSize <= 1) {
126+
throw new Error('Board size must be at least 1');
127+
}
128+
}
129+
130+
private ensureMoreThanOnePlayerExists() {
131+
if (this._game.players.length < 2) {
132+
throw new Error('Need at least 2 players to switch turns');
133+
}
134+
}
135+
136+
private ensureCellNotAlreadyUsed(currentState: string, target: Coordinate): void {
137+
if (this.isCellAlreadyFired(currentState)) {
138+
throw new Error(`Already fired at (${target.x}, ${target.y})`);
139+
}
140+
}
141+
142+
private getShotResult(target: Coordinate, game: GameState, playerName: string): ShotResult {
143+
const ship = this.findShipAtCoordinate(game.ships, target);
144+
ship.hits.add(`${target.x},${target.y}`);
145+
const shipDestroyed = this.isShipDestroyed(ship);
146+
const isWinner = this.checkForWin(game.ships);
147+
if (isWinner) {
148+
game.winner = playerName;
149+
}
150+
151+
return {
152+
hit: true,
153+
message: shipDestroyed ? 'Ship destroyed!' : 'Hit!',
154+
shipDestroyed,
155+
gameWon: isWinner,
156+
};
157+
}
158+
159+
private isCellAlreadyFired(currentState: string): boolean {
160+
return currentState === CellState.Hit || currentState === CellState.Miss;
161+
}
162+
163+
private fetchGame(playerName: string): GameState {
164+
const gameState = this._game.states.get(playerName);
165+
this.ensureGameExists(gameState, playerName);
166+
return gameState;
167+
}
168+
169+
private ensureGameExists(gameState: GameState, playerName: string): void {
170+
if (!gameState) {
171+
throw new Error(`No board found for player: ${playerName}`);
172+
}
151173
}
152174

153-
private ensurePlayerTurn(playerName: string) {
175+
private ensurePlayerTurn(playerName: string): void {
154176
const currentPlayer = this.getCurrentPlayer();
155177
if (currentPlayer !== playerName) {
156178
throw new Error(`Not your turn. Current player: ${currentPlayer}`);
157179
}
158180
}
159181

160-
private ensureNoShipOverlap(board: string[][], coord: Coordinate) {
182+
private ensureNoShipOverlap(board: string[][], coord: Coordinate): void {
161183
if (this.checkForShipOverlap(board, coord)) {
162184
throw new Error(`Ship overlap at (${coord.x}, ${coord.y})`);
163185
}
164186
}
165187

166-
private ensureWithinBoard(coord: Coordinate) {
167-
if (this.outsideBoard(coord)) {
188+
private ensureWithinBoard(coord: Coordinate): void {
189+
if (this.isOutsideBoard(coord)) {
168190
throw new Error(`Ship placement out of bounds: (${coord.x}, ${coord.y})`);
169191
}
170192
}
171193

172-
private ensurePlayerExists(playerName: string) {
194+
private ensurePlayerExists(playerName: string): void {
173195
if (!this.hasPlayer(playerName)) {
174196
throw new Error(`Unknown player: ${playerName}`);
175197
}
@@ -199,11 +221,11 @@ export class GameService implements IGameService {
199221
return board[coord.y][coord.x] !== CellState.Empty;
200222
}
201223

202-
private outsideBoard(coord: Coordinate) {
224+
private isOutsideBoard(coord: Coordinate): boolean {
203225
return coord.x < 0 || coord.x >= this._boardSize || coord.y < 0 || coord.y >= this._boardSize;
204226
}
205227

206-
private printContent(board: string[][], lines: string[]) {
228+
private addContent(board: string[][], lines: string[]): void {
207229
for (let y = 0; y < this._boardSize; y++) {
208230
let row = ` ${y}|`;
209231
for (let x = 0; x < this._boardSize; x++) {
@@ -213,7 +235,7 @@ export class GameService implements IGameService {
213235
}
214236
}
215237

216-
private printHeader(lines: string[]) {
238+
private addHeader(lines: string[]): void {
217239
let header = ' |';
218240
for (let x = 0; x < this._boardSize; x++) {
219241
header += ` ${x} |`;

0 commit comments

Comments
 (0)