From 4463e804f4ab0581651f53cad4c49f57337c7d57 Mon Sep 17 00:00:00 2001 From: Evan Powell Date: Mon, 7 Sep 2020 19:07:32 -0500 Subject: [PATCH 01/12] added a get player score helper method(#2) --- src/services/GameState.service.js | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/src/services/GameState.service.js b/src/services/GameState.service.js index 5195d67..e88961e 100644 --- a/src/services/GameState.service.js +++ b/src/services/GameState.service.js @@ -138,6 +138,30 @@ export class GameStateService { }) }) } + + /** + * get the "score" (the number of hits) that the + * current player has (counting sunk ships) + * @return {number} + * @private + */ + get_player_score () { + let i = 1; + let j = 1; + let score = 0; + for(i; i<=9; i++) + { + for(j; j<=9; j++) + { + let cell = this.player_x_game_board[this.current_opponent][i][j]; + if(cell.render === GridCellState.Damaged || cell.render === GridCellState.Sunk ) + { + score++; + } + } + } + return(score); + } } // Export a single instance, so it can be shared by all files From b6e756dae9e589c82f5938ee8fc7ba4271094d07 Mon Sep 17 00:00:00 2001 From: Evan Powell Date: Mon, 7 Sep 2020 19:12:20 -0500 Subject: [PATCH 02/12] added a get boat count helper method(#2) --- src/services/GameState.service.js | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/services/GameState.service.js b/src/services/GameState.service.js index e88961e..9b041ca 100644 --- a/src/services/GameState.service.js +++ b/src/services/GameState.service.js @@ -162,6 +162,24 @@ export class GameStateService { } return(score); } + + get_boat_count(){ + let i = 1; + let j = 1; + let boat_count = 0; + for(i; i<=9; i++) + { + for(j; j<=9; j++) + { + let cell = this.player_x_game_board[this.current_opponent][i][j]; + if(cell.render === GridCellState.Damaged || cell.render === GridCellState.Sunk || cell.render === GridCellState.Ship ) + { + boat_count++; + } + } + } + return(boat_count); + } } // Export a single instance, so it can be shared by all files From eb5bcc6a541a9464c398f89e1bb141a66a793da3 Mon Sep 17 00:00:00 2001 From: Evan Powell Date: Mon, 7 Sep 2020 19:19:32 -0500 Subject: [PATCH 03/12] added a get progress method(#2) --- src/services/GameState.service.js | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/services/GameState.service.js b/src/services/GameState.service.js index 9b041ca..22aa54e 100644 --- a/src/services/GameState.service.js +++ b/src/services/GameState.service.js @@ -163,6 +163,12 @@ export class GameStateService { return(score); } + /** + * gets the number of the boats (sunken, damaged or not) that the opponent has + * used to help keep get progress method looking clean + * @return {number} + * @private + */ get_boat_count(){ let i = 1; let j = 1; @@ -180,6 +186,15 @@ export class GameStateService { } return(boat_count); } + + /** + * gets the progress (hits/total boats) that the player has + * @return {number} + * @private + */ + get_progress(){ + return(this.get_player_score() / this.get_progress() ) + } } // Export a single instance, so it can be shared by all files From 97e797f6e3fee159a81a5e23fa5dc7a8dad781b4 Mon Sep 17 00:00:00 2001 From: Evan Powell Date: Mon, 7 Sep 2020 19:41:21 -0500 Subject: [PATCH 04/12] added an outline for the future helpers of advance game state(#2) --- src/services/GameState.service.js | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/src/services/GameState.service.js b/src/services/GameState.service.js index 22aa54e..a7b1696 100644 --- a/src/services/GameState.service.js +++ b/src/services/GameState.service.js @@ -195,6 +195,27 @@ export class GameStateService { get_progress(){ return(this.get_player_score() / this.get_progress() ) } + + + /** + * responsible for advancing the game state + * will be consisting of + * @return + * @private + */ + advance_game_state(){ + /** functions to be made that validate: + * 1) number of ships + * 2) player one placement + * 3) player two placement + * 4) player one turn + * 5) advance to player 2 + * 6) player 2 turn + * 7) advance to player one + * 8) player win + * + */ + } } // Export a single instance, so it can be shared by all files From a9945e084eb6c02abb35c67e445d0fd3159bce0a Mon Sep 17 00:00:00 2001 From: Evan Powell Date: Mon, 7 Sep 2020 19:56:09 -0500 Subject: [PATCH 05/12] added the n_boat variable, defined a get method for n_boat, and defined a set method for n_boat(#2) --- src/services/GameState.service.js | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/src/services/GameState.service.js b/src/services/GameState.service.js index a7b1696..57c044b 100644 --- a/src/services/GameState.service.js +++ b/src/services/GameState.service.js @@ -32,6 +32,34 @@ export class GameStateService { */ n_cols = 9 + /** + * Number boats placed on the board. + * @private + * @type {number} + */ + n_boats = 1 + + /** + * gets the number of boats placed on the board + * @private + * @return {number} + */ + get_n_boats(){ + return (this.n_boats); + } + + /** + * sets the number of boats to a valid number + * @private + * @return none + */ + set_n_boats(number){ + if(number >= 1 && number <= 5 ) + { + this.n_boats = number; + } + } + /** * The current state of the game. * @private From 6d5050fa793d95f6557ddcdb3c580b0daad2bd29 Mon Sep 17 00:00:00 2001 From: Evan Powell Date: Mon, 7 Sep 2020 20:13:18 -0500 Subject: [PATCH 06/12] added to the advance game state: added the choosing boats number of boats prereq, started on the player's placement prereq(#2) --- src/services/GameState.service.js | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/src/services/GameState.service.js b/src/services/GameState.service.js index 57c044b..1fdf812 100644 --- a/src/services/GameState.service.js +++ b/src/services/GameState.service.js @@ -243,6 +243,34 @@ export class GameStateService { * 8) player win * */ + //1 + if(this.current_state === GameState.ChoosingNumberOfShips) + { + if(this.n_boats >= 1 && this.n_boats <= 5 ) + { + this.current_state = GameState.PlayerSetup; + this.current_player = Player.One; + this.current_opponent = Player.Two; + } + else + { + throw new Error("Invalid Number of Boats"); + } + + } + if(this.current_state === GameState.PlayerSetup) + { + if(this.current_player === Player.One) + { + //wait + } + if(this.current_player === Player.Two) + { + //wait for now + } + } + + } } From 1a6d916f85ef7c7d4c01d1c5bbe64b08417a8a56 Mon Sep 17 00:00:00 2001 From: garrettmills Date: Mon, 7 Sep 2020 21:02:37 -0500 Subject: [PATCH 07/12] Add ShipType enum and isShipCell helper to utils (#2) --- src/module/util.js | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/src/module/util.js b/src/module/util.js index 487e788..e618f95 100644 --- a/src/module/util.js +++ b/src/module/util.js @@ -22,6 +22,19 @@ export const GridCellState = { Missed: 'missed', } +/** + * Returns true if the given grid cell state represents a ship in some way. + * @param {GridCellState} grid_cell_state + * @return {boolean} + */ +export function isShipCell(grid_cell_state) { + return [ + GridCellState.Ship, + GridCellState.Damaged, + GridCellState.Sunk, + ].includes(grid_cell_state) +} + /** * Enum of all possible players. * @type {object} @@ -52,6 +65,22 @@ export const GameState = { PlayerVictory: 'player_victory', } +/** + * The various supported ship types, by dimension. + * @type {object} + */ +export const ShipType = { + x1: '1x1', + x2: '1x2', + x3: '1x3', + x4: '1x4', + x5: '1x5', +} + +export function isShipType(type) { + return ['1x1', '1x2', '1x3', '1x4', '1x5'].includes(type) +} + /** * Makes a deep copy of the value passed in. * @param {*} obj From a803c6937d20979da1834d3031dfbd6306e153c4 Mon Sep 17 00:00:00 2001 From: garrettmills Date: Mon, 7 Sep 2020 21:04:28 -0500 Subject: [PATCH 08/12] Add custom error classes for state and placement (#2) --- src/module/errors.js | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 src/module/errors.js diff --git a/src/module/errors.js b/src/module/errors.js new file mode 100644 index 0000000..ff7ddec --- /dev/null +++ b/src/module/errors.js @@ -0,0 +1,13 @@ +/** + * Placeholder class for an error that is thrown when a ship is placed + * in an invalid position. + * @extends Error + */ +export class InvalidShipPlacementError extends Error {} + +/** + * Error thrown when the program tries to advance the state, but it is + * invalid. + * @extends Error + */ +export class InvalidAdvanceStateError extends Error {} From 9a2afe523e0ef667bc33cc003ceefacd8bbd9ce3 Mon Sep 17 00:00:00 2001 From: garrettmills Date: Mon, 7 Sep 2020 21:05:48 -0500 Subject: [PATCH 09/12] add helper method to handle placing and validating a ship (#2) --- src/services/GameState.service.js | 254 +++++++++++++++++++++++++----- 1 file changed, 218 insertions(+), 36 deletions(-) diff --git a/src/services/GameState.service.js b/src/services/GameState.service.js index 1fdf812..7a5580e 100644 --- a/src/services/GameState.service.js +++ b/src/services/GameState.service.js @@ -1,4 +1,5 @@ -import { Player, GridCellState, GameState, clone } from '../module/util.js' +import { Player, GridCellState, GameState, clone, ShipType, isShipType, isShipCell } from '../module/util.js' +import { InvalidShipPlacementError, InvalidAdvanceStateError } from '../module/errors.js' /** * Singleton service for managing the state of the game. @@ -11,6 +12,13 @@ export class GameStateService { */ player_x_game_board = {} + /** + * A mapping of player => ship definitions. + * @private + * @type {object} + */ + player_x_ships = {} + /** * An array of all players. This is mostly for internal purposes. * @private @@ -88,6 +96,7 @@ export class GameStateService { // Generate empty boards for each player for ( const player of this.players ) { this.player_x_game_board[player] = this._build_empty_board() + this.player_x_ships[player] = [] } } @@ -152,21 +161,6 @@ export class GameStateService { }) } - /** - * Build an empty structure of grid cells. - * @return {object[][]} - * @private - */ - _build_empty_board() { - return Array(this.n_rows).fill('').map(_ => { - return Array(this.n_cols).fill('').map(_ => { - return { - render: GridCellState.Available, - } - }) - }) - } - /** * get the "score" (the number of hits) that the * current player has (counting sunk ships) @@ -231,7 +225,7 @@ export class GameStateService { * @return * @private */ - advance_game_state(){ + advance_game_state() { /** functions to be made that validate: * 1) number of ships * 2) player one placement @@ -241,36 +235,224 @@ export class GameStateService { * 6) player 2 turn * 7) advance to player one * 8) player win - * + * */ //1 - if(this.current_state === GameState.ChoosingNumberOfShips) - { - if(this.n_boats >= 1 && this.n_boats <= 5 ) - { - this.current_state = GameState.PlayerSetup; + if (this.current_state === GameState.ChoosingNumberOfShips) { + if (this.n_boats >= 1 && this.n_boats <= 5) { + this.current_state = GameState.PlayerSetup; this.current_player = Player.One; - this.current_opponent = Player.Two; + this.current_opponent = Player.Two; + } else { + throw new InvalidAdvanceStateError("Invalid Number of Boats"); } - else - { - throw new Error("Invalid Number of Boats"); - } - + } - if(this.current_state === GameState.PlayerSetup) - { - if(this.current_player === Player.One) - { + if (this.current_state === GameState.PlayerSetup) { + if (this.current_player === Player.One) { //wait + // because the place_ship handles all the validation + // all you need to do is make sure they have placed all the appropriate ships + // e.g. if ( this.get_ship_entities(this.current_player).length === this.n_boats ) { ... } } - if(this.current_player === Player.Two) - { + if (this.current_player === Player.Two) { //wait for now } } + } + + /** + * Attempt to place a ship of the given type at the given coordinates. + * Throws an InvalidShipPlacementError if the coordinates are invalid. + * Coordinates should be [row_index, column_index] of either end of the ship. + * + * @example + * If I am placing a 1x3 ship and I want it to be in row 3 column 2 horizontal + * to row 3 column 4, then I would call: + * game_service.place_ship(ShipType.x3, [3,2], [3,4]) + * + * @param {ShipType} ship_type + * @param {[number, number]} coords_one + * @param {[number, number]} coords_two + */ + place_ship(ship_type, coords_one, coords_two) { + // make sure the coordinates are valid for the given ship type + this.validate_coordinates(ship_type, coords_one, coords_two) + + // get the ships for the current player + const player_ships = this.get_ship_entities(this.current_player) + + // make sure they don't already have this ship type + const have_ship_type = player_ships.some(ship => ship.ship_type === ship_type) + if ( have_ship_type ) + throw new InvalidShipPlacementError('A ship with this type has already been placed.') + + // make sure they don't already have too many + if ( player_ships.length >= this.n_boats ) + throw new InvalidShipPlacementError('This player cannot place any more ships.') + + // place the ship + this.player_x_ships[this.current_player].push({ ship_type, coords_one, coords_two }) + + // mark the cells as having a ship in them + this.get_covered_cells(coords_one, coords_two).some(([row_i, col_i]) => { + this._set_cell_state(this.current_player, row_i, col_i, GridCellState.Ship) + }) + } + + /** + * Get an array of cell coordinates that are covered by a ship that spans + * the given coordinates. + * + * @example + * If a ship goes from row 1 column 1 to row 4 column 1, then I can get + * the coordinates of all cells covered by that ship using: + * game_service.get_covered_cells([1,1], [4,1]) + * Which would return [[1,1], [2,1], [3,1], [4,1]]. + * + * @param {[number, number]} coords_one + * @param {[number, number]} coords_two + * @return {[number, number][]} + */ + get_covered_cells(coords_one, coords_two) { + const [row_one, col_one] = coords_one + const [row_two, col_two] = coords_two + const [left_col, right_col] = [Math.min(col_one, col_two), Math.max(col_one, col_two)] + const [top_row, bottom_row] = [Math.min(row_one, row_two), Math.max(row_one, row_two)] + const is_horizontal = top_row === bottom_row + + if ( is_horizontal ) { + return Array((right_col - left_col) + 1).fill('').map((_, i) => { + return [top_row, i + left_col] + }) + } else { + return Array((bottom_row - top_row) + 1).fill('').map((_, i) => { + return [i + top_row, left_col] + }) + } + } + + /** + * Validate the given coordinates for the given ship type. + * Throws an InvalidShipPlacementError if the coordinates are invalid. + * Coordinates should be [row_index, column_index] of either end of the ship. + * + * @example + * If I am placing a 1x3 ship and I want it to be in row 3 column 2 horizontal + * to row 3 column 4, then I would call: + * game_service.validate_coordinates(ShipType.x3, [3,2], [3,4]) + * + * @param {ShipType} ship_type + * @param {[number, number]} coords_one + * @param {[number, number]} coords_two + */ + validate_coordinates(ship_type, coords_one, coords_two) { + if ( !isShipType(ship_type) ) throw new InvalidShipPlacementError('Invalid ship type: '+ship_type) + + const ship_length = this.get_ship_length(ship_type) + const [row_one, col_one] = coords_one + const [row_two, col_two] = coords_two + const [left_col, right_col] = [Math.min(col_one, col_two), Math.max(col_one, col_two)] + const [top_row, bottom_row] = [Math.min(row_one, row_two), Math.max(row_one, row_two)] + + const is_horizontal = top_row === bottom_row + const ship_cells = this.get_ship_cells(this.current_player) + const placement_cells = [] + + if ( is_horizontal ) { + // Make sure the input length matches the given ship type + if ( (right_col - left_col) !== (ship_length - 1) ) + throw new InvalidShipPlacementError('Invalid coordinates: ship length is invalid') + + Array(ship_length).fill('').map((_, i) => { + placement_cells.push([top_row, i + left_col]) + }) + } else { + // Make sure the input length matches the given ship type + if ( (bottom_row - top_row) !== (ship_length - 1) ) + throw new InvalidShipPlacementError('Invalid coordinates: ship length is invalid') + + Array(ship_length).fill('').map((_, i) => { + placement_cells.push([i + top_row, left_col]) + }) + } + + // Make sure none of the placement cells overlap with existing ships + const has_overlap = ship_cells.some(([ship_row, ship_col]) => { + return placement_cells.some(([placement_row, placement_col]) => { + return ship_row === placement_row && ship_col === placement_col + }) + }) - + if ( has_overlap ) + throw new InvalidShipPlacementError('Invalid coordinates: ship overlaps with others') + } + + /** + * Get the number of cells the given ship type should occupy. + * @param {ShipType} ship_type + * @return {number} + */ + get_ship_length(ship_type) { + if ( ship_type === ShipType.x1 ) return 1 + if ( ship_type === ShipType.x2 ) return 2 + if ( ship_type === ShipType.x3 ) return 3 + if ( ship_type === ShipType.x4 ) return 4 + if ( ship_type === ShipType.x5 ) return 5 + } + + /** + * Get the coordinates of all cells that have ships in them, for the given player. + * @param {Player} player + * @return {[number, number]} + */ + get_ship_cells(player) { + const cells = [] + this.player_x_game_board[player].some((row, row_i) => { + row.some((col, col_i) => { + if ( isShipCell(col.render) ) { + cells.push([row_i, col_i]) + } + }) + }) + return cells + } + + /** + * Get an array of ship entities for the given player. + * @param {Player} player + * @return {object[]} + */ + get_ship_entities(player) { + return clone(this.player_x_ships[player]) + } + + /** + * Build an empty structure of grid cells. + * @return {object[][]} + * @private + */ + _build_empty_board() { + return Array(this.n_rows).fill('').map(_ => { + return Array(this.n_cols).fill('').map(_ => { + return { + render: GridCellState.Available, + } + }) + }) + } + + /** + * Set the state of the cell at the given coordinates on the player's board + * to the specified state. + * @param {Player} player + * @param {number} row_i + * @param {number} col_i + * @param {GridCellState} state + * @private + */ + _set_cell_state(player, row_i, col_i, state) { + this.player_x_game_board[player][row_i][col_i].render = state } } From 99759bad0922a5f41307341808ff7733677e021b Mon Sep 17 00:00:00 2001 From: garrettmills Date: Tue, 8 Sep 2020 07:38:03 -0500 Subject: [PATCH 10/12] Add custom error class for invalid missile fire (#2) --- src/module/errors.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/module/errors.js b/src/module/errors.js index ff7ddec..3db0685 100644 --- a/src/module/errors.js +++ b/src/module/errors.js @@ -11,3 +11,9 @@ export class InvalidShipPlacementError extends Error {} * @extends Error */ export class InvalidAdvanceStateError extends Error {} + +/** + * Error thrown when a missile is fired at an invalid cell. + * @extends Error + */ +export class InvalidMissileFireAttemptError extends Error {} From a4f13f0d17dfa15db84e0a679f0250f5c86974dd Mon Sep 17 00:00:00 2001 From: garrettmills Date: Tue, 8 Sep 2020 07:38:24 -0500 Subject: [PATCH 11/12] Add util method for checking if cell can be fired upon (#2) --- src/module/util.js | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/module/util.js b/src/module/util.js index e618f95..cf2d52e 100644 --- a/src/module/util.js +++ b/src/module/util.js @@ -35,6 +35,18 @@ export function isShipCell(grid_cell_state) { ].includes(grid_cell_state) } +/** + * Returns true if the given grid cell state can be fired upon. + * @param {GridCellState} grid_cell_state + * @return {boolean} + */ +export function isValidTargetCell(grid_cell_state) { + return [ + GridCellState.Ship, + GridCellState.Available, + ].includes(grid_cell_state) +} + /** * Enum of all possible players. * @type {object} From aaadd82d319b143a046ad160894fcaadb20772d2 Mon Sep 17 00:00:00 2001 From: garrettmills Date: Tue, 8 Sep 2020 07:39:09 -0500 Subject: [PATCH 12/12] Add helper methods to game service for firing on opponent (#2) --- src/services/GameState.service.js | 70 ++++++++++++++++++++++++++++++- 1 file changed, 68 insertions(+), 2 deletions(-) diff --git a/src/services/GameState.service.js b/src/services/GameState.service.js index 7a5580e..572a635 100644 --- a/src/services/GameState.service.js +++ b/src/services/GameState.service.js @@ -1,5 +1,5 @@ -import { Player, GridCellState, GameState, clone, ShipType, isShipType, isShipCell } from '../module/util.js' -import { InvalidShipPlacementError, InvalidAdvanceStateError } from '../module/errors.js' +import { Player, GridCellState, GameState, clone, ShipType, isShipType, isShipCell, isValidTargetCell } from '../module/util.js' +import { InvalidShipPlacementError, InvalidAdvanceStateError, InvalidMissileFireAttemptError } from '../module/errors.js' /** * Singleton service for managing the state of the game. @@ -261,6 +261,60 @@ export class GameStateService { } } + /** + * Attempt to fire a missile at the current opponent at the given coordinates. + * The coordinates should be an array of [row_index, column_index] where the missile should fire. + * Returns true if the missile hit an undamaged cell of a ship. + * + * @example + * If I want to fire a missile at row 5 column 7, then: + * game_service.attempt_missile_fire([5, 7]) + * + * @param {[number, number]} coords + * @return {boolean} + */ + attempt_missile_fire([target_row_i, target_col_i]) { + const target_cell = this._get_cell_state(this.current_opponent, target_row_i, target_col_i) + if ( !isValidTargetCell(target_cell.render) ) + throw new InvalidMissileFireAttemptError('Cannot fire on cell with state: ' + target_cell.render) + + if ( target_cell.render === GridCellState.Ship ) { + // We hit an un-hit ship cell! + this._set_cell_state(this.current_opponent, target_row_i, target_col_i, GridCellState.Damaged) + + // set ships to sunk where appropriate + this._sink_damaged_ships(this.current_opponent) + return true + } else if ( target_cell.render === GridCellState.Available ) { + // We missed... + this._set_cell_state(this.current_opponent, target_row_i, target_col_i, GridCellState.Missed) + } + + return false + } + + /** + * Checks the player's ships. If any are fully damaged, it flags that ship's cells + * as "sunk" rather than damaged. + * @param {Player} player + * @private + */ + _sink_damaged_ships(player) { + this.get_ship_entities(player).some(ship => { + const covered_cells = this.get_covered_cells(ship.coords_one, ship.coords_two) + const all_damaged = covered_cells.every(([cell_row, cell_col]) => { + return this._get_cell_state(player, cell_row, cell_col).render === GridCellState.Damaged + }) + + if ( all_damaged ) { + // The entire boat was damaged, so sink it + covered_cells.some(([cell_row, cell_col]) => { + this._set_cell_state(player, cell_row, cell_col, GridCellState.Sunk) + }) + } + }) + } + /** * Attempt to place a ship of the given type at the given coordinates. * Throws an InvalidShipPlacementError if the coordinates are invalid. @@ -454,6 +508,18 @@ export class GameStateService { _set_cell_state(player, row_i, col_i, state) { this.player_x_game_board[player][row_i][col_i].render = state } + + /** + * Get the state of the cell at the given coordinates on the player's board. + * @param {Player} player + * @param {number} row_i + * @param {number} col_i + * @return {object} + * @private + */ + _get_cell_state(player, row_i, col_i) { + return this.player_x_game_board[player][row_i][col_i] + } } // Export a single instance, so it can be shared by all files