You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
231 lines
6.1 KiB
231 lines
6.1 KiB
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 `
|
|
<app-grid-slots parent-grid="self"></app-grid-slots>
|
|
<app-grid-row parent-grid="self" row-number="0"></app-grid-row>
|
|
<app-grid-row parent-grid="self" row-number="1"></app-grid-row>
|
|
<app-grid-row parent-grid="self" row-number="2"></app-grid-row>
|
|
<app-grid-row parent-grid="self" row-number="3"></app-grid-row>
|
|
<app-grid-row parent-grid="self" row-number="4"></app-grid-row>
|
|
<app-grid-row parent-grid="self" row-number="5"></app-grid-row>
|
|
` }
|
|
|
|
// 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()
|
|
}
|
|
})()
|
|
}
|
|
}
|