Split generic scorecard with 2 and 4 player variants
All checks were successful
/ publish (push) Successful in 1m33s

This commit is contained in:
Leon Mika 2025-12-23 00:29:40 +01:00
parent cbd55175ae
commit 6f258bf13d
9 changed files with 373 additions and 5 deletions

View file

@ -24,7 +24,8 @@
<li><a href="/gradient-bands/">Gradient Bands</a></li>
<li><a href="/2lcc/">Two-letter Country Codes</a></li>
<li><a href="/timestamps/">Timestamp Converter</a></li>
<li><a href="/scorecard/">Generic Scorecard</a></li>
<li><a href="/scorecard-2p/">Generic Scorecard - 2 Players</a></li>
<li><a href="/scorecard-4p/">Generic Scorecard - 4 Players</a></li>
<li><a href="/mahjong/">Mahjong Scorecard</a></li>
<li><a href="/finska/">Finska Scorecard</a></li>
</ul>

View file

@ -0,0 +1,63 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Score Card - Tools</title>
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.min.css"
>
<link rel="stylesheet" href="./style.css">
<script src="./scripts/main.js" type="module"></script>
</head>
<body class="container">
<header>
<hgroup>
<h1>Scorecard</h1>
<p>2 Players</p>
</hgroup>
</header>
<div data-controller="finska-scorecard">
<table class="score-table">
<thead>
<tr>
<td colspan="2">Player A</td>
<td colspan="2">Player B</td>
</tr>
</thead>
<tbody data-finska-scorecard-target="scoreTable">
</tbody>
<tfoot>
<tr>
<td>
<form>
<input class="score-input" type="number"
data-finska-scorecard-target="score1Input" data-action="keydown->finska-scorecard#score1KeyDown">
</form>
</td>
<td></td>
<td>
<form>
<input class="score-input" type="number"
data-finska-scorecard-target="score2Input" data-action="keydown->finska-scorecard#score2KeyDown">
</form>
</td>
<td></td>
</tr>
</tfoot>
</table>
<div class="action-bar">
<button data-action="finska-scorecard#undoLast">
Undo
</button>
<button data-action="finska-scorecard#resetAll">
Reset
</button>
</div>
</div>
</body>
</html>

View file

View file

@ -0,0 +1,137 @@
import { Controller } from "https://unpkg.com/@hotwired/stimulus/dist/stimulus.js";
import { Scorecard, getStoreDAO } from "./models.js";
const storeDao = getStoreDAO();
export class FinskaScorecardController extends Controller {
static targets = ["score1Input", "score2Input", "scoreTable"];
connect() {
this._undoStack = [];
this._scorecard = storeDao.loadOrCreate();
this.updateTable();
}
updateTable() {
let tableBody = this.scoreTableTarget;
let tableRows = tableBody.querySelectorAll("tr");
let pairs = this._scorecard.pairs();
for (let pairIndex = 0; pairIndex < pairs.length; pairIndex++) {
let tableRow;
if (pairIndex >= tableRows.length) {
tableRow = this._appendRow();
} else {
tableRow = tableRows[pairIndex];
}
this._updateRow(tableRow, pairs[pairIndex])
}
// Remove any extra rows
for (let i = pairs.length; i < tableRows.length; i++) {
tableBody.removeChild(tableRows[i]);
}
console.log(JSON.stringify(this._scorecard.toJson()));
}
_updateRow(tableRow, pair) {
let tds = tableRow.querySelectorAll("td");
this._updateCell(pair.p1, tds[0], tds[1]);
this._updateCell(pair.p2, tds[2], tds[3]);
}
_updateCell(score, scoreCell, totalCell) {
scoreCell.classList.value = "";
totalCell.classList.value = "";
if (score != null) {
scoreCell.textContent = score.score;
totalCell.textContent = score.total;
} else {
scoreCell.textContent = "";
totalCell.textContent = "";
}
}
_appendRow() {
let newRow = document.createElement("tr");
newRow.classList.add("score-entry");
for (let i = 0; i < 4; i++) {
newRow.appendChild(document.createElement("td"));
}
this.scoreTableTarget.appendChild(newRow);
return newRow;
}
addScore1() {
this._addScore(this.score1InputTarget, this.score2InputTarget,
this._scorecard.addPlayer1Score.bind(this._scorecard),
this._scorecard.removeLastPlayer1Score.bind(this._scorecard));
}
addScore2() {
this._addScore(this.score2InputTarget, this.score1InputTarget,
this._scorecard.addPlayer2Score.bind(this._scorecard),
this._scorecard.removeLastPlayer2Score.bind(this._scorecard));
}
_addScore(inputElem, focusToInputElem, addScoreFn, queueUndoFn) {
let score = parseInt(inputElem.value);
if (isNaN(score)) {
score = 0;
}
addScoreFn(score);
this._undoStack.push(queueUndoFn);
inputElem.value = "";
storeDao.save(this._scorecard);
this.updateTable();
focusToInputElem.focus();
}
score1KeyDown(e) {
this._handleKeyDown(e, this.addScore1.bind(this));
}
score2KeyDown(e) {
this._handleKeyDown(e, this.addScore2.bind(this));
}
_handleKeyDown(e, addScoreFn) {
if (e.key === "Enter") {
e.preventDefault();
addScoreFn();
}
}
undoLast() {
if (this._undoStack.length === 0) {
return;
}
(this._undoStack.pop())();
storeDao.save(this._scorecard);
this.updateTable();
}
resetAll() {
if (!confirm("Really reset?")) {
return;
}
this._scorecard.reset();
storeDao.clear();
this._undoStack = [];
this.updateTable();
}
}

View file

@ -0,0 +1,7 @@
import { Application } from "https://unpkg.com/@hotwired/stimulus/dist/stimulus.js";
import { FinskaScorecardController } from "./controllers.js"
window.Stimulus = Application.start();
window.Stimulus.register('finska-scorecard', FinskaScorecardController);

View file

@ -0,0 +1,133 @@
export class ScoreEntry {
constructor(score, total) {
this.score = score;
this.total = total;
}
};
export class Scorecard {
constructor() {
this.reset();
}
reset() {
this._player1Scores = [];
this._player2Scores = [];
}
addPlayer1Score(newScore) {
this._addScore(this._player1Scores, newScore);
}
removeLastPlayer1Score() {
this._player1Scores.pop();
}
addPlayer2Score(newScore) {
this._addScore(this._player2Scores, newScore);
}
removeLastPlayer2Score() {
this._player2Scores.pop();
}
_addScore(playerScores, newScore) {
let lastEntry;
if (playerScores.length === 0) {
lastEntry = new ScoreEntry(0, 0, 0, false, false);
} else {
lastEntry = playerScores[playerScores.length - 1];
}
let newEntry = this._newEntryFromPrevious(newScore, lastEntry);
playerScores.push(newEntry);
}
length() {
return Math.max(this._player1Scores.length, this._player2Scores.length);
}
pairs() {
let pairs = [];
for (let i = 0; i < this.length(); i++) {
pairs.push({
p1: (i < this._player1Scores.length ? this._player1Scores[i] : null),
p2: (i < this._player2Scores.length ? this._player2Scores[i] : null),
})
}
return pairs;
}
_newEntryFromPrevious(score, previousScoreEntry) {
if (previousScoreEntry === null) {
return new ScoreEntry(score, score, (score === 0 ? 1 : 0));
}
let newTotal = previousScoreEntry.total + score;
return new ScoreEntry(score, newTotal);
}
toJson() {
return {
"version": 1,
"p1": { "scores": this._player1Scores.map(p => p.score) },
"p2": { "scores": this._player2Scores.map(p => p.score) },
};
}
static fromJson(o) {
let scorecard = new Scorecard();
o["p1"]["scores"].forEach(x => scorecard.addPlayer1Score(x));
o["p2"]["scores"].forEach(x => scorecard.addPlayer2Score(x));
return scorecard;
}
}
class StoreDAO {
constructor(localStorage) {
this._localStorage = localStorage;
}
save(scoreCard) {
this._localStorage.setItem('generic-scorecard-2p', JSON.stringify(scoreCard.toJson()));
}
loadOrCreate() {
try {
console.log("Loading scorecard");
let scoreCardJson = this._localStorage.getItem('generic-scorecard-2p');
if (scoreCardJson !== null) {
return Scorecard.fromJson(JSON.parse(scoreCardJson));
}
} catch (e) {
console.log(`Could not restore game: ${e}`);
}
return new Scorecard();
}
clear() {
this._localStorage.removeItem('generic-scorecard-2p');
}
}
class DummyStoreDAO {
save(scoreCard) { }
loadOrCreate() {
return new Scorecard();
}
clear() { }
}
export function getStoreDAO() {
if (!!window.localStorage) {
return new StoreDAO(window.localStorage);
} else {
return new DummyStoreDAO();
}
}

View file

@ -0,0 +1,21 @@
tfoot input {
border-width: 1px;
}
.score-foul {
color: #861D13;
background-color: #F8DCD6;
font-weight: bold;
}
.score-overflow {
color: #5B4200;
background-color: #FCEFD9;
font-weight: bold;
}
.score-win {
color: #394D00;
background-color: #DEFC85;
font-weight: bold;
}

View file

@ -12,7 +12,13 @@
<script src="./scripts/main.js" type="module"></script>
</head>
<body class="container">
<h1>Scorecard</h1>
<header>
<hgroup>
<h1>Scorecard</h1>
<p>4 Players</p>
</hgroup>
</header>
<div data-controller="finska-scorecard">
<table class="score-table">

View file

@ -119,13 +119,13 @@ class StoreDAO {
}
save(scoreCard) {
this._localStorage.setItem('generic-scorecard', JSON.stringify(scoreCard.toJson()));
this._localStorage.setItem('generic-scorecard-4p', JSON.stringify(scoreCard.toJson()));
}
loadOrCreate() {
try {
console.log("Loading scorecard");
let scoreCardJson = this._localStorage.getItem('generic-scorecard');
let scoreCardJson = this._localStorage.getItem('generic-scorecard-4p');
if (scoreCardJson !== null) {
return Scorecard.fromJson(JSON.parse(scoreCardJson));
}
@ -137,7 +137,7 @@ class StoreDAO {
}
clear() {
this._localStorage.removeItem('generic-scorecard');
this._localStorage.removeItem('generic-scorecard-4p');
}
}