Added finska scorecard
This commit is contained in:
parent
c5420c97eb
commit
792c54f691
59
site/finska/index.html
Normal file
59
site/finska/index.html
Normal 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>
|
||||
160
site/finska/scripts/controllers.js
Normal file
160
site/finska/scripts/controllers.js
Normal 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();
|
||||
}
|
||||
}
|
||||
7
site/finska/scripts/main.js
Normal file
7
site/finska/scripts/main.js
Normal 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);
|
||||
|
||||
152
site/finska/scripts/models.js
Normal file
152
site/finska/scripts/models.js
Normal 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
17
site/finska/style.css
Normal 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;
|
||||
}
|
||||
Loading…
Reference in a new issue