Compare commits

...

2 commits

Author SHA1 Message Date
Leon Mika 6caadc9c00 Added to the finksa scorecard
Some checks failed
/ publish (push) Failing after 33s
2025-12-21 22:59:51 +01:00
Leon Mika 792c54f691 Added finska scorecard 2025-12-21 22:57:31 +01:00
6 changed files with 396 additions and 0 deletions

59
site/finska/index.html Normal file
View file

@ -0,0 +1,59 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Mahjong 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">
<h1>Finska Scorecard</h1>
<div data-controller="finska-scorecard"
data-finska-scorecard-max-score-value="50"
data-finska-scorecard-overflow-score-to-value="25">
<table class="score-table">
<thead>
<tr>
<td colspan="2">Team A</td>
<td colspan="2">Team B</td>
</tr>
</thead>
<tbody data-finska-scorecard-target="scoreTable">
</tbody>
<tfoot>
<tr>
<td>
<form>
<input class="score-input" type="number" tabindex="0"
data-finska-scorecard-target="score1Input" data-action="keydown->finska-scorecard#score1KeyDown">
</form>
</td>
<td></td>
<td>
<form>
<input class="score-input" type="number" tabindex="0"
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

@ -0,0 +1,160 @@
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"];
static values = {
maxScore: Number,
overflowScoreTo: Number
}
connect() {
let rules = {
maxScore: Math.floor(this.maxScoreValue),
overflowScoreTo: Math.floor(this.overflowScoreToValue)
};
this._undoStack = [];
this._scorecard = storeDao.loadOrCreate(rules);
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;
if (score.score === 0) {
scoreCell.classList.add("score-foul");
totalCell.classList.add("score-foul");
} else if (score.wasOverflow) {
scoreCell.classList.add("score-overflow");
totalCell.classList.add("score-overflow");
} else if (score.wasWin) {
scoreCell.classList.add("score-win");
totalCell.classList.add("score-win");
}
} 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;
}
if (!confirm("Really undo last move?")) {
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,152 @@
export class ScoreEntry {
constructor(score, total, foul, wasOverflow, wasWin) {
this.score = score;
this.total = total;
this.fouls = foul;
this.wasOverflow = wasOverflow;
this.wasWin = wasWin;
}
};
export class Scorecard {
constructor(rules) {
this.rules = rules;
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 wasOverflow = false, wasWin = false;
let newTotal = previousScoreEntry.total + score;
if (newTotal === this.rules.maxScore) {
wasWin = true;
} else if (newTotal > this.rules.maxScore) {
newTotal = this.rules.overflowScoreTo;
wasOverflow = true;
}
let newFouls = previousScoreEntry.foul;
if (score === 0) {
newFouls++;
}
return new ScoreEntry(score, newTotal, newFouls, wasOverflow, wasWin);
}
toJson() {
return {
"version": 1,
"rules": this.rules,
"p1": { "scores": this._player1Scores.map(p => p.score) },
"p2": { "scores": this._player2Scores.map(p => p.score) },
};
}
static fromJson(o) {
let scorecard = new Scorecard(o.rules);
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('score_card', JSON.stringify(scoreCard.toJson()));
}
loadOrCreate(rules) {
try {
console.log("Loading scorecard");
let scoreCardJson = this._localStorage.getItem('score_card');
if (scoreCardJson !== null) {
return Scorecard.fromJson(JSON.parse(scoreCardJson));
}
} catch (e) {
console.log(`Could not restore game: ${e}`);
}
return new Scorecard(rules);
}
clear() {
this._localStorage.clear();
}
}
class DummyStoreDAO {
save(scoreCard) { }
loadOrCreate(rules) {
return new Scorecard(rules);
}
clear() { }
}
export function getStoreDAO() {
if (!!window.localStorage) {
return new StoreDAO(window.localStorage);
} else {
return new DummyStoreDAO();
}
}

17
site/finska/style.css Normal file
View file

@ -0,0 +1,17 @@
.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

@ -25,6 +25,7 @@
<li><a href="/2lcc/">Two-letter Country Codes</a></li>
<li><a href="/timestamps/">Timestamp Converter</a></li>
<li><a href="/mahjong/">Mahjong Scorecard</a></li>
<li><a href="/finska/">Finska Scorecard</a></li>
</ul>
</main>
</body>