-
+
+
-
+
+
+
diff --git a/main.js b/main.js
index 4cfa21c..d7d7545 100644
--- a/main.js
+++ b/main.js
@@ -1,6 +1,8 @@
import "./style.scss";
+//import 'bootstrap';
import Alpine from "alpinejs";
import cardSvgs from "./cardSvgs";
+import SolverWorker from "./solverWorker?worker";
// Some helpful constants
const suits = {
@@ -61,9 +63,10 @@ Object.keys(cardSvgs).forEach((ckey) => {
Alpine.store("global", {
deck,
cardsToSolve: Array(deck.length).fill(0),
+ solvingInProgress: false,
});
-// card preview component data
+// card preview component logic
Alpine.data("playingCardsPreview", () => ({
cardSvgs,
cardsBySlice(start, length) {
@@ -74,7 +77,7 @@ Alpine.data("playingCardsPreview", () => ({
},
}));
-// input component data
+// input component logic
Alpine.data("cardsInputForm", () => ({
// "constants" for validation etc
nonAlphaNumRegEx: /[\W_]+/g,
@@ -184,6 +187,103 @@ Alpine.data("cardsInputForm", () => ({
},
}));
+// long-running solve messages
+const encouragements = [
+ "Hang in there!",
+ "Let go like a bird flies, not fighting the wind but gliding on it",
+ "Stay patient and trust the journey.",
+ "Everything is coming together…",
+ "Solitaire is a journey, not a destination.",
+ "For things to reveal themselves to us, we need to be ready to abandon our views about them.",
+ "Patience is bitter, but its fruit is sweet.",
+ "Strive for progress, not perfection.",
+ "I wish only to be alive and to experience this living to the fullest.",
+ "This too shall pass.",
+ "Patience is the companion of wisdom.",
+ "The mountains are calling and I must go.",
+ "Give time time.",
+ "If you find a path with no obstacles, it probably doesn't lead anywhere",
+ "A smooth sea never made a good sailor.",
+ "Stick with the winners.",
+ "I immerse myself in the experience of living without having to evaluate or understand it.",
+ "Why fit in when you were born to stand out?",
+ "Don't let yesterday take up too much of today.",
+ "The least I owe the mountains is a body.",
+ "Getting so close!",
+ "Many people think excitement is happiness. But when you are excited you are not peaceful.",
+ "Misery is optional.",
+ "Mistakes are proof that you're trying.",
+ "Life would be so much easier if we only had the source code.",
+ "A computer once beat me at chess, but it was no match for me at kickboxing.",
+ "Patience is not simply the ability to wait, it's how we behave while we're waiting.",
+ "We must let go of the life we have planned so as to accept the one that is waiting for us.",
+ "Somewhere, something incredible is waiting to be known.",
+ "If you spend your whole life waiting for the storm, you'll never enjoy the sunshine.",
+];
+
+// game solving component logic
+Alpine.data("gameSolving", () => ({
+ encouragements,
+ solverWorker: null,
+ headerText: "",
+ moveCount: 23,
+ statusMessages: [],
+ solutionMoves: [],
+ nodesTried: 0,
+ nodesTriedFloor: 0,
+ reset() {
+ this.$store.global.solvingInProgress = false;
+ this.moveCount = 0;
+ this.statusMessages = [];
+ this.nodesTried = 0;
+ this.nodesTriedFloor = 0;
+ },
+ onInit() {
+ this.solverWorker = new SolverWorker();
+ this.solverWorker.addEventListener("message", async (e) => {
+ if (e.data.msg === "solve-progress") {
+ this.nodesTried++;
+ this.moveCount = e.data.moveCount;
+ this.statusMessages[0] = `Most moves found so far: ${this.moveCount}`;
+
+ let newFloor = Math.floor(this.nodesTried / 10000) * 10000;
+ if (newFloor > this.nodesTriedFloor) {
+ this.nodesTriedFloor = newFloor;
+ if (this.nodesTriedFloor > 50000) {
+ this.statusMessages[1] = `Over ${this.nodesTriedFloor.toLocaleString(
+ "en"
+ )} possibilities tried. Still working…`;
+ }
+ if (this.nodesTriedFloor % 250000 === 0) {
+ let randInRange = Math.floor(
+ Math.random() * this.encouragements.length
+ );
+ this.statusMessages.push(this.encouragements[randInRange]);
+ }
+ }
+ } else if (e.data.msg === "solve-result") {
+ if (e.data.result[0]) {
+ this.headerText = "Solution found:";
+ this.solutionMoves = e.data.result[1];
+ this.reset();
+ } else {
+ this.headerText = "Could not solve. Best moves found:";
+ this.solutionMoves = e.data.result[2];
+ this.reset();
+ }
+ }
+ });
+ },
+ async startSolver() {
+ this.headerText = "Solving…";
+ this.solutionMoves = [];
+ this.$store.global.solvingInProgress = true;
+ await this.$nextTick();
+ let game = JSON.parse(JSON.stringify(this.$store.global.cardsToSolve));
+ this.solverWorker.postMessage({ msg: "try-to-solve", game: game });
+ },
+}));
+
window.Alpine = Alpine;
Alpine.start();
diff --git a/package-lock.json b/package-lock.json
index 5f14bc7..1034970 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -37,9 +37,9 @@
}
},
"node_modules/@eslint/eslintrc": {
- "version": "1.3.1",
- "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.3.1.tgz",
- "integrity": "sha512-OhSY22oQQdw3zgPOOwdoj01l/Dzl1Z+xyUP33tkSN+aqyEhymJCcPHyXt+ylW8FSe0TfRC2VG+ROQOapD0aZSQ==",
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.3.2.tgz",
+ "integrity": "sha512-AXYd23w1S/bv3fTs3Lz0vjiYemS08jWkI3hYyS9I1ry+0f+Yjs1wm+sU0BS8qDOPrBIkp4qHYC16I8uVtpLajQ==",
"dev": true,
"dependencies": {
"ajv": "^6.12.4",
@@ -1031,12 +1031,12 @@
}
},
"node_modules/eslint": {
- "version": "8.23.0",
- "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.23.0.tgz",
- "integrity": "sha512-pBG/XOn0MsJcKcTRLr27S5HpzQo4kLr+HjLQIyK4EiCsijDl/TB+h5uEuJU6bQ8Edvwz1XWOjpaP2qgnXGpTcA==",
+ "version": "8.23.1",
+ "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.23.1.tgz",
+ "integrity": "sha512-w7C1IXCc6fNqjpuYd0yPlcTKKmHlHHktRkzmBPZ+7cvNBQuiNjx0xaMTjAJGCafJhQkrFJooREv0CtrVzmHwqg==",
"dev": true,
"dependencies": {
- "@eslint/eslintrc": "^1.3.1",
+ "@eslint/eslintrc": "^1.3.2",
"@humanwhocodes/config-array": "^0.10.4",
"@humanwhocodes/gitignore-to-minimatch": "^1.0.2",
"@humanwhocodes/module-importer": "^1.0.1",
@@ -1055,7 +1055,6 @@
"fast-deep-equal": "^3.1.3",
"file-entry-cache": "^6.0.1",
"find-up": "^5.0.0",
- "functional-red-black-tree": "^1.0.1",
"glob-parent": "^6.0.1",
"globals": "^13.15.0",
"globby": "^11.1.0",
@@ -1064,6 +1063,7 @@
"import-fresh": "^3.0.0",
"imurmurhash": "^0.1.4",
"is-glob": "^4.0.0",
+ "js-sdsl": "^4.1.4",
"js-yaml": "^4.1.0",
"json-stable-stringify-without-jsonify": "^1.0.1",
"levn": "^0.4.1",
@@ -1386,12 +1386,6 @@
"integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==",
"dev": true
},
- "node_modules/functional-red-black-tree": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz",
- "integrity": "sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g==",
- "dev": true
- },
"node_modules/get-caller-file": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
@@ -1691,6 +1685,12 @@
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
"dev": true
},
+ "node_modules/js-sdsl": {
+ "version": "4.1.4",
+ "resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.1.4.tgz",
+ "integrity": "sha512-Y2/yD55y5jteOAmY50JbUZYwk3CP3wnLPEZnlR1w9oKhITrBEtAxwuWKebFf8hMrPMgbYwFoWK/lH2sBkErELw==",
+ "dev": true
+ },
"node_modules/js-yaml": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
@@ -2589,9 +2589,9 @@
"optional": true
},
"@eslint/eslintrc": {
- "version": "1.3.1",
- "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.3.1.tgz",
- "integrity": "sha512-OhSY22oQQdw3zgPOOwdoj01l/Dzl1Z+xyUP33tkSN+aqyEhymJCcPHyXt+ylW8FSe0TfRC2VG+ROQOapD0aZSQ==",
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.3.2.tgz",
+ "integrity": "sha512-AXYd23w1S/bv3fTs3Lz0vjiYemS08jWkI3hYyS9I1ry+0f+Yjs1wm+sU0BS8qDOPrBIkp4qHYC16I8uVtpLajQ==",
"dev": true,
"requires": {
"ajv": "^6.12.4",
@@ -3226,12 +3226,12 @@
"dev": true
},
"eslint": {
- "version": "8.23.0",
- "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.23.0.tgz",
- "integrity": "sha512-pBG/XOn0MsJcKcTRLr27S5HpzQo4kLr+HjLQIyK4EiCsijDl/TB+h5uEuJU6bQ8Edvwz1XWOjpaP2qgnXGpTcA==",
+ "version": "8.23.1",
+ "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.23.1.tgz",
+ "integrity": "sha512-w7C1IXCc6fNqjpuYd0yPlcTKKmHlHHktRkzmBPZ+7cvNBQuiNjx0xaMTjAJGCafJhQkrFJooREv0CtrVzmHwqg==",
"dev": true,
"requires": {
- "@eslint/eslintrc": "^1.3.1",
+ "@eslint/eslintrc": "^1.3.2",
"@humanwhocodes/config-array": "^0.10.4",
"@humanwhocodes/gitignore-to-minimatch": "^1.0.2",
"@humanwhocodes/module-importer": "^1.0.1",
@@ -3250,7 +3250,6 @@
"fast-deep-equal": "^3.1.3",
"file-entry-cache": "^6.0.1",
"find-up": "^5.0.0",
- "functional-red-black-tree": "^1.0.1",
"glob-parent": "^6.0.1",
"globals": "^13.15.0",
"globby": "^11.1.0",
@@ -3259,6 +3258,7 @@
"import-fresh": "^3.0.0",
"imurmurhash": "^0.1.4",
"is-glob": "^4.0.0",
+ "js-sdsl": "^4.1.4",
"js-yaml": "^4.1.0",
"json-stable-stringify-without-jsonify": "^1.0.1",
"levn": "^0.4.1",
@@ -3501,12 +3501,6 @@
"integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==",
"dev": true
},
- "functional-red-black-tree": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz",
- "integrity": "sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g==",
- "dev": true
- },
"get-caller-file": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
@@ -3726,6 +3720,12 @@
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
"dev": true
},
+ "js-sdsl": {
+ "version": "4.1.4",
+ "resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.1.4.tgz",
+ "integrity": "sha512-Y2/yD55y5jteOAmY50JbUZYwk3CP3wnLPEZnlR1w9oKhITrBEtAxwuWKebFf8hMrPMgbYwFoWK/lH2sBkErELw==",
+ "dev": true
+ },
"js-yaml": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
diff --git a/package.json b/package.json
index 83217c6..1f82bb8 100644
--- a/package.json
+++ b/package.json
@@ -9,7 +9,7 @@
"solver",
"cards"
],
- "main": "solver.js",
+ "type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
diff --git a/run.js b/run.js
new file mode 100644
index 0000000..2fb3f27
--- /dev/null
+++ b/run.js
@@ -0,0 +1,39 @@
+const solver = require("./solver");
+
+// // Easy MS Tripeaks level
+// let userCardsInput = " js as ts ad tc qd 9s 4s 2h 9h 8s jh 6c 3d ks 5s 5c 6h 9c 2c ac 8c 6d 5d th 8d kc kd 9d 4c 5h 8h qh 6s "
+// userCardsInput = " 2d 7c 7d 3s kh qs jc 2s 7s " + userCardsInput
+// userCardsInput = " qc 3h 3c 7h td 4h " + userCardsInput
+// userCardsInput = " jd 4d ah " + userCardsInput
+// Unsolvable board
+let userCardsInput =
+ "2D 6D AD 9S 4C 7C 7S 7D 9C 2S AC 8D 6S 6H 3C 5H QS JS 4S JH 5C AS 3H 3S AH TD 4D 5S TH 7H KS QH 6C KD 8S 2C TC JC 5D 3D 2H TS 4H JD KC KH 8H QC 8C QD 9D 9H";
+
+// continue!
+userCardsInput = userCardsInput.toUpperCase();
+let userCards = userCardsInput.split(" ").filter((s) => s);
+userCards = Array(52 - userCards.length)
+ .fill("0")
+ .concat(userCards)
+ .map((c) => (c === "0" ? 0 : c));
+
+let start_time = process.hrtime.bigint();
+let result = solver.solve(
+ userCards.slice(0, 28),
+ userCards.slice(28, 52),
+ 0,
+ []
+);
+let end_time = process.hrtime.bigint();
+
+let resultStr = JSON.stringify(result, null, 2);
+process.stdout.write(resultStr + "\n");
+process.stdout.write(`moves array length: ${result[1].length}\n`);
+process.stdout.write(`best moves array length: ${result[2].length}\n`);
+
+const MS_PER_NS = 1e-6;
+const NS_PER_SEC = 1e9;
+let elapsedMs = Number(end_time - start_time) * MS_PER_NS;
+let elapsedS = Number(end_time - start_time) / NS_PER_SEC;
+process.stdout.write(`solving took: ${elapsedMs} milliseconds\n`);
+process.stdout.write(`solving took: ${elapsedS} seconds\n`);
diff --git a/solver.js b/solver.js
index d1eb777..3306929 100644
--- a/solver.js
+++ b/solver.js
@@ -103,11 +103,12 @@ function getBestMoveArray(bestMoveArray, newMoveArray) {
* @param stockArray The cards in the stock.
* @param stockIndex The index of the top stock card.
* @param moveArray The list of moves that have been made to get a deck in this configuration.
- * @returns {*[]|([*, *, *]|[*, *, *]|[*, *, *])}
+ * @returns {Promise<{*[]|([*, *, *]|[*, *, *]|[*, *, *])}>}
*/
-function solve(
+async function solve(
pyramidArray,
stockArray,
+ worker = null,
stockIndex = 0,
moveArray = [],
bestMoveArray = []
@@ -121,6 +122,12 @@ function solve(
// If yes, replace the known best move array
newBestMoveArray = getBestMoveArray(newBestMoveArray, newMoveArray);
newBestMoveArray = JSON.parse(JSON.stringify(newBestMoveArray));
+ if (worker) {
+ worker.postMessage({
+ msg: "solve-progress",
+ moveCount: newBestMoveArray.length,
+ });
+ }
// We cleared the pyramid
if (pyramid.isCleared) {
@@ -150,9 +157,10 @@ function solve(
let newPyramidArray = JSON.parse(JSON.stringify(pyramidArray));
newPyramidArray[freeCardsIndices[i]] = 0;
- let result = solve(
+ let result = await solve(
newPyramidArray,
newStock,
+ worker,
stockIndex,
newMoveArray,
newBestMoveArray
@@ -167,9 +175,10 @@ function solve(
// Flip over a new card
newMoveArray = JSON.parse(JSON.stringify(moveArray));
newMoveArray.push(MoveString.flipStock());
- let result = solve(
+ let result = await solve(
pyramidArray,
stockArray,
+ worker,
++stockIndex,
newMoveArray,
newBestMoveArray
@@ -184,4 +193,4 @@ function solve(
return [GameStates.lost, moveArray, newBestMoveArray];
}
-module.exports = { Card, solve };
+export { Card, solve };
diff --git a/solverWorker.js b/solverWorker.js
new file mode 100644
index 0000000..3249c3a
--- /dev/null
+++ b/solverWorker.js
@@ -0,0 +1,12 @@
+import { solve } from "./solver";
+
+const runSolve = async (game) => {
+ let result = await solve(game.slice(0, 28), game.slice(28, 52), self);
+ postMessage({ msg: "solve-result", result: result });
+};
+
+addEventListener("message", async (e) => {
+ if (e.data.msg === "try-to-solve") {
+ runSolve(e.data.game);
+ }
+});
diff --git a/test.js b/test.js
index de769dc..a3045d7 100644
--- a/test.js
+++ b/test.js
@@ -1,6 +1,5 @@
-const assert = require("assert");
-const solver = require("./solver");
-const Card = solver.Card;
+import { Card, solve } from "./solver.js";
+import assert from "assert";
/*
* Tests for classes
@@ -56,17 +55,17 @@ const partial_games = [
];
it("solve should solve known games", () => {
- solvable_games.forEach((i) => {
+ solvable_games.forEach(async (i) => {
const array = i.split(" ");
- const result = solver.solve(array.slice(0, 28), array.slice(28, 52), 0, []);
+ const result = await solve(array.slice(0, 28), array.slice(28, 52));
assert.equal(result[0], true);
});
}).timeout(200000);
it("solve should solve partial games", () => {
- partial_games.forEach((i) => {
+ partial_games.forEach(async (i) => {
const array = i.split(" ").map((x) => (x === "0" ? 0 : x));
- const result = solver.solve(array.slice(0, 28), array.slice(28, 52), 0, []);
+ const result = await solve(array.slice(0, 28), array.slice(28, 52));
assert.equal(result[0], true);
});
});