several layout & appearance changes
This commit is contained in:
parent
b0c1c4c648
commit
f3e1875a1b
15 changed files with 330 additions and 271 deletions
18
README.md
18
README.md
|
@ -1,19 +1,15 @@
|
|||
# javascript-tri-peaks-solitaire-solver
|
||||
# Tripeaks Solitaire Solver 73k
|
||||
|
||||
A brute force solver for Microsoft Tri-Peaks solitaire written in javascript.
|
||||
A brute force solver for Microsoft Tripeaks solitaire written in javascript.
|
||||
|
||||
This is a fork of [Courtney Pitcher's project](https://github.com/IgniparousTempest/javascript-tri-peaks-solitaire-solver), with several changes.
|
||||
The `dist` folder contains an HTML+JS interface for entering cards and getting solutions. You can open it directly in a browser to try it out.
|
||||
|
||||
## Changes
|
||||
The solver can provide a "best moves" list (the first set of moves found that removes the most cards from the board) for unsolvable games, and games-in-progress (when you don't already know all the cards).
|
||||
|
||||
- fixed card matching, implemented card matching tests
|
||||
- solver now returns a "first best set of moves found" for unsolvable games (the first set of moves found that removes the most cards from the board)
|
||||
This began as a fork of [Courtney Pitcher's project](https://github.com/IgniparousTempest/javascript-tri-peaks-solitaire-solver), and I'm grateful for the solving algorithm & inspiration.
|
||||
|
||||
## Notes
|
||||
|
||||
- Unsolvable games can take 6 or more minutes to solve, so be patient.
|
||||
- Per Courtney Pitcher, "This is probably quite a poor implementation." Please don't fault either of us, he was teaching himself javascript, and I'm probably even less qualified...
|
||||
|
||||
## TODO
|
||||
|
||||
- [ ] HTML ui/demo _in progress_
|
||||
- Per Courtney Pitcher, "This is probably quite a poor implementation." Please don't fault either of us, he was teaching himself javascript, and I'm just having fun.
|
||||
- The HTML+JS interface is built with [Vite](https://vitejs.dev/), [Alpine.js](https://alpinejs.dev/), [Bootstrap](https://getbootstrap.com/), and some [free customized SVG playing cards](https://www.me.uk/cards/). Linting & formatting is included.
|
||||
|
|
64
dist/assets/index.432054e5.js
vendored
Normal file
64
dist/assets/index.432054e5.js
vendored
Normal file
File diff suppressed because one or more lines are too long
6
dist/assets/index.58a4d054.css
vendored
Normal file
6
dist/assets/index.58a4d054.css
vendored
Normal file
File diff suppressed because one or more lines are too long
1
dist/assets/solverWorker.25a6ccd7.js
vendored
Normal file
1
dist/assets/solverWorker.25a6ccd7.js
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
(function(){"use strict";class O{constructor(t){this.rank=t[0],this.suit=t[1]}get integerValue(){return"A23456789TJQK".indexOf(this.rank)}isSequential(t){return(this.integerValue+13+1)%13===t.integerValue||(this.integerValue+13-1)%13===t.integerValue}get toString(){return this.rank+this.suit}}class v{constructor(t){this.array=t}get isCleared(){return this.array.every(t=>t===0)}get freeCardIndices(){let t=[];for(let e=this.array.length-1;e>=0;e--){if(this.array[e]===0)continue;const n=Math.floor((e-3)/2);(e>=18||e<=17&&e>=9&&this.array[e+9]===0&&this.array[e+10]===0||e<=8&&e>=3&&this.array[e+6+n]===0&&this.array[e+7+n]===0||e<=2&&e>=0&&this.array[e+3+e]===0&&this.array[e+4+e]===0)&&t.push(e)}return t}}class o{static gameWon(){return"You have won."}static gameLost(){return"There are no more valid moves."}static match(t){return"Move "+t+" onto the stock."}static flipStock(){return"Draw a new stock card."}}const l=Object.freeze({won:!0,lost:!1});function g(r,t){let e=r.filter(a=>a.startsWith("Move")).length;return t.filter(a=>a.startsWith("Move")).length>e?t:r}async function h(r,t,e=null,n=0,a=[],d=[]){let i=JSON.parse(JSON.stringify(a)),s=JSON.parse(JSON.stringify(d)),J=new v(r);if(s=g(s,i),s=JSON.parse(JSON.stringify(s)),e&&e.postMessage({msg:"solve-progress",moveCount:s.length}),J.isCleared)return i.push(o.gameWon()),[l.won,i,[]];if(n>=t.length)return i.push(o.gameLost()),[l.lost,i,s];const m=new O(t[n]);let f=J.freeCardIndices;for(let u=0;u<f.length;u++){let S=new O(r[f[u]]);if(!S.isSequential(m))continue;let N=JSON.parse(JSON.stringify(t));N.splice(++n,0,S.toString),i=JSON.parse(JSON.stringify(a)),i.push(o.match(S.toString));let y=JSON.parse(JSON.stringify(r));y[f[u]]=0;let p=await h(y,N,e,n,i,s);if(p[0]===l.won)return p;s=g(s,p[2]),s=JSON.parse(JSON.stringify(s))}i=JSON.parse(JSON.stringify(a)),i.push(o.flipStock());let c=await h(r,t,e,++n,i,s);return c[0]===l.won?c:(s=g(s,c[2]),s=JSON.parse(JSON.stringify(s)),[l.lost,a,s])}const w=async r=>{let t=await h(r.slice(0,28),r.slice(28,52),self);postMessage({msg:"solve-result",result:t})};addEventListener("message",async r=>{r.data.msg==="try-to-solve"&&w(r.data.game)})})();
|
1
dist/index.html
vendored
Normal file
1
dist/index.html
vendored
Normal file
File diff suppressed because one or more lines are too long
4
package-lock.json
generated
4
package-lock.json
generated
|
@ -1,11 +1,11 @@
|
|||
{
|
||||
"name": "javascript-tri-peaks-solitaire-solver",
|
||||
"name": "tripeaks-solitaire-solver-js-73k",
|
||||
"version": "1.0.1",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "javascript-tri-peaks-solitaire-solver",
|
||||
"name": "tripeaks-solitaire-solver-js-73k",
|
||||
"version": "1.0.1",
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
|
|
39
run.js
39
run.js
|
@ -1,39 +0,0 @@
|
|||
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`);
|
|
@ -101,8 +101,10 @@ function getBestMoveArray(bestMoveArray, newMoveArray) {
|
|||
* Solves a Tri Peaks solitaire game.
|
||||
* @param pyramidArray The cards in the pyramids, starting in the top-left peak. The cards are in left-to-right, then top-to-bottom order.
|
||||
* @param stockArray The cards in the stock.
|
||||
* @param worker Optional Web Worker for reporting progress.
|
||||
* @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.
|
||||
* @param bestMoveArray The best list of moves that have been found so far.
|
||||
* @returns {Promise<{*[]|([*, *, *]|[*, *, *]|[*, *, *])}>}
|
||||
*/
|
||||
async function solve(
|
||||
|
|
|
@ -1,2 +1,2 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="card" face="2B" height="3.5in" preserveAspectRatio="none" viewBox="-120 -168 240 336" width="2.5in"><defs><pattern id="B2" width="6" height="6" patternUnits="userSpaceOnUse"><path d="M3 0L6 3L3 6L0 3Z" fill="#d40000"></path></pattern></defs><rect width="239" height="335" x="-119.5" y="-167.5" rx="12" ry="12" fill="#e4dccd" stroke="black"></rect><rect fill="url(#B2)" width="216" height="312" x="-108" y="-156" rx="12" ry="12"></rect></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="card" face="2B" height="3.5in" preserveAspectRatio="none" viewBox="-120 -168 240 336" width="2.5in"><defs><pattern id="B2" width="6" height="6" patternUnits="userSpaceOnUse"><path d="M3 0L6 3L3 6L0 3Z" fill="#001B50"></path></pattern></defs><rect width="239" height="335" x="-119.5" y="-167.5" rx="12" ry="12" fill="#e4dccd" stroke="black"></rect><rect fill="url(#B2)" width="216" height="312" x="-108" y="-156" rx="12" ry="12"></rect></svg>
|
Before Width: | Height: | Size: 543 B After Width: | Height: | Size: 543 B |
1
src/img/73k.svg
Normal file
1
src/img/73k.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 35 KiB |
3
src/img/bi-github.svg
Normal file
3
src/img/bi-github.svg
Normal file
|
@ -0,0 +1,3 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-github" viewBox="0 0 16 16">
|
||||
<path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.012 8.012 0 0 0 16 8c0-4.42-3.58-8-8-8z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 716 B |
3
src/img/bi-twitter.svg
Normal file
3
src/img/bi-twitter.svg
Normal file
|
@ -0,0 +1,3 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-twitter" viewBox="0 0 16 16">
|
||||
<path d="M5.026 15c6.038 0 9.341-5.003 9.341-9.334 0-.14 0-.282-.006-.422A6.685 6.685 0 0 0 16 3.542a6.658 6.658 0 0 1-1.889.518 3.301 3.301 0 0 0 1.447-1.817 6.533 6.533 0 0 1-2.087.793A3.286 3.286 0 0 0 7.875 6.03a9.325 9.325 0 0 1-6.767-3.429 3.289 3.289 0 0 0 1.018 4.382A3.323 3.323 0 0 1 .64 6.575v.045a3.288 3.288 0 0 0 2.632 3.218 3.203 3.203 0 0 1-.865.115 3.23 3.23 0 0 1-.614-.057 3.283 3.283 0 0 0 3.067 2.277A6.588 6.588 0 0 1 .78 13.58a6.32 6.32 0 0 1-.78-.045A9.344 9.344 0 0 0 5.026 15z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 640 B |
410
src/index.html
410
src/index.html
|
@ -1,69 +1,33 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<!-- Required meta tags -->
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
|
||||
<title>Tripeaks Solver 73k</title>
|
||||
</head>
|
||||
<body class="text-dark">
|
||||
<nav class="navbar navbar-expand navbar-light bg-light mb-4">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand text-dark" href="#">Tripeaks Solver 73k</a>
|
||||
<nav class="navbar navbar-expand navbar-dark bg-dark mb-4" x-data="navbar">
|
||||
<div class="container">
|
||||
<div class="navbar-brand d-flex flex-column">
|
||||
<span class="text-light fs-4">Tripeaks Solver 73k</span>
|
||||
<span class="text-white-50 fs-6">by Adam Piontek</span>
|
||||
</div>
|
||||
<ul class="navbar-nav ms-auto">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link fs-5" href="https://73k.us/"
|
||||
>73k
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
fill="currentColor"
|
||||
class="bi bi-box-arrow-up-right"
|
||||
viewBox="0 0 16 16"
|
||||
style="margin-top: -0.27em"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M8.636 3.5a.5.5 0 0 0-.5-.5H1.5A1.5 1.5 0 0 0 0 4.5v10A1.5 1.5 0 0 0 1.5 16h10a1.5 1.5 0 0 0 1.5-1.5V7.864a.5.5 0 0 0-1 0V14.5a.5.5 0 0 1-.5.5h-10a.5.5 0 0 1-.5-.5v-10a.5.5 0 0 1 .5-.5h6.636a.5.5 0 0 0 .5-.5z"
|
||||
/>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M16 .5a.5.5 0 0 0-.5-.5h-5a.5.5 0 0 0 0 1h3.793L6.146 9.146a.5.5 0 1 0 .708.708L15 1.707V5.5a.5.5 0 0 0 1 0v-5z"
|
||||
/></svg
|
||||
></a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a
|
||||
class="nav-link"
|
||||
href="https://github.com/apiontek/tripeaks-solitaire-solver-js-73k"
|
||||
><svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
fill="currentColor"
|
||||
class="bi bi-github"
|
||||
viewBox="0 0 16 16"
|
||||
>
|
||||
<path
|
||||
d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.012 8.012 0 0 0 16 8c0-4.42-3.58-8-8-8z"
|
||||
/></svg
|
||||
x-html="biGithub"
|
||||
></a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="https://twitter.com/adampiontek"
|
||||
><svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
fill="currentColor"
|
||||
class="bi bi-twitter"
|
||||
viewBox="0 0 16 16"
|
||||
>
|
||||
<path
|
||||
d="M5.026 15c6.038 0 9.341-5.003 9.341-9.334 0-.14 0-.282-.006-.422A6.685 6.685 0 0 0 16 3.542a6.658 6.658 0 0 1-1.889.518 3.301 3.301 0 0 0 1.447-1.817 6.533 6.533 0 0 1-2.087.793A3.286 3.286 0 0 0 7.875 6.03a9.325 9.325 0 0 1-6.767-3.429 3.289 3.289 0 0 0 1.018 4.382A3.323 3.323 0 0 1 .64 6.575v.045a3.288 3.288 0 0 0 2.632 3.218 3.203 3.203 0 0 1-.865.115 3.23 3.23 0 0 1-.614-.057 3.283 3.283 0 0 0 3.067 2.277A6.588 6.588 0 0 1 .78 13.58a6.32 6.32 0 0 1-.78-.045A9.344 9.344 0 0 0 5.026 15z"
|
||||
/></svg
|
||||
<a class="nav-link" href="https://73k.us/" x-html="svg73k"></a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a
|
||||
class="nav-link"
|
||||
href="https://twitter.com/adampiontek"
|
||||
x-html="biTwitter"
|
||||
></a>
|
||||
</li>
|
||||
</ul>
|
||||
|
@ -71,15 +35,9 @@
|
|||
</nav>
|
||||
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<!-- <h1 class="mb-3">Tripeaks Solver</h1> -->
|
||||
|
||||
<div
|
||||
x-data="{show: false}"
|
||||
class="accordion mb-4"
|
||||
id="readMeAccordion"
|
||||
>
|
||||
<div class="row justify-content-center my-4">
|
||||
<div class="col-xl-9">
|
||||
<div x-data="{show: false}" class="accordion" id="readMeAccordion">
|
||||
<div class="accordion-item">
|
||||
<h2 id="readMeHeader" class="accordion-header">
|
||||
<button
|
||||
|
@ -108,17 +66,22 @@
|
|||
</p>
|
||||
|
||||
<p>
|
||||
Use a single zero ("0") for unknown cards. However, the last
|
||||
34 cards (bottom row & stock) must be known, and you don't
|
||||
need to enter unknown cards before the first card you know.
|
||||
You don't have to know all the cards, this solver will give
|
||||
you as many moves as it can. Enter a "0" (a single zero) for
|
||||
unknown cards. You don't need to enter unknown cards before
|
||||
the first card you know. (However, the last 34 cards (base
|
||||
of peaks, stock) must be known.)
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Entry is case-insensitive, and you can separate cards with
|
||||
any character (space, comma, etc), or use no separator.
|
||||
Valid examples:
|
||||
</p>
|
||||
|
||||
<h5>Valid examples</h5>
|
||||
|
||||
<p>These will complete quickly:</p>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
7S 5D 7C 2D 0 0 3S 2H 3H 9H KC QC TD 8D 9C 7H 9D JS QS 4H
|
||||
|
@ -134,164 +97,185 @@
|
|||
7c03s0qsjc00JSasTSadtcqd9s4s2h9h8sjh6c3dks5s5c6h9C2Cac8C6d5DTH8dkckd9d4c5h8hqh6s
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<p>
|
||||
This example will take several minutes, but will provide a
|
||||
solution, and should demonstrate that the page stays
|
||||
responsive and provides feedback on the solving progress:
|
||||
</p>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
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
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-12 col-lg-9 col-xl-8">
|
||||
<!-- User input of game cards -->
|
||||
<form
|
||||
id="cardsInputForm"
|
||||
x-data="cardsInputForm"
|
||||
@submit.prevent="$dispatch('solve-game')"
|
||||
class="needs-validation"
|
||||
:class="{'was-validated': isFormValid && inputValue.length > 2 }"
|
||||
novalidate
|
||||
>
|
||||
<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"
|
||||
:class="{ 'd-block': validCards.length > 0 }"
|
||||
x-text="msg"
|
||||
></div>
|
||||
</template>
|
||||
<template x-for="msg in invalidMessages">
|
||||
<div
|
||||
class="invalid-feedback"
|
||||
:class="{ 'd-block': invalidMessages.length > 0 && inputValue.length > 2 }"
|
||||
x-text="msg"
|
||||
></div>
|
||||
</template>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<!-- Friendly display of game cards -->
|
||||
<div
|
||||
id="playingCardsPreview"
|
||||
x-data="playingCardsPreview"
|
||||
class="mb-4"
|
||||
>
|
||||
<div
|
||||
id="gamePyramid"
|
||||
class="game-pyramid-container p-2 p-md-3 p-lg-4"
|
||||
>
|
||||
<!-- first/top row (index 0, row 1) -->
|
||||
<span class="visually-hidden">Game Row 1, Top of Peaks</span>
|
||||
<template x-for="(card, index) in cardsBySlice(0, 3)">
|
||||
<div
|
||||
x-html="cardSvg(card)"
|
||||
:class="`game-pyramid-card game-pyramid-card-0-${index}`"
|
||||
></div>
|
||||
</template>
|
||||
|
||||
<!-- second row (index 1, row 2) -->
|
||||
<span class="visually-hidden">Game Row 2, Second Row</span>
|
||||
<template x-for="(card, index) in cardsBySlice(3, 9)">
|
||||
<div
|
||||
x-html="cardSvg(card)"
|
||||
:class="`game-pyramid-card game-pyramid-card-1-${index}`"
|
||||
></div>
|
||||
</template>
|
||||
|
||||
<!-- third row (index 2, row 3) -->
|
||||
<span class="visually-hidden">Game Row 3, Third Row</span>
|
||||
<template x-for="(card, index) in cardsBySlice(9, 18)">
|
||||
<div
|
||||
x-html="cardSvg(card)"
|
||||
:class="`game-pyramid-card game-pyramid-card-2-${index}`"
|
||||
></div>
|
||||
</template>
|
||||
|
||||
<!-- last/bottom row (index 3, row 4) -->
|
||||
<span class="visually-hidden">Game Row 4, Base of Peaks</span>
|
||||
<template x-for="(card, index) in cardsBySlice(18, 28)">
|
||||
<div
|
||||
x-html="cardSvg(card)"
|
||||
:class="`game-pyramid-card game-pyramid-card-3-${index}`"
|
||||
></div>
|
||||
</template>
|
||||
|
||||
<!-- stock cards -->
|
||||
<span class="visually-hidden"
|
||||
>Game Stock, the Draw Cards</span
|
||||
>
|
||||
<template x-for="(card, index) in cardsBySlice(28, 52)">
|
||||
<div
|
||||
x-html="card === 0 || card === '0' ? cardSvgs['2B'] : cardSvgs[card]"
|
||||
:class="`game-pyramid-card game-pyramid-card-4-${index}`"
|
||||
></div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-lg-3 col-xl-4">
|
||||
<!-- 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>
|
||||
</div>
|
||||
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-12 col-lg-9 col-xl-8 col-xxl-7">
|
||||
<!-- User input of game cards -->
|
||||
<form
|
||||
id="cardsInputForm"
|
||||
x-data="cardsInputForm"
|
||||
@submit.prevent="$dispatch('solve-game')"
|
||||
class="needs-validation"
|
||||
:class="{'was-validated': isFormValid && inputValue.length > 2 }"
|
||||
novalidate
|
||||
>
|
||||
<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"
|
||||
:class="{ 'd-block': validCards.length > 0 }"
|
||||
x-text="msg"
|
||||
></div>
|
||||
</template>
|
||||
<template x-for="msg in invalidMessages">
|
||||
<div
|
||||
class="invalid-feedback"
|
||||
:class="{ 'd-block': invalidMessages.length > 0 && inputValue.length > 2 }"
|
||||
x-text="msg"
|
||||
></div>
|
||||
</template>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<!-- Friendly display of game cards -->
|
||||
<div
|
||||
id="playingCardsPreview"
|
||||
x-data="playingCardsPreview"
|
||||
class="mb-5"
|
||||
>
|
||||
<div
|
||||
id="gamePyramid"
|
||||
class="game-pyramid-container p-2 p-md-3 p-lg-4"
|
||||
>
|
||||
<!-- first/top row (index 0, row 1) -->
|
||||
<span class="visually-hidden">Game Row 1, Top of Peaks</span>
|
||||
<template x-for="(card, index) in cardsBySlice(0, 3)">
|
||||
<div
|
||||
x-html="cardSvg(card)"
|
||||
:class="`game-pyramid-card game-pyramid-card-0-${index}`"
|
||||
></div>
|
||||
</template>
|
||||
|
||||
<!-- second row (index 1, row 2) -->
|
||||
<span class="visually-hidden">Game Row 2, Second Row</span>
|
||||
<template x-for="(card, index) in cardsBySlice(3, 9)">
|
||||
<div
|
||||
x-html="cardSvg(card)"
|
||||
:class="`game-pyramid-card game-pyramid-card-1-${index}`"
|
||||
></div>
|
||||
</template>
|
||||
|
||||
<!-- third row (index 2, row 3) -->
|
||||
<span class="visually-hidden">Game Row 3, Third Row</span>
|
||||
<template x-for="(card, index) in cardsBySlice(9, 18)">
|
||||
<div
|
||||
x-html="cardSvg(card)"
|
||||
:class="`game-pyramid-card game-pyramid-card-2-${index}`"
|
||||
></div>
|
||||
</template>
|
||||
|
||||
<!-- last/bottom row (index 3, row 4) -->
|
||||
<span class="visually-hidden">Game Row 4, Base of Peaks</span>
|
||||
<template x-for="(card, index) in cardsBySlice(18, 28)">
|
||||
<div
|
||||
x-html="cardSvg(card)"
|
||||
:class="`game-pyramid-card game-pyramid-card-3-${index}`"
|
||||
></div>
|
||||
</template>
|
||||
|
||||
<!-- stock cards -->
|
||||
<span class="visually-hidden">Game Stock, the Draw Cards</span>
|
||||
<template x-for="(card, index) in cardsBySlice(28, 52)">
|
||||
<div
|
||||
x-html="card === 0 || card === '0' ? cardSvgs['2B'] : cardSvgs[card]"
|
||||
:class="`game-pyramid-card game-pyramid-card-4-${index}`"
|
||||
></div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-lg-3 col-xxl-4">
|
||||
<!-- 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>
|
||||
|
||||
<p
|
||||
x-show="solutionMoves.length === 0 && statusMessages.length === 0"
|
||||
>
|
||||
Enter cards and click Solve to get a solution.
|
||||
</p>
|
||||
|
||||
<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>
|
||||
|
||||
<footer class="py-3 my-4">
|
||||
<p class="text-center text-muted">
|
||||
© 2022 <a href="https://73k.us/">Adam Piontek</a>
|
||||
</p>
|
||||
<footer class="py-3 mt-5 bg-light">
|
||||
<div class="text-center text-muted">
|
||||
Written in 2022 by
|
||||
<a href="https://73k.us/" class="link-secondary">Adam Piontek</a> |
|
||||
<a
|
||||
href="https://github.com/apiontek/tripeaks-solitaire-solver-js-73k"
|
||||
class="link-secondary"
|
||||
>Source on Github</a
|
||||
>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<script type="module" src="./js/main.js"></script>
|
||||
|
|
|
@ -3,6 +3,11 @@ import Alpine from "alpinejs";
|
|||
import cardSvgs from "./cardSvgs";
|
||||
import SolverWorker from "./solverWorker?worker";
|
||||
|
||||
// navbar svg icons
|
||||
import svg73k from "../img/73k.svg?raw";
|
||||
import biTwitter from "../img/bi-twitter.svg?raw";
|
||||
import biGithub from "../img/bi-github.svg?raw";
|
||||
|
||||
// Some helpful constants
|
||||
const suits = {
|
||||
C: "Clubs",
|
||||
|
@ -65,6 +70,13 @@ Alpine.store("global", {
|
|||
solvingInProgress: false,
|
||||
});
|
||||
|
||||
// navbar logic
|
||||
Alpine.data("navbar", () => ({
|
||||
svg73k: svg73k.replaceAll('="16"', '="24"'),
|
||||
biTwitter: biTwitter.replaceAll('="16"', '="24"'),
|
||||
biGithub: biGithub.replaceAll('="16"', '="24"'),
|
||||
}));
|
||||
|
||||
// card preview component logic
|
||||
Alpine.data("playingCardsPreview", () => ({
|
||||
cardSvgs,
|
||||
|
@ -198,11 +210,16 @@ const encouragements = [
|
|||
"Strive for progress, not perfection.",
|
||||
"I wish only to be alive and to experience this living to the fullest.",
|
||||
"This too shall pass.",
|
||||
"Life is available only in the present moment.",
|
||||
"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.",
|
||||
"To be upset over what you don’t have is to waste what you do have.",
|
||||
"The ultimate freedom lies in being able to wait patiently for a good thing.",
|
||||
"If it isn't good, let it die. If it doesn't die, make it good.",
|
||||
"A watched pot never boils without applying heat.",
|
||||
"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?",
|
||||
|
@ -218,13 +235,18 @@ const encouragements = [
|
|||
"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.",
|
||||
"My actions are my only true belongings.",
|
||||
"The way to have enough time is to never be in a hurry.",
|
||||
"The more you know, the less you think you know.",
|
||||
"Patience is also a form of action.",
|
||||
];
|
||||
|
||||
// game solving component logic
|
||||
Alpine.data("gameSolving", () => ({
|
||||
encouragements,
|
||||
encourageIndex: null,
|
||||
solverWorker: null,
|
||||
headerText: "Solution will go here:",
|
||||
headerText: "Solution",
|
||||
moveCount: 23,
|
||||
statusMessages: [],
|
||||
solutionMoves: [],
|
||||
|
@ -236,6 +258,7 @@ Alpine.data("gameSolving", () => ({
|
|||
this.statusMessages = [];
|
||||
this.nodesTried = 0;
|
||||
this.nodesTriedFloor = 0;
|
||||
this.encourageIndex = null;
|
||||
},
|
||||
onInit() {
|
||||
this.solverWorker = new SolverWorker();
|
||||
|
@ -254,10 +277,21 @@ Alpine.data("gameSolving", () => ({
|
|||
)} possibilities tried. Still working…`;
|
||||
}
|
||||
if (this.nodesTriedFloor % 250000 === 0) {
|
||||
let randInRange = Math.floor(
|
||||
Math.random() * this.encouragements.length
|
||||
if (this.encourageIndex === null) {
|
||||
this.encourageIndex = Math.floor(
|
||||
Math.random() * this.encouragements.length
|
||||
);
|
||||
}
|
||||
this.statusMessages.splice(
|
||||
2,
|
||||
0,
|
||||
this.encouragements[this.encourageIndex]
|
||||
);
|
||||
this.statusMessages.push(this.encouragements[randInRange]);
|
||||
let newEncourageIndex = this.encourageIndex + 1;
|
||||
this.encourageIndex =
|
||||
newEncourageIndex === this.encouragements.length
|
||||
? 0
|
||||
: newEncourageIndex;
|
||||
}
|
||||
}
|
||||
} else if (e.data.msg === "solve-result") {
|
||||
|
|
|
@ -1,7 +1,10 @@
|
|||
import { defineConfig } from "vite";
|
||||
import path from 'path';
|
||||
import path from "path";
|
||||
|
||||
export default defineConfig({
|
||||
root: path.resolve(__dirname, "src"),
|
||||
base: "./",
|
||||
build: {
|
||||
outDir: "../dist",
|
||||
},
|
||||
});
|
||||
|
|
Loading…
Reference in a new issue