UNPKG

@woosh/meep-engine

Version:

Pure JavaScript game engine. Fully featured and production ready.

445 lines (360 loc) • 11.3 kB
import { assert } from "../../assert.js"; import { ceilPowerOfTwo } from "../../binary/operations/ceilPowerOfTwo.js"; import { UINT32_MAX } from "../../binary/UINT32_MAX.js"; import { max2 } from "../../math/max2.js"; import { array_copy } from "../array/array_copy.js"; const DEFAULT_SIZE = 16; const STATUS_FULL = 0; const STATUS_EMPTY = 1; const STATUS_NORMAL = 2; /** * When growing the underlying array, how much to grow by. This is a multiplier. * Must be greater than 1 * @type {number} */ const GROWTH_FACTOR = 2; const EMPTY_ARRAY = new Array(0); /** * Queue data structure, first-in-first-out (FIFO). * Double-ended queue backed by an array. * The fact that it's double-ended means you can add and remove from both ends. * * Note that the implementation is optimized for high speed and low memory usage; it avoids allocations entirely. * @template T */ export class Deque { /** * Using static array allocator to preserve data locality. * Initialization via "[]" would give us a dynamically sized array, * but it would have unpredictable locality as array may or may not have its elements stored next to each other in memory * @type {T[]} * @private */ #data = EMPTY_ARRAY; /** * Index of the front of the queue inside the internal data array * @type {number} * @private */ #head = 0; /** * Index of the back of the queue inside the internal data array * @type {number} * @private */ #tail = 0; /** * * @type {number} * @private */ #status = STATUS_EMPTY; /** * @template T * @param {number} [min_size] */ constructor(min_size = DEFAULT_SIZE) { assert.isNonNegativeInteger(min_size, 'minSize'); const size = ceilPowerOfTwo(max2(1, min_size)); this.#data = new Array(size); } /** * * @param {boolean} adding * @private */ #reset_status(adding) { const head = this.#head; const tail = this.#tail; if (head === tail) { this.#status = adding ? STATUS_FULL : STATUS_EMPTY; } else { this.#status = STATUS_NORMAL; } } /** * * @param {number} current * @returns {number} * @private */ #circular_next_position(current) { const next = current + 1; const length = this.#data.length; return (next >= length) ? 0 : next; } /** * * @param {number} current * @returns {number} * @private */ #circular_previous_position(current) { const prev = current - 1; const length = this.#data.length; const last_index = length - 1; return (prev < 0) ? last_index : prev; } #check_and_expand() { const status = this.#status; if (status !== STATUS_FULL) { // queue still has space, we're done return; } const length = this.#data.length; if (UINT32_MAX === length) { // array is already as big as it can get throw new Error('Maximum array size exceeded'); } // double existing length let new_length = length * GROWTH_FACTOR; if (new_length > UINT32_MAX) { // clamp to max uint32 value new_length = UINT32_MAX; } /** * * @type {T[]} */ const new_data = new Array(new_length); const head = this.#head; // copy the front portion array_copy(this.#data, head, new_data, 0, length - head); // copy the remainder array_copy(this.#data, 0, new_data, length - head, head); this.#head = 0; this.#tail = length; this.#status = STATUS_NORMAL; this.#data = new_data; } /** * * @return {boolean} */ isEmpty() { return this.#status === STATUS_EMPTY; } /** * Clear data from the queue. * Makes the queue empty. * @returns {void} */ clear() { if (this.#status !== STATUS_EMPTY) { let cursor = this.#head; const tail = this.#tail; do { this.#data[cursor] = undefined; cursor = this.#circular_next_position(cursor); } while (cursor !== tail); this.#status = STATUS_EMPTY; } this.#head = 0; this.#tail = 0; } /** * Number of elements in the collection * @returns {number} */ size() { const data = this.#data; if (this.#status === STATUS_FULL) { return data.length; } const head = this.#head; const tail = this.#tail; return (head <= tail) ? (tail - head) : (tail + data.length - head); } /** * * @param {number} current * @private */ #remove_internal_shift_backward(current) { let cursor = current; // shift towards head, this has a better data access pattern than shifting the other way as we're going through the data in forward sequence const tail = this.#tail; while (cursor !== tail) { const next = this.#circular_next_position(cursor); this.#data[cursor] = this.#data[next]; cursor = next; } this.#tail = this.#circular_previous_position(tail); // fill in slot of last moved element this.#data[cursor] = undefined; this.#reset_status(false); } /** * Remove the first occurrence of an element from the queue. This means that if there are 2 instances of an element in the queue, only one will be removed. * * Note: the queue is intended for sequential access primarily, so if you often find yourself needing this method - consider using an alternative data structure, such as a {@link Map} or a {@link Set} as those excel at random access. * * @param {T} e element to remove * @returns {boolean} true if the element was removed, false if the element was not found. */ remove(e) { const i = this.#index_of(e); if (i === -1) { return false; } if(i === this.#head){ // special case this.removeFirst(); } else if(i === this.#circular_previous_position(this.#tail)){ // special case this.removeLast(); }else { // somewhere in the middle, slower option this.#remove_internal_shift_backward(i); } return true; } /** * * @param {T} e * @returns {number} absolute index of the element in the backing array, or -1 if not found * @private */ #index_of(e) { const size = this.size(); const data = this.#data; const capacity = data.length; const head = this.#head; for (let i = 0; i < size; i++) { const index = (head + i) % capacity; const el = data[index]; if (el === e) { return index; } } return -1; } /** * * @param {T} e * @returns {boolean} if the queue contains the element */ has(e) { return this.#index_of(e) !== -1; } /** * Add the element at the front of the queue * @param {T} e */ addFirst(e) { this.#check_and_expand(); this.#head = this.#circular_previous_position(this.#head); this.#data[this.#head] = e; this.#reset_status(true); } /** * Remove an element from the front of the queue * @returns {T|undefined} */ removeFirst() { const element = this.#data[this.#head]; this.#data[this.#head] = undefined; this.#head = this.#circular_next_position(this.#head); this.#reset_status(false); return element; } /** * Peek an element from the front of the queue without removing it * @returns {T|undefined} */ getFirst() { return this.#data[this.#head]; } /** * Add the element at the end of the queue * @param {T} e * @returns {void} */ addLast(e) { this.#check_and_expand(); this.#data[this.#tail] = e; this.#tail = this.#circular_next_position(this.#tail); this.#reset_status(true); } /** * Remove an element from the end of the queue * @returns {T} */ removeLast() { const last = this.#circular_previous_position(this.#tail); const element = this.#data[last]; this.#data[last] = undefined; this.#tail = last; this.#reset_status(false); return element; } /** * Peek an element from the end of the queue without removing it * @returns {T|undefined} */ getLast() { const last = this.#circular_previous_position(this.#tail); return this.#data[last]; } /** * Retrieves the element by sequential position from the start of the queue; the element at the start is position 0. * @param {number} index * @returns {T|undefined} undefined if indexing the element that is past the end, otherwise returns element at the position given */ getElementByIndex(index) { assert.isNonNegativeInteger(index, 'index'); if (index >= this.size()) { // overflow return undefined; } const data = this.#data; /** * * @type {number} */ const position = (this.#head + index) % data.length; return data[position]; } /** * Returns a copy of this queue represented as an array without gaps, with the head of the queue (first element) being at position 0 * @param {T[]} [result] * @param {number} [result_offset] * @returns {T[]} */ toArray(result = [], result_offset = 0) { const size = this.size(); for (let i = 0; i < size; i++) { result[result_offset + i] = this.getElementByIndex(i); } return result; } /** * * @returns {Generator<T,void>} */ * [Symbol.iterator]() { const size = this.size(); for (let i = 0; i < size; i++) { yield this.getElementByIndex(i); } } } /* Stack methods */ /** * Stack operation. Alias of {@link Deque.prototype.getFirst} */ Deque.prototype.peek = Deque.prototype.getFirst; /** * Stack operation. Alias of {@link Deque.prototype.addFirst} */ Deque.prototype.push = Deque.prototype.addFirst; /** * Stack operation. Alias of {@link Deque.prototype.removeFirst} */ Deque.prototype.pop = Deque.prototype.removeFirst; /** * Alias of {@link Deque.prototype.addLast} */ Deque.prototype.add = Deque.prototype.addLast;