namespace(function() { class RegionData { constructor() { this.invalidElements = [] this.veryInvalidElements = [] this.negations = [] } addInvalid(elem) { this.invalidElements.push(elem) } addVeryInvalid(elem) { this.veryInvalidElements.push(elem) } valid() { return (this.invalidElements.length === 0 && this.veryInvalidElements.length === 0) } } // Sanity checks for data which comes from the user. Now that people have learned that /publish is an open endpoint, // we have to make sure they don't submit data which passes validation but is untrustworthy. // These checks should always pass for puzzles created by the built-in editor. window.validateUserData = function(puzzle, path) { if (path == null) throw Error('Path cannot be null') var sizeError = puzzle.getSizeError(puzzle.width, puzzle.height) if (sizeError != null) throw Error(sizeError) var puzzleHasStart = false var puzzleHasEnd = false if (puzzle.grid.length !== puzzle.width) throw Error('Puzzle width does not match grid size') for (var x=0; x window.LINE_NONE) { if (cell.gap > window.GAP_NONE) { console.log('Solution line goes over a gap at', x, y) puzzleData.invalidElements.push({'x': x, 'y': y}) if (quick) return puzzleData } if ((cell.dot === window.DOT_BLUE && cell.line === window.LINE_YELLOW) || (cell.dot === window.DOT_YELLOW && cell.line === window.LINE_BLUE)) { console.log('Incorrectly covered dot: Dot is', cell.dot, 'but line is', cell.line) puzzleData.invalidElements.push({'x': x, 'y': y}) if (quick) return puzzleData } } } } if (needsRegions) { var regions = puzzle.getRegions() } else { var monoRegion = [] for (var x=0; x 0 && veryInvalidElements.length > 0) { var source = negationSymbols.pop() var target = veryInvalidElements.pop() puzzle.setCell(source.x, source.y, null) puzzle.setCell(target.x, target.y, null) baseCombination.push({'source':source, 'target':target}) } var regionData = regionCheckNegations2(puzzle, region, negationSymbols, invalidElements) // Restore required negations for (var combination of baseCombination) { puzzle.setCell(combination.source.x, combination.source.y, combination.source.cell) puzzle.setCell(combination.target.x, combination.target.y, combination.target.cell) regionData.negations.push(combination) } return regionData } // Recursively matches negations and invalid elements from the grid. Note that this function // doesn't actually modify the two lists, it just iterates through them with index/index2. function regionCheckNegations2(puzzle, region, negationSymbols, invalidElements, index=0, index2=0) { if (index2 >= negationSymbols.length) { console.debug('0 negation symbols left, returning negation-less regionCheck') return regionCheck(puzzle, region, false) // @Performance: We could pass quick here. } if (index >= invalidElements.length) { var i = index2 // pair off all negation symbols, 2 at a time if (puzzle.settings.NEGATIONS_CANCEL_NEGATIONS) { for (; i window.DOT_NONE) { console.log('Dot at', pos.x, pos.y, 'is not covered') regionData.addVeryInvalid(pos) if (quick) return regionData } // Check for triangles if (cell.type === 'triangle') { var count = 0 if (puzzle.getLine(pos.x - 1, pos.y) > window.LINE_NONE) count++ if (puzzle.getLine(pos.x + 1, pos.y) > window.LINE_NONE) count++ if (puzzle.getLine(pos.x, pos.y - 1) > window.LINE_NONE) count++ if (puzzle.getLine(pos.x, pos.y + 1) > window.LINE_NONE) count++ if (cell.count !== count) { console.log('Triangle at grid['+pos.x+']['+pos.y+'] has', count, 'borders') regionData.addVeryInvalid(pos) if (quick) return regionData } } // Count color-based elements if (cell.color != null) { var count = coloredObjects[cell.color] if (count == null) { count = 0 } coloredObjects[cell.color] = count + 1 if (cell.type === 'square') { squares.push(pos) if (squareColor == null) { squareColor = cell.color } else if (squareColor != cell.color) { squareColor = -1 // Signal value which indicates square color collision } } if (cell.type === 'star') { pos.color = cell.color stars.push(pos) } } } if (squareColor === -1) { regionData.invalidElements = regionData.invalidElements.concat(squares) if (quick) return regionData } for (var star of stars) { var count = coloredObjects[star.color] if (count === 1) { console.log('Found a', star.color, 'star in a region with 1', star.color, 'object') regionData.addVeryInvalid(star) if (quick) return regionData } else if (count > 2) { console.log('Found a', star.color, 'star in a region with', count, star.color, 'objects') regionData.addInvalid(star) if (quick) return regionData } } if (puzzle.hasPolyominos) { if (!window.polyFit(region, puzzle)) { for (var pos of region) { var cell = puzzle.grid[pos.x][pos.y] if (cell == null) continue if (cell.type === 'poly' || cell.type === 'ylop') { regionData.addInvalid(pos) if (quick) return regionData } } } } if (puzzle.settings.CUSTOM_MECHANICS) { window.validateBridges(puzzle, region, regionData) window.validateArrows(puzzle, region, regionData) window.validateSizers(puzzle, region, regionData) } console.debug('Region has', regionData.veryInvalidElements.length, 'very invalid elements') console.debug('Region has', regionData.invalidElements.length, 'invalid elements') return regionData } })