web page fully working as desired in dev

This commit is contained in:
Adam Piontek 2022-09-13 20:05:14 -04:00
parent 242243b246
commit 1ae0c4237c
10 changed files with 263 additions and 63 deletions

View File

@ -1,13 +0,0 @@
module.exports = {
parserOptions: {
sourceType: "module",
},
env: {
browser: true,
es2022: true,
mocha: true,
node: true,
},
plugins: ["html"],
extends: ["eslint:recommended", "prettier"],
};

13
.eslintrc.json Normal file
View File

@ -0,0 +1,13 @@
{
"extends": ["eslint:recommended", "prettier"],
"parserOptions": {
"sourceType": "module"
},
"plugins": ["html"],
"env": {
"browser": true,
"es2022": true,
"mocha": true,
"node": true
}
}

View File

@ -50,26 +50,37 @@
<form
id="cardsInputForm"
x-data="cardsInputForm"
@submit.prevent="submit"
@submit.prevent="$dispatch('solve-game')"
class="needs-validation"
:class="{'was-validated': isFormValid && inputValue.length > 2 }"
novalidate
>
<div class="mb-3">
<label id="cardsInputLabel" for="cardsInput" class="form-label"
>Enter Your Cards</label
>
<h3 id="cardsInputLabel" for="cardsInput" class="form-label">
Enter Your Cards
</h3>
<div class="input-group">
<input
id="cardsInput"
aria-describedby="cardsInputLabel"
type="text"
x-model="inputValue"
class="form-control"
:class="{ 'is-valid': isFormValid && inputValue.length > 2, 'is-invalid': !isFormValid && inputValue.length > 2 }"
maxlength="206"
@keyup.debounce="validateCardsInput()"
placeholder="e.g., 2S TC 4S QD KH 5S 9S ..."
:disabled="$store.global.solvingInProgress"
/>
<button
id="cardsInputButton"
type="submit"
class="btn btn-outline-primary"
:disabled="!isFormValid || $store.global.solvingInProgress"
>
Solve
</button>
</div>
<div class="mb-3">
<template x-for="msg in validMessages">
<div
class="valid-feedback"
@ -88,7 +99,11 @@
</form>
<!-- Friendly display of game cards -->
<div id="playingCardsPreview" x-data="playingCardsPreview">
<div
id="playingCardsPreview"
x-data="playingCardsPreview"
class="mb-4"
>
<div
id="gamePyramid"
class="game-pyramid-container p-2 p-md-3 p-lg-4"
@ -139,6 +154,32 @@
</template>
</div>
</div>
<!-- Display of solvingprogress or solution -->
<div id="gameSolving" x-data="gameSolving" x-init="onInit()">
<div class="d-flex justify-content-start">
<div
class="spinner-border me-2"
role="status"
x-show="$store.global.solvingInProgress"
></div>
<h3 @solve-game.window="startSolver()" x-text="headerText"></h3>
</div>
<ul x-show="$store.global.solvingInProgress">
<template x-for="msg in statusMessages">
<li x-text="msg"></li>
</template>
</ul>
<!-- <p x-show="" x-text="statusMessage"></p> -->
<ol x-show="solutionMoves.length > 0">
<template x-for="move in solutionMoves">
<li x-text="move"></li>
</template>
</ol>
</div>
</div>
</div>
</div>

104
main.js
View File

@ -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();

56
package-lock.json generated
View File

@ -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",

View File

@ -9,7 +9,7 @@
"solver",
"cards"
],
"main": "solver.js",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",

39
run.js Normal file
View File

@ -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`);

View File

@ -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 };

12
solverWorker.js Normal file
View File

@ -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);
}
});

13
test.js
View File

@ -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);
});
});