@logic-pad/core
Version:
310 lines (309 loc) • 12.1 kB
JavaScript
import { array } from '../../dataHelper.js';
import { Color } from '../../primitives.js';
import { instance as banPatternInstance, } from '../../rules/banPatternRule.js';
import { instance as cellCountInstance, } from '../../rules/cellCountRule.js';
import { instance as regionAreaInstance, } from '../../rules/regionAreaRule.js';
import { instance as sameShapeInstance, } from '../../rules/sameShapeRule.js';
import { instance as symbolsPerRegionInstance, } from '../../rules/symbolsPerRegionRule.js';
import { instance as undercluedInstance } from '../../rules/undercluedRule.js';
import { instance as uniqueShapeInstance, } from '../../rules/uniqueShapeRule.js';
import { Serializer } from '../../serializer/allSerializers.js';
import { instance as areaNumberInstance, } from '../../symbols/areaNumberSymbol.js';
import { instance as dartInstance, } from '../../symbols/dartSymbol.js';
import { instance as galaxyInstance, } from '../../symbols/galaxySymbol.js';
import { instance as letterInstance, } from '../../symbols/letterSymbol.js';
import { instance as lotusInstance, } from '../../symbols/lotusSymbol.js';
import { instance as minesweeperInstance, } from '../../symbols/minesweeperSymbol.js';
import { instance as focusInstance, } from '../../symbols/focusSymbol.js';
import { instance as myopiaInstance, } from '../../symbols/myopiaSymbol.js';
import { instance as viewpointInstance, } from '../../symbols/viewpointSymbol.js';
import { instance as connectAllInstance } from '../z3/modules/connectAllModule.js';
import { BTGridData, BTTile } from './data.js';
import BanPatternBTModule from './rules/banPattern.js';
import CellCountBTModule from './rules/cellCount.js';
import ConnectAllBTModule from './rules/connectAll.js';
import RegionAreaBTModule from './rules/regionArea.js';
import SameShapeBTModule from './rules/sameShape.js';
import SymbolsPerRegionBTModule from './rules/symbolsPerRegion.js';
import UniqueShapeBTModule from './rules/uniqueShape.js';
import AreaNumberBTModule from './symbols/areaNumber.js';
import DartBTModule from './symbols/dart.js';
import GalaxyBTModule from './symbols/galaxy.js';
import LetterBTModule from './symbols/letter.js';
import LotusBTModule from './symbols/lotus.js';
import MinesweeperBTModule from './symbols/minesweeper.js';
import MyopiaBTModule from './symbols/myopia.js';
import ViewpointBTModule from './symbols/viewpoint.js';
import FocusBTModule from './symbols/focus.js';
function translateToBTGridData(grid) {
const tiles = array(grid.width, grid.height, (x, y) => {
const tile = grid.getTile(x, y);
if (!tile.exists)
return BTTile.NonExist;
else if (tile.color === Color.Dark)
return BTTile.Dark;
else if (tile.color === Color.Light)
return BTTile.Light;
else
return BTTile.Empty;
});
const connections = array(grid.width, grid.height, (x, y) => grid.connections.getConnectedTiles({ x, y }));
const modules = [];
for (const [id, symbolList] of grid.symbols) {
for (const symbol of symbolList) {
let module;
if (id === areaNumberInstance.id) {
module = new AreaNumberBTModule(symbol);
}
else if (id === dartInstance.id) {
module = new DartBTModule(symbol);
}
else if (id === viewpointInstance.id) {
module = new ViewpointBTModule(symbol);
}
else if (id === galaxyInstance.id) {
module = new GalaxyBTModule(symbol);
}
else if (id === lotusInstance.id) {
module = new LotusBTModule(symbol);
}
else if (id === myopiaInstance.id) {
module = new MyopiaBTModule(symbol);
}
else if (id === minesweeperInstance.id) {
module = new MinesweeperBTModule(symbol);
}
else if (id === focusInstance.id) {
module = new FocusBTModule(symbol);
}
else if (id === letterInstance.id) {
continue;
}
if (!module && symbol.necessaryForCompletion)
throw new Error('Symbol not supported.');
if (module)
modules.push(module);
}
}
const letterSymbols = grid.symbols.get(letterInstance.id);
if (letterSymbols) {
modules.push(new LetterBTModule(letterSymbols, grid.width, grid.height));
}
for (const rule of grid.rules) {
if (!rule.necessaryForCompletion)
continue;
let module;
if (rule.id === connectAllInstance.id) {
module = new ConnectAllBTModule(rule);
}
else if (rule.id === regionAreaInstance.id) {
module = new RegionAreaBTModule(rule);
}
else if (rule.id === banPatternInstance.id) {
module = new BanPatternBTModule(rule);
}
else if (rule.id === symbolsPerRegionInstance.id) {
const allSymbols = [];
grid.symbols.forEach(symbols => allSymbols.push(...symbols));
module = new SymbolsPerRegionBTModule(rule, grid.width, grid.height, allSymbols);
}
else if (rule.id === cellCountInstance.id) {
module = new CellCountBTModule(rule);
}
else if (rule.id === sameShapeInstance.id) {
module = new SameShapeBTModule(rule);
}
else if (rule.id === uniqueShapeInstance.id) {
module = new UniqueShapeBTModule(rule);
}
else if (rule.id === undercluedInstance.id) {
continue;
}
if (!module)
throw new Error('Rule not supported.');
modules.push(module);
}
return new BTGridData(tiles, connections, modules, grid.width, grid.height);
}
function translateBackGridData(grid, btGrid) {
const tiles = array(grid.width, grid.height, (x, y) => {
const origTile = grid.getTile(x, y);
if (!origTile.exists || origTile.fixed || origTile.color !== Color.Gray)
return origTile;
else
return origTile.withColor(btGrid.getTile(x, y) === BTTile.Dark ? Color.Dark : Color.Light);
});
return grid.withTiles(tiles);
}
function isValid(grid, places, checkable, ratings) {
const newCheckable = [...checkable];
const newRatings = [...ratings];
for (let i = 0; i < grid.modules.length; i++) {
const module = grid.modules[i];
// Check if skippable
if (checkable[i] && !places.some(pos => checkable[i].get(pos.x, pos.y)))
continue;
const result = module.checkLocal(grid, places);
if (!result)
return false;
// If returns true, it means do not change checkable and ratings
if (result === true)
continue;
newCheckable[i] = result.tilesNeedCheck;
newRatings[i] = result.ratings;
}
return [newCheckable, newRatings];
}
// This function chooses the next empty tile to search.
function getNextTile(grid, ratings) {
const scores = [];
// TODO: Sum up all the scores of connected tiles without overcounting
for (let y = 0; y < grid.height; y++) {
scores[y] = [];
for (let x = 0; x < grid.width; x++) {
scores[y][x] = 0;
}
}
for (const rating of ratings) {
if (!rating)
continue;
for (const score of rating) {
scores[score.pos.y][score.pos.x] += score.score;
}
}
let highest = 0;
let pos = null;
let fallback = null;
for (let y = 0; y < grid.height; y++) {
for (let x = 0; x < grid.width; x++) {
if (grid.getTile(x, y) !== BTTile.Empty)
continue;
if (scores[y][x] > highest) {
highest = scores[y][x];
pos = { x, y };
}
if (!fallback)
fallback = { x, y };
}
}
return pos ?? fallback;
}
function backtrack(grid, checkable, ratings, solutionFn) {
// Find the best empty cell to guess
const pos = getNextTile(grid, ratings);
// Found a solution
if (!pos)
return !solutionFn(grid.clone());
for (let i = 0; i <= 1; i++) {
// TODO: Use a better method to determine the order
const tile = i === 0 ? BTTile.Light : BTTile.Dark;
grid.setTileWithConnection(pos.x, pos.y, tile);
const places = grid.connections[pos.y][pos.x];
const result = isValid(grid, places, checkable, ratings);
if (result && backtrack(grid, result[0], result[1], solutionFn))
return true;
}
// If both fail, returns to initial state
grid.setTileWithConnection(pos.x, pos.y, BTTile.Empty);
return false;
}
function solveNormal(input, solutionFn) {
// Translate to BT data types
const grid = translateToBTGridData(input);
const checkable = [];
const ratings = [];
for (const module of grid.modules) {
const res = module.checkGlobal(grid);
if (!res)
return [];
checkable.push(res.tilesNeedCheck);
ratings.push(res.ratings);
}
// Call backtrack
backtrack(grid, checkable, ratings, sol => solutionFn(translateBackGridData(input, sol)));
}
function solveUnderclued(input) {
let grid = input;
// let count = 0;
const possibles = array(grid.width, grid.height, () => ({
dark: false,
light: false,
}));
function search(x, y, tile, color) {
// count++;
// console.log(`Trying (${x}, ${y}) with ${color}`);
const newGrid = grid.fastCopyWith({
tiles: grid.setTile(x, y, tile.withColor(color)),
});
// Solve
let solution;
solveNormal(newGrid, sol => {
solution = sol;
return false;
});
if (!solution)
return false;
// Update the new possible states
solution.forEach((solTile, solX, solY) => {
if (solTile.color === Color.Dark) {
possibles[solY][solX].dark = true;
}
else {
possibles[solY][solX].light = true;
}
});
return true;
}
for (let y = 0; y < grid.height; y++) {
for (let x = 0; x < grid.width; x++) {
const tile = grid.getTile(x, y);
if (!tile.exists || tile.color !== Color.Gray)
continue;
// We can skip this solve if it is proved to be solvable
const darkPossible = possibles[y][x].dark || search(x, y, tile, Color.Dark);
const lightPossible = possibles[y][x].light || search(x, y, tile, Color.Light);
// No solution
if (!darkPossible && !lightPossible)
return null;
if (darkPossible && !lightPossible)
grid = grid.fastCopyWith({
tiles: grid.setTile(x, y, tile.withColor(Color.Dark)),
});
if (!darkPossible && lightPossible)
grid = grid.fastCopyWith({
tiles: grid.setTile(x, y, tile.withColor(Color.Light)),
});
}
}
// console.log(`Solve count: ${count}`);
return grid;
}
function solve(grid, solutionFn) {
if (grid.findRule(rule => rule.id === undercluedInstance.id)) {
const res = solveUnderclued(grid);
if (res)
solutionFn(res);
}
else {
solveNormal(grid, solutionFn);
}
}
onmessage = e => {
const grid = Serializer.parseGrid(e.data);
// console.time('Solve time');
let count = 0;
solve(grid, solution => {
// if (count === 0) console.timeLog('Solve time', 'First solution');
if (solution) {
if (solution.resetTiles().colorEquals(solution)) {
postMessage(null);
return false;
}
}
postMessage(Serializer.stringifyGrid(solution));
count += 1;
return count < 2;
});
// console.timeEnd('Solve time');
postMessage(null);
};