import Component from '../../rivets/Component.js' import { wait } from '../../rivets/helpers.js' // Enum for the different players const PLAYER = { one: Symbol('one'), two: Symbol('two'), none: Symbol('none'), } // Enum for some commonly used colors const COLORS = { empty: 'darkblue', player_1: '#f30000', player_2: '#daf500', victory: 'darkgreen', dark: '#333', } export { PLAYER, COLORS } // Component for the Connect-4 grid export default class Grid extends Component { static selector() { return 'app-grid' } static template() { return ` ` } // Array of GridRow components rows = Array(6).fill() // GridSlots only allows drop if this is true allow_drop = true // The current player current_player = PLAYER.one // The designated winner winner = undefined constructor(el, data) { super(el, data) this.app = data.parentApp this.app.register_grid(this) } on_drop(column_number) { // Check if a drop is possible on the column if ( this._can_drop_on_column(column_number) ) { // Disallow drops temporarily this.allow_drop = false // Wrap async so we can use wait non-blocking ;(async () => { // Animate the move so there's an actual 'drop' for ( let i = 0; i < this.rows.length; i++ ) { const cell = this.rows[i].cells[column_number] await wait(20) if ( cell.is_available() ) { if ( i > 0 ) this.rows[i - 1].cells[column_number].set_player() cell.set_player(this.current_player) await wait(20) } else break // stop if we hit the end, or this row has a chip already } // Check for a winner const winner = this.get_winner() if ( winner ) { this.winner = winner this.on_game_over() return } // Check for full grid if ( this.is_full() ) { this.winner = PLAYER.none this.on_game_over() return } // Unblock dropping and switch to the other player this.allow_drop = true this.change_player() })() } } // Callback from row creation - register the rows' component classes register_row(row, row_number) { this.rows[row_number] = row; } // Change the current player (e.g. P1 -> P2/P2 -> P1) change_player() { if ( this.current_player === PLAYER.one ) this.current_player = PLAYER.two else this.current_player = PLAYER.one } // Clears the grid and resets the game to play again reset() { this.get_cells().some(cell => cell.set_player()) this.winner = undefined this.current_player = PLAYER.one this.allow_drop = true } // Get a flat array of all the grid's cells get_cells() { return this.rows.map(row => row.cells).flat() } // Get the cells in groups of columns, not rows get_columns() { return Array(8).fill().map((x,i) => this.rows.map(row => row.cells[i])) } // Get all sets of 4 diagonal cells get_diagonals() { const columns = this.get_columns() const diagonal_groups = [] // Build the upward-right diagonals (possible from cell 3 on columns 0 -> 4) const up_right_cols = columns.slice(0, 5); up_right_cols.some((col, i) => { for ( const cell_index of [3, 4, 5] ) { const diagonal_group = [ col[cell_index], columns[i + 1][cell_index - 1], columns[i + 2][cell_index - 2], columns[i + 3][cell_index - 3], ] diagonal_groups.push(diagonal_group) } }) // Build the upward-left diagonals (possible from cell 3 on columns 3 -> 7) const up_left_cols = columns.slice(3, 8); up_left_cols.some((col, ind) => { // Correct the index with respect to this.columns (because we sliced from the front) const i = ind + 3; for ( const cell_index of [3, 4, 5] ) { const diagonal_group = [ col[cell_index], columns[i - 1][cell_index - 1], columns[i - 2][cell_index - 2], columns[i - 3][cell_index - 3], ] diagonal_groups.push(diagonal_group) } }) return diagonal_groups } // True if the grid is full, false otherwise is_full() { return Array(8).fill().every((x, i) => !this._can_drop_on_column(i)) } // Returns true if a drop is possible on the given column number _can_drop_on_column(column_number) { return this.rows[0].cells[column_number].is_available() } // Check if either of the players has won the game // If they have, return the winner, otherwise return undefined get_winner() { // Check if there are any row-wise runs of 4 const row_wise = this.rows.map(row => this._check_cells_for_winner(row.cells)) .filter(player => player !== PLAYER.none) .shift() // Check if there are any column-wise runs of 4 const column_wise = this.get_columns() .map(col_group => this._check_cells_for_winner(col_group)) .filter(player => player !== PLAYER.none) .shift() // Check if there are any diagonal runs of 4 const diagonal_wise = this.get_diagonals() .map(diag_group => this._check_cells_for_winner(diag_group)) .filter(player => player !== PLAYER.none) .shift() return [row_wise, column_wise, diagonal_wise].filter(Boolean).shift() } // Check the group of cells for a 4-long run // Returns the player with the run, or PLAYER.none if no run was found _check_cells_for_winner(cells = []) { let current_player = PLAYER.none let run_length = 0 for ( const cell of cells ) { if ( cell.is_available() ) { current_player = PLAYER.none run_length = 0 } else if ( cell.player === current_player ) { run_length += 1; if ( run_length >= 4 ) return current_player } else { current_player = cell.player run_length = 1 } } return PLAYER.none } // Fired when the game has ended, either from a winner or a draw on_game_over() { ;(async () => { if ( this.winner !== PLAYER.none ) { await this.app.scoreboard.increment(this.winner) this.reset() } })() } }