@woosh/meep-engine
Version:
Pure JavaScript game engine. Fully featured and production ready.
445 lines (360 loc) • 11.3 kB
JavaScript
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;