p5.bitboard
Version:
A bitboard-based addon for p5.quadrille.js to teach and render grid-based logic in p5.js.
762 lines (708 loc) • 21.5 kB
JavaScript
import p5 from 'p5';
import Quadrille from 'p5.quadrille';
/**
* @file Defines the Bitboard class — the core class of the p5.bitboard library.
* @version 0.3.0
* @author JP Charalambos
* @license GPL-3.0-only
*
* @description
* Bitboard p5.js toolkit.
* This module defines the Bitboard class.
*/
// TODOs
class Bitboard {
static VERSION = '0.3.0'
/**
* Returns a new bitboard with bitwise AND of a and b.
* @param {Bitboard} a
* @param {Bitboard} b
* @returns {Bitboard}
*/
static and(a, b) {
const fitted = b._fit(a);
return a._create((a._bigint & fitted.bigint) & a.mask(), a._width, a._height, a._littleEndian)
}
/**
* Returns a new bitboard with bitwise OR of a and b.
* @param {Bitboard} a
* @param {Bitboard} b
* @returns {Bitboard}
*/
static or(a, b) {
const fitted = b._fit(a);
return a._create((a._bigint | fitted.bigint) & a.mask(), a._width, a._height, a._littleEndian)
}
/**
* Returns a new bitboard with bitwise XOR of a and b.
* @param {Bitboard} a
* @param {Bitboard} b
* @returns {Bitboard}
*/
static xor(a, b) {
const fitted = b._fit(a);
return a._create((a._bigint ^ fitted.bigint) & a.mask(), a._width, a._height, a._littleEndian)
}
/**
* Returns a new bitboard with bitwise NOT of a.
* @param {Bitboard} a
* @returns {Bitboard}
*/
static not(a) {
return a._create((~a._bigint) & a.mask(), a._width, a._height, a._littleEndian)
}
/**
* Creates a new Bitboard.
* Accepts up to four arguments in any of the following combinations:
* - `bitboard` (bigint or binary string)
* - `bitboard`, `width`
* - `bitboard`, `width`, `height`
* - `bitboard`, `width`, `height`, `littleEndian`
* If `height` is omitted, it is inferred from bit length and width.
* Binary strings may be prefixed with "0b" or passed raw.
* @param {...(bigint|string|number|boolean)} args Bitboard value, dimensions, or endianness.
*/
constructor(...args) {
let bigint = 0n;
let width = 8;
let height;
let littleEndian = false;
args.forEach(arg =>
typeof arg === 'bigint'
? bigint = arg
: typeof arg === 'string'
? bigint = BigInt(arg.startsWith('0b') ? arg : '0b' + arg)
: typeof arg === 'number' && width === 8
? width = arg
: typeof arg === 'number' && height === undefined
? height = arg
: typeof arg === 'boolean'
? littleEndian = arg
: null
);
if (height === undefined) {
const bits = bigint.toString(2).length;
height = Math.ceil(bits / width);
console.log(`Bitboard: computed height = ${height}`);
}
const dim = width * height;
const bits = bigint.toString(2).length;
if (bits > dim) {
console.warn('Bitboard is too long and will be cropped');
const mask = (1n << BigInt(dim)) - 1n;
const cropped = bigint & mask;
console.warn('Cropped bitboard:', cropped.toString(2).padStart(dim, '0'));
bigint = cropped;
}
this._bigint = bigint;
this._width = width;
this._height = height;
this._littleEndian = littleEndian;
}
_patch(other) {
other._p = this._p;
other._cellLength = this._cellLength;
other._x = this._x;
other._y = this._y;
other._origin = this._origin;
other.screenRow = this.screenRow;
other.screenCol = this.screenCol;
}
_create(...args) {
const bb = new Bitboard(...args);
this._patch(bb);
return bb
}
/**
* Creates a deep clone of the bitboard.
* @returns {Bitboard}
*/
clone() {
return this._create(this._bigint, this._width, this._height, this._littleEndian)
}
/**
* Returns current mouse row based on screen position.
* @returns {number}
*/
get mouseRow() {
return this.screenRow(this._p.mouseY)
}
/**
* Returns current mouse column based on screen position.
* @returns {number}
*/
get mouseCol() {
return this.screenCol(this._p.mouseX)
}
/**
* Returns the raw bigint value of the bitboard.
* @returns {bigint}
*/
get bigint() {
return this._bigint
}
/**
* Returns the number of columns in the bitboard.
* @returns {number}
*/
get width() {
return this._width
}
/**
* Returns the number of rows in the bitboard.
* @returns {number}
*/
get height() {
return this._height
}
/**
* Returns whether the bitboard is little-endian.
* @returns {boolean}
*/
get littleEndian() {
return this._littleEndian
}
/**
* Returns the total number of cells (width × height).
* @returns {number}
*/
get size() {
return this._width * this._height
}
/**
* Counts how many bits are set to 1.
* @returns {number}
*/
get order() {
let count = 0n;
let v = this._bigint;
while (v) {
count += v & 1n;
v >>= 1n;
}
return Number(count)
}
/**
* Returns true if (row, col) is within the bounds of the bitboard.
* @param {number} row
* @param {number} col
* @returns {boolean}
*/
isValid(row, col) {
return row >= 0 && row < this.height && col >= 0 && col < this.width;
}
/**
* Lazily iterates in row-major order (top to bottom, left to right)
* over all matching cells in the bitboard.
* The optional `filter` is a predicate function of the form
* `({ row, col, bit }) => boolean` that selects which cells to yield.
* If omitted, all cells are yielded.
* @generator
* @param {Function|null} [filter=null] - Optional predicate function.
* @yields {{ row: number, col: number, bit: 0|1 }}
*/
*cells(filter = null) {
const isFn = typeof filter === 'function';
for (let row = 0; row < this._height; row++) {
for (let col = 0; col < this._width; col++) {
const index = BigInt(this.index(row, col));
const bit = Number((this._bigint >> index) & 1n);
const cell = { row, col, bit };
if (!filter || (isFn && filter(cell))) {
yield cell;
}
}
}
}
/**
* Default iterator for the bitboard.
* Allows iteration over all cells using `for...of`.
* Equivalent to `this.cells()` with no filter.
* @generator
* @returns {IterableIterator<{ row: number, col: number, bit: 0|1 }>}
*/
*[Symbol.iterator]() {
yield* this.cells();
}
/**
* Iterates over cells using `for...of`, calling the given function with each cell object.
* @param {(cell: { row: number, col: number, bit: 0|1 }) => void} callback - Function to apply to each cell.
* @param {Function|null} [filter] - Optional predicate function for filtering.
*/
visit(callback, filter = null) {
for (const cell of this.cells(filter)) {
callback(cell);
}
}
/**
* Returns the bit index of a cell at (row, col).
* @param {number} row
* @param {number} col
* @returns {number}
*/
index(row, col) {
const index = row * this._width + col;
return this._littleEndian
? index
: this.size - 1 - index
}
/**
* Converts a bit index to its (row, col) cell position.
* @param {number|bigint} bitIndex
* @returns {{ row: number, col: number }}
*/
cell(bitIndex) {
const maxIndex = this.size - 1;
const raw = Number(bitIndex);
const index = this._littleEndian ? raw : maxIndex - raw;
return {
row: Math.floor(index / this._width),
col: index % this._width
}
}
/**
* Transposes the bitboard (rows become columns).
* @returns {Bitboard}
*/
transpose() {
let result = 0n;
for (const { row, col } of this.cells(({ bit }) => bit === 1)) {
// Transpose: (row, col) → (col, row)
const raw = col * this._height + row;
const index = this._littleEndian
? BigInt(raw)
: BigInt(this._width * this._height - 1 - raw);
result |= 1n << index;
}
[this._width, this._height] = [this._height, this._width];
this._bigint = result;
return this
}
/**
* Reflects the bitboard vertically.
* @returns {Bitboard}
*/
reflect() {
let result = 0n;
for (const { row, col } of this.cells(({ bit }) => bit === 1)) {
const i = BigInt((this._height - 1 - row) * this._width + col);
const index = this._littleEndian
? i
: BigInt(this.size - 1) - i;
result |= 1n << index;
}
this._bigint = result;
return this
}
/**
* Rotates the bitboard 90° clockwise (transpose + reflect).
* @returns {Bitboard}
*/
rotate() {
return this.reflect().transpose()
}
/**
* Randomly fills or clears bits in available cells.
* @param {number} times - Positive to fill, negative to clear.
* @returns {Bitboard}
*/
rand(times = 1) {
if (times === 0) return this
const filter = ({ bit }) => times > 0 ? bit === 0 : bit === 1;
const pool = [...this.cells(filter)];
const n = Math.min(Math.abs(times), pool.length);
for (let i = 0; i < n; i++) {
const j = Math.floor(Math.random() * pool.length);
const { row, col } = pool.splice(j, 1)[0];
times > 0 ? this.fill(row, col) : this.clear(row, col);
}
return this
}
/**
* Clears and fills the bitboard randomly with the same number of set bits.
* @returns {Bitboard}
*/
randomize() {
const times = this.order;
return this.clear().rand(times)
}
/**
* Fills the entire board, a row, or a single cell.
* - `fill()` fills the entire bitboard.
* - `fill(row)` fills the entire row.
* - `fill(row, col)` fills a single cell.
* @param {...number} args Optional row and column indices.
* @returns {Bitboard}
*/
fill(...args) {
if (args.length === 0) {
// Fill entire bitboard
this._bigint = (1n << BigInt(this.size)) - 1n;
return this
}
let row, col;
args.forEach(arg =>
typeof arg === 'number' && row === undefined ? row = arg :
typeof arg === 'number' && col === undefined ? col = arg : null
);
if (col === undefined) {
// Fill entire row
for (let c = 0; c < this._width; c++) this.fill(row, c);
} else {
// Fill single cell
const index = BigInt(this.index(row, col));
this._bigint |= (1n << index);
}
return this
}
/**
* Clears the entire board, a row, or a single cell.
* - `clear()` clears the entire bitboard.
* - `clear(row)` clears the entire row.
* - `clear(row, col)` clears a single cell.
* @param {...number} args Optional row and column indices.
* @returns {Bitboard}
*/
clear(...args) {
if (args.length === 0) {
// Clear entire bitboard
this._bigint = 0n;
return this
}
let row, col;
args.forEach(arg =>
typeof arg === 'number' && row === undefined ? row = arg :
typeof arg === 'number' && col === undefined ? col = arg : null
);
if (col === undefined) {
// Clear entire row
for (let c = 0; c < this._width; c++) this.clear(row, c);
} else {
// Clear single cell
const index = BigInt(this.index(row, col));
this._bigint &= ~(1n << index);
}
return this
}
/**
* Toggles the entire board, a row, or a single cell.
* - `toggle()` inverts all bits.
* - `toggle(row)` inverts all bits in the given row.
* - `toggle(row, col)` inverts the bit at (row, col).
* @param {...number} args Optional row and column indices.
* @returns {Bitboard}
*/
toggle(...args) {
if (args.length === 0) {
// Toggle all bits
const mask = (1n << BigInt(this.size)) - 1n;
this._bigint ^= mask;
return this
}
let row, col;
args.forEach(arg =>
typeof arg === 'number' && row === undefined ? row = arg :
typeof arg === 'number' && col === undefined ? col = arg : null
);
if (col === undefined) {
// Toggle entire row (non-recursive)
for (let c = 0; c < this._width; c++) {
const index = BigInt(this.index(row, c));
this._bigint ^= (1n << index);
}
} else {
// Toggle single cell
const index = BigInt(this.index(row, col));
this._bigint ^= (1n << index);
}
return this
}
/**
* Checks if the cell at (row, col) is filled.
* @param {number} row
* @param {number} col
* @returns {boolean}
*/
isFilled(row, col) {
const index = BigInt(this.index(row, col));
return (this._bigint >> index) & 1n ? true : false
}
/**
* Checks if the cell at (row, col) is empty.
* @param {number} row
* @param {number} col
* @returns {boolean}
*/
isEmpty(row, col) {
return !this.isFilled(row, col)
}
/**
* Returns the bitboard as a binary string.
* @returns {string}
*/
toBinaryString() {
return this._bigint.toString(2)
}
/**
* Returns a mask with all valid bits set.
* @returns {bigint}
*/
mask() {
return (1n << BigInt(this.size)) - 1n
}
/**
* Bitwise AND with another bitboard (in place).
* @param {Bitboard} other
* @returns {Bitboard}
*/
and(other) {
this._bigint = this.constructor.and(this, other)._bigint;
return this
}
/**
* Bitwise OR with another bitboard (in place).
* @param {Bitboard} other
* @returns {Bitboard}
*/
or(other) {
this._bigint = this.constructor.or(this, other)._bigint;
return this
}
/**
* Bitwise XOR with another bitboard (in place).
* @param {Bitboard} other
* @returns {Bitboard}
*/
xor(other) {
this._bigint = this.constructor.xor(this, other)._bigint;
return this
}
/**
* Bitwise NOT (in place).
* @returns {Bitboard}
*/
not() {
this._bigint = this.constructor.not(this)._bigint;
return this
}
_fit(other) {
const width = other.width;
const height = other.height;
const original = this.bigint;
const fitted = new Bitboard(0n, width, height, this._littleEndian);
for (const { row, col, bit } of this) {
if (row < height && col < width && bit) {
fitted.fill(row, col);
}
}
if (fitted.bigint !== original) {
console.warn(`Bitboard value changed from ${original} to ${fitted.bigint} to fit dimensions (${width}, ${height})`);
}
return fitted
}
/**
* Returns a new bitboard with a ring of size radius centered at (row, col).
* @param {number} row
* @param {number} col
* @param {number} [radius=1]
* @param {boolean} [wrap=true]
* @returns {Bitboard}
*/
ring(row, col, radius = 1, wrap = true) {
const W = this._width;
const H = this._height;
const S = 2 * radius + 1;
let bits = 0n;
for (let dr = -radius; dr <= radius; dr++) {
for (let dc = -radius; dc <= radius; dc++) {
const isRing = Math.abs(dr) === radius || Math.abs(dc) === radius || (dr === 0 && dc === 0);
if (!isRing) continue
let rr = row + dr;
let cc = col + dc;
if (!wrap) {
if (rr < 0 || rr >= H || cc < 0 || cc >= W) continue
} else {
rr = (rr + H) % H;
cc = (cc + W) % W;
}
const originalIndex = BigInt(this.index(rr, cc));
const bit = (this._bigint >> originalIndex) & 1n;
if (bit) {
const newIndex = BigInt((dr + radius) * S + (dc + radius));
bits |= 1n << newIndex;
}
}
}
return this._create(bits, S, S, false)
}
/**
* Performs a left shift on the bitboard.
* @param {boolean} [wrap=true]
* @returns {Bitboard}
*/
shift(wrap = true) {
const totalBits = BigInt(this.size);
const mask = (1n << totalBits) - 1n;
const leftShifted = this._bigint << 1n;
this._bigint = wrap
? (leftShifted | (this._bigint >> (totalBits - 1n))) & mask // circular shift
: leftShifted & mask; // logical shift
return this
}
/**
* Slides all filled cells by (dx, dy) across the bitboard grid.
* Optionally wraps around edges when `wrap` is true.
* @param {number} [dx=0] - Horizontal shift (positive is right, negative is left)
* @param {number} [dy=0] - Vertical shift (positive is down, negative is up)
* @param {boolean} [wrap=true] - Whether to wrap cells around edges
* @returns {Bitboard} - The modified bitboard (for chaining)
*/
slide(dx = 0, dy = 0, wrap = true) {
let result = 0n;
for (const { row, col } of this.cells(({ bit }) => bit === 1)) {
let r2 = row + dy;
let c2 = col + dx;
if (wrap) {
r2 = (r2 + this._height) % this._height;
c2 = (c2 + this._width) % this._width;
}
if (r2 >= 0 && r2 < this._height && c2 >= 0 && c2 < this._width) {
const index = BigInt(this.index(r2, c2));
result |= 1n << index;
}
}
this._bigint = result;
return this
}
/**
* Computes the bounding box of filled cells.
* @returns {{ row: number, col: number, width: number, height: number }|undefined}
*/
bounds() {
let minRow = this._height, maxRow = -1, minCol = this._width, maxCol = -1;
for (let row = 0; row < this._height; row++) {
for (let col = 0; col < this._width; col++) {
if (this.isFilled(row, col)) {
if (row < minRow) minRow = row;
if (row > maxRow) maxRow = row;
if (col < minCol) minCol = col;
if (col > maxCol) maxCol = col;
}
}
}
return minRow <= maxRow
? { row: minRow, col: minCol, width: maxCol - minCol + 1, height: maxRow - minRow + 1 }
: undefined
}
/**
* Crops a sub-region from the bitboard.
* @param {number} row
* @param {number} col
* @param {number} w
* @param {number} h
* @returns {Bitboard}
*/
crop(row, col, w, h) {
let result = 0n;
for (let r = 0; r < h; r++) {
for (let c = 0; c < w; c++) {
if (this.isFilled(row + r, col + c)) {
const targetIndex = BigInt(new Bitboard(0n, w, h, this._littleEndian).index(r, c));
result |= 1n << targetIndex;
}
}
}
return this._create(result, w, h, this._littleEndian)
}
}
/**
* @file Adds `createBitboard` and `drawBitboard` functions to the p5 prototype.
* @version 0.3.0
* @author JP Charalambos
* @license GPL-3.0-only
*
* @description
* Prototype extensions for p5.js that support creating and rendering Bitboard instances.
* Part of the p5.bitboard.js library.
*/
p5.registerAddon((_, fn) => {
fn.createBitboard = function (...args) {
let bb;
if (args[0] instanceof Quadrille) {
const [quadrille, littleEndian = false] = args;
const bigint = quadrille.toBigInt(littleEndian);
bb = new Bitboard(bigint, quadrille.width, quadrille.height, littleEndian);
} else {
bb = new Bitboard(...args);
}
bb._p = this;
bb._cellLength = Quadrille.cellLength;
bb._x = 0;
bb._y = 0;
bb._origin = 'corner';
bb.screenRow = (pixelY, y = bb._y, cl = bb._cellLength || Quadrille.cellLength) =>
bb._p.floor((pixelY - (bb._origin === 'center' ? bb._p.height / 2 : y)) / cl);
bb.screenCol = (pixelX, x = bb._x, cl = bb._cellLength || Quadrille.cellLength) =>
bb._p.floor((pixelX - (bb._origin === 'center' ? bb._p.width / 2 : x)) / cl);
return bb
};
fn.drawBitboard = function (bitboard, value = 0, {
graphics = this,
x,
y,
row,
col,
filter,
textFont,
origin,
options = {},
functionDisplay = Quadrille.functionDisplay,
imageDisplay = Quadrille.imageDisplay,
colorDisplay = Quadrille.colorDisplay,
stringDisplay = Quadrille.stringDisplay,
numberDisplay = Quadrille.numberDisplay,
tileDisplay = Quadrille.tileDisplay,
booleanDisplay = Quadrille.booleanDisplay,
bigintDisplay = Quadrille.bigintDisplay,
symbolDisplay,
arrayDisplay,
objectDisplay,
cellLength = Quadrille.cellLength,
outlineWeight = Quadrille.outlineWeight,
outline = Quadrille.outline,
textColor = Quadrille.textColor,
textZoom = Quadrille.textZoom
} = {}) {
const mode = graphics._renderer instanceof p5.RendererGL ? 'webgl' : 'p2d';
origin ??= mode === 'webgl' ? 'center' : 'corner';
options.origin ??= origin;
bitboard._cellLength = cellLength;
bitboard._x = x ? x : col ? col * cellLength : 0;
bitboard._y = y ? y : row ? row * cellLength : 0;
graphics.push();
mode === 'webgl'
? (origin === 'corner' && graphics.translate(-graphics.width / 2, -graphics.height / 2))
: (origin === 'center' && graphics.translate(graphics.width / 2, graphics.height / 2));
graphics.translate(bitboard._x, bitboard._y);
for (const { row, col, bit } of bitboard.cells(filter)) {
graphics.push();
graphics.translate(col * cellLength, row * cellLength);
options.row = row;
options.col = col;
const params = {
value: bit ? value : null, graphics, options, origin, row, col,
width: bitboard.width, height: bitboard.height, mode,
outline, outlineWeight, cellLength, textColor, textZoom, textFont,
functionDisplay, imageDisplay, colorDisplay, stringDisplay,
numberDisplay, arrayDisplay, objectDisplay, tileDisplay,
booleanDisplay, bigintDisplay, symbolDisplay
};
Quadrille._display(params);
graphics.pop();
}
graphics.pop();
return bitboard
};
});
export { Bitboard as default };
//# sourceMappingURL=p5.bitboard.esm.js.map