diff --git a/src/components/GameBoard.component.js b/src/components/GameBoard.component.js index c08665f..52f34ee 100644 --- a/src/components/GameBoard.component.js +++ b/src/components/GameBoard.component.js @@ -1,5 +1,6 @@ import {Component} from '../../lib/vues6.js' -import {Player} from '../module/util.js' +import {ShipType, isShipCell} from '../module/util.js' +import game_service from '../services/GameState.service.js' /* * This is the HTML/JavaScript for the game board component. @@ -20,7 +21,7 @@ import {Player} from '../module/util.js' * Battleship grid is 14x14. */ const template = ` -
+

{{ i + 1 }} @@ -28,6 +29,8 @@ const template = ` v-for="(cell,j) of row" v-bind:render="cell.render" @click="on_cell_click(i,j)" + @hover="on_cell_hover(i,j)" + v-bind:has_ghost_ship="is_ghost_cell(i,j)" >
@@ -39,7 +42,7 @@ const template = ` export default class GameBoardComponent extends Component { static get selector() { return 'app-game-board' } static get template() { return template } - static get props() { return ['rows'] } + static get props() { return ['rows', 'is_placement_mode', 'ships_to_place'] } /** * If true, the grid is ready to be rendered. If false, @@ -49,24 +52,168 @@ export default class GameBoardComponent extends Component { ready = false /** - * Array of grid rows. Each element in this array is itself - * an array of grid cell values. - * @type {Array>} + * The various column labels to display. + * @type {string[]} */ - // rows = [] column_labels = ["A", "B", "C", "D", "E", "F", "G", "H", "I"] <<<<<<< Updated upstream ======= >>>>>>> Stashed changes + /** + * Array of coordinates as [row_index, column_index] of cells which should + * show a ghost ship overlay. + * @type {[number, number][]} + */ + ship_ghost_cells = [] + + /** + * The ship currently being placed. + * @type {string} + */ + current_placement = ShipType.x3 + + /** + * Set to true when the shift key is pressed. + * @type {boolean} + */ + shift_pressed = false + + /** + * Array of functions bound to event listeners. Used to + * remove event listeners on destroy. + * @type {function[]} + */ + bound_fns = [] + async vue_on_create() { this.ready = true + + // We need to listen for keyup/keydown so we can tell when the user has + // pressed/released the shift key. + const keyup_fn = this.on_keyup.bind(this) + const keydown_fn = this.on_keydown.bind(this) + this.bound_fns.push(keyup_fn, keydown_fn) + + window.addEventListener('keyup', keyup_fn) + window.addEventListener('keydown', keydown_fn) + } + + async vue_on_destroy() { + // Remove the event listeners for the shift key + const [keyup_fn, keydown_fn] = this.bound_fns + window.removeEventListener('keyup', keyup_fn) + window.removeEventListener('keydown', keydown_fn) } <<<<<<< Updated upstream on_cell_click(row_i, cell_i) { - alert(`${row_i} : ${cell_i}`) + if ( this.is_placement_mode && this.ships_to_place[0] ) { + // We should try to place a ship here + if ( this.ship_ghost_cells.length > 0 ) { + // We have some valid ship placement coordinates + const coord_one = this.ship_ghost_cells[0] + const coord_two = this.ship_ghost_cells.slice(-1)[0] + + game_service.place_ship(this.ships_to_place[0], coord_one, coord_two) + this.$emit('shipplaced') + } + } + } + + /** + * Called when the user hovers over a cell. + * When in placement mode, this updates the cells that show the ghost ship. + * @param {number} row_i + * @param {number} cell_i + */ + on_cell_hover(row_i, cell_i) { + if ( this.is_placement_mode ) { + // If we're in placement mode, determine if the cell the user is hovering + // over is a valid place to place the ship. + const ghost_cells = [[row_i, cell_i]] + const is_horizontal = this.shift_pressed + let is_valid_hover = true + + if ( !is_horizontal ) { + const num_cells = game_service.get_ship_length(this.ships_to_place[0]) + for ( let i = row_i + 1; i < row_i + num_cells; i += 1 ) { + ghost_cells.push([i, cell_i]) + if ( i > 8 ) is_valid_hover = false + } + } else { + const num_cells = game_service.get_ship_length(this.ships_to_place[0]) + for ( let i = cell_i + 1; i < cell_i + num_cells; i += 1 ) { + ghost_cells.push([row_i, i]) + if ( i > 8 ) is_valid_hover = false + } + } + + // Don't allow placing on existing ships + is_valid_hover = is_valid_hover && !ghost_cells.some(([row_i, col_i]) => this.is_ship_cell(row_i, col_i)) + + if ( is_valid_hover ) { + this.ship_ghost_cells = ghost_cells + } else { + this.ship_ghost_cells = [] + } + } else { + this.ship_ghost_cells = [] + } + } + + /** + * Returns true if the cell at [row_index, column_index] is a ship. + * @param {number} row_i + * @param {number} col_i + * @return {boolean} + */ + is_ship_cell(row_i, col_i) { + return this.rows[row_i] && this.rows[row_i][col_i] && isShipCell(this.rows[row_i][col_i].render) + } + + /** + * Hide the ghost ship when the mouse leaves the grid. + */ + on_mouse_leave() { + this.ship_ghost_cells = [] + } + + /** + * Returns a truthy value if the given cell is a ghost ship. + * @param {number} row_i + * @param {number} col_i + * @return {boolean} + */ + is_ghost_cell(row_i, col_i) { + return !!this.ship_ghost_cells.find(([cell_row_i, cell_col_i]) => cell_row_i === row_i && cell_col_i === col_i) + } + + /** + * When keydown, check if shift was pressed. If so, update the placement. + * @param event + */ + on_keydown(event) { + if ( event.key === 'Shift' ) { + this.shift_pressed = true + if ( this.ship_ghost_cells.length > 0 ) { + this.on_cell_hover(this.ship_ghost_cells[0][0], this.ship_ghost_cells[0][1]) + } + } + } + + /** + * When keyup, check if shift was released. If so, update the placement. + * @param event + */ + on_keyup(event) { + if ( event.key === 'Shift' ) { + this.shift_pressed = false + if ( this.ship_ghost_cells.length > 0 ) { + this.on_cell_hover(this.ship_ghost_cells[0][0], this.ship_ghost_cells[0][1]) + } + } } ======= diff --git a/src/components/GridCell.component.js b/src/components/GridCell.component.js index 738ad0a..74f6d35 100644 --- a/src/components/GridCell.component.js +++ b/src/components/GridCell.component.js @@ -5,9 +5,11 @@ const template = `
@@ -20,6 +22,7 @@ export default class GridCellComponent extends Component { static get props() { return [ 'render', + 'has_ghost_ship', ] } @@ -29,4 +32,12 @@ export default class GridCellComponent extends Component { on_click() { this.$emit('click') } + + on_hover($event) { + this.$emit('hover', $event) + } + + on_mouse_leave() { + this.$emit('hoverchange') + } } diff --git a/src/components/ScoreBoard.component.js b/src/components/ScoreBoard.component.js index 7c424f9..3cdda0e 100644 --- a/src/components/ScoreBoard.component.js +++ b/src/components/ScoreBoard.component.js @@ -13,15 +13,23 @@ export default class ScoreBoardComponent extends Component { static get props() { return [] } player_one_score = 0 + player_two_score = 0 + player_one_progress = 0 + player_two_progress = 0 async vue_on_create() { game_service.on_state_change(() => { this.update() }) + + this.update() } update() { // here is where you should fetch the data from the game service and update variables on the class this.player_one_score = game_service.get_player_score(Player.One) + this.player_two_score = game_service.get_player_score(Player.Two) + this.player_one_progress = game_service.get_progress(Player.One) + this.player_two_progress = game_service.get_progress(Player.Two) } } diff --git a/src/components/TopLevel.component.js b/src/components/TopLevel.component.js index f362d8f..3ecfbbd 100644 --- a/src/components/TopLevel.component.js +++ b/src/components/TopLevel.component.js @@ -1,5 +1,5 @@ import {Component} from '../../lib/vues6.js' -import {GameState} from '../module/util.js' +import {GameState, ShipType} from '../module/util.js' import game_service from '../services/GameState.service.js' const template = ` @@ -40,7 +40,12 @@ const template = `
- +
@@ -66,26 +71,73 @@ export default class TopLevelComponent extends Component { */ current_state = undefined + /** + * The opponent's grid data. + * @type {object[][]} + */ opponent_rows = [] + /** + * The player's grid data. + * @type {object[][]} + */ player_rows = [] + /** + * The current instructions to be shown to the user. + * @type {string} + */ instructions = '' + /** + * True if the player should be able to place their ships. + * @type {boolean} + */ + player_is_placing_ships = false + + /** + * If in placement mode, the ships that are yet to be placed. + * @type {ShipType[]} + */ + ships_to_place = [] + async vue_on_create() { console.log('game service', game_service) this.current_state = game_service.get_game_state() - game_service.on_state_change((next_state) => { + + // Called every time the game state is updated + game_service.on_state_change((next_state, was_refresh) => { this.current_state = next_state this.opponent_rows = game_service.get_current_opponent_state() this.player_rows = game_service.get_current_player_state() - // add code for instructions + this.player_is_placing_ships = next_state === GameState.PlayerSetup + if ( !was_refresh && this.player_is_placing_ships ) { + // TODO replace with call to game state service + this.ships_to_place = [ShipType.x1, ShipType.x2, ShipType.x3] + } + + // TODO add code for instructions }) } + /** + * Set the number of boats. + * @param {number} n + */ ship(n) { game_service.set_n_boats(n) game_service.advance_game_state() } + + /** + * Called when the current user has placed a ship. + */ + on_ship_placed() { + this.ships_to_place.shift() + if ( this.ships_to_place.length < 1 ) { + // We've placed all the ships. Let's move on. + game_service.advance_game_state() + } + } } diff --git a/src/services/GameState.service.js b/src/services/GameState.service.js index a1335d9..023ef59 100644 --- a/src/services/GameState.service.js +++ b/src/services/GameState.service.js @@ -331,7 +331,7 @@ export class GameStateService { } this.current_turn_had_missile_attempt = false - this.game_state_change_listeners.forEach(fn => fn(this.current_state)) + this.game_state_change_listeners.forEach(fn => fn(this.current_state, false)) } /** @@ -439,6 +439,9 @@ export class GameStateService { 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) }) + + // refresh the view + this._trigger_view_update() } /** @@ -644,6 +647,14 @@ export class GameStateService { _get_cell_state(player, row_i, col_i) { return this.player_x_game_board[player][row_i][col_i] } + + /** + * 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)) + } } // Export a single instance, so it can be shared by all files diff --git a/src/style/components.css b/src/style/components.css index 5891f56..548a3a5 100644 --- a/src/style/components.css +++ b/src/style/components.css @@ -67,6 +67,10 @@ background: #ffbbbb; } +.game-board-cell-component.ghost { + background: #507090; +} + .column_labels { display: flex; margin-top: 5px;