@woosh/meep-engine
Version:
Pure JavaScript game engine. Fully featured and production ready.
271 lines (211 loc) • 6.67 kB
JavaScript
import { assert } from "../../../core/assert.js";
import Signal from "../../../core/events/signal/Signal.js";
import DataType from "../../../core/parser/simple/DataType.js";
import { AbstractBlackboard } from "./AbstractBlackboard.js";
import { BlackboardValue } from "./BlackboardValue.js";
import { make_blackboard_proxy } from "./make_blackboard_proxy.js";
/**
*
* @author Alex Goldring
* @copyright Company Named Limited (c) 2025
*/
export class Blackboard extends AbstractBlackboard {
constructor() {
super();
this.on = {
/**
* Property added
* @type {Signal}
*/
added: new Signal()
};
/**
*
* @type {Object<BlackboardValue>}
*/
this.data = {};
/**
* @private
*/
this.proxy = make_blackboard_proxy(this);
}
getKeys() {
return Reflect.ownKeys(this.data);
}
/**
*
* @returns {Object}
*/
getValueProxy() {
return this.proxy;
}
/**
*
* @param {string} name
* @param {DataType} type
* @return {boolean} true only if entry exists and matches type, or if supplied parameter type is set to "Any"
*/
contains(name, type) {
assert.isString(name, 'name');
assert.enum(type, DataType, 'type');
const datum = this.data[name];
if (datum === undefined) {
return false;
}
if (type !== DataType.Any && datum.type !== type) {
return false;
}
return true;
}
/**
*
* @param {function(name:string, value:*, type: DataType)} visitor
* @param {*} [thisArg]
*/
traverse(visitor, thisArg) {
assert.isFunction(visitor, 'visitor');
for (let name in this.data) {
if (!this.data.hasOwnProperty(name)) {
continue;
}
const blackboardValue = this.data[name];
visitor.call(thisArg, name, blackboardValue.value, blackboardValue.type);
}
}
/**
*
* @param {function(name:string, value:*, type: DataType)} visitor
* @param {RegExp} pattern
*/
traverseWithPattern(pattern, visitor) {
assert.defined(pattern, 'pattern');
assert.ok(pattern instanceof RegExp, 'pattern is not a RegExp');
this.traverse(function (name, value, type) {
if (pattern.test(name)) {
visitor(name, value, type);
}
});
}
/**
* @template T
* @param {string} name
* @param {DataType} type
* @param {number|boolean} [initialValue]
* @returns {T}
*/
acquire(name, type, initialValue) {
assert.isString(name, 'name');
let datum;
if (this.data.hasOwnProperty(name)) {
// property exists
datum = this.data[name];
if (type !== DataType.Any && datum.type !== type) {
throw new TypeError(`Value '${name}' exists, but is type(='${datum.type}'), expected type '${type}'`);
}
} else {
//doesn't exist - create it
datum = new BlackboardValue(type);
const concrete_value = datum.value;
if (initialValue !== undefined) {
concrete_value.set(initialValue);
}
this.data[name] = datum;
this.on.added.send4(name, concrete_value.getValue(), type, this);
}
datum.referenceCount++;
return datum.value;
}
/**
*
* @param {string} name
*/
release(name) {
assert.isString(name, 'name');
assert.ok(this.data.hasOwnProperty(name), `Attempting to release a value '${name}' that doesn't exist`);
const datum = this.data[name];
datum.referenceCount--;
//todo cleanup value from blackboard if no one is using it
}
/**
* Drop all values
*/
reset() {
this.data = {};
}
/**
*
* NOTE: where variables of same type with same names exist - they are kept and their values are updated
* @param {Blackboard} other
*/
copy(other) {
// perform coalescence
const this_data = this.data;
other.traverse((name, value, type) => {
const variable = this_data[name];
const other_value = value.getValue();
if (variable === undefined) {
this.acquire(name, type, other_value);
} else if (variable.type !== type) {
// type mismatch
delete this_data[name]
this.acquire(name, type, other_value);
} else {
// variable exists and types match
variable.value.set(other_value);
}
});
// find out which keys should be removed
const garbage = [];
for (const dataKey in this_data) {
const variable = this_data[dataKey];
if (!other.contains(dataKey, variable.type)) {
garbage.push(dataKey);
}
}
for (let i = 0; i < garbage.length; i++) {
const key = garbage[i];
delete this_data[key];
}
}
/**
*
* @returns {Blackboard}
*/
clone() {
const r = new Blackboard();
r.copy(this);
return r;
}
toJSON() {
const result = {};
this.traverse((name, value, type) => {
result[name] = value.toJSON();
});
return result;
}
fromJSON(json) {
this.reset();
for (let propName in json) {
const value = json[propName];
const value_type = typeof value;
if (value_type === 'number') {
this.acquireNumber(propName, value).set(value);
} else if (value_type === 'boolean') {
this.acquireBoolean(propName, value).set(value);
} else if (value_type === 'string') {
this.acquireString(propName, value).set(value);
}
}
}
/**
*
* @param {any} json
* @return {Blackboard}
*/
static fromJSON(json) {
const bb = new Blackboard();
bb.fromJSON(json);
return bb;
}
}
Blackboard.typeName = 'Blackboard';