@reis/seki
Version:
Seki – A modern javascript based Go board renderer and player, that is simple to use, extensible, compact and intuitive.
383 lines (310 loc) • 7.49 kB
JavaScript
import GridChanges from './grid-changes.js'
/**
* This class represents a board grid of a given size. It acts as a
* container for values (e.g. stone colors, markup types) for board layers,
* as well as a container for stone color values for the game position class.
*/
export default class Grid {
/**
* Constructor
*/
constructor(width, height) {
//Initialize size and underlying grid map
this.width = 0
this.height = 0
this.grid = new Map()
//Size given? Set it
if (width || height) {
this.setSize(width, height)
}
}
/**
* Set a value
*/
set(x, y, value) {
if (!this.isOnGrid(x, y)) {
return
}
const key = this.getGridMapKey(x, y)
this.grid.set(key, value)
}
/**
* Get a value
*/
get(x, y) {
if (!this.isOnGrid(x, y)) {
return null
}
return this.getGridMapValue(x, y)
}
/**
* Delete a value
*/
delete(x, y) {
if (!this.isOnGrid(x, y)) {
return
}
const key = this.getGridMapKey(x, y)
this.grid.delete(key)
}
/**
* Check if we have a non null value on the coordinates
*/
has(x, y) {
if (!this.isOnGrid(x, y)) {
return false
}
const key = this.getGridMapKey(x, y)
return this.grid.has(key)
}
/**
* Check if we have a specific value on the coordinates
*/
is(x, y, check) {
//Nothing on grid on these coordinates
if (!this.isOnGrid(x, y)) {
return false
}
//Get value
const value = this.getGridMapValue(x, y)
//Object, check keys (1 level deep)
if (value && typeof value === 'object') {
//Check is not an object
if (!check || typeof check !== 'object') {
return false
}
//Compare
return Object
.keys(check)
.every(key => value[key] === check[key])
}
//Simple check
return (value === check)
}
/*****************************************************************************
* Bulk operations
***/
/**
* Iterator
*/
*[Symbol.iterator]() {
for (const [key, value] of this.grid) {
const {x, y} = this.getCoords(key)
yield {x, y, value}
}
}
/**
* Get all items in the grid. If you specify a value key, a list of objects
* with coordinates and the value in the given value key will be returned.
*/
getAll() {
//Initialize objects
const objects = []
//Loop map
for (const [key, value] of this.grid) {
const {x, y} = this.getCoords(key)
objects.push({x, y, value})
}
//Return objects list
return objects
}
/**
* Check if there is anything
*/
isEmpty() {
return (this.grid.size === 0)
}
/**
* Clear the grid
*/
clear() {
this.grid.clear()
}
/**
* Clone grid
*/
clone() {
//Create new instance
const {width, height, grid} = this
const clone = new Grid(width, height)
//Copy grid
clone.grid = new Map(grid)
//Return
return clone
}
/**
* Transform grid into another grid applying a transformation function
* on each entry. This is used to transform simple game position grids
* into grids with Stone or Markup instances
*/
map(fn) {
//Create new instance
const {width, height, grid} = this
const clone = new Grid(width, height)
//Process each entry
for (const [key, value] of grid) {
clone.grid.set(key, fn(value))
}
//Return cloned grid
return clone
}
/**
* Create a new grid, filtering out entries
*/
filter(fn) {
//Create new instance
const {width, height, grid} = this
const clone = new Grid(width, height)
//Process each entry
for (const [key, value] of grid) {
if (fn(value)) {
clone.grid.set(key, value)
}
}
//Return clone
return clone
}
/**
* Iterate each item in the grid
*/
forEach(fn) {
//Get grid
const {grid} = this
//Process each entry
for (const [key, value] of grid) {
const {x, y} = this.getCoords(key)
fn(value, x, y)
}
}
/**
* Checks if a given grid is the same as the current grid
*/
isSameAs(other) {
//Get data
const {width, height, grid} = this
//Must have the same size
if (width !== other.width || height !== other.height) {
return false
}
//Must have the same amount of entries
if (grid.size !== other.grid.size) {
return false
}
//Each entry should be the same
for (const [key, value] of grid) {
if (other.grid.get(key) !== value) {
return false
}
}
//No differences found
return true
}
/**
* Compares this position with another position and return change object
*/
compare(newGrid) {
//Get data
const {width, height, grid} = this
//Must have the same size
if (width !== newGrid.width || height !== newGrid.height) {
throw new Error('Trying to compare grids of a different size')
}
//Initialize grid changes instance
const changes = new GridChanges()
//Go over each entry in the existing grid
for (const [key, value] of grid) {
//Something to remove?
if (!newGrid.grid.has(key)) {
const {x, y} = this.getCoords(key)
changes.remove.push({x, y, value})
}
}
//Go over each entry in the new grid
for (const [key, value] of newGrid.grid) {
//Something to add?
if (!grid.has(key)) {
const {x, y} = this.getCoords(key)
changes.add.push({x, y, value})
}
}
//Return changes
return changes
}
/**
* Convert position to a matrix of arrays, compatible with
* for example https://github.com/SabakiHQ/deadstones
*/
toMatrix(transformFn) {
//Get size and initialise matrix
const {width, height} = this
const matrix = []
//Loop through each column
for (let y = 0; y < height; y++) {
//Create row
const row = []
//Loop through each row
for (let x = 0; x < width; x++) {
//Get value and transform it if needed
const value = this.getGridMapValue(x, y)
const transformed = transformFn ? transformFn(value) : value
//Push value
row.push(transformed)
}
//Add to matrix
matrix.push(row)
}
//Return
return matrix
}
/*****************************************************************************
* Helpers
***/
/**
* Helper to validate coordinates (first param can be an object)
*/
isOnGrid(x, y) {
const {width, height} = this
return (x >= 0 && y >= 0 && x < width && y < height)
}
/**
* Set the grid size
*/
setSize(width, height) {
//Only if anything changed
if (this.width === width && this.height === height) {
return
}
//Set
this.width = width || 0
this.height = height || width || 0
//Clear grid
this.grid.clear()
}
/**
* Get the grid size object
*/
getSize() {
const {width, height} = this
return {width, height}
}
/**
* Get grid key for a given coordinate
*/
getGridMapKey(x, y) {
return `${x},${y}`
}
/**
* Get grid value for a given coordinate
*/
getGridMapValue(x, y) {
const key = this.getGridMapKey(x, y)
return this.grid.get(key)
}
/**
* Get coordinates based on map key
*/
getCoords(mapKey) {
const [x, y] = mapKey.split(',')
return {x: parseInt(x), y: parseInt(y)}
}
}