It all works....
This commit is contained in:
102
src/solver/js/SomaSolution.js
Normal file
102
src/solver/js/SomaSolution.js
Normal file
@@ -0,0 +1,102 @@
|
||||
export default class SomaSolution {
|
||||
constructor(dimX, dimY, dimZ) {
|
||||
this.dimX = dimX;
|
||||
this.dimY = dimY;
|
||||
this.dimZ = dimZ;
|
||||
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.getRotations()) {
|
||||
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;
|
||||
}
|
||||
getRotations() {
|
||||
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.dimX, this.dimY, this.dimZ);
|
||||
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.dimX; x++) {
|
||||
for (let y = 0; y < this.dimY; y++) {
|
||||
for (let z = 0; z < this.dimZ; z++) {
|
||||
for (const space of this.solutionSpaces) {
|
||||
if (space.at(x, y, z)) {
|
||||
accum += space.getId();
|
||||
}
|
||||
}
|
||||
}
|
||||
console.log(accum);
|
||||
accum = "";
|
||||
}
|
||||
if (x !== this.dimX - 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.dimX, this.dimY, this.dimZ);
|
||||
clone.solutionSpaces = this.solutionSpaces.slice();
|
||||
return clone;
|
||||
}
|
||||
getDims() {
|
||||
return [this.dimX, this.dimY, this.dimZ];
|
||||
}
|
||||
forEachCell(cb) {
|
||||
loopStart: for (let x = 0; x < this.dimX; x++) {
|
||||
for (let y = 0; y < this.dimY; y++) {
|
||||
for (let z = 0; z < this.dimZ; z++) {
|
||||
cb(this.at(x, y, z), x, y, z);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
getPieces() {
|
||||
return this.solutionSpaces.slice();
|
||||
}
|
||||
}
|
||||
119
src/solver/js/SomaSolution.ts
Normal file
119
src/solver/js/SomaSolution.ts
Normal file
@@ -0,0 +1,119 @@
|
||||
import type VoxelSpaceBoolean from "./VoxelSpaceBoolean";
|
||||
|
||||
export default class SomaSolution {
|
||||
private solutionSpaces: VoxelSpaceBoolean[];
|
||||
private dimX: number;
|
||||
private dimY: number;
|
||||
private dimZ: number;
|
||||
|
||||
constructor(dimX: number, dimY: number, dimZ: number) {
|
||||
this.dimX = dimX;
|
||||
this.dimY = dimY;
|
||||
this.dimZ = dimZ;
|
||||
this.solutionSpaces = [];
|
||||
}
|
||||
|
||||
static filterUnique(solutions: SomaSolution[]): SomaSolution[] {
|
||||
if (solutions.length === 0) {
|
||||
return [];
|
||||
}
|
||||
const uniqueSolns = [solutions[0]];
|
||||
for (const solution of solutions) {
|
||||
let foundMatch = false;
|
||||
for (const rotation of solution.getRotations()) {
|
||||
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;
|
||||
}
|
||||
|
||||
getRotations(): SomaSolution[] {
|
||||
if (this.solutionSpaces.length === 0) {
|
||||
return [];
|
||||
}
|
||||
const result: SomaSolution[] = [];
|
||||
const allRots = this.solutionSpaces.map(space => space.getAllRotations());
|
||||
for (let i = 0; i < allRots[0].length; i++) {
|
||||
const solnRot = new SomaSolution(this.dimX, this.dimY, this.dimZ);
|
||||
allRots.forEach(rotGroup => solnRot.addSpace(rotGroup[i]));
|
||||
result.push(solnRot);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
matches(solution: SomaSolution) {
|
||||
for (let i = 0; i < this.solutionSpaces.length; i++) {
|
||||
if (!this.solutionSpaces[i].matches(solution.solutionSpaces[i])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
addSpace(space: VoxelSpaceBoolean) {
|
||||
this.solutionSpaces.push(space);
|
||||
}
|
||||
|
||||
print() {
|
||||
let accum = "";
|
||||
console.log("---");
|
||||
for (let x = 0; x < this.dimX; x++) {
|
||||
for (let y = 0; y < this.dimY; y++) {
|
||||
for (let z = 0; z < this.dimZ; z++) {
|
||||
for (const space of this.solutionSpaces) {
|
||||
if (space.at(x, y, z)) {
|
||||
accum += space.getId();
|
||||
}
|
||||
}
|
||||
}
|
||||
console.log(accum);
|
||||
accum = "";
|
||||
}
|
||||
if (x !== this.dimX - 1) {
|
||||
console.log("-");
|
||||
}
|
||||
}
|
||||
console.log("---");
|
||||
}
|
||||
|
||||
at(x: number, y: number, z: number) {
|
||||
for (const space of this.solutionSpaces) {
|
||||
if (space.at(x, y, z)) {
|
||||
return space.getId();
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
clone() {
|
||||
const clone = new SomaSolution(this.dimX, this.dimY, this.dimZ);
|
||||
clone.solutionSpaces = this.solutionSpaces.slice();
|
||||
return clone;
|
||||
}
|
||||
|
||||
getDims() {
|
||||
return [this.dimX, this.dimY, this.dimZ];
|
||||
}
|
||||
|
||||
forEachCell(cb: (val: number, x: number, y: number, z: number) => any) {
|
||||
loopStart: for (let x = 0; x < this.dimX; x++) {
|
||||
for (let y = 0; y < this.dimY; y++) {
|
||||
for (let z = 0; z < this.dimZ; z++) {
|
||||
cb(this.at(x, y, z), x, y, z);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getPieces() {
|
||||
return this.solutionSpaces.slice();
|
||||
}
|
||||
}
|
||||
60
src/solver/js/SomaSolver.js
Normal file
60
src/solver/js/SomaSolver.js
Normal file
@@ -0,0 +1,60 @@
|
||||
import VoxelSpaceBoolean from "./VoxelSpaceBoolean";
|
||||
import SomaSolution from "./SomaSolution";
|
||||
export default class SomaSolver {
|
||||
constructor(dimX, dimY, dimZ) {
|
||||
this.visualiser = { async showSoln(soln) { }, async showSpace(cube) { } };
|
||||
this.solutions = new Array();
|
||||
this.iterations = 0;
|
||||
this.dimX = dimX;
|
||||
this.dimY = dimY;
|
||||
this.dimZ = dimZ;
|
||||
this.solutionCube = new VoxelSpaceBoolean({ id: 0, dims: [dimX, dimY, dimZ], cullEmpty: false });
|
||||
}
|
||||
setDebug(visualiser) {
|
||||
this.visualiser = visualiser;
|
||||
}
|
||||
async solve(polycubes) {
|
||||
if (polycubes.length === 0) {
|
||||
throw new Error("You must pass at least one polycube to solve the puzzle.");
|
||||
}
|
||||
this.solutions.splice(0, this.solutions.length);
|
||||
const combosWithRots = new Array();
|
||||
for (let i = 1; i < polycubes.length; i++) {
|
||||
const rots = polycubes[i].getAllPermutationsInPrism(this.dimX, this.dimY, this.dimZ);
|
||||
combosWithRots.push(rots);
|
||||
}
|
||||
let combos = new Array();
|
||||
combos.push(polycubes[0].getAllPositionsInPrism(this.dimX, this.dimY, this.dimZ));
|
||||
combos = combos.concat(combosWithRots);
|
||||
for (const combo of combos) {
|
||||
for (const rot of combo) {
|
||||
await this.visualiser.showSpace(rot);
|
||||
}
|
||||
}
|
||||
await this.backtrackSolve(this.solutionCube, combos, new SomaSolution(this.dimX, this.dimY, this.dimZ));
|
||||
this.solutions = SomaSolution.filterUnique(this.solutions);
|
||||
}
|
||||
getSolutions() {
|
||||
return this.solutions.slice();
|
||||
}
|
||||
async backtrackSolve(workingSolution, polycubes, currentSoln, depth = 0) {
|
||||
const nextCubeGroup = polycubes[0];
|
||||
for (let i = 0; i < nextCubeGroup.length; i++) {
|
||||
const fusionAttempt = workingSolution.plus(nextCubeGroup[i]);
|
||||
++this.iterations;
|
||||
if (fusionAttempt) {
|
||||
const nextSoln = currentSoln.clone();
|
||||
nextSoln.addSpace(nextCubeGroup[i]);
|
||||
await this.visualiser.showSoln(nextSoln);
|
||||
if (polycubes.length === 1) {
|
||||
this.solutions.push(nextSoln);
|
||||
currentSoln = new SomaSolution(this.dimX, this.dimY, this.dimZ);
|
||||
return;
|
||||
}
|
||||
else {
|
||||
await this.backtrackSolve(fusionAttempt, polycubes.slice(1), nextSoln, depth + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
73
src/solver/js/SomaSolver.ts
Normal file
73
src/solver/js/SomaSolver.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
import VoxelSpaceBoolean from "./VoxelSpaceBoolean";
|
||||
import SomaSolution from "./SomaSolution";
|
||||
|
||||
interface DebugVisualiser {
|
||||
showSoln(soln: SomaSolution): Promise<void>;
|
||||
showSpace(cube: VoxelSpaceBoolean): Promise<void>;
|
||||
}
|
||||
|
||||
export default class SomaSolver {
|
||||
private solutionCube: VoxelSpaceBoolean;
|
||||
private dimX: number;
|
||||
private dimY: number;
|
||||
private dimZ: number;
|
||||
private visualiser: DebugVisualiser = {async showSoln(soln: SomaSolution) {}, async showSpace(cube: VoxelSpaceBoolean) {}};
|
||||
private solutions: SomaSolution[] = new Array<SomaSolution>();
|
||||
private iterations: number = 0;
|
||||
constructor(dimX: number, dimY: number, dimZ: number) {
|
||||
this.dimX = dimX;
|
||||
this.dimY = dimY;
|
||||
this.dimZ = dimZ;
|
||||
this.solutionCube = new VoxelSpaceBoolean({id: 0, dims: [dimX, dimY, dimZ], cullEmpty: false});
|
||||
}
|
||||
|
||||
setDebug(visualiser: DebugVisualiser) {
|
||||
this.visualiser = visualiser;
|
||||
}
|
||||
|
||||
async solve(polycubes: VoxelSpaceBoolean[]) {
|
||||
if (polycubes.length === 0) {
|
||||
throw new Error("You must pass at least one polycube to solve the puzzle.");
|
||||
}
|
||||
this.solutions.splice(0, this.solutions.length);
|
||||
const combosWithRots: VoxelSpaceBoolean[][] = new Array<Array<VoxelSpaceBoolean>>();
|
||||
for (let i = 1; i < polycubes.length; i++) {
|
||||
const rots = polycubes[i].getAllPermutationsInPrism(this.dimX, this.dimY, this.dimZ);
|
||||
combosWithRots.push(rots);
|
||||
}
|
||||
let combos: VoxelSpaceBoolean[][] = new Array<Array<VoxelSpaceBoolean>>();
|
||||
combos.push(polycubes[0].getAllPositionsInPrism(this.dimX, this.dimY, this.dimZ));
|
||||
combos = combos.concat(combosWithRots);
|
||||
for (const combo of combos) {
|
||||
for (const rot of combo) {
|
||||
await this.visualiser.showSpace(rot);
|
||||
}
|
||||
}
|
||||
await this.backtrackSolve(this.solutionCube, combos, new SomaSolution(this.dimX, this.dimY, this.dimZ));
|
||||
this.solutions = SomaSolution.filterUnique(this.solutions);
|
||||
}
|
||||
|
||||
getSolutions() {
|
||||
return this.solutions.slice();
|
||||
}
|
||||
|
||||
private async backtrackSolve(workingSolution: VoxelSpaceBoolean, polycubes: VoxelSpaceBoolean[][], currentSoln: SomaSolution, depth = 0) {
|
||||
const nextCubeGroup = polycubes[0];
|
||||
for (let i = 0; i < nextCubeGroup.length; i++) {
|
||||
const fusionAttempt = workingSolution.plus(nextCubeGroup[i]);
|
||||
++this.iterations;
|
||||
if (fusionAttempt) {
|
||||
const nextSoln = currentSoln.clone();
|
||||
nextSoln.addSpace(nextCubeGroup[i]);
|
||||
await this.visualiser.showSoln(nextSoln);
|
||||
if (polycubes.length === 1) {
|
||||
this.solutions.push(nextSoln);
|
||||
currentSoln = new SomaSolution(this.dimX, this.dimY, this.dimZ);
|
||||
return;
|
||||
} else {
|
||||
await this.backtrackSolve(fusionAttempt, polycubes.slice(1), nextSoln, depth + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
321
src/solver/js/VoxelSpaceBoolean.js
Normal file
321
src/solver/js/VoxelSpaceBoolean.js
Normal file
@@ -0,0 +1,321 @@
|
||||
export default class VoxelSpaceBoolean {
|
||||
constructor(options) {
|
||||
this.length = options.dims[0] * options.dims[1] * options.dims[2];
|
||||
if (!options.space) {
|
||||
options.space = new Array(options.dims[0] * options.dims[1] * options.dims[2]);
|
||||
options.space.fill(false);
|
||||
}
|
||||
else if (!Array.isArray(options.space)) {
|
||||
const newSpace = [];
|
||||
for (let i = 0; i < this.length; i++) {
|
||||
const mask = 1n << BigInt(i);
|
||||
newSpace.push((options.space & mask) !== 0n);
|
||||
}
|
||||
options.space = newSpace;
|
||||
}
|
||||
this.id = options.id;
|
||||
this.dims = options.dims;
|
||||
this.space = options.space;
|
||||
this.color = options.color ?? "red";
|
||||
if (options.cullEmpty !== false) {
|
||||
this.cullEmptySpace();
|
||||
}
|
||||
}
|
||||
setColor(color) {
|
||||
this.color = color;
|
||||
}
|
||||
getColor() {
|
||||
return this.color;
|
||||
}
|
||||
binaryRep() {
|
||||
return this.space.reduce((prev, curr) => prev + (curr ? "1" : "0"), "");
|
||||
}
|
||||
getExtrema() {
|
||||
const extrema = {
|
||||
xMax: 0,
|
||||
xMin: this.dims[0],
|
||||
yMax: 0,
|
||||
yMin: this.dims[1],
|
||||
zMax: 0,
|
||||
zMin: this.dims[2],
|
||||
};
|
||||
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();
|
||||
const newX = extrema.xMax - extrema.xMin + 1;
|
||||
const newY = extrema.yMax - extrema.yMin + 1;
|
||||
const newZ = extrema.zMax - extrema.zMin + 1;
|
||||
const newSpace = new Array(newX * newY * newZ);
|
||||
newSpace.fill(false);
|
||||
let index = 0;
|
||||
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[index] = true;
|
||||
}
|
||||
index++;
|
||||
}
|
||||
}
|
||||
}
|
||||
this.dims[0] = newX;
|
||||
this.dims[1] = newY;
|
||||
this.dims[2] = newZ;
|
||||
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();
|
||||
VoxelSpaceBoolean.pushNewUniqueSpaces(rotations, refSpace.getAxisSpins('x'));
|
||||
refSpace.rot90('y');
|
||||
VoxelSpaceBoolean.pushNewUniqueSpaces(rotations, refSpace.getAxisSpins('x'));
|
||||
refSpace.rot90('y');
|
||||
VoxelSpaceBoolean.pushNewUniqueSpaces(rotations, refSpace.getAxisSpins('x'));
|
||||
refSpace.rot90('y');
|
||||
VoxelSpaceBoolean.pushNewUniqueSpaces(rotations, refSpace.getAxisSpins('x'));
|
||||
refSpace.rot90('z');
|
||||
VoxelSpaceBoolean.pushNewUniqueSpaces(rotations, refSpace.getAxisSpins('x'));
|
||||
refSpace.rot90('z');
|
||||
refSpace.rot90('z');
|
||||
VoxelSpaceBoolean.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);
|
||||
}
|
||||
}
|
||||
}
|
||||
getAllPositionsInPrism(cubeDimX, cubeDimY, cubeDimZ) {
|
||||
const cubePositions = [];
|
||||
if (this.dims[0] > cubeDimX || this.dims[1] > cubeDimY || this.dims[2] > cubeDimZ) {
|
||||
return cubePositions;
|
||||
}
|
||||
for (let xOffset = 0; xOffset < (cubeDimX - this.dims[0] + 1); xOffset++) {
|
||||
for (let yOffset = 0; yOffset < (cubeDimY - this.dims[1] + 1); yOffset++) {
|
||||
for (let zOffset = 0; zOffset < (cubeDimZ - this.dims[2] + 1); zOffset++) {
|
||||
const cubePos = new VoxelSpaceBoolean({ id: this.id, dims: [cubeDimX, cubeDimY, cubeDimZ], color: this.color, cullEmpty: false });
|
||||
this.forEachCell((val, x, y, z) => {
|
||||
cubePos.set(xOffset + x, yOffset + y, zOffset + z, val);
|
||||
});
|
||||
cubePositions.push(cubePos);
|
||||
}
|
||||
}
|
||||
}
|
||||
return cubePositions;
|
||||
}
|
||||
matches(space) {
|
||||
const otherDims = space.getDims();
|
||||
for (let i = 0; i < this.dims.length; i++) {
|
||||
if (otherDims[i] !== this.dims[i]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
const otherRaw = space.getRaw();
|
||||
if (typeof otherRaw === "bigint") {
|
||||
return space.binaryRep() === this.binaryRep();
|
||||
}
|
||||
return this.space.reduce((prev, unit, i) => (unit === otherRaw[i]) && prev, true);
|
||||
}
|
||||
clone() {
|
||||
return new VoxelSpaceBoolean({ id: this.id, dims: this.getDims(), space: this.getRaw(), color: this.getColor(), cullEmpty: false });
|
||||
}
|
||||
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.slice();
|
||||
}
|
||||
// [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) {
|
||||
return this.space[this.index(x, y, z)];
|
||||
}
|
||||
index(x, y, z) {
|
||||
return this.dims[1] * this.dims[2] * x + this.dims[2] * y + z;
|
||||
}
|
||||
toggle(x, y, z) {
|
||||
const index = this.index(x, y, z);
|
||||
this.space[index] = !this.space[index];
|
||||
}
|
||||
set(x, y, z, val) {
|
||||
this.space[this.index(x, y, z)] = val;
|
||||
}
|
||||
rotated90(dim) {
|
||||
const newSpace = new Array(this.dims[0] * this.dims[1] * this.dims[2]);
|
||||
newSpace.fill(false);
|
||||
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[rotIndex(i, j, k)] = true;
|
||||
}
|
||||
});
|
||||
return new VoxelSpaceBoolean({ id: this.id, dims: newDims, space: newSpace, color: this.color, cullEmpty: false });
|
||||
}
|
||||
rot90(dim) {
|
||||
const rot = this.rotated90(dim);
|
||||
this.space = rot.getRaw();
|
||||
this.dims = rot.getDims();
|
||||
}
|
||||
plus(space) {
|
||||
const newSpace = new Array(this.dims[0] * this.dims[1] * this.dims[2]);
|
||||
newSpace.fill(false);
|
||||
let clash = false;
|
||||
space.forEachCell((val, x, y, z) => {
|
||||
if (this.at(x, y, z) !== val) {
|
||||
newSpace[this.index(x, y, z)] = true;
|
||||
}
|
||||
else {
|
||||
if (val) {
|
||||
clash = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
if (!clash) {
|
||||
return new VoxelSpaceBoolean({ id: this.id, dims: this.getDims(), space: newSpace, color: this.color, cullEmpty: false });
|
||||
}
|
||||
return null;
|
||||
}
|
||||
size() {
|
||||
let size = 0;
|
||||
this.forEachCell((val) => {
|
||||
if (val) {
|
||||
size++;
|
||||
}
|
||||
});
|
||||
return size;
|
||||
}
|
||||
getDirectNeighbourProfile(x, y, z) {
|
||||
let result = 0;
|
||||
if (x < this.dims[0] - 1 && this.at(x + 1, y, z)) {
|
||||
result += 1;
|
||||
}
|
||||
if (y < this.dims[1] - 1 && this.at(x, y + 1, z)) {
|
||||
result += 2;
|
||||
}
|
||||
if (z < this.dims[2] - 1 && this.at(x, y, z + 1)) {
|
||||
result += 4;
|
||||
}
|
||||
if (x > 0 && this.at(x - 1, y, z)) {
|
||||
result += 8;
|
||||
}
|
||||
if (y > 0 && this.at(x, y - 1, z)) {
|
||||
result += 16;
|
||||
}
|
||||
if (z > 0 && this.at(x, y, z - 1)) {
|
||||
result += 32;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
getAllPermutationsInPrism(prismDimX, prismDimY, prismDimZ) {
|
||||
const rotations = this.getUniqueRotations();
|
||||
let result = new Array();
|
||||
for (let i = 0; i < rotations.length; i++) {
|
||||
result = result.concat(rotations[i].getAllPositionsInPrism(prismDimX, prismDimY, prismDimZ));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
364
src/solver/js/VoxelSpaceBoolean.ts
Normal file
364
src/solver/js/VoxelSpaceBoolean.ts
Normal file
@@ -0,0 +1,364 @@
|
||||
export type DimensionDef = [number, number, number];
|
||||
|
||||
const enum NeighbourDirection {
|
||||
POSX,
|
||||
POSY,
|
||||
POSZ,
|
||||
NEGX,
|
||||
NEGY,
|
||||
NEGZ,
|
||||
}
|
||||
|
||||
export default class VoxelSpaceBoolean {
|
||||
private dims: DimensionDef;
|
||||
private length: number;
|
||||
private space: boolean[];
|
||||
private id: number;
|
||||
private color: string;
|
||||
|
||||
constructor(options: {id: number, dims: DimensionDef, space?: boolean[] | bigint, cullEmpty: boolean, color?: string}) {
|
||||
this.length = options.dims[0] * options.dims[1] * options.dims[2];
|
||||
if (!options.space) {
|
||||
options.space = new Array<boolean>(options.dims[0] * options.dims[1] * options.dims[2]);
|
||||
options.space.fill(false);
|
||||
} else if (!Array.isArray(options.space)) {
|
||||
const newSpace = [];
|
||||
for (let i = 0; i < this.length; i++) {
|
||||
const mask = 1n << BigInt(i);
|
||||
newSpace.push((options.space & mask) !== 0n);
|
||||
}
|
||||
options.space = newSpace;
|
||||
}
|
||||
this.id = options.id;
|
||||
this.dims = options.dims;
|
||||
this.space = options.space;
|
||||
this.color = options.color ?? "red";
|
||||
if (options.cullEmpty !== false) {
|
||||
this.cullEmptySpace();
|
||||
}
|
||||
}
|
||||
|
||||
setColor(color: string) {
|
||||
this.color = color;
|
||||
}
|
||||
|
||||
getColor() {
|
||||
return this.color;
|
||||
}
|
||||
|
||||
binaryRep() {
|
||||
return this.space.reduce((prev, curr) => prev + (curr ? "1" : "0"), "");
|
||||
}
|
||||
|
||||
getExtrema() {
|
||||
const extrema = {
|
||||
xMax: 0,
|
||||
xMin: this.dims[0],
|
||||
yMax: 0,
|
||||
yMin: this.dims[1],
|
||||
zMax: 0,
|
||||
zMin: this.dims[2],
|
||||
};
|
||||
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();
|
||||
const newX = extrema.xMax - extrema.xMin + 1;
|
||||
const newY = extrema.yMax - extrema.yMin + 1;
|
||||
const newZ = extrema.zMax - extrema.zMin + 1;
|
||||
const newSpace = new Array<boolean>(newX * newY * newZ);
|
||||
newSpace.fill(false);
|
||||
let index = 0;
|
||||
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[index] = true;
|
||||
}
|
||||
index++;
|
||||
}
|
||||
}
|
||||
}
|
||||
this.dims[0] = newX;
|
||||
this.dims[1] = newY;
|
||||
this.dims[2] = newZ;
|
||||
this.space = newSpace;
|
||||
}
|
||||
|
||||
forEachCell(cb: (val: boolean, x: number, y: number, z: number) => any) {
|
||||
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: VoxelSpaceBoolean[] = [];
|
||||
const refSpace = this.clone();
|
||||
VoxelSpaceBoolean.pushNewUniqueSpaces(rotations, refSpace.getAxisSpins('x'));
|
||||
refSpace.rot90('y');
|
||||
VoxelSpaceBoolean.pushNewUniqueSpaces(rotations, refSpace.getAxisSpins('x'));
|
||||
refSpace.rot90('y');
|
||||
VoxelSpaceBoolean.pushNewUniqueSpaces(rotations, refSpace.getAxisSpins('x'));
|
||||
refSpace.rot90('y');
|
||||
VoxelSpaceBoolean.pushNewUniqueSpaces(rotations, refSpace.getAxisSpins('x'));
|
||||
refSpace.rot90('z');
|
||||
VoxelSpaceBoolean.pushNewUniqueSpaces(rotations, refSpace.getAxisSpins('x'));
|
||||
refSpace.rot90('z');
|
||||
refSpace.rot90('z');
|
||||
VoxelSpaceBoolean.pushNewUniqueSpaces(rotations, refSpace.getAxisSpins('x'));
|
||||
return rotations;
|
||||
}
|
||||
|
||||
getAllRotations() {
|
||||
const rotations: VoxelSpaceBoolean[] = [];
|
||||
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;
|
||||
}
|
||||
|
||||
protected static pushNewUniqueSpaces(existingSpaces: VoxelSpaceBoolean[], newSpaces: VoxelSpaceBoolean[]) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getAllPositionsInPrism(cubeDimX: number, cubeDimY: number, cubeDimZ: number): VoxelSpaceBoolean[] {
|
||||
const cubePositions: VoxelSpaceBoolean[] = [];
|
||||
if (this.dims[0] > cubeDimX || this.dims[1] > cubeDimY || this.dims[2] > cubeDimZ) {
|
||||
return cubePositions;
|
||||
}
|
||||
for (let xOffset = 0; xOffset < (cubeDimX - this.dims[0] + 1); xOffset++) {
|
||||
for (let yOffset = 0; yOffset < (cubeDimY - this.dims[1] + 1); yOffset++) {
|
||||
for (let zOffset = 0; zOffset < (cubeDimZ - this.dims[2] + 1); zOffset++) {
|
||||
const cubePos = new VoxelSpaceBoolean({id: this.id, dims: [cubeDimX, cubeDimY, cubeDimZ], color: this.color, cullEmpty: false});
|
||||
this.forEachCell((val, x, y, z) => {
|
||||
cubePos.set(xOffset + x, yOffset + y, zOffset + z, val);
|
||||
});
|
||||
cubePositions.push(cubePos);
|
||||
}
|
||||
}
|
||||
}
|
||||
return cubePositions;
|
||||
}
|
||||
|
||||
matches(space: VoxelSpaceBoolean) {
|
||||
const otherDims = space.getDims();
|
||||
for (let i = 0; i < this.dims.length; i++) {
|
||||
if (otherDims[i] !== this.dims[i]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
const otherRaw = space.getRaw();
|
||||
if (typeof otherRaw === "bigint") {
|
||||
return space.binaryRep() === this.binaryRep();
|
||||
}
|
||||
return this.space.reduce((prev, unit, i) => (unit === otherRaw[i]) && prev, true);
|
||||
}
|
||||
|
||||
clone() {
|
||||
return new VoxelSpaceBoolean({id: this.id, dims: this.getDims(), space: this.getRaw(), color: this.getColor(), cullEmpty: false});
|
||||
}
|
||||
|
||||
private getAxisSpins(axis: 'x' | 'y' | 'z'): VoxelSpaceBoolean[] {
|
||||
const rotations = [this.clone()];
|
||||
for (let i = 0; i < 3; i++) {
|
||||
rotations.push(rotations[i].rotated90(axis));
|
||||
}
|
||||
return rotations;
|
||||
}
|
||||
|
||||
getDims(): DimensionDef {
|
||||
return this.dims.slice() as DimensionDef;
|
||||
}
|
||||
|
||||
getRaw() {
|
||||
return this.space.slice();
|
||||
}
|
||||
|
||||
// [1, 0, 0] [x] [ x]
|
||||
// [0, 0, -1] * [y] = [-z]
|
||||
// [0, 1, 0] [z] [ y]
|
||||
private newIndexRotX(x: number, y: number, z: number) {
|
||||
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]
|
||||
private newIndexRotY(x: number, y: number, z: number) {
|
||||
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]
|
||||
private newIndexRotZ(x: number, y: number, z: number) {
|
||||
return this.dims[0] * this.dims[2] * (this.dims[1] - 1 - y) + this.dims[2] * x + z;
|
||||
}
|
||||
|
||||
at(x: number, y: number, z: number) {
|
||||
return this.space[this.index(x, y, z)];
|
||||
}
|
||||
|
||||
private index(x: number, y: number, z: number) {
|
||||
return this.dims[1] * this.dims[2] * x + this.dims[2] * y + z;
|
||||
}
|
||||
|
||||
toggle(x: number, y: number, z: number) {
|
||||
const index = this.index(x, y, z);
|
||||
this.space[index] = !this.space[index];
|
||||
}
|
||||
|
||||
set(x: number, y: number, z: number, val: boolean) {
|
||||
this.space[this.index(x, y, z)] = val;
|
||||
}
|
||||
|
||||
rotated90(dim: 'x' | 'y' | 'z') {
|
||||
const newSpace = new Array<boolean>(this.dims[0] * this.dims[1] * this.dims[2]);
|
||||
newSpace.fill(false);
|
||||
let newDims: DimensionDef;
|
||||
let rotIndex: (i: number, j: number, k: number) => number;
|
||||
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[rotIndex(i, j, k)] = true;
|
||||
}
|
||||
})
|
||||
return new VoxelSpaceBoolean({id: this.id, dims: newDims, space: newSpace, color: this.color, cullEmpty: false});
|
||||
}
|
||||
|
||||
rot90(dim: 'x' | 'y' | 'z') {
|
||||
const rot = this.rotated90(dim);
|
||||
this.space = rot.getRaw();
|
||||
this.dims = rot.getDims();
|
||||
}
|
||||
|
||||
plus(space: VoxelSpaceBoolean): VoxelSpaceBoolean | null {
|
||||
const newSpace = new Array<boolean>(this.dims[0] * this.dims[1] * this.dims[2]);
|
||||
newSpace.fill(false);
|
||||
let clash = false;
|
||||
space.forEachCell((val, x, y, z) => {
|
||||
if (this.at(x, y, z) !== val) {
|
||||
newSpace[this.index(x, y, z)] = true;
|
||||
} else {
|
||||
if (val) {
|
||||
clash = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
if (!clash) {
|
||||
return new VoxelSpaceBoolean({id: this.id, dims: this.getDims(), space: newSpace, color: this.color, cullEmpty: false});
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
size() {
|
||||
let size = 0;
|
||||
this.forEachCell((val) => {
|
||||
if (val) {
|
||||
size++;
|
||||
}
|
||||
});
|
||||
return size;
|
||||
}
|
||||
|
||||
getDirectNeighbourProfile(x: number, y: number, z: number): number {
|
||||
let result = 0;
|
||||
if (x < this.dims[0] - 1 && this.at(x + 1, y, z)) {
|
||||
result += 1;
|
||||
}
|
||||
if (y < this.dims[1] - 1 && this.at(x, y + 1, z)) {
|
||||
result += 2;
|
||||
}
|
||||
if (z < this.dims[2] - 1 && this.at(x, y, z + 1)) {
|
||||
result += 4;
|
||||
}
|
||||
if (x > 0 && this.at(x - 1, y, z)) {
|
||||
result += 8;
|
||||
}
|
||||
if (y > 0 && this.at(x, y - 1, z)) {
|
||||
result += 16;
|
||||
}
|
||||
if (z > 0 && this.at(x, y, z - 1)) {
|
||||
result += 32;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
getAllPermutationsInPrism(prismDimX: number, prismDimY: number, prismDimZ: number): VoxelSpaceBoolean[] {
|
||||
const rotations = this.getUniqueRotations();
|
||||
let result = new Array<VoxelSpaceBoolean>();
|
||||
for (let i = 0; i < rotations.length; i++) {
|
||||
result = result.concat(rotations[i].getAllPositionsInPrism(prismDimX, prismDimY, prismDimZ));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
16
src/solver/js/main.js
Normal file
16
src/solver/js/main.js
Normal file
@@ -0,0 +1,16 @@
|
||||
import SomaSolver from "./SomaSolver";
|
||||
import VoxelSpaceBoolean from "./VoxelSpaceBoolean";
|
||||
export function solve(polycubes, dimX, dimY, dimZ) {
|
||||
const solver = new SomaSolver(dimX, dimY, dimZ);
|
||||
const voxelSpaces = new Array();
|
||||
for (let i = 0; i < polycubes.length; i++) {
|
||||
voxelSpaces.push(new VoxelSpaceBoolean({
|
||||
id: i,
|
||||
dims: [dimX, dimY, dimZ],
|
||||
space: polycubes[i],
|
||||
cullEmpty: true
|
||||
}));
|
||||
}
|
||||
solver.solve(voxelSpaces);
|
||||
return solver.getSolutions();
|
||||
}
|
||||
18
src/solver/js/main.ts
Normal file
18
src/solver/js/main.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import SomaSolver from "./SomaSolver";
|
||||
import VoxelSpaceBoolean from "./VoxelSpaceBoolean";
|
||||
import type SomaSolution from "./SomaSolution";
|
||||
|
||||
export function solve(polycubes: bigint[], dimX: number, dimY: number, dimZ: number): SomaSolution[] {
|
||||
const solver = new SomaSolver(dimX, dimY, dimZ);
|
||||
const voxelSpaces = new Array<VoxelSpaceBoolean>();
|
||||
for (let i = 0; i < polycubes.length; i++) {
|
||||
voxelSpaces.push(new VoxelSpaceBoolean({
|
||||
id: i,
|
||||
dims: [dimX, dimY, dimZ],
|
||||
space: polycubes[i],
|
||||
cullEmpty: true
|
||||
}));
|
||||
}
|
||||
solver.solve(voxelSpaces);
|
||||
return solver.getSolutions();
|
||||
}
|
||||
11
src/solver/js/package.json
Normal file
11
src/solver/js/package.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"name": "soma-solve-js",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "index.ts",
|
||||
"scripts": {
|
||||
"build": "npx tsc main.ts --target ES2020 --moduleResolution node"
|
||||
},
|
||||
"author": "Daniel Ledda",
|
||||
"license": "ISC"
|
||||
}
|
||||
Reference in New Issue
Block a user