2020-09-08 12:39:09 +00:00
|
|
|
import { Player, GridCellState, GameState, clone, ShipType, isShipType, isShipCell, isValidTargetCell } from '../module/util.js'
|
|
|
|
import { InvalidShipPlacementError, InvalidAdvanceStateError, InvalidMissileFireAttemptError } from '../module/errors.js'
|
2020-09-07 20:23:52 +00:00
|
|
|
|
2020-09-07 19:34:43 +00:00
|
|
|
/**
|
|
|
|
* Singleton service for managing the state of the game.
|
|
|
|
*/
|
|
|
|
export class GameStateService {
|
2020-09-07 20:08:05 +00:00
|
|
|
/**
|
|
|
|
* A mapping of player => game board state.
|
|
|
|
* @private
|
|
|
|
* @type {object}
|
|
|
|
*/
|
|
|
|
player_x_game_board = {}
|
|
|
|
|
2020-09-08 02:05:48 +00:00
|
|
|
/**
|
|
|
|
* A mapping of player => ship definitions.
|
|
|
|
* @private
|
|
|
|
* @type {object}
|
|
|
|
*/
|
|
|
|
player_x_ships = {}
|
|
|
|
|
2020-09-07 20:08:05 +00:00
|
|
|
/**
|
|
|
|
* An array of all players. This is mostly for internal purposes.
|
|
|
|
* @private
|
|
|
|
* @type {(string)[]}
|
|
|
|
*/
|
|
|
|
players = [Player.One, Player.Two]
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Number of rows in the grid.
|
|
|
|
* @private
|
|
|
|
* @type {number}
|
|
|
|
*/
|
|
|
|
n_rows = 9
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Number of cols in the grid.
|
|
|
|
* @private
|
|
|
|
* @type {number}
|
|
|
|
*/
|
|
|
|
n_cols = 9
|
|
|
|
|
2020-09-08 00:56:09 +00:00
|
|
|
/**
|
|
|
|
* 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);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2020-09-11 19:58:01 +00:00
|
|
|
* sets the number of boats to a valid number
|
2020-09-08 00:56:09 +00:00
|
|
|
* @private
|
2020-09-11 19:58:01 +00:00
|
|
|
* @return none
|
2020-09-08 00:56:09 +00:00
|
|
|
*/
|
|
|
|
set_n_boats(number){
|
|
|
|
if(number >= 1 && number <= 5 )
|
|
|
|
{
|
2020-09-11 19:58:01 +00:00
|
|
|
this.n_boats = number;
|
2020-09-08 00:56:09 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-09-12 18:14:25 +00:00
|
|
|
/**
|
|
|
|
* Given the number of boats set by the player (n_boats), return an array
|
|
|
|
* of possible ShipTypes.
|
|
|
|
* @return {ShipType[]}
|
|
|
|
*/
|
2020-09-12 18:00:13 +00:00
|
|
|
get_possible_boats(){
|
2020-09-12 18:14:25 +00:00
|
|
|
if (this.get_n_boats() === 1) {
|
|
|
|
return [ShipType.x1]
|
|
|
|
}
|
|
|
|
else if (this.get_n_boats() === 2) {
|
|
|
|
return [ShipType.x1, ShipType.x2]
|
|
|
|
}
|
|
|
|
else if (this.get_n_boats() === 3) {
|
|
|
|
return [ShipType.x1, ShipType.x2, ShipType.x3]
|
|
|
|
}
|
|
|
|
else if (this.get_n_boats() === 4) {
|
|
|
|
return [ShipType.x1, ShipType.x2, ShipType.x3, ShipType.x4]
|
|
|
|
}
|
|
|
|
else if (this.get_n_boats() === 5) {
|
|
|
|
return [ShipType.x1, ShipType.x2, ShipType.x3, ShipType.x4, ShipType.x5]
|
|
|
|
}
|
2020-09-11 19:58:01 +00:00
|
|
|
}
|
2020-09-12 18:14:25 +00:00
|
|
|
|
2020-09-07 20:08:05 +00:00
|
|
|
/**
|
|
|
|
* The current state of the game.
|
|
|
|
* @private
|
|
|
|
* @type {string}
|
|
|
|
*/
|
|
|
|
current_state = GameState.ChoosingNumberOfShips
|
|
|
|
|
|
|
|
/**
|
|
|
|
* The current player.
|
2020-09-07 20:23:52 +00:00
|
|
|
* @private
|
2020-09-07 20:08:05 +00:00
|
|
|
* @type {string}
|
|
|
|
*/
|
|
|
|
current_player = Player.One
|
|
|
|
|
2020-09-07 20:23:52 +00:00
|
|
|
/**
|
|
|
|
* The current opponent.
|
|
|
|
* @private
|
|
|
|
* @type {string}
|
|
|
|
*/
|
|
|
|
current_opponent = Player.Two
|
|
|
|
|
2020-09-12 18:14:25 +00:00
|
|
|
/**
|
|
|
|
* If the current state is the PromptPlayerChange, then this is
|
|
|
|
* the state that we should move to next.
|
|
|
|
* @type {undefined|GameState}
|
|
|
|
*/
|
|
|
|
post_player_change_state = undefined
|
|
|
|
|
2020-09-09 01:23:49 +00:00
|
|
|
/**
|
|
|
|
* True if, during the current turn, the user has tried to fire a missile.
|
|
|
|
* @type {boolean}
|
|
|
|
*/
|
|
|
|
current_turn_had_missile_attempt = false
|
|
|
|
|
2020-09-09 01:58:35 +00:00
|
|
|
/**
|
|
|
|
* Array of functions that are called when the game state changes.
|
|
|
|
* @type {function[]}
|
|
|
|
*/
|
|
|
|
game_state_change_listeners = []
|
|
|
|
|
2020-09-07 20:08:05 +00:00
|
|
|
/**
|
|
|
|
* Construct a new game service. Initialize any internal states.
|
|
|
|
*/
|
|
|
|
constructor() {
|
|
|
|
// Generate empty boards for each player
|
|
|
|
for ( const player of this.players ) {
|
|
|
|
this.player_x_game_board[player] = this._build_empty_board()
|
2020-09-08 02:05:48 +00:00
|
|
|
this.player_x_ships[player] = []
|
2020-09-07 20:08:05 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get the dimensions of the board as [rows, cols].
|
|
|
|
* @example const [n_rows, n_cols] = game_service.get_dimensions()
|
|
|
|
* @return {[number, number]}
|
|
|
|
*/
|
|
|
|
get_dimensions() {
|
|
|
|
return [this.n_rows, this.n_cols]
|
|
|
|
}
|
2020-09-07 19:34:43 +00:00
|
|
|
|
2020-09-07 20:23:52 +00:00
|
|
|
/**
|
|
|
|
* Get the player who is the focus of the current game state.
|
|
|
|
* @return {string}
|
|
|
|
*/
|
|
|
|
get_current_player() {
|
|
|
|
return this.current_player
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get the player who is NOT the focus of the current game state.
|
|
|
|
* @return {string}
|
|
|
|
*/
|
|
|
|
get_current_opponent() {
|
|
|
|
return this.current_opponent
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get the state of the current player's board, as it should appear to them.
|
|
|
|
* @return {object[][]}
|
|
|
|
*/
|
|
|
|
get_current_player_state() {
|
|
|
|
// The player can see everything about their own board, so just return it.
|
|
|
|
// Return a deep-copy, so internal state can't be modified.
|
|
|
|
return clone(this.player_x_game_board[this.current_player])
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get the state of the current opponent's board, as it should appear to the
|
|
|
|
* current player. Note that the current player cannot see "ship" spaces, only
|
|
|
|
* available, damaged, missed, or sunk.
|
|
|
|
* @return {object[][]}
|
|
|
|
*/
|
|
|
|
get_current_opponent_state() {
|
|
|
|
// Return a deep-copy, so internal state can't be modified.
|
|
|
|
const state = clone(this.player_x_game_board[this.current_opponent])
|
|
|
|
const hidden_states = [
|
|
|
|
GridCellState.Disabled,
|
|
|
|
GridCellState.Ship,
|
|
|
|
]
|
|
|
|
|
|
|
|
return state.map(row => {
|
|
|
|
return row.map(cell => {
|
|
|
|
if ( hidden_states.includes(cell.render) ) {
|
|
|
|
// This is a hidden state, so hide it
|
|
|
|
cell.render = GridCellState.Available
|
|
|
|
}
|
|
|
|
|
|
|
|
return cell
|
|
|
|
})
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2020-09-12 19:50:35 +00:00
|
|
|
/**
|
|
|
|
* Get the states that should be shown on the victory screen.
|
|
|
|
* First element is the winner's state, second element is the loser's state.
|
|
|
|
* @return {object[][][]}
|
|
|
|
*/
|
2020-09-12 19:43:21 +00:00
|
|
|
get_player_victory_state(){
|
|
|
|
return [clone(this.player_x_game_board[this.current_player]), clone(this.player_x_game_board[this.current_opponent])]
|
|
|
|
}
|
|
|
|
|
2020-09-08 00:07:32 +00:00
|
|
|
/**
|
2020-09-11 19:58:01 +00:00
|
|
|
* get the "score" (the number of hits) that the
|
2020-09-08 00:07:32 +00:00
|
|
|
* current player has (counting sunk ships)
|
|
|
|
* @return {number}
|
2020-09-11 19:58:01 +00:00
|
|
|
* @private
|
2020-09-08 00:07:32 +00:00
|
|
|
*/
|
2020-09-10 01:02:15 +00:00
|
|
|
get_player_score(player) {
|
2020-09-12 19:39:38 +00:00
|
|
|
let score = 0
|
|
|
|
this.player_x_game_board[player].some(row => {
|
|
|
|
row.some(cell => {
|
|
|
|
if ( cell.render === GridCellState.Damaged || cell.render === GridCellState.Sunk ) {
|
|
|
|
score += 1
|
2020-09-08 00:07:32 +00:00
|
|
|
}
|
2020-09-12 19:39:38 +00:00
|
|
|
})
|
|
|
|
})
|
|
|
|
|
|
|
|
return score
|
2020-09-08 00:07:32 +00:00
|
|
|
}
|
2020-09-08 00:12:20 +00:00
|
|
|
|
2020-09-08 00:19:32 +00:00
|
|
|
/**
|
|
|
|
* 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}
|
2020-09-11 19:58:01 +00:00
|
|
|
* @private
|
2020-09-08 00:19:32 +00:00
|
|
|
*/
|
2020-09-10 01:02:15 +00:00
|
|
|
get_boat_count(player){
|
2020-09-12 19:39:38 +00:00
|
|
|
let boat_count = 0
|
|
|
|
this.player_x_game_board[player].some(row => {
|
|
|
|
row.some(cell => {
|
|
|
|
if ( isShipCell(cell.render) ) {
|
|
|
|
boat_count += 1
|
2020-09-08 00:12:20 +00:00
|
|
|
}
|
2020-09-12 19:39:38 +00:00
|
|
|
})
|
|
|
|
})
|
|
|
|
|
|
|
|
return boat_count
|
2020-09-08 00:12:20 +00:00
|
|
|
}
|
2020-09-08 00:19:32 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* gets the progress (hits/total boats) that the player has
|
|
|
|
* @return {number}
|
2020-09-11 19:58:01 +00:00
|
|
|
* @private
|
2020-09-08 00:19:32 +00:00
|
|
|
*/
|
2020-09-10 01:02:15 +00:00
|
|
|
get_progress(player){
|
2020-09-12 19:39:38 +00:00
|
|
|
const boat_count = this.get_boat_count(player)
|
|
|
|
if ( boat_count !== 0 ) {
|
|
|
|
return (this.get_player_score(player) / boat_count).toFixed(2)
|
|
|
|
} else {
|
|
|
|
return 0
|
|
|
|
}
|
2020-09-08 00:19:32 +00:00
|
|
|
}
|
2020-09-08 00:41:21 +00:00
|
|
|
|
2020-09-09 02:10:55 +00:00
|
|
|
/**
|
|
|
|
* Get the current game state.
|
|
|
|
* @return {GameState}
|
|
|
|
*/
|
|
|
|
get_game_state() {
|
|
|
|
return clone(this.current_state)
|
|
|
|
}
|
|
|
|
|
2020-09-08 00:41:21 +00:00
|
|
|
/**
|
2020-09-11 19:58:01 +00:00
|
|
|
* responsible for advancing the game state
|
|
|
|
* will be consisting of
|
|
|
|
* @return
|
2020-09-08 00:41:21 +00:00
|
|
|
*/
|
2020-09-08 02:05:48 +00:00
|
|
|
advance_game_state() {
|
2020-09-08 00:41:21 +00:00
|
|
|
/** 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
|
|
|
|
*/
|
2020-09-08 02:05:48 +00:00
|
|
|
if (this.current_state === GameState.ChoosingNumberOfShips) {
|
|
|
|
if (this.n_boats >= 1 && this.n_boats <= 5) {
|
2020-09-12 18:14:25 +00:00
|
|
|
this.current_state = GameState.PromptPlayerChange;
|
|
|
|
this.post_player_change_state = GameState.PlayerSetup;
|
2020-09-08 01:13:18 +00:00
|
|
|
this.current_player = Player.One;
|
2020-09-08 02:05:48 +00:00
|
|
|
this.current_opponent = Player.Two;
|
|
|
|
} else {
|
|
|
|
throw new InvalidAdvanceStateError("Invalid Number of Boats");
|
2020-09-08 01:13:18 +00:00
|
|
|
}
|
|
|
|
}
|
2020-09-09 16:32:44 +00:00
|
|
|
else if (this.current_state === GameState.PlayerSetup) {
|
2020-09-08 02:05:48 +00:00
|
|
|
if (this.current_player === Player.One) {
|
|
|
|
// because the place_ship handles all the validation
|
|
|
|
// all you need to do is make sure they have placed all the appropriate ships
|
2020-09-09 01:16:43 +00:00
|
|
|
if ( this.get_ship_entities(this.current_player).length === this.n_boats ) {
|
2020-09-12 18:14:25 +00:00
|
|
|
this.current_state = GameState.PromptPlayerChange;
|
|
|
|
this.post_player_change_state = GameState.PlayerSetup;
|
2020-09-09 01:16:43 +00:00
|
|
|
this.current_player = Player.Two;
|
2020-09-11 19:58:01 +00:00
|
|
|
this.current_opponent = Player.One;
|
2020-09-09 01:16:43 +00:00
|
|
|
}
|
|
|
|
else{
|
|
|
|
throw new InvalidAdvanceStateError("Player One has a problem with the number of boats selected");
|
|
|
|
}
|
2020-09-08 01:13:18 +00:00
|
|
|
}
|
2020-09-09 16:32:44 +00:00
|
|
|
else if (this.current_player === Player.Two) {
|
2020-09-09 01:16:43 +00:00
|
|
|
if ( this.get_ship_entities(this.current_player).length === this.n_boats ) {
|
2020-09-12 18:14:25 +00:00
|
|
|
this.current_state = GameState.PromptPlayerChange;
|
|
|
|
this.post_player_change_state = GameState.PlayerTurn;
|
2020-09-09 01:16:43 +00:00
|
|
|
this.current_player = Player.One;
|
|
|
|
this.current_opponent = Player.Two;
|
|
|
|
}
|
|
|
|
else{
|
|
|
|
throw new InvalidAdvanceStateError("Player Two has a problem with the number of boats selected");
|
|
|
|
}
|
2020-09-08 01:13:18 +00:00
|
|
|
}
|
2020-09-09 01:35:06 +00:00
|
|
|
}
|
2020-09-09 16:32:44 +00:00
|
|
|
else if (this.current_state === GameState.PlayerTurn && this.current_player === Player.One) {
|
2020-09-09 01:35:06 +00:00
|
|
|
if (this.current_turn_had_missile_attempt === true) {
|
2020-09-12 18:14:25 +00:00
|
|
|
this.current_state = GameState.PromptPlayerChange;
|
|
|
|
this.post_player_change_state = GameState.PlayerTurn;
|
2020-09-09 01:35:06 +00:00
|
|
|
this.current_player = Player.Two;
|
|
|
|
this.current_opponent = Player.One;
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
throw new InvalidAdvanceStateError("the player has not fired a missle");
|
|
|
|
}
|
|
|
|
}
|
2020-09-09 16:32:44 +00:00
|
|
|
else if (this.current_state === GameState.PlayerTurn && this.current_player === Player.Two) {
|
2020-09-09 01:35:06 +00:00
|
|
|
if (this.current_turn_had_missile_attempt === true) {
|
2020-09-12 18:14:25 +00:00
|
|
|
this.current_state = GameState.PromptPlayerChange;
|
|
|
|
this.post_player_change_state = GameState.PlayerTurn;
|
2020-09-09 01:35:06 +00:00
|
|
|
this.current_player = Player.One;
|
|
|
|
this.current_opponent = Player.Two;
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
throw new InvalidAdvanceStateError("the player has not fired a missle");
|
|
|
|
}
|
2020-09-08 01:13:18 +00:00
|
|
|
}
|
2020-09-12 18:14:25 +00:00
|
|
|
else if ( this.current_state === GameState.PromptPlayerChange ) {
|
|
|
|
if ( !this.post_player_change_state ) {
|
|
|
|
throw new InvalidAdvanceStateError('No state to advance to after player change!')
|
|
|
|
}
|
|
|
|
|
|
|
|
this.current_state = this.post_player_change_state
|
|
|
|
this.post_player_change_state = undefined
|
|
|
|
}
|
2020-09-09 01:42:07 +00:00
|
|
|
|
|
|
|
let winner = this.get_winner();
|
|
|
|
if(winner) {
|
2020-09-11 19:58:01 +00:00
|
|
|
this.current_state = GameState.PlayerVictory;
|
2020-09-09 01:42:07 +00:00
|
|
|
this.current_player = winner;
|
2020-09-12 19:40:48 +00:00
|
|
|
this.current_opponent = this.get_other_player(winner);
|
2020-09-09 01:42:07 +00:00
|
|
|
}
|
|
|
|
|
2020-09-09 16:32:44 +00:00
|
|
|
this.current_turn_had_missile_attempt = false
|
2020-09-10 13:16:52 +00:00
|
|
|
this.game_state_change_listeners.forEach(fn => fn(this.current_state, false))
|
2020-09-08 02:05:48 +00:00
|
|
|
}
|
|
|
|
|
2020-09-09 16:32:44 +00:00
|
|
|
/**
|
|
|
|
* Register a handler to be called when the game state changes.
|
|
|
|
* @param {function} handler
|
|
|
|
*/
|
|
|
|
on_state_change(handler) {
|
|
|
|
this.game_state_change_listeners.push(handler)
|
|
|
|
}
|
|
|
|
|
2020-09-08 12:39:09 +00:00
|
|
|
/**
|
|
|
|
* 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]) {
|
2020-09-09 01:23:49 +00:00
|
|
|
if ( this.current_turn_had_missile_attempt ) {
|
|
|
|
throw new InvalidMissileFireAttemptError('Cannot fire more than once per turn.')
|
|
|
|
} else {
|
|
|
|
this.current_turn_had_missile_attempt = true
|
|
|
|
}
|
|
|
|
|
2020-09-08 12:39:09 +00:00
|
|
|
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)
|
2020-09-12 19:05:43 +00:00
|
|
|
this._trigger_view_update()
|
2020-09-08 12:39:09 +00:00
|
|
|
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)
|
|
|
|
}
|
|
|
|
|
2020-09-12 19:05:43 +00:00
|
|
|
this._trigger_view_update()
|
2020-09-08 12:39:09 +00:00
|
|
|
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)
|
|
|
|
})
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2020-09-08 02:05:48 +00:00
|
|
|
/**
|
|
|
|
* 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)
|
|
|
|
})
|
2020-09-10 13:16:52 +00:00
|
|
|
|
|
|
|
// refresh the view
|
|
|
|
this._trigger_view_update()
|
2020-09-08 02:05:48 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 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
|
|
|
|
})
|
|
|
|
})
|
2020-09-08 01:13:18 +00:00
|
|
|
|
2020-09-08 02:05:48 +00:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2020-09-09 01:23:49 +00:00
|
|
|
/**
|
|
|
|
* If there is a winner, this will return the Player that won.
|
|
|
|
* If no winner has been decided yet, will return undefined.
|
|
|
|
* @return {Player|undefined}
|
|
|
|
*/
|
|
|
|
get_winner() {
|
|
|
|
const [player_1, player_2] = this.players
|
|
|
|
|
|
|
|
// Make sure to sink any fully-damaged ships
|
|
|
|
this._sink_damaged_ships(player_1)
|
2020-09-09 16:32:44 +00:00
|
|
|
const player_1_ship_cells = this.get_ship_cells(player_1)
|
2020-09-09 16:50:57 +00:00
|
|
|
const player_1_loses = (
|
|
|
|
(player_1_ship_cells.length > 0)
|
|
|
|
&& player_1_ship_cells.every(cell => this._get_cell_state(player_1, cell[0], cell[1]).render === GridCellState.Sunk)
|
|
|
|
)
|
2020-09-09 01:23:49 +00:00
|
|
|
if ( player_1_loses ) return player_2
|
|
|
|
|
|
|
|
// Make sure to sink any fully-damaged ships
|
|
|
|
this._sink_damaged_ships(player_2)
|
2020-09-09 16:32:44 +00:00
|
|
|
const player_2_ship_cells = this.get_ship_cells(player_2)
|
2020-09-09 16:50:57 +00:00
|
|
|
const player_2_loses = (
|
|
|
|
(player_2_ship_cells.length > 0)
|
|
|
|
&& player_2_ship_cells.every(cell => this._get_cell_state(player_2, cell[0], cell[1]).render === GridCellState.Sunk)
|
|
|
|
)
|
2020-09-09 01:23:49 +00:00
|
|
|
if ( player_2_loses ) return player_2
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns the other player.
|
|
|
|
* @param {Player} player
|
|
|
|
* @return {Player}
|
|
|
|
*/
|
|
|
|
get_other_player(player) {
|
|
|
|
if ( player === Player.One ) return Player.Two
|
|
|
|
else if ( player === Player.Two ) return Player.One
|
|
|
|
}
|
|
|
|
|
2020-09-12 18:14:25 +00:00
|
|
|
/**
|
|
|
|
* Given a Player type, return the display value of that player.
|
|
|
|
* @param {Player} player
|
|
|
|
* @return {string}
|
|
|
|
*/
|
|
|
|
get_player_display(player) {
|
|
|
|
if ( player === Player.One ) return 'Player 1'
|
|
|
|
else if ( player === Player.Two ) return 'Player 2'
|
|
|
|
}
|
|
|
|
|
2020-09-08 02:05:48 +00:00
|
|
|
/**
|
|
|
|
* 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
|
2020-09-08 00:41:21 +00:00
|
|
|
}
|
2020-09-08 12:39:09 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* 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]
|
|
|
|
}
|
2020-09-10 13:16:52 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Force a view update without changing the current state.
|
|
|
|
* @private
|
|
|
|
*/
|
|
|
|
_trigger_view_update() {
|
|
|
|
this.game_state_change_listeners.forEach(fn => fn(this.current_state, true))
|
|
|
|
}
|
2020-09-07 19:34:43 +00:00
|
|
|
}
|
2020-09-07 20:29:16 +00:00
|
|
|
|
|
|
|
// Export a single instance, so it can be shared by all files
|
|
|
|
// To use the game state service, you should do:
|
|
|
|
// import game_service from './services/GameState.service.js'
|
|
|
|
const game_service = new GameStateService()
|
|
|
|
export default game_service
|