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 A |
+ Player 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');
}
}