initial HTML implementation of user input with validation

This commit is contained in:
Adam Piontek 2022-09-10 14:13:49 -04:00
parent 4b51cc3b57
commit 63242326a4
8 changed files with 1388 additions and 12 deletions

View file

@ -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
View file

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

View file

@ -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
View 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
View 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

File diff suppressed because it is too large Load diff

View file

@ -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
View file

@ -0,0 +1 @@
@import "../node_modules/bootstrap/scss/bootstrap";