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' ;
33import { EOL } from 'os' ;
44
55export 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