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

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()
}
})()
}
}