@woosh/meep-engine
Version:
Pure JavaScript game engine. Fully featured and production ready.
249 lines (196 loc) • 5.4 kB
JavaScript
import { assert } from "../assert.js";
import { min2 } from "../math/min2.js";
/**
* Fixed-sized container structure, where ends are connected into a circle/ring
* This is a FIFO structure (first in, first out)
* @see https://en.wikipedia.org/wiki/Circular_buffer
* @template V
*/
export class RingBuffer {
/**
* @param {number} size
*/
constructor(size) {
assert.isNumber(size, 'size');
assert.greaterThan(size, 0, `size`);
assert.isNonNegativeInteger(size, 'size');
/**
* Capacity of the buffer, number of slots
* @type {number}
*/
this.size = size;
/**
*
* @type {number}
*/
this.head = 0;
/**
*
* @type {number}
*/
this.tail = 0;
/**
* Number of used slots. Starts at 0, goes up to {@link #size}
* @type {number}
*/
this.count = 0;
/**
*
* @type {V[]}
*/
this.data = new Array(size);
}
/**
*
* @param {number} new_size
*/
resize(new_size) {
assert.isNonNegativeInteger(new_size, 'new_size');
assert.greaterThan(new_size, 0, `new_size`);
if (new_size === this.size) {
// already the right size
return;
}
/**
*
* @type {V[]}
*/
const array = new Array(new_size);
this.data = array;
this.size = new_size;
this.count = min2(new_size, this.count);
this.clear();
// TODO implement a way to keep the old data, need to figure out the correct adjustments to head and tail
}
/**
*
* @return {V}
*/
getHead() {
return this.data[this.head - 1];
}
/**
*
* @param {number} offset
* @returns {V}
*/
getFromHead(offset) {
assert.isNonNegativeInteger(offset, 'offset');
let i = this.head - (offset + 1);
while (i < 0) {
i += this.size;
}
return this.data[i];
}
clear() {
this.head = 0
this.tail = 0;
this.count = 0;
}
/**
*
* @param {V} element
*/
push(element) {
const head = this.head;
this.data[head] = element;
const size = this.size;
const newHead = (head + 1) % size;
this.head = newHead;
if (this.count === size) {
//update tail
this.tail = (this.tail + 1) % size;
} else {
this.count++;
}
if (newHead < head) {
//wrap around just happened
}
}
/**
* Remove element from the tail
* @returns {V|undefined}
*/
shift() {
if (this.count === 0) {
//nothing to remove
return undefined;
}
const element = this.data[this.tail];
this.count--;
this.tail = (this.tail + 1) % this.size;
return element;
}
/**
*
* @param {number} index
*/
removeElementByIndex(index) {
if (index >= this.count) {
return;
}
//shift elements back
const size = this.size;
const tail = this.tail;
const count = this.count;
const data = this.data;
for (let i = index; i < count; i++) {
const p = tail + i;
const j = (p) % size;
const k = (p + 1) % size;
data[j] = data[k];
}
// Clear the last element (important for GC if holding objects)
data[(tail + count - 1) % size] = undefined;
// update head and handle underflow
this.head = (this.head - 1 + size) % size;
this.count--;
}
/**
* Removed first element that fulfills the criteria
* @param {function(V):boolean} condition
* @param {*} [thisArg]
* @returns {V|undefined}
*/
removeIf(condition, thisArg) {
assert.isFunction(condition, 'condition');
const count = this.count;
const size = this.size;
const tail = this.tail;
const data = this.data;
for (let i = 0; i < count; i++) {
const index = (tail + i) % size;
const el = data[index];
if (condition.call(thisArg, el)) {
this.removeElementByIndex(i);
return el;
}
}
}
/**
*
* @param {function(V)} visitor
* @param {*} [thisArg]
*/
forEach(visitor, thisArg) {
assert.isFunction(visitor, 'visitor');
const count = this.count;
const size = this.size;
const tail = this.tail;
const data = this.data;
for (let i = 0; i < count; i++) {
const index = (tail + i) % size;
const el = data[index];
visitor.call(thisArg, el);
}
}
/**
*
* @param {V} value
* @returns {boolean}
*/
contains(value) {
// TODO do a bounds check
return this.data.indexOf(value) !== -1;
}
}