Merge branch 'issue-2'

master
Garrett Mills 4 years ago
commit fc84c4f415
Signed by: garrettmills
GPG Key ID: D2BF5FBA8298F246

@ -0,0 +1,19 @@
/**
* 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 {}
/**
* Error thrown when a missile is fired at an invalid cell.
* @extends Error
*/
export class InvalidMissileFireAttemptError extends Error {}

@ -22,6 +22,31 @@ 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)
}
/**
* 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}
@ -52,6 +77,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

@ -1,4 +1,5 @@
import { Player, GridCellState, GameState, clone } from '../module/util.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.
@ -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
@ -32,6 +40,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
@ -60,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] = []
}
}
@ -124,6 +161,326 @@ 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);
}
/**
* 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;
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);
}
/**
* gets the progress (hits/total boats) that the player has
* @return {number}
* @private
*/
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
*
*/
//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 InvalidAdvanceStateError("Invalid Number of Boats");
}
}
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) {
//wait for now
}
}
}
/**
* 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.
* 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[][]}
@ -138,6 +495,31 @@ export class GameStateService {
})
})
}
/**
* 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
}
/**
* 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

Loading…
Cancel
Save