UNPKG

puzzlescript

Version:

Play PuzzleScript games in your terminal!

1,203 lines 79.5 kB
"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