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 = {
|
module.exports = {
|
||||||
parserOptions: {
|
parserOptions: {
|
||||||
ecmaVersion: "latest",
|
sourceType: "module",
|
||||||
},
|
},
|
||||||
env: {
|
env: {
|
||||||
browser: false,
|
browser: true,
|
||||||
es6: true,
|
es2022: true,
|
||||||
node: true,
|
|
||||||
mocha: true,
|
mocha: true,
|
||||||
|
node: true,
|
||||||
},
|
},
|
||||||
|
plugins: ["html"],
|
||||||
extends: ["eslint:recommended", "prettier"],
|
extends: ["eslint:recommended", "prettier"],
|
||||||
};
|
};
|
||||||
|
|
19
.gitignore
vendored
19
.gitignore
vendored
|
@ -6,6 +6,7 @@ yarn-debug.log*
|
||||||
yarn-error.log*
|
yarn-error.log*
|
||||||
lerna-debug.log*
|
lerna-debug.log*
|
||||||
.pnpm-debug.log*
|
.pnpm-debug.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
|
||||||
# Diagnostic reports (https://nodejs.org/api/report.html)
|
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||||
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||||
|
@ -102,7 +103,6 @@ dist
|
||||||
|
|
||||||
# vuepress v2.x temp and cache directory
|
# vuepress v2.x temp and cache directory
|
||||||
.temp
|
.temp
|
||||||
.cache
|
|
||||||
|
|
||||||
# Docusaurus cache and generated files
|
# Docusaurus cache and generated files
|
||||||
.docusaurus
|
.docusaurus
|
||||||
|
@ -128,3 +128,20 @@ dist
|
||||||
.yarn/build-state.yml
|
.yarn/build-state.yml
|
||||||
.yarn/install-state.gz
|
.yarn/install-state.gz
|
||||||
.pnp.*
|
.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.
|
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.
|
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.
|
||||||
|
|
||||||
|
|
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",
|
"main": "solver.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "vite build",
|
||||||
|
"preview": "vite preview",
|
||||||
"test": "mocha",
|
"test": "mocha",
|
||||||
"lint": "eslint --ext .js --ignore-path .gitignore --fix .",
|
"lint": "eslint --ext .js,.html --ignore-path .gitignore --fix .",
|
||||||
"format": "prettier . --write"
|
"format": "prettier . --write"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
|
@ -30,9 +33,14 @@
|
||||||
},
|
},
|
||||||
"homepage": "https://github.com/apiontek/javascript-tri-peaks-solitaire-solver#readme",
|
"homepage": "https://github.com/apiontek/javascript-tri-peaks-solitaire-solver#readme",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"alpinejs": "^3.10.3",
|
||||||
|
"bootstrap": "^5.2.1",
|
||||||
"eslint": "^8.23.0",
|
"eslint": "^8.23.0",
|
||||||
"eslint-config-prettier": "^8.5.0",
|
"eslint-config-prettier": "^8.5.0",
|
||||||
|
"eslint-plugin-html": "^7.1.0",
|
||||||
"mocha": "^10.0.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