This commit is contained in:
Daniel Ledda
2021-06-02 08:50:52 +02:00
parent 3d7df31097
commit e7b8ae6120
50 changed files with 4609 additions and 2272 deletions

View File

@@ -0,0 +1,88 @@
export default class SomaSolution {
constructor(dim) {
if (dim < 0 || dim % 1 !== 0) {
throw new Error("Dimension must be a whole positive integer!");
}
this.dim = dim;
this.solutionSpaces = [];
}
static filterUnique(solutions) {
if (solutions.length === 0) {
return [];
}
const uniqueSolns = [solutions[0]];
for (const solution of solutions) {
let foundMatch = false;
for (const rotation of solution.getUniqueRotations()) {
let end = uniqueSolns.length;
for (let i = 0; i < end; i++) {
if (rotation.matches(uniqueSolns[i])) {
foundMatch = true;
}
}
}
if (!foundMatch) {
uniqueSolns.push(solution);
}
}
return uniqueSolns;
}
getUniqueRotations() {
if (this.solutionSpaces.length === 0) {
return [];
}
const result = [];
const allRots = this.solutionSpaces.map(space => space.getAllRotations());
for (let i = 0; i < allRots[0].length; i++) {
const solnRot = new SomaSolution(this.dim);
allRots.forEach(rotGroup => solnRot.addSpace(rotGroup[i]));
result.push(solnRot);
}
return result;
}
matches(solution) {
for (let i = 0; i < this.solutionSpaces.length; i++) {
if (!this.solutionSpaces[i].matches(solution.solutionSpaces[i])) {
return false;
}
}
return true;
}
addSpace(space) {
this.solutionSpaces.push(space);
}
print() {
let accum = "";
console.log("---");
for (let x = 0; x < this.dim; x++) {
for (let y = 0; y < this.dim; y++) {
for (let z = 0; z < this.dim; z++) {
for (const space of this.solutionSpaces) {
if (space.at(x, y, z)) {
accum += space.getId();
}
}
}
console.log(accum);
accum = "";
}
if (x !== this.dim - 1) {
console.log("-");
}
}
console.log("---");
}
at(x, y, z) {
for (const space of this.solutionSpaces) {
if (space.at(x, y, z)) {
return space.getId();
}
}
return 0;
}
clone() {
const clone = new SomaSolution(this.dim);
clone.solutionSpaces = this.solutionSpaces.slice();
return clone;
}
}

View File

@@ -0,0 +1,48 @@
import VoxelSpace from "./VoxelSpace.js";
import SomaSolution from "./SomaSolution.js";
export default class SomaSolver {
constructor(dimension) {
this.solutions = [];
this.iterations = 0;
if (dimension % 1 !== 0 || dimension < 0) {
throw new Error("The argument 'dimension' must be a positive whole number");
}
this.dim = dimension;
this.solutionCube = new VoxelSpace(0, [dimension, dimension, dimension], Array(dimension ** 3).fill(0));
}
async solve(polycubes) {
if (polycubes.length === 0) {
throw new Error("You must pass at least one polycube to solve the puzzle.");
}
let cumulativeSize = polycubes.reduce((prev, curr) => prev + curr.size(), 0);
if (cumulativeSize !== this.dim ** 3) {
throw new Error(`The polycubes passed do not add up to exactly enough units to form a cube of dimension ${this.dim}! Got: ${cumulativeSize}, need: ${this.dim ** 3}`);
}
this.solutions = [];
const combosWithRots = polycubes.slice(1).map(polycube => polycube.getUniqueRotations().map((rot) => rot.getAllPositionsInCube(this.dim)).flat());
const combos = [polycubes[0].getAllPositionsInCube(this.dim), ...combosWithRots];
this.backtrackSolve(this.solutionCube, combos, new SomaSolution(this.dim));
this.solutions = SomaSolution.filterUnique(this.solutions);
}
getSolutions() {
return this.solutions.slice();
}
backtrackSolve(workingSolution, polycubes, currentSoln) {
const nextCubeGroup = polycubes[0];
for (let i = 0; i < nextCubeGroup.length; i++) {
const fusionAttempt = workingSolution.plus(nextCubeGroup[i]);
if (fusionAttempt) {
const nextSoln = currentSoln.clone();
nextSoln.addSpace(nextCubeGroup[i]);
if (polycubes.length === 1) {
this.solutions.push(nextSoln);
currentSoln = new SomaSolution(this.dim);
return;
}
else {
this.backtrackSolve(fusionAttempt, polycubes.slice(1), nextSoln);
}
}
}
}
}

275
public/solver/VoxelSpace.js Normal file
View File

@@ -0,0 +1,275 @@
export default class VoxelSpace {
constructor(id, dims, space, cullEmpty) {
if (!space) {
space = 0n;
}
else if (Array.isArray(space)) {
if (space.length !== dims[0] * dims[1] * dims[2]) {
throw new Error("Vals don't fit in given dimensions.");
}
space = VoxelSpace.boolArrayToBigInt(space);
}
this.id = id;
this.length = dims[0] * dims[1] * dims[2];
this.dims = dims;
this.space = space;
if (cullEmpty) {
this.cullEmptySpace();
}
}
static boolArrayToBigInt(boolArray) {
let result = 0n;
for (let i = 0; i < boolArray.length; i++) {
if (boolArray[i]) {
result |= BigInt(1 << i);
}
}
return result;
}
binaryRep() {
return this.space.toString(2);
}
getExtrema() {
const extrema = {
xMax: -Infinity,
xMin: Infinity,
yMax: -Infinity,
yMin: Infinity,
zMax: -Infinity,
zMin: Infinity,
};
this.forEachCell((val, x, y, z) => {
if (val) {
extrema.xMax = Math.max(extrema.xMax, x);
extrema.xMin = Math.min(extrema.xMin, x);
extrema.yMax = Math.max(extrema.yMax, y);
extrema.yMin = Math.min(extrema.yMin, y);
extrema.zMax = Math.max(extrema.zMax, z);
extrema.zMin = Math.min(extrema.zMin, z);
}
});
return extrema;
}
cullEmptySpace() {
const extrema = this.getExtrema();
let index = 0n;
let newSpace = 0n;
for (let x = extrema.xMin; x <= extrema.xMax; x++) {
for (let y = extrema.yMin; y <= extrema.yMax; y++) {
for (let z = extrema.zMin; z <= extrema.zMax; z++) {
if (this.at(x, y, z)) {
newSpace |= 1n << index;
}
index++;
}
}
}
this.dims[0] = extrema.xMax - extrema.xMin + 1;
this.dims[1] = extrema.yMax - extrema.yMin + 1;
this.dims[2] = extrema.zMax - extrema.zMin + 1;
this.space = newSpace;
}
forEachCell(cb) {
loopStart: for (let x = 0; x < this.dims[0]; x++) {
for (let y = 0; y < this.dims[1]; y++) {
for (let z = 0; z < this.dims[2]; z++) {
if (cb(this.at(x, y, z), x, y, z) === 0) {
break loopStart;
}
}
}
}
}
getId() {
return this.id;
}
print() {
let accum = "";
console.log("---");
for (let i = 0; i < this.dims[0]; i++) {
for (let j = 0; j < this.dims[1]; j++) {
for (let k = 0; k < this.dims[2]; k++) {
accum += this.at(i, j, k) ? '#' : 'O';
}
console.log(accum);
accum = "";
}
if (i !== this.dims[0] - 1) {
console.log("-");
}
}
console.log("---");
}
getUniqueRotations() {
const rotations = [];
const refSpace = this.clone();
VoxelSpace.pushNewUniqueSpaces(rotations, refSpace.getAxisSpins('x'));
refSpace.rot90('y');
VoxelSpace.pushNewUniqueSpaces(rotations, refSpace.getAxisSpins('x'));
refSpace.rot90('y');
VoxelSpace.pushNewUniqueSpaces(rotations, refSpace.getAxisSpins('x'));
refSpace.rot90('y');
VoxelSpace.pushNewUniqueSpaces(rotations, refSpace.getAxisSpins('x'));
refSpace.rot90('z');
VoxelSpace.pushNewUniqueSpaces(rotations, refSpace.getAxisSpins('x'));
refSpace.rot90('z');
refSpace.rot90('z');
VoxelSpace.pushNewUniqueSpaces(rotations, refSpace.getAxisSpins('x'));
return rotations;
}
getAllRotations() {
const rotations = [];
const refSpace = this.clone();
rotations.push(...refSpace.getAxisSpins('x'));
refSpace.rot90('y');
rotations.push(...refSpace.getAxisSpins('x'));
refSpace.rot90('y');
rotations.push(...refSpace.getAxisSpins('x'));
refSpace.rot90('y');
rotations.push(...refSpace.getAxisSpins('x'));
refSpace.rot90('z');
rotations.push(...refSpace.getAxisSpins('x'));
refSpace.rot90('z');
refSpace.rot90('z');
rotations.push(...refSpace.getAxisSpins('x'));
return rotations;
}
static pushNewUniqueSpaces(existingSpaces, newSpaces) {
for (const newSpace of newSpaces) {
let matchFound = false;
for (const existingSpace of existingSpaces) {
if (newSpace.matches(existingSpace)) {
matchFound = true;
break;
}
}
if (!matchFound) {
existingSpaces.push(newSpace);
}
}
}
getAllPositionsInCube(cubeDim) {
if ((cubeDim > 0) && (cubeDim % 1 === 0)) {
const cubePositions = [];
for (let x = 0; x < cubeDim - this.dims[0] + 1; x++) {
for (let y = 0; y < cubeDim - this.dims[1] + 1; y++) {
for (let z = 0; z < cubeDim - this.dims[2] + 1; z++) {
const cubePos = new VoxelSpace(this.id, [cubeDim, cubeDim, cubeDim]);
this.forEachCell((val, rotX, rotY, rotZ) => {
cubePos.set(x + rotX, y + rotY, z + rotZ, val);
});
cubePositions.push(cubePos);
}
}
}
return cubePositions;
}
else {
throw new Error("cubeDim must be a positive integer.");
}
}
matches(space) {
const otherDims = space.getDims();
for (let i = 0; i < this.dims.length; i++) {
if (otherDims[i] !== this.dims[i]) {
return false;
}
}
return this.space === space.getRaw();
}
clone() {
return new VoxelSpace(this.id, this.getDims(), this.getRaw());
}
getAxisSpins(axis) {
const rotations = [this.clone()];
for (let i = 0; i < 3; i++) {
rotations.push(rotations[i].rotated90(axis));
}
return rotations;
}
getDims() {
return this.dims.slice();
}
getRaw() {
return this.space;
}
// [1, 0, 0] [x] [ x]
// [0, 0, -1] * [y] = [-z]
// [0, 1, 0] [z] [ y]
newIndexRotX(x, y, z) {
return this.dims[2] * this.dims[1] * x + this.dims[1] * (this.dims[2] - 1 - z) + y;
}
// [ 0, 0, 1] [x] [ z]
// [ 0, 1, 0] * [y] = [ y]
// [-1, 0, 0] [z] [-x]
newIndexRotY(x, y, z) {
return this.dims[1] * this.dims[0] * z + this.dims[0] * y + (this.dims[0] - 1 - x);
}
// [0, -1, 0] [x] [-y]
// [1, 0, 0] * [y] = [ x]
// [0, 0, 1] [z] [ z]
newIndexRotZ(x, y, z) {
return this.dims[0] * this.dims[2] * (this.dims[1] - 1 - y) + this.dims[2] * x + z;
}
at(x, y, z) {
const mask = 1n << BigInt(this.dims[1] * this.dims[2] * x + this.dims[2] * y + z);
return (this.space & mask) !== 0n;
}
toggle(x, y, z) {
const mask = BigInt(1 << this.dims[1] * this.dims[2] * x + this.dims[2] * y + z);
this.space ^= mask;
}
set(x, y, z, val) {
const mask = BigInt(1 << this.dims[1] * this.dims[2] * x + this.dims[2] * y + z);
if (val) {
this.space |= mask;
}
else {
this.space &= ~mask;
}
}
rotated90(dim) {
let newSpace = 0n;
let newDims;
let rotIndex;
if (dim === 'x') {
newDims = [this.dims[0], this.dims[2], this.dims[1]];
rotIndex = this.newIndexRotX.bind(this);
}
else if (dim === 'y') {
newDims = [this.dims[2], this.dims[1], this.dims[0]];
rotIndex = this.newIndexRotY.bind(this);
}
else {
newDims = [this.dims[1], this.dims[0], this.dims[2]];
rotIndex = this.newIndexRotZ.bind(this);
}
this.forEachCell((val, i, j, k) => {
if (val) {
newSpace |= BigInt(1 << rotIndex(i, j, k));
}
});
return new VoxelSpace(this.id, newDims, newSpace);
}
rot90(dim) {
const rot = this.rotated90(dim);
this.space = rot.getRaw();
this.dims = rot.getDims();
}
plus(space) {
const otherSpace = space.getRaw();
if ((this.space | otherSpace) === (this.space ^ otherSpace)) {
return new VoxelSpace(this.id, this.dims, otherSpace | this.space);
}
return null;
}
size() {
let size = 0;
this.forEachCell((val) => {
if (val) {
size++;
}
});
return size;
}
}

8
public/solver/main.js Normal file
View File

@@ -0,0 +1,8 @@
import SomaSolver from "./SomaSolver.js";
import VoxelSpace from "./VoxelSpace.js";
self.addEventListener('message', (event) => {
const { polycubes, dims } = event.data;
const solver = new SomaSolver(event.data.dims);
solver.solve(polycubes.map((cubeRep, i) => new VoxelSpace(i, [dims, dims, dims], cubeRep, true)));
self.postMessage(solver.getSolutions());
});