From 6f258bf13d97cc9086e3156d296c682a7eea1240 Mon Sep 17 00:00:00 2001 From: Leon Mika Date: Tue, 23 Dec 2025 00:29:40 +0100 Subject: [PATCH] Split generic scorecard with 2 and 4 player variants --- site/index.html | 3 +- site/scorecard-2p/index.html | 63 +++++++++++ site/scorecard-2p/main.js | 0 site/scorecard-2p/scripts/controllers.js | 137 +++++++++++++++++++++++ site/scorecard-2p/scripts/main.js | 7 ++ site/scorecard-2p/scripts/models.js | 133 ++++++++++++++++++++++ site/scorecard-2p/style.css | 21 ++++ site/scorecard-4p/index.html | 8 +- site/scorecard-4p/scripts/models.js | 6 +- 9 files changed, 373 insertions(+), 5 deletions(-) create mode 100644 site/scorecard-2p/index.html create mode 100644 site/scorecard-2p/main.js create mode 100644 site/scorecard-2p/scripts/controllers.js create mode 100644 site/scorecard-2p/scripts/main.js create mode 100644 site/scorecard-2p/scripts/models.js create mode 100644 site/scorecard-2p/style.css diff --git a/site/index.html b/site/index.html index 683ba05..d36f391 100644 --- a/site/index.html +++ b/site/index.html @@ -24,7 +24,8 @@
  • Gradient Bands
  • Two-letter Country Codes
  • Timestamp Converter
  • -
  • Generic Scorecard
  • +
  • Generic Scorecard - 2 Players
  • +
  • Generic Scorecard - 4 Players
  • Mahjong Scorecard
  • Finska Scorecard
  • diff --git a/site/scorecard-2p/index.html b/site/scorecard-2p/index.html new file mode 100644 index 0000000..0bf3b74 --- /dev/null +++ b/site/scorecard-2p/index.html @@ -0,0 +1,63 @@ + + + + + + Score Card - Tools + + + + + +
    +
    +

    Scorecard

    +

    2 Players

    +
    +
    + + +
    + + + + + + + + + + + + + + + + + +
    Player APlayer B
    +
    + +
    +
    +
    + +
    +
    + +
    + + +
    +
    + + \ No newline at end of file diff --git a/site/scorecard-2p/main.js b/site/scorecard-2p/main.js new file mode 100644 index 0000000..e69de29 diff --git a/site/scorecard-2p/scripts/controllers.js b/site/scorecard-2p/scripts/controllers.js new file mode 100644 index 0000000..d06c0a5 --- /dev/null +++ b/site/scorecard-2p/scripts/controllers.js @@ -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(); + } +} diff --git a/site/scorecard-2p/scripts/main.js b/site/scorecard-2p/scripts/main.js new file mode 100644 index 0000000..34ece68 --- /dev/null +++ b/site/scorecard-2p/scripts/main.js @@ -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); + diff --git a/site/scorecard-2p/scripts/models.js b/site/scorecard-2p/scripts/models.js new file mode 100644 index 0000000..64cd910 --- /dev/null +++ b/site/scorecard-2p/scripts/models.js @@ -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(); + } +} \ No newline at end of file diff --git a/site/scorecard-2p/style.css b/site/scorecard-2p/style.css new file mode 100644 index 0000000..e2cd398 --- /dev/null +++ b/site/scorecard-2p/style.css @@ -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; +} \ No newline at end of file diff --git a/site/scorecard-4p/index.html b/site/scorecard-4p/index.html index a54d119..d495e38 100644 --- a/site/scorecard-4p/index.html +++ b/site/scorecard-4p/index.html @@ -12,7 +12,13 @@ -

    Scorecard

    +
    +
    +

    Scorecard

    +

    4 Players

    +
    +
    +
    diff --git a/site/scorecard-4p/scripts/models.js b/site/scorecard-4p/scripts/models.js index 1b0e894..2472177 100644 --- a/site/scorecard-4p/scripts/models.js +++ b/site/scorecard-4p/scripts/models.js @@ -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'); } }