UNPKG

@woosh/meep-engine

Version:

Pure JavaScript game engine. Fully featured and production ready.

271 lines (211 loc) • 6.67 kB
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';