UNPKG

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
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