puzzlescript
Version:
Play PuzzleScript games in your terminal!
1,203 lines • 79.5 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.SimpleNeighbor = exports.A11Y_MESSAGE_TYPE = exports.SimpleEllipsisBracket = exports.SimpleBracket = exports.ISimpleBracket = exports.SimpleTileWithModifier = exports.SimpleRule = exports.SimpleRuleLoop = exports.SimpleRuleGroup = exports.SIMPLE_DIRECTION_DIRECTIONS = void 0;
const logger_1 = require("../logger");
const lruCache_1 = __importDefault(require("../lruCache"));
const astTypes_1 = require("../parser/astTypes");
const spriteBitSet_1 = require("../spriteBitSet");
// import TerminalUI from '../ui/terminal'
const util_1 = require("../util");
const BaseForLines_1 = require("./BaseForLines");
const BitSet2 = require('bitset'); // tslint:disable-line:no-var-requires
const MAX_ITERATIONS_IN_LOOP = 350; // Set by the Random World Generation game
const LRU_CACHE_SIZE = 100; // 1000
exports.SIMPLE_DIRECTION_DIRECTIONS = [
util_1.RULE_DIRECTION.RIGHT,
util_1.RULE_DIRECTION.DOWN,
util_1.RULE_DIRECTION.LEFT,
util_1.RULE_DIRECTION.UP
];
class BracketPair {
constructor(condition, action) {
this.condition = condition;
this.action = action;
}
}
class ExtraPair extends BracketPair {
constructor(condition, action, extra) {
super(condition, action);
this.extra = extra;
}
}
class CellMutation {
constructor(cell, messages) {
this.cell = cell;
this.messages = messages;
}
hasCell() { return true; }
getCell() { return this.cell; }
getCommand() {
throw new Error(`BUG: check hasCommand first`);
}
}
class CommandMutation {
constructor(command) {
this.command = command;
this.messages = []; // TODO: Decide if something should be here
}
getCommand() { return this.command; }
hasCell() { return false; }
getCell() {
throw new Error(`BUG: check hasCell first`);
}
}
// Converts `[ [1,2], [a,b] ]` to:
// `[ [1,a], [2,a], [1,b], [2,b] ]`
function buildPermutations(cells) {
let tuples = [[]];
for (const row of cells) {
const newtuples = [];
for (const valtoappend of row) {
for (const tuple of tuples) {
const newtuple = tuple.concat([valtoappend]);
newtuples.push(newtuple);
}
}
tuples = newtuples;
}
return tuples;
}
function commandToKey(c) {
switch (c.type) {
case astTypes_1.COMMAND_TYPE.AGAIN:
case astTypes_1.COMMAND_TYPE.CANCEL:
case astTypes_1.COMMAND_TYPE.CHECKPOINT:
case astTypes_1.COMMAND_TYPE.RESTART:
case astTypes_1.COMMAND_TYPE.WIN:
return c.type;
case astTypes_1.COMMAND_TYPE.MESSAGE:
return `${c.type}:${c.message}`;
case astTypes_1.COMMAND_TYPE.SFX:
return `${c.type}:${c.sound.type}:${c.sound.soundCode}`;
}
}
class SimpleRuleGroup extends BaseForLines_1.BaseForLines {
constructor(source, isRandom, rules) {
super(source);
this.rules = rules;
this.isRandom = isRandom;
}
hasMatches(level) {
for (const rule of this.rules) {
if (rule.hasMatches(level)) {
return true;
}
}
return false;
}
evaluate(level, onlyEvaluateFirstMatch) {
let start;
if (logger_1.logger.isLevel(logger_1.LOG_LEVEL.DEBUG)) {
start = Date.now();
}
// Keep looping as long as one of the rules evaluated something
const allMutations = [];
let iteration;
for (iteration = 0; iteration < MAX_ITERATIONS_IN_LOOP; iteration++) {
if (process.env.NODE_ENV === 'development' && iteration === MAX_ITERATIONS_IN_LOOP - 10) {
// Provide a breakpoint just before we run out of MAX_ITERATIONS_IN_LOOP
// so that we can step through the evaluations.
logger_1.logger.warn(this.toString());
logger_1.logger.warn('BUG: Iterated too many times in startloop or + (rule group)');
// if (process.stdout) { TerminalUI.debugRenderScreen() } debugger // tslint:disable-line:no-debugger
}
if (iteration === MAX_ITERATIONS_IN_LOOP - 1) {
throw new Error(`BUG: Iterated too many times in startloop or + (rule group)\n${this.toString()}`);
}
if (this.isRandom) {
// Randomly pick one of the rules. I wonder if it needs to be smart
// It is important that it only be evaluated once (hence the returns)
const evaluatableRules = this.rules.filter((r) => r.hasMatches(level));
if (evaluatableRules.length === 0) {
return [];
}
else if (evaluatableRules.length === 1) {
const ret = evaluatableRules[0].evaluate(level, true /*only evaluate the 1st match because we are RANDOM and in a loop*/);
return ret;
}
else {
const randomIndex = (0, util_1.nextRandom)(evaluatableRules.length);
const rule = evaluatableRules[randomIndex];
const ret = rule.evaluate(level, true /*only evaluate the 1st match because we are RANDOM and in a loop*/);
return ret;
}
}
else {
let evaluatedSomething = false;
for (const rule of this.rules) {
// Keep evaluating the rule until nothing changes
const ret = rule.evaluate(level, onlyEvaluateFirstMatch);
if (ret.length > 0) {
// filter because a Rule may have caused only command mutations
if (ret.filter((m) => m.hasCell()).length > 0) {
evaluatedSomething = true;
}
if (onlyEvaluateFirstMatch) {
return ret;
}
allMutations.push(ret);
}
}
if (!evaluatedSomething) {
break;
}
}
}
if (logger_1.logger.isLevel(logger_1.LOG_LEVEL.DEBUG)) {
if (allMutations.length > 0) {
if (start && (Date.now() - start) > 30 /*only show times for rules that took a long time*/) {
logger_1.logger.debug(`Rule ${this.__getSourceLineAndColumn().lineNum} applied. ${iteration === 1 ? '' : `(x${iteration})`} [[${Date.now() - start}ms]]`);
}
else {
logger_1.logger.debug(`Rule ${this.__getSourceLineAndColumn().lineNum} applied. ${iteration === 1 ? '' : `(x${iteration})`}`);
}
}
}
return (0, util_1._flatten)(allMutations);
// let mutations = []
// for (const rule of this._rules) {
// const ret = rule.evaluate()
// if (ret.length > 0) {
// mutations = mutations.concat(ret)
// }
// }
// return mutations
}
clearCaches() {
for (const rule of this.rules) {
rule.clearCaches();
}
}
getChildRules() {
return this.rules;
}
isLate() {
// All rules in a group should be parked as late if any is marked as late
return this.rules[0].isLate();
}
hasRigid() {
for (const rule of this.rules) {
if (rule.hasRigid()) {
return true;
}
}
return false;
}
addCellsToEmptyRules(cells) {
for (const rule of this.rules) {
rule.addCellsToEmptyRules(cells);
}
}
toKey() {
return this.rules.map((r) => r.toKey()).join('\n');
}
a11yGetConditionSprites() {
let sprites = [];
for (const rule of this.rules) {
sprites = [...sprites, ...rule.a11yGetConditionSprites()];
}
return sprites;
}
}
exports.SimpleRuleGroup = SimpleRuleGroup;
class SimpleRuleLoop extends SimpleRuleGroup {
}
exports.SimpleRuleLoop = SimpleRuleLoop;
// This is a rule that has been expanded from `DOWN [ > player < cat RIGHT dog ] -> [ ^ crate ]` to:
// DOWN [ DOWN player UP cat RIGHT dog ] -> [ RIGHT crate ]
//
// And a more complicated example:
// DOWN [ > player LEFT cat HORIZONTAL dog < crate VERTICAL wall ] -> [ ^ crate HORIZONTAL dog ]
//
// DOWN [ DOWN player LEFT cat LEFT dog UP crate UP wall ] -> [ right crate LEFT dog ]
// DOWN [ DOWN player LEFT cat LEFT dog UP crate DOWN wall ] -> [ right crate LEFT dog ]
// DOWN [ DOWN player LEFT cat RIGHT dog UP crate UP wall ] -> [ RIGHT crate RIGHT dog ]
// DOWN [ DOWN player LEFT cat RIGHT dog UP crate DOWN wall ] -> [ RIGHT crate RIGHT dog ]
class SimpleRule extends BaseForLines_1.BaseForLines {
constructor(source, conditionBrackets, actionBrackets, commands, isLate, isRigid, debugFlag) {
super(source);
// this.evaluationDirection = evaluationDirection
this.conditionBrackets = conditionBrackets;
this.actionBrackets = actionBrackets;
this.commands = commands;
this._isLate = isLate;
this.isRigid = isRigid;
this.debugFlag = debugFlag;
this.isSubscribedToCellChanges = false;
if (actionBrackets.length > 0) {
for (let index = 0; index < conditionBrackets.length; index++) {
conditionBrackets[index].prepareAction(actionBrackets[index]);
}
}
}
toKey() {
// const dir = this.dependsOnDirection() ? this.evaluationDirection : ''
const conditions = this.conditionBrackets.map((x) => x.toKey());
const actions = this.actionBrackets.map((x) => x.toKey());
const commands = this.commands.map((c) => commandToKey(c));
return `{Late?${this._isLate}} {Rigid?${this.isRigid}} ${conditions} -> ${actions} ${commands.join(' ')} {debugger?${this.debugFlag}}`;
}
getChildRules() {
return [];
}
subscribeToCellChanges() {
if (!this.isSubscribedToCellChanges) {
// Subscribe the bracket and neighbors to cell Changes (only the condition side)
for (const bracket of this.conditionBrackets) {
bracket.subscribeToNeighborChanges();
}
this.isSubscribedToCellChanges = true;
}
}
clearCaches() {
for (const bracket of this.conditionBrackets) {
bracket.clearCaches();
}
}
getMatches(level) {
const allBracketsToProcess = [];
for (let index = 0; index < this.conditionBrackets.length; index++) {
const condition = this.conditionBrackets[index];
const action = this.actionBrackets[index];
const bracketMatches = condition.getMatches(level, action);
if (bracketMatches.length === 0) {
return [];
}
allBracketsToProcess.push(bracketMatches);
}
return allBracketsToProcess;
}
evaluate(level, onlyEvaluateFirstMatch) {
// Verify that each condition bracket has matches
// for (const condition of this.conditionBrackets) {
// if (!condition.hasFirstCells()) {
// if (process.env['NODE_ENV'] === 'development' && this.debugFlag === DEBUG_FLAG.BREAKPOINT_REMOVE) {
// // A "DEBUGGER_REMOVE" flag was set in the game so we are pausing here
// // if (process.stdout) { TerminalUI.debugRenderScreen(); } debugger
// }
// return [] // Rule did not match, so nothing ran
// }
// }
// If a Rule cannot impact itself then the evaluation order does not matter.
// We can vastly simplify the evaluation in that case
let ret = [];
if (process.env.NODE_ENV === 'development') {
// A "DEBUGGER" flag was set in the game so we are pausing here
// if ((logger.isLevel(LOG_LEVEL.TRACE) && process.stdout) || (logger.isLevel(LOG_LEVEL.DEBUG) && this.debugFlag === DEBUG_FLAG.BREAKPOINT)) {
// // TerminalUI.renderScreen(false)
// }
if (this.debugFlag === util_1.DEBUG_FLAG.BREAKPOINT) {
debugger; // tslint:disable-line:no-debugger
}
}
const allBracketsToProcess = this.getMatches(level);
if (allBracketsToProcess.length === 0) {
return [];
}
// Some rules only contain commands.
// If there are actionBrackets then evaluate them.
// Get ready to Evaluate
if (this.actionBrackets.length > 0) {
const cellPermutations = buildPermutations(allBracketsToProcess);
const allMutations = [];
for (const permutation of cellPermutations) {
let didAllBracketsStillMatch = true;
const magicOrTiles = new Map();
// Populate the magicOrTiles. This needs to be done first because of things like:
// [ Player ] [ Color ] -> [ Player Color ] [ ]
// Check that all Cells still match
for (const x of permutation) {
if (!x.doesStillMatch()) {
didAllBracketsStillMatch = false;
break;
}
x.populateMagicOrTiles(magicOrTiles);
}
// break if the cells no longer match
if (!didAllBracketsStillMatch) {
continue;
}
for (const x of permutation) {
if (!x.doesStillMatch()) {
// part of the rule not longer matches so stop. (Test if that is the correct behavior)
// E.g. [ Player ] [ Player ] [ ] -> [ ] [ ] [ SeeIfThisIsExecuted ]
continue;
}
allMutations.push(x.evaluate(magicOrTiles));
if (process.env.NODE_ENV === 'development') {
this.__incrementCoverage();
}
}
// Only evaluate once. This is a HACK since it always picks the 1st cell that matched rather than a RANDOM cell
if (onlyEvaluateFirstMatch) {
break; // evaluate the subsequent brackets but do not continue evaluating cells
}
}
ret = (0, util_1._flatten)(allMutations);
}
// Append any Commands that need to be evaluated (only if the rule was evaluated at least once)
for (const command of this.commands) {
ret.push(new CommandMutation(command));
}
return ret;
}
hasMatches(level) {
for (let index = 0; index < this.conditionBrackets.length; index++) {
const condition = this.conditionBrackets[index];
const action = this.actionBrackets[index];
if (!condition.hasMatches(level, action)) {
return false;
}
}
return true;
}
isLate() { return this._isLate; }
hasRigid() { return this.isRigid; }
canCollapseBecauseBracketsMatch(rule) {
for (let index = 0; index < this.conditionBrackets.length; index++) {
if (this.conditionBrackets[index] !== rule.conditionBrackets[index]) {
return false;
}
// also ensure there is only one neighbor.
// that way we can de-duplicate the rule
if (this.conditionBrackets[index]._getAllNeighbors().length > 1) {
return false;
}
}
for (let index = 0; index < this.actionBrackets.length; index++) {
if (this.actionBrackets[index] !== rule.actionBrackets[index]) {
return false;
}
}
return true;
}
addCellsToEmptyRules(cells) {
for (const bracket of this.conditionBrackets) {
bracket.addCellsToEmptyRules(cells);
}
}
a11yGetConditionSprites() {
let sprites = [];
for (const b of this.conditionBrackets) {
sprites = [...sprites, ...b.a11yGetSprites()];
}
return sprites;
}
}
exports.SimpleRule = SimpleRule;
class SimpleTileWithModifier extends BaseForLines_1.BaseForLines {
constructor(source, isNegated, isRandom, direction, tile, debugFlag) {
super(source);
this._isNegated = isNegated;
this._isRandom = isRandom;
this._direction = direction;
this._tile = tile;
this.neighbors = new Set();
this._debugFlag = debugFlag;
this.trickleCells = new Set();
}
toKey(ignoreDebugFlag) {
const sprites = this._tile.getSprites().map((sprite) => sprite.getName()).sort();
if (ignoreDebugFlag) {
return `{-?${this._isNegated}} {#?${this._isRandom}} dir="${this._direction}" [${sprites.join(' ')}]`;
}
else {
return `{-?${this._isNegated}} {#?${this._isRandom}} dir="${this._direction}" [${sprites.join(' ')}]{debugging?${!!this._debugFlag}}`;
}
}
equals(t) {
return this._isNegated === t._isNegated && this._tile.equals(t._tile) && this._direction === t._direction && this._isRandom === t._isRandom;
}
clearCaches() {
// this._localCache.clear()
}
isNo() {
return this._isNegated;
}
isRandom() {
return this._isRandom;
}
getCollisionLayers() {
const collisionLayers = new Set();
for (const sprite of this._tile.getSprites()) {
collisionLayers.add(sprite.getCollisionLayer());
}
return collisionLayers;
}
// This should only be called on Condition Brackets
subscribeToCellChanges(neighbor) {
this.neighbors.add(neighbor);
this._tile.subscribeToCellChanges(this);
}
addCells(tile, sprite, cells, wantsToMove) {
if (process.env.NODE_ENV === 'development' && this._debugFlag === util_1.DEBUG_FLAG.BREAKPOINT) {
// Pause here because it was marked in the code
// if (process.stdout) { TerminalUI.debugRenderScreen() } debugger // tslint:disable-line:no-debugger
}
// Cells all have the same sprites, so if the 1st matches, they all do.
// Also, we only need to check that the direction matches (optimization),
// we do not need to re-check that the Tile matches
let shouldAdd = true;
if (!this._direction || wantsToMove === this._direction) {
shouldAdd = !this.isNo();
}
else if (this._tile.isOr() && this.matchesFirstCell(cells, wantsToMove)) {
// In OR tiles, one of the sprites may add/remove a direction but
// another sprite may still have the direction
// so we check by doing the long and expensive comparison above
shouldAdd = !this.isNo();
}
else {
shouldAdd = this.isNo();
}
if (shouldAdd) {
for (const cell of cells) {
this.trickleCells.add(cell);
}
// const cellsNotInCache = setDifference(new Set(cells), new Set(this._localCache.keys()))
for (const neighbor of this.neighbors) {
// neighbor.addCells(this, sprite, cellsNotInCache, wantsToMove)
neighbor.addCells(this, sprite, cells, wantsToMove);
}
}
else {
for (const cell of cells) {
this.trickleCells.delete(cell);
}
// const cellsInCache = setIntersection(new Set(cells), new Set(this._localCache.keys()))
for (const neighbor of this.neighbors) {
// neighbor.removeCells(this, sprite, cellsInCache)
neighbor.removeCells(this, sprite, cells);
}
}
}
updateCells(sprite, cells, wantsToMove) {
if (process.env.NODE_ENV === 'development' && this._debugFlag === util_1.DEBUG_FLAG.BREAKPOINT) {
// Pause here because it was marked in the code
// if (process.stdout) { TerminalUI.debugRenderScreen() } debugger // tslint:disable-line:no-debugger
}
// Cells all have the same sprites, so if the 1st matches, they all do
if (this.matchesFirstCell(cells, wantsToMove)) {
for (const cell of cells) {
this.trickleCells.add(cell);
}
if (wantsToMove) {
for (const neighbor of this.neighbors) {
neighbor.updateCells(this, sprite, cells, wantsToMove);
}
}
}
else {
for (const cell of cells) {
this.trickleCells.delete(cell);
}
for (const neighbor of this.neighbors) {
neighbor.removeCells(this, sprite, cells);
}
}
}
removeCells(tile, sprite, cells) {
if (process.env.NODE_ENV === 'development' && this._debugFlag === util_1.DEBUG_FLAG.BREAKPOINT_REMOVE) {
// Pause here because it was marked in the code
// if (process.stdout) { TerminalUI.debugRenderScreen() } debugger // tslint:disable-line:no-debugger
}
// Cells all have the same sprites, so if the 1st matches, they all do
// OR Tiles need to be checked to see if the tile still matches.
// Non-OR tiles can be safely removed
let shouldAdd = false;
if (this._tile.isOr()) {
shouldAdd = this.matchesFirstCell(cells, null);
}
else {
shouldAdd = this.isNo();
}
if (shouldAdd) {
for (const cell of cells) {
this.trickleCells.add(cell);
}
for (const neighbor of this.neighbors) {
neighbor.addCells(this, sprite, cells, util_1.RULE_DIRECTION.STATIONARY);
}
}
else {
for (const cell of cells) {
this.trickleCells.delete(cell);
}
for (const neighbor of this.neighbors) {
neighbor.removeCells(this, sprite, cells);
}
}
}
hasCell(cell) {
return this.trickleCells.has(cell);
}
matchesCellWantsToMove(cell, wantsToMove) {
const hasTile = this._tile.hasCell(cell);
const didMatch = this._isNegated !== (hasTile && (this._direction === wantsToMove || !this._direction));
if (didMatch) {
return true;
}
else if (!this._direction) {
return false;
}
else {
// do the more expensive match
for (const sprite of this._tile.getSpritesThatMatch(cell)) {
if (this._direction === cell.getWantsToMove(sprite)) {
return true;
}
}
return false;
}
}
matchesFirstCell(cells, wantsToMove) {
return this.matchesCellWantsToMove(cells[0], wantsToMove);
}
}
exports.SimpleTileWithModifier = SimpleTileWithModifier;
class ISimpleBracket extends BaseForLines_1.BaseForLines {
constructor(source, direction, allNeighbors, debugFlag) {
super(source);
this.direction = direction;
this.debugFlag = debugFlag;
this.allNeighbors = allNeighbors;
this.firstCells = new Set();
}
_getAllNeighbors() {
return this.allNeighbors;
}
hasMatches(level, actionBracket) {
return this.getMatches(level, actionBracket).length > 0;
}
}
exports.ISimpleBracket = ISimpleBracket;
class MatchedCellsForRule {
constructor(cellsAndNeighbors) {
this.cellsAndNeighbors = cellsAndNeighbors;
this.cellKeys = new Map();
for (const { cell } of this.cellsAndNeighbors) {
this.cellKeys.set(cell, cell.toKey());
}
}
firstCell() {
for (const { cell } of this.cellsAndNeighbors) {
return cell;
}
throw new Error(`BUG: ? No cells were included in the match`);
}
lastCell() {
let ret;
for (const { cell } of this.cellsAndNeighbors) {
ret = cell;
}
if (ret) {
return ret;
}
throw new Error(`BUG: ? No cells were included in the match`);
}
doesStillMatch() {
for (const { cell, condition } of this.cellsAndNeighbors) {
// Check the cell key (cheap) to see if the cell still matches
if (cell.toKey() === this.cellKeys.get(cell)) {
continue;
}
if (!condition.matchesCellSimple(cell)) {
return false;
}
// if the cell updated but still matches then update the cellKey
this.cellKeys.set(cell, cell.toKey());
}
return true;
}
populateMagicOrTiles(magicOrTiles) {
// populate the OR tiles in all neighbors first. Example:
// [ | Player ] -> [ Player | ]
for (const { cell, condition } of this.cellsAndNeighbors) {
condition.populateMagicOrTiles(cell, magicOrTiles);
}
}
evaluate(magicOrTiles) {
const mutations = [];
for (const { cell, condition, action } of this.cellsAndNeighbors) {
if (!action) {
throw new Error(`BUG: Should not have tried to evaluate something when there is no action`);
}
const mutation = condition.evaluate(action, cell, magicOrTiles);
if (mutation) {
mutations.push(mutation);
}
}
return mutations;
}
}
class SimpleBracket extends ISimpleBracket {
constructor(source, direction, neighbors, debugFlag) {
super(source, direction, neighbors, debugFlag);
this.actionDebugFlag = null;
this.neighbors = neighbors;
this.ellipsisBracketListeners = new Map();
// Compute which sprites need to be in the Row/Column to check cells in that row/column (optimization)
this.spritesPresentInRowOrColumn = this.neighbors[0].spritesPresent.union(this.neighbors.map((n) => n.spritesPresent));
const anySprites = [];
for (const neighbor of this.neighbors) {
for (const a of neighbor.anySpritesPresent) {
anySprites.push(a);
}
}
this.anySpritesPresentInRowOrColumn = (new spriteBitSet_1.SpriteBitSet()).union(anySprites);
}
toKey() {
const dir = this.dependsOnDirection() ? this.direction : '';
return `{${dir}[${this.neighbors.map((n) => n.toKey()).join('|')}]{debugging?${!!this.debugFlag}}}`;
}
dependsOnDirection() {
return this.neighbors.length > 1 || !!this.neighbors.find((n) => n.dependsOnDirection());
}
subscribeToNeighborChanges() {
if (this.shouldUseOnDemandMethod()) {
return;
} // Skip. Do not subscribe to changes because we will use .getMatches() to find the matches
this.neighbors.forEach((neighbor, index) => {
neighbor.subscribeToTileChanges(this, index);
});
}
addEllipsisBracket(bracket, token) {
this.ellipsisBracketListeners.set(bracket, token);
}
clearCaches() {
this.firstCells.clear();
for (const neighbor of this.neighbors) {
neighbor.clearCaches();
}
}
getNeighbors() { return this.neighbors; }
prepareAction(actionBracket) {
const actionBracketSimple = actionBracket; // since we know the condition and action side need to match
this.actionDebugFlag = actionBracketSimple.debugFlag;
for (let index = 0; index < this.neighbors.length; index++) {
const condition = this.neighbors[index];
const action = actionBracketSimple.neighbors[index];
condition.prepareAction(action);
}
}
addCellsToEmptyRules(cells) {
if (this.neighbors.length === 1) {
if (this.neighbors[0]._tilesWithModifier.size === 0) {
for (const cell of cells) {
this._addFirstCell(cell);
}
}
}
}
_addFirstCell(firstCell) {
if (process.env.NODE_ENV === 'development' && this.debugFlag === util_1.DEBUG_FLAG.BREAKPOINT) {
// Pausing here because it was marked in the code
// if (process.stdout) { TerminalUI.debugRenderScreen() } debugger // tslint:disable-line:no-debugger
}
this.firstCells.add(firstCell);
for (const [ellipsisBracket, token] of this.ellipsisBracketListeners) {
ellipsisBracket.addFirstCell(this, firstCell, token);
}
}
addCell(index, neighbor, t, sprite, cell, wantsToMove) {
// check if downstream neighbors match
if (!this.matchesDownstream(cell, index)) {
// Try to remove the match if there is one
const firstCell = this.getUpstream(cell, index);
if (firstCell) {
this._removeFirstCell(cell);
}
return;
}
// Loop Upstream
// check the neighbors upstream of curCell
const firstCellUp = this.matchesUpstream(cell, index);
if (!firstCellUp) {
// Try to remove the match if there is one
const firstCellMatched = this.getUpstream(cell, index);
if (firstCellMatched) {
this._removeFirstCell(firstCellMatched);
}
return;
}
// Add to the set of firstNeighbors
// We have a match. Add to the firstCells set.
this._addFirstCell(firstCellUp);
}
removeCell(index, neighbor, t, sprite, cell) {
// cell was removed
// Loop Upstream
const firstCell = this.getFirstCellToRemove(cell, index);
// Bracket might not match for all directions (likely not), so we might not find a firstCell to remove
// But that's OK.
if (firstCell && this.firstCells.has(firstCell)) {
this._removeFirstCell(firstCell);
}
}
shouldUseOnDemandMethod() {
// return true
// return false
// return this.neighbors.length === 1
// return this.neighbors.length !== 1
return process.env.PUZZLESCRIPT_METHOD === 'ondemand';
}
getMatchesByTrickling(level, actionBracket) {
const matches = [];
for (const firstCell of this.firstCells) {
this.addToCellMatches(matches, firstCell, actionBracket);
}
return matches;
}
getMatchesByLooping(level, actionBracket) {
const matches = [];
// Naiive version:
// for (const row of level.getCells()) {
// for (const cell of row) {
// checkCell(cell)
// }
// }
const cells = level.getCells();
const rowCount = cells.length;
const colCount = cells[0].length;
switch (this.direction) {
case util_1.RULE_DIRECTION.UP:
case util_1.RULE_DIRECTION.DOWN:
for (let colIndex = 0; colIndex < colCount; colIndex++) {
if (level.colContainsSprites(colIndex, this.spritesPresentInRowOrColumn, this.anySpritesPresentInRowOrColumn)) {
for (let rowIndex = 0; rowIndex < rowCount; rowIndex++) {
this.addIfCellMatches(matches, level.getCell(rowIndex, colIndex), actionBracket);
}
}
}
break;
case util_1.RULE_DIRECTION.LEFT:
case util_1.RULE_DIRECTION.RIGHT:
for (let rowIndex = 0; rowIndex < rowCount; rowIndex++) {
if (level.rowContainsSprites(rowIndex, this.spritesPresentInRowOrColumn, this.anySpritesPresentInRowOrColumn)) {
for (let colIndex = 0; colIndex < colCount; colIndex++) {
this.addIfCellMatches(matches, level.getCell(rowIndex, colIndex), actionBracket);
}
}
}
break;
default:
throw new Error(`BUG: Unsupported Direction "${this.direction}"`);
}
return matches;
}
getMatches(level, actionBracket) {
if (process.env.NODE_ENV === 'development' && this.debugFlag === util_1.DEBUG_FLAG.BREAKPOINT) {
// A "DEBUGGER" flag was set in the game so we are pausing here
// if (process.stdout) { TerminalUI.debugRenderScreen() } debugger // tslint:disable-line:no-debugger
}
let matches;
if (!this.shouldUseOnDemandMethod()) {
matches = this.getMatchesByTrickling(level, actionBracket);
if (process.env.VERIFY_MATCHES) {
const loopingMatches = this.getMatchesByLooping(level, actionBracket);
if (matches.length !== loopingMatches.length) {
debugger; // tslint:disable-line:no-debugger
this.getMatchesByTrickling(level, actionBracket); // run again so we can step through
this.getMatchesByLooping(level, actionBracket); // run again so we can step through
throw new Error(`Match lengths differ. Expected ${loopingMatches.length} but found ${matches.length}. \n${this.toString()}`);
}
}
}
else {
matches = this.getMatchesByLooping(level, actionBracket);
}
return matches;
}
a11yGetSprites() {
const sprites = new Set();
for (const n of this.neighbors) {
for (const t of n._tilesWithModifier) {
for (const s of t._tile.getSprites()) {
sprites.add(s);
}
}
}
return [sprites];
}
_removeFirstCell(firstCell) {
if (this.firstCells.has(firstCell)) {
if (process.env.NODE_ENV === 'development' && this.debugFlag === util_1.DEBUG_FLAG.BREAKPOINT_REMOVE) {
// Pausing here because it was marked in the code
// if (process.stdout) { TerminalUI.debugRenderScreen() } debugger // tslint:disable-line:no-debugger
}
this.firstCells.delete(firstCell);
for (const [ellipsisBracket, token] of this.ellipsisBracketListeners) {
ellipsisBracket.removeFirstCell(this, firstCell, token);
}
}
}
matchesDownstream(cell, index) {
// Check all the neighbors and add the firstNeighbor to the set of matches for this direction
let matched = true;
let curCell = cell;
// Loop Downstream
// check the neighbors downstream of curCell
for (let x = index + 1; x < this.neighbors.length; x++) {
curCell = curCell.getNeighbor(this.direction);
// TODO: Convert the neighbor check into a method
if (curCell && (this.neighbors[x]._tilesWithModifier.size === 0 || this.neighbors[x].matchesCellSimple(curCell))) {
// keep going
}
else {
matched = false;
break;
}
}
return matched;
}
getUpstream(cell, index) {
let curCell = cell;
for (let x = index - 1; x >= 0; x--) {
curCell = curCell.getNeighbor((0, util_1.opposite)(this.direction));
if (curCell) {
// keep going
}
else {
return null;
}
}
return curCell;
}
matchesUpstream(cell, index) {
let matched = true;
let curCell = cell;
// check the neighbors upstream of curCell
for (let x = index - 1; x >= 0; x--) {
curCell = curCell.getNeighbor((0, util_1.opposite)(this.direction));
if (curCell && (this.neighbors[x]._tilesWithModifier.size === 0 || this.neighbors[x].matchesCellSimple(curCell))) {
// keep going
}
else {
matched = false;
break;
}
}
return matched ? curCell : null;
}
getFirstCellToRemove(cell, index) {
// Loop Upstream
// check the neighbors upstream of curCell
let matched = true;
let curCell = cell;
// check the neighbors upstream of curCell
for (let x = index - 1; x >= 0; x--) {
curCell = curCell.getNeighbor((0, util_1.opposite)(this.direction));
if (curCell) {
// keep going
}
else {
matched = false;
break;
}
}
return matched ? curCell : null;
}
addToCellMatches(matches, cell, actionBracket) {
const cellMatches = [];
let curCell = cell;
let didAllNeighborsMatch = true;
for (let index = 0; index < this.neighbors.length; index++) {
if (!curCell) {
didAllNeighborsMatch = false;
break;
}
const condition = this.neighbors[index];
let action = null;
// Some rules only contain a condition bracket and a command
if (actionBracket) {
action = actionBracket.neighbors[index] || null;
}
const x = {
cell: curCell,
condition,
action
};
cellMatches.push(x);
curCell = curCell.getNeighbor(this.direction);
}
if (didAllNeighborsMatch) {
matches.push(new MatchedCellsForRule(cellMatches));
}
}
addIfCellMatches(matches, cell, actionBracket) {
if (this.neighbors[0].matchesCellSimple(cell) && this.matchesDownstream(cell, 0)) {
this.addToCellMatches(matches, cell, actionBracket);
}
}
}
exports.SimpleBracket = SimpleBracket;
var BEFORE_OR_AFTER;
(function (BEFORE_OR_AFTER) {
BEFORE_OR_AFTER[BEFORE_OR_AFTER["BEFORE"] = 0] = "BEFORE";
BEFORE_OR_AFTER[BEFORE_OR_AFTER["AFTER"] = 1] = "AFTER";
})(BEFORE_OR_AFTER || (BEFORE_OR_AFTER = {}));
class MultiMap {
constructor() {
this.map = new Map();
}
clear() {
this.map.clear();
}
// public has(a: A, b: B) {
// const set = this.map.get(a)
// if (set) {
// return set.has(b)
// }
// return false
// }
getB(a) {
return this.map.get(a);
}
add(a, b) {
let set = this.map.get(a);
if (!set) {
set = new Set();
this.map.set(a, set);
}
if (!set.has(b)) {
set.add(b);
return true;
}
return false;
}
deleteAllA(a) {
this.map.delete(a);
}
deleteAllB(b) {
const asRemoved = new Set();
for (const [a, set] of this.map) {
if (set.has(b)) {
set.delete(b);
if (set.size === 0) {
this.map.delete(a);
asRemoved.add(a);
}
}
}
return asRemoved;
}
sizeA() {
return this.map.size;
}
}
class SimpleEllipsisBracket extends ISimpleBracket {
constructor(source, direction, beforeEllipsisNeighbors, afterEllipsisNeighbors, debugFlag) {
super(source, direction, [...beforeEllipsisNeighbors, ...afterEllipsisNeighbors], debugFlag);
this.beforeEllipsisBracket = new SimpleBracket(source, direction, beforeEllipsisNeighbors, debugFlag);
this.afterEllipsisBracket = new SimpleBracket(source, direction, afterEllipsisNeighbors, debugFlag);
this.linkages = new MultiMap();
}
subscribeToNeighborChanges() {
this.beforeEllipsisBracket.subscribeToNeighborChanges();
this.afterEllipsisBracket.subscribeToNeighborChanges();
this.beforeEllipsisBracket.addEllipsisBracket(this, BEFORE_OR_AFTER.BEFORE);
this.afterEllipsisBracket.addEllipsisBracket(this, BEFORE_OR_AFTER.AFTER);
}
toKey() {
return `[${this.direction} ${this.beforeEllipsisBracket.toKey()} ... ${this.afterEllipsisBracket.toKey()}]}`;
}
clearCaches() {
this.firstCells.clear();
this.linkages.clear();
this.beforeEllipsisBracket.clearCaches();
this.afterEllipsisBracket.clearCaches();
}
prepareAction(action) {
const actionBracket = action; // since we know the condition and action side need to match
this.beforeEllipsisBracket.prepareAction(actionBracket.beforeEllipsisBracket);
this.afterEllipsisBracket.prepareAction(actionBracket.afterEllipsisBracket);
}
addCellsToEmptyRules(cells) {
this.beforeEllipsisBracket.addCellsToEmptyRules(cells);
this.afterEllipsisBracket.addCellsToEmptyRules(cells);
}
addCell(index, neighbor, t, sprite, cell, wantsToMove) {
throw new Error(`BUG: We should not be subscribed to these events`);
}
removeCell(index, neighbor, t, sprite, cell) {
throw new Error(`BUG: We should not be subscribed to these events`);
}
addFirstCell(bracket, firstCell, token) {
// // check to see if the new cell is in line with any firstCells in the other bracket. If so, we have a match!
// let firstBeforeCells
// let firstAfterCells
// if (bracket == this.beforeEllipsisBracket) {
// firstBeforeCells = new Set([firstCell])
// // search for a matching afterCell
// firstAfterCells = this.findMatching(firstCell, this.direction, this.afterEllipsisBracket)
// } else if (bracket === this.afterEllipsisBracket) {
// firstAfterCells = new Set([firstCell])
// // search for a matching beforeCell
// firstBeforeCells = this.findMatching(firstCell, opposite(this.direction), this.beforeEllipsisBracket)
// } else {
// throw new Error(`BUG: Bracket should only ever be the before-ellipsis or after-ellipsis one`)
// }
// for (const firstBeforeCell of firstBeforeCells) {
// for (const firstAfterCell of firstAfterCells) {
// this.checkInvariants()
// // Check if we need to actually change anything first. Becauase the !doesEvaluationOrderMatter case
// // keeps iterating on the set of firstCells but if they keep flipping then it's a problem because it
// // runs in an infinite loop
// // Delete any mapping that may have existed before
// if (this.linkages.has(firstBeforeCell, firstAfterCell)) {
// // nothing to do. we already have those entries
// } else {
// this.linkages.add(firstBeforeCell, firstAfterCell)
// this.firstCells.add(firstBeforeCell)
// }
// this.checkInvariants()
// }
// }
}
removeFirstCell(bracket, firstCell, token) {
// Figure out the 1st cell for us and remove it (by maybe looking at the matching bracket)
this.checkInvariants();
if (bracket === this.beforeEllipsisBracket) {
this.linkages.deleteAllA(firstCell);
if (this.firstCells.has(firstCell)) {
throw new Error(`BUG: Unreachable code`);
}
}
else if (bracket === this.afterEllipsisBracket) {
const beforeCellsRemoved = this.linkages.deleteAllB(firstCell);
if (beforeCellsRemoved.size > 0) {
throw new Error(`BUG: Unreachable code`);
}
}
else {
throw new Error(`BUG: Bracket should only ever be the before-ellipsis or after-ellipsis one`);
}
this.checkInvariants();
}
// private findMatching(cell: Cell, direction: RULE_DIRECTION, inBracket: SimpleBracket) {
// const matches = new Set()
// for (const inBracketCell of inBracket.getFirstCells()) {
// switch (direction) {
// case RULE_DIRECTION.UP:
// if (cell.colIndex === inBracketCell.colIndex && cell.rowIndex > inBracketCell.rowIndex) {
// matches.add(inBracketCell)
// }
// break
// case RULE_DIRECTION.DOWN:
// if (cell.colIndex === inBracketCell.colIndex && cell.rowIndex < inBracketCell.rowIndex) {
// matches.add(inBracketCell)
// }
// break
// case RULE_DIRECTION.LEFT:
// if (cell.colIndex > inBracketCell.colIndex && cell.rowIndex === inBracketCell.rowIndex) {
// matches.add(inBracketCell)
// }
// break
// case RULE_DIRECTION.RIGHT:
// if (cell.colIndex < inBracketCell.colIndex && cell.rowIndex === inBracketCell.rowIndex) {
// matches.add(inBracketCell)
// }
// break
// default:
// throw new Error(`BUG: Invalid direction`)
// }
// }
// return matches
// }
getMatches(level, actionBracket) {
const ret = [];
let beforeMatches;
let afterMatches;
if (actionBracket) {
beforeMatches = this.beforeEllipsisBracket.getMatches(level, actionBracket.beforeEllipsisBracket);
afterMatches = this.afterEllipsisBracket.getMatches(level, actionBracket.afterEllipsisBracket);
}
else {
beforeMatches = this.beforeEllipsisBracket.getMatches(level, null);
afterMatches = this.afterEllipsisBracket.getMatches(level, null);
}
const beforeMatchesByIndex = new MultiMap();
if (beforeMatches.length === 0 || afterMatches.length === 0) {
return [];
}
switch (this.direction) {
case util_1.RULE_DIRECTION.UP:
case util_1.RULE_DIRECTION.DOWN:
for (const beforeMatch of beforeMatches) {
beforeMatchesByIndex.add(beforeMatch.lastCell().colIndex, beforeMatch);
}
for (const afterMatch of afterMatches) {
const { colIndex, rowIndex } = afterMatch.firstCell();
for (const beforeMatch of beforeMatchesByIndex.getB(colIndex) || []) {
// check if the afterMatch matches it.
// If so, remove the beforeMatch and include the whole match
const { rowIndex: beforeRowIndex } = beforeMatch.lastCell();
const isAfter = (this.direction === util_1.RULE_DIRECTION.DOWN) ? beforeRowIndex < rowIndex : rowIndex < beforeRowIndex;
if (isAfter) {
ret.push(new MatchedCellsForRule([...beforeMatch.cellsAndNeighbors].concat([...afterMatch.cellsAndNeighbors])));
// beforeMatchesByIndex.delete(colIndex, beforeMatch)
}
}
}
break;
case util_1.RULE_DIRECTION.LEFT:
case util_1.RULE_DIRECTION.RIGHT:
for (const beforeMatch of beforeMatches) {
beforeMatchesByIndex.add(beforeMatch.lastCell().rowIndex, beforeMatch);
}
for (const afterMatch of afterMatches) {
const { rowIndex, colIndex } = afterMatch.firstCell();
for (const beforeMatch of beforeMatchesByIndex.getB(rowIndex) || []) {
// check if the afterMatch matches it.
// If so, remove the beforeMatch and include the whole match
const { colIndex: beforeColIndex } = beforeMatch.lastCell();
const isAfter = (this.direction === util_1.RULE_DIRECTION.RIGHT) ? beforeColIndex < colIndex : colIndex < beforeColIndex;
if (isAfter) {
ret.push(new MatchedCellsForRule([...beforeMatch.cellsAndNeighbors].concat([...afterMatch.cellsAndNeighbors])));
// beforeMatchesByIndex.delete(rowIndex, beforeMatch)
}
}
}
break;
default:
throw new Error(`BUG: Invalid direction ${this.direction}`);
}
return ret;
}
a11yGetSprites() {
return [...this.beforeEllipsisBracket.a11yGetSprites(), ...this.afterEllipsisBracket.a11yGetSprites()];
}
checkInvariants() {
if (this.firstCells.size !== this.linkages.sizeA()) {
throw new Error(`BUG: Invariant violation`);
}
}
}
exports.SimpleEllipsisBracket = SimpleEllipsisBracket;
var A11Y_MESSAGE_TYPE;
(function (A11Y_MESSAGE_TYPE) {
A11Y_MESSAGE_TYPE["ADD"] = "ADD";
A11Y_MESSAGE_TYPE["REPLACE"] = "REPLACE";
A11Y_MESSAGE_TYPE["REMOVE"] = "REMOVE";
A11Y_MESSAGE_TYPE["MOVE"] = "MOVE";
})(A11Y_MESSAGE_TYPE = exports.A11Y_MESSAGE_TYPE || (exports.A11Y_MESSAGE_TYPE = {}));
class ReplaceTile {
constructor(collisionLayer, actionTileWithModifier, mightNotFindConditionButThatIsOk, conditionSpritesToRemove, newDirection) {
if (!collisionLayer) {
throw new Error('BUG: collisionLayer is not set');
}
this.collisionLayer = collisionLayer;
this.acti