namespace(function() { // A 2x2 grid is internally a 5x5: // corner, edge, corner, edge, corner // edge, cell, edge, cell, edge // corner, edge, corner, edge, corner // edge, cell, edge, cell, edge // corner, edge, corner, edge, corner // // Corners and edges will have a value of true if the line passes through them // Cells will contain an object if there is an element in them window.Puzzle = class { constructor(width, height, pillar=false) { if (pillar === true) { this.newGrid(2 * width, 2 * height + 1) } else { this.newGrid(2 * width + 1, 2 * height + 1) } this.pillar = pillar this.settings = { // If true, negation symbols are allowed to cancel other negation symbols. NEGATIONS_CANCEL_NEGATIONS: true, // If true, and the count of polyominos and onimoylops is zero, they cancel regardless of shape. SHAPELESS_ZERO_POLY: false, // If true, the traced line cannot go through the placement of a polyomino. PRECISE_POLYOMINOS: true, // If false, incorrect elements will not flash when failing the puzzle. FLASH_FOR_ERRORS: true, // If true, mid-segment startpoints will constitute solid lines, and form boundaries for the region. FAT_STARTPOINTS: false, // If true, custom mechanics are displayed (and validated) in this puzzle. CUSTOM_MECHANICS: false, // If true, polyominos may be placed partially off of the grid as an intermediate solution step. // OUT_OF_BOUNDS_POLY: false, // If true, the symmetry line will be invisible. INVISIBLE_SYMMETRY: false, } } static deserialize(json) { var parsed = JSON.parse(json) // Claim that it's not a pillar (for consistent grid sizing), then double-check ourselves later. var puzzle = new Puzzle((parsed.grid.length - 1)/2, (parsed.grid[0].length - 1)/2) puzzle.name = parsed.name puzzle.autoSolved = parsed.autoSolved puzzle.grid = parsed.grid // Legacy: Grid squares used to use 'false' to indicate emptiness. // Legacy: Cells may use {} to represent emptiness // Now, we use: // Cells default to null // During onTraceStart, empty cells that are still inbounds are changed to {'type': 'nonce'} for tracing purposes. // Lines default to {'type':'line', 'line':0} for (var x=0; x= this.width) return false if (y < 0 || y >= this.height) return false return true } getCell(x, y) { x = this._mod(x) if (!this._safeCell(x, y)) return null return this.grid[x][y] } setCell(x, y, value) { x = this._mod(x) if (!this._safeCell(x, y)) return this.grid[x][y] = value } getSymmetricalDir(dir) { if (this.symType == SYM_TYPE_VERTICAL || this.symType == SYM_TYPE_ROTATIONAL || this.symType == SYM_TYPE_PARALLEL_H_FLIP) { if (dir === 'left') return 'right' if (dir === 'right') return 'left' } if (this.symType == SYM_TYPE_HORIZONTAL || this.symType == SYM_TYPE_ROTATIONAL || this.symType == SYM_TYPE_PARALLEL_V_FLIP) { if (dir === 'top') return 'bottom' if (dir === 'bottom') return 'top' } if (this.symType == SYM_TYPE_ROTATE_LEFT || this.symType == SYM_TYPE_FLIP_NEG_XY) { if (dir === 'left') return 'bottom' if (dir === 'right') return 'top' } if (this.symType == SYM_TYPE_ROTATE_RIGHT || this.symType == SYM_TYPE_FLIP_NEG_XY) { if (dir === 'top') return 'right' if (dir === 'bottom') return 'left' } if (this.symType == SYM_TYPE_ROTATE_LEFT || this.symType == SYM_TYPE_FLIP_XY) { if (dir === 'top') return 'left' if (dir === 'bottom') return 'right' } if (this.symType == SYM_TYPE_ROTATE_RIGHT || this.symType == SYM_TYPE_FLIP_XY) { if (dir === 'right') return 'bottom' if (dir === 'left') return 'top' } return dir } // The resulting position is guaranteed to be gridsafe. getSymmetricalPos(x, y) { var origx = x var origy = y if (this.symType == SYM_TYPE_VERTICAL || this.symType == SYM_TYPE_ROTATIONAL || this.symType == SYM_TYPE_PARALLEL_H_FLIP) { x = (this.width - 1) - origx } if (this.symType == SYM_TYPE_HORIZONTAL || this.symType == SYM_TYPE_ROTATIONAL || this.symType == SYM_TYPE_PARALLEL_V_FLIP) { y = (this.height - 1) - origy } if (this.symType == SYM_TYPE_ROTATE_LEFT || this.symType == SYM_TYPE_FLIP_XY) { x = origy } if (this.symType == SYM_TYPE_ROTATE_RIGHT || this.symType == SYM_TYPE_FLIP_XY) { y = origx } if (this.symType == SYM_TYPE_ROTATE_LEFT || this.symType == SYM_TYPE_FLIP_NEG_XY) { y = (this.width - 1) - origx } if (this.symType == SYM_TYPE_ROTATE_RIGHT || this.symType == SYM_TYPE_FLIP_NEG_XY) { x = (this.height - 1) - origy } if (this.symType == SYM_TYPE_PARALLEL_H || this.symType == SYM_TYPE_PARALLEL_H_FLIP) { y = (origy == this.height / 2) ? (this.height / 2) : ((origy + (this.height + 1) / 2) % (this.height + 1)) } if (this.symType == SYM_TYPE_PARALLEL_V || this.symType == SYM_TYPE_PARALLEL_V_FLIP) { x = (origx == this.width / 2) ? (this.width / 2) : ((origx + (this.width + 1) / 2) % (this.width + 1)) } return {'x':this._mod(x), 'y':y} } getSymmetricalCell(x, y) { var pos = this.getSymmetricalPos(x, y) return this.getCell(pos.x, pos.y) } matchesSymmetricalPos(x1, y1, x2, y2) { return (y1 === y2 && this._mod(x1) === x2) } // A variant of getCell which specifically returns line values, // and treats objects as being out-of-bounds getLine(x, y) { var cell = this.getCell(x, y) if (cell == null) return null if (cell.type !== 'line') return null return cell.line } updateCell2(x, y, key, value) { x = this._mod(x) if (!this._safeCell(x, y)) return var cell = this.grid[x][y] if (cell == null) return cell[key] = value } getValidEndDirs(x, y) { x = this._mod(x) if (!this._safeCell(x, y)) return [] var dirs = [] var leftCell = this.getCell(x - 1, y) if (leftCell == null || leftCell.gap === window.GAP_FULL) dirs.push('left') var topCell = this.getCell(x, y - 1) if (topCell == null || topCell.gap === window.GAP_FULL) dirs.push('top') var rightCell = this.getCell(x + 1, y) if (rightCell == null || rightCell.gap === window.GAP_FULL) dirs.push('right') var bottomCell = this.getCell(x, y + 1) if (bottomCell == null || bottomCell.gap === window.GAP_FULL) dirs.push('bottom') return dirs } // Note: Does not use this.width/this.height, so that it may be used to ask about resizing. getSizeError(width, height) { if (this.pillar && width < 4) return 'Pillars may not have a width of 1' if (width * height < 25) return 'Puzzles may not be smaller than 2x2 or 1x4' if (width > 21 || height > 21) return 'Puzzles may not be larger than 10 in either dimension' if (this.symmetry != null) { if (this.symmetry.x && width <= 2) return 'Symmetrical puzzles must be sufficiently wide for both lines' if (this.symmetry.y && height <= 2) return 'Symmetrical puzzles must be sufficiently wide for both lines' if (this.pillar && this.symmetry.x && width % 4 !== 0) return 'X + Pillar symmetry must be an even number of rows, to keep both startpoints at the same parity' } return null } // Called on a solution. Computes a list of gaps to show as hints which *do not* // break the path. loadHints() { this.hints = [] for (var x=0; x window.LINE_NONE) { this.hints.push({'x':x, 'y':y}) } } } } // Show a hint on the grid. // If no hint is provided, will select the best one it can find, // prioritizing breaking current lines on the grid. // Returns the shown hint. showHint(hint) { if (hint != null) { this.grid[hint.x][hint.y].gap = window.GAP_BREAK return } var goodHints = [] var badHints = [] for (var hint of this.hints) { if (this.getLine(hint.x, hint.y) > window.LINE_NONE) { // Solution will be broken by this hint goodHints.push(hint) } else { badHints.push(hint) } } if (goodHints.length > 0) { var hint = goodHints.splice(window.randInt(goodHints.length), 1)[0] } else if (badHints.length > 0) { var hint = badHints.splice(window.randInt(badHints.length), 1)[0] } else { return } this.grid[hint.x][hint.y].gap = window.GAP_BREAK this.hints = badHints.concat(goodHints) return hint } clearLines() { for (var x=0; x 0) this._floodFill(x, y - 1, region, col) if (x < this.width - 1) this._floodFill(x + 1, y, region, this.grid[x+1]) else if (this.pillar !== false) this._floodFill(0, y, region, this.grid[0]) if (x > 0) this._floodFill(x - 1, y, region, this.grid[x-1]) else if (this.pillar !== false) this._floodFill(this.width-1, y, region, this.grid[this.width-1]) } // Re-uses the same grid, but only called on edges which border the outside // Called first to mark cells that are connected to the outside, i.e. should not be part of any region. _floodFillOutside(x, y, col) { var cell = col[y] if (cell === MASKED_PROCESSED) return if (x%2 !== y%2 && cell !== MASKED_GAP2) return // Only flood-fill through gap-2 if (x%2 === 0 && y%2 === 0 && cell === MASKED_DOT) return // Don't flood-fill through dots col[y] = MASKED_PROCESSED if (x%2 === 0 && y%2 === 0) return // Don't flood fill through corners (what? Clarify.) if (y < this.height - 1) this._floodFillOutside(x, y + 1, col) if (y > 0) this._floodFillOutside(x, y - 1, col) if (x < this.width - 1) this._floodFillOutside(x + 1, y, this.grid[x+1]) else if (this.pillar !== false) this._floodFillOutside(0, y, this.grid[0]) if (x > 0) this._floodFillOutside(x - 1, y, this.grid[x-1]) else if (this.pillar !== false) this._floodFillOutside(this.width-1, y, this.grid[this.width-1]) } // Returns the original grid (pre-masking). You will need to switch back once you are done flood filling. switchToMaskedGrid() { // Make a copy of the grid -- we will be overwriting it var savedGrid = this.grid this.grid = new Array(this.width) // Override all elements with empty lines -- this means that flood fill is just // looking for lines with line=0. for (var x=0; x window.LINE_NONE) { row[y] = MASKED_PROCESSED // Traced lines should not be a part of the region } else if (cell.gap === window.GAP_FULL) { row[y] = MASKED_GAP2 } else if (cell.dot > window.DOT_NONE) { row[y] = MASKED_DOT } else { row[y] = MASKED_INB_COUNT } } this.grid[x] = row } // Starting at a mid-segment startpoint if (this.startPoint != null && this.startPoint.x%2 !== this.startPoint.y%2) { if (this.settings.FAT_STARTPOINTS) { // This segment is not in any region (acts as a barrier) this.grid[this.startPoint.x][this.startPoint.y] = MASKED_OOB } else { // This segment is part of this region (acts as an empty cell) this.grid[this.startPoint.x][this.startPoint.y] = MASKED_INB_NONCOUNT } } // Ending at a mid-segment endpoint if (this.endPoint != null && this.endPoint.x%2 !== this.endPoint.y%2) { // This segment is part of this region (acts as an empty cell) this.grid[this.endPoint.x][this.endPoint.y] = MASKED_INB_NONCOUNT } // Mark all outside cells as 'not in any region' (aka null) // Top and bottom edges for (var x=1; x 0) row[x] = ' ' if (cell.dot > 0) row[x] = 'X' if (cell.line === 0) row[x] = '.' if (cell.line === 1) row[x] = '#' if (cell.line === 2) row[x] = '#' if (cell.line === 3) row[x] = 'o' } else row[x] = '?' } output += row.join('') + '\n' } console.info(output) } } // The grid contains 5 colors: // null: Out of bounds or already processed var MASKED_OOB = null var MASKED_PROCESSED = null // 0: In bounds, awaiting processing, but should not be part of the final region. var MASKED_INB_NONCOUNT = 0 // 1: In bounds, awaiting processing var MASKED_INB_COUNT = 1 // 2: Gap-2. After _floodFillOutside, this means "treat normally" (it will be null if oob) var MASKED_GAP2 = 2 // 3: Dot (of any kind), otherwise identical to 1. Should not be flood-filled through (why the f do we need this) var MASKED_DOT = 3 })