initial HTML implementation of user input with validation
This commit is contained in:
parent
4b51cc3b57
commit
63242326a4
8 changed files with 1388 additions and 12 deletions
|
@ -1,12 +1,13 @@
|
|||
module.exports = {
|
||||
parserOptions: {
|
||||
ecmaVersion: "latest",
|
||||
sourceType: "module",
|
||||
},
|
||||
env: {
|
||||
browser: false,
|
||||
es6: true,
|
||||
node: true,
|
||||
browser: true,
|
||||
es2022: true,
|
||||
mocha: true,
|
||||
node: true,
|
||||
},
|
||||
plugins: ["html"],
|
||||
extends: ["eslint:recommended", "prettier"],
|
||||
};
|
||||
|
|
19
.gitignore
vendored
19
.gitignore
vendored
|
@ -6,6 +6,7 @@ yarn-debug.log*
|
|||
yarn-error.log*
|
||||
lerna-debug.log*
|
||||
.pnpm-debug.log*
|
||||
pnpm-debug.log*
|
||||
|
||||
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||
|
@ -102,7 +103,6 @@ dist
|
|||
|
||||
# vuepress v2.x temp and cache directory
|
||||
.temp
|
||||
.cache
|
||||
|
||||
# Docusaurus cache and generated files
|
||||
.docusaurus
|
||||
|
@ -128,3 +128,20 @@ dist
|
|||
.yarn/build-state.yml
|
||||
.yarn/install-state.gz
|
||||
.pnp.*
|
||||
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
# private
|
||||
TODO.md
|
||||
|
|
|
@ -4,7 +4,9 @@ A brute force solver for Microsoft Tri-Peaks solitaire written in javascript.
|
|||
|
||||
This is a fork of [Courtney Pitcher's project](https://github.com/IgniparousTempest/javascript-tri-peaks-solitaire-solver), which I've modified for my own purposes.
|
||||
|
||||
## Fix
|
||||
_NOTE:_ Due to a "hard" game being included in `test.js` now, testing takes longer (almost 3 minutes to complete on my computer).
|
||||
|
||||
## Fix Card Matching
|
||||
|
||||
It seemed I was getting solutions that didn't make sense, which I tracked down to the logic used to compare cards. I believe I've fixed this, and have now been getting real solutions.
|
||||
|
||||
|
@ -24,7 +26,7 @@ I have yet to implement this in a website but it can be run directly in node. Ev
|
|||
|
||||
- solvable: "8S TS 4D 7S 5D 7C 2D JH AC 3S 2H 3H 9H KC QC TD 8D 9C 7H 9D JS QS 4H 5C 5S 4C 2C QD 8C KD 3D KS JD 2S 7D KH AH 5H 9S 4S QH 6S 6D 3C JC TC 8H 6C TH AS AD 6H"
|
||||
- partial board: "0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 2D KH 8C 6S 6H 2C 8H JC 9C 4D AD TH 2S AS QH 5H AH 3H 2H 4S 6D 3C TS JD 9H KD AC JS 9S 4H 4C 5S 5D 5C"
|
||||
~~- unsolvable: "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"~~
|
||||
~~- unsolvable: "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"~~
|
||||
|
||||
## Notes
|
||||
|
||||
|
|
102
index.html
Normal file
102
index.html
Normal file
|
@ -0,0 +1,102 @@
|
|||
<!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</title>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col pt-3 pt-lg-5">
|
||||
<h1>Tripeaks Solver</h1>
|
||||
|
||||
<p x-data="helloilove" x-text="message"></p>
|
||||
|
||||
<form
|
||||
id="cardsInputForm"
|
||||
x-data="cardsInputForm"
|
||||
x-init="onInit()"
|
||||
@submit.prevent="submit"
|
||||
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
|
||||
>
|
||||
<input
|
||||
id="cardsInput"
|
||||
aria-describedby="cardsInputLabel"
|
||||
x-model="inputValue"
|
||||
class="form-control"
|
||||
:class="{ 'is-valid': isFormValid && inputValue.length > 2, 'is-invalid': !isFormValid && inputValue.length > 2 }"
|
||||
maxlength="155"
|
||||
@keyup.debounce="validateCardsInput($event)"
|
||||
placeholder="e.g., 2S TC 4S QD KH 5S 9S ..."
|
||||
/>
|
||||
|
||||
<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>
|
||||
|
||||
<div class="mb-3">
|
||||
<label
|
||||
id="cardsToSolveLabel"
|
||||
for="cardsToSolve"
|
||||
class="form-label"
|
||||
>Cards Identified For Solving</label
|
||||
>
|
||||
<textarea
|
||||
id="cardsToSolve"
|
||||
aria-describedby="cardsToSolveLabel"
|
||||
class="form-control"
|
||||
x-text="cardsToSolve.join(' ')"
|
||||
rows="4"
|
||||
disabled
|
||||
readonly
|
||||
></textarea>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<!-- <div x-data="cardInput">
|
||||
|
||||
<div class="mb-3">
|
||||
|
||||
<label for="userCardEntryInput" class="form-label" id="userCardEntryInputHelp">Enter Game Cards</label>
|
||||
<input @keyup.debounce="validateCardString($event)" class="form-control" :class="{ 'is-valid': inputIsValid, 'is-invalid': !inputIsValid }" id="userCardEntryInput" aria-describedby="userCardEntryInputHelp" maxlength="155" placeholder="e.g., 2S TC 4S QD KH 5S 9S ...">
|
||||
|
||||
<div class="valid-feedback" :class="{ 'd-block': validUserCards.filter(c => c !== '0').length > 0 }" x-text="validatedCardsStr(validUserCards.filter(c => c != '0'), 'valid')"></div>
|
||||
|
||||
<div class="valid-feedback text-muted" :class="{ 'd-block': validUserCards.filter(c => c === '0').length > 0 }" x-text="`${52 - validUserCards.filter(c => c !== '0').length} unknown card${validUserCards.filter(c => c === '0').length > 1 ? 's' : ''}`"></div>
|
||||
|
||||
<div class="invalid-feedback" :class="{ 'd-block': dupedUserCards.length > 0 }" x-text="validatedCardsStr(dupedUserCards, 'duplicate')"></div>
|
||||
|
||||
<div class="invalid-feedback" :class="{ 'd-block': invalidUserCards.length > 0 }" x-text="validatedCardsStr(invalidUserCards, 'invalid')"></div>
|
||||
|
||||
<div class="invalid-feedback" :class="{ 'd-block': invalidLength !== '' }" x-text="invalidLength"></div>
|
||||
</div>
|
||||
|
||||
</div> -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script type="module" src="/main.js"></script>
|
||||
</body>
|
||||
</html>
|
129
main.js
Normal file
129
main.js
Normal file
|
@ -0,0 +1,129 @@
|
|||
import "./style.scss";
|
||||
import Alpine from "alpinejs";
|
||||
// import 'bootstrap/dist/js/bootstrap'
|
||||
|
||||
Alpine.data("helloilove", () => ({
|
||||
message: "I ❤️ Alpine",
|
||||
}));
|
||||
|
||||
Alpine.data("cardsInputForm", () => ({
|
||||
// "constants" for validation etc
|
||||
suits: [],
|
||||
ranks: [],
|
||||
deck: [],
|
||||
minCards: 34,
|
||||
stockCount: 24,
|
||||
peaksCount: 28,
|
||||
onInit() {
|
||||
this.suits = "CDHS".split("");
|
||||
this.ranks = "A23456789TJQK".split("");
|
||||
this.deck = this.suits.flatMap((suit) => {
|
||||
return this.ranks.map((cval) => {
|
||||
return cval + suit;
|
||||
});
|
||||
});
|
||||
console.log(this.deck.join(", "));
|
||||
console.log(`deck size: ${this.deck.length}`);
|
||||
},
|
||||
|
||||
// input validation
|
||||
inputValue: "",
|
||||
validCards: [],
|
||||
dupedCards: [],
|
||||
invalidCards: [],
|
||||
validMessages: [],
|
||||
invalidMessages: [],
|
||||
cardsToSolve: [],
|
||||
get isFormValid() {
|
||||
return this.isValidCardsLengthInRange && this.invalidMessages.length === 0;
|
||||
},
|
||||
get isValidCardsLengthTooSmall() {
|
||||
return this.validCards.length < this.minCards;
|
||||
},
|
||||
get isValidCardsLengthTooBig() {
|
||||
return this.validCards.length > this.deck.length;
|
||||
},
|
||||
get isValidCardsLengthInRange() {
|
||||
return !this.isValidCardsLengthTooSmall && !this.isValidCardsLengthTooBig;
|
||||
},
|
||||
validateCardsInput(event) {
|
||||
// reset arrays and parse the input
|
||||
this.validCards = [];
|
||||
this.invalidCards = [];
|
||||
this.dupedCards = [];
|
||||
this.validMessages = [];
|
||||
this.invalidMessages = [];
|
||||
let userCards = event.target.value
|
||||
.toUpperCase()
|
||||
.split(" ")
|
||||
.filter((c) => c);
|
||||
|
||||
// check the input
|
||||
userCards.forEach((card) => {
|
||||
if (card === "0") {
|
||||
// user marking a slot with an unknown card
|
||||
this.validCards.push(card);
|
||||
} else if (this.validCards.includes(card)) {
|
||||
// this card was already seen in user's input, now it's a duplicate
|
||||
this.dupedCards.push(card);
|
||||
} else if (this.deck.includes(card)) {
|
||||
// not a duplicate, and in the reference deck? Valid, add to valid cards
|
||||
this.validCards.push(card);
|
||||
} else {
|
||||
// not a dupe, but not in reference deck: invalid, add to invalid cards
|
||||
this.invalidCards.push(card);
|
||||
}
|
||||
});
|
||||
|
||||
// set validation messages based on length
|
||||
if (this.isValidCardsLengthTooSmall) {
|
||||
this.invalidMessages.push(`Must enter at least ${this.minCards} cards`);
|
||||
} else if (this.isValidCardsLengthTooBig) {
|
||||
this.invalidMessages.push(
|
||||
`Must not enter more than ${this.deck.length} cards`
|
||||
);
|
||||
}
|
||||
if (this.validCards.slice(this.validCards.length - 34).includes("0")) {
|
||||
this.invalidMessages.push(
|
||||
`Stock + bottom row (last 34 cards) must not contain unknown ('0') cards`
|
||||
);
|
||||
}
|
||||
|
||||
// set other validation messages
|
||||
if (this.dupedCards.length > 0) {
|
||||
let s = this.dupedCards.length > 1 ? "s" : "";
|
||||
this.invalidMessages.push(
|
||||
`${this.dupedCards.length} duplicate card${s}: ${this.dupedCards.join(
|
||||
" "
|
||||
)}`
|
||||
);
|
||||
}
|
||||
if (this.invalidCards.length > 0) {
|
||||
let s = this.invalidCards.length > 1 ? "s" : "";
|
||||
this.invalidMessages.push(
|
||||
`${this.invalidCards.length} invalid card${s}: ${this.invalidCards.join(
|
||||
" "
|
||||
)}`
|
||||
);
|
||||
}
|
||||
if (this.validCards.length > 0) {
|
||||
let s = this.validCards.length > 1 ? "s" : "";
|
||||
this.validMessages.push(
|
||||
`${this.validCards.length} valid card${s}: ${this.validCards.join(" ")}`
|
||||
);
|
||||
}
|
||||
if (event.target.value.includes(" ")) {
|
||||
this.invalidMessages.push("Too many spaces between cards");
|
||||
}
|
||||
|
||||
// set the game cards to try solving, based on current input
|
||||
this.cardsToSolve = Array(this.deck.length - this.validCards.length)
|
||||
.fill(0)
|
||||
.concat(this.validCards)
|
||||
.map((c) => (c === "0" ? 0 : c));
|
||||
},
|
||||
}));
|
||||
|
||||
window.Alpine = Alpine;
|
||||
|
||||
Alpine.start();
|
1122
package-lock.json
generated
1122
package-lock.json
generated
File diff suppressed because it is too large
Load diff
12
package.json
12
package.json
|
@ -11,8 +11,11 @@
|
|||
],
|
||||
"main": "solver.js",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"test": "mocha",
|
||||
"lint": "eslint --ext .js --ignore-path .gitignore --fix .",
|
||||
"lint": "eslint --ext .js,.html --ignore-path .gitignore --fix .",
|
||||
"format": "prettier . --write"
|
||||
},
|
||||
"repository": {
|
||||
|
@ -30,9 +33,14 @@
|
|||
},
|
||||
"homepage": "https://github.com/apiontek/javascript-tri-peaks-solitaire-solver#readme",
|
||||
"devDependencies": {
|
||||
"alpinejs": "^3.10.3",
|
||||
"bootstrap": "^5.2.1",
|
||||
"eslint": "^8.23.0",
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
"eslint-plugin-html": "^7.1.0",
|
||||
"mocha": "^10.0.0",
|
||||
"prettier": "^2.7.1"
|
||||
"prettier": "^2.7.1",
|
||||
"sass": "^1.54.9",
|
||||
"vite": "^3.1.0"
|
||||
}
|
||||
}
|
||||
|
|
1
style.scss
Normal file
1
style.scss
Normal file
|
@ -0,0 +1 @@
|
|||
@import "../node_modules/bootstrap/scss/bootstrap";
|
Loading…
Reference in a new issue