From c5420c97eb779ac50f856faa8f281fffd4b9f2e4 Mon Sep 17 00:00:00 2001 From: Leon Mika Date: Sun, 21 Dec 2025 10:56:49 +1100 Subject: [PATCH] Finished the scorecard --- site/index.html | 1 + site/mahjong/guide.html | 26 ++++++++++++++++ site/mahjong/index.html | 12 ++++++-- site/mahjong/main.js | 46 ++++++++++++++++++---------- site/mahjong/models.js | 68 ++++++++++++++++++++++++++++------------- site/mahjong/style.css | 41 +++++++++++++++++++++++++ 6 files changed, 154 insertions(+), 40 deletions(-) diff --git a/site/index.html b/site/index.html index c71b5cc..444f37f 100644 --- a/site/index.html +++ b/site/index.html @@ -24,6 +24,7 @@
  • Gradient Bands
  • Two-letter Country Codes
  • Timestamp Converter
  • +
  • Mahjong Scorecard
  • diff --git a/site/mahjong/guide.html b/site/mahjong/guide.html index 43ceeb7..0609f8f 100644 --- a/site/mahjong/guide.html +++ b/site/mahjong/guide.html @@ -32,66 +32,92 @@ m Mahjong + 20 ps Pung of simples + 4 pt Pung of terminals + 8 ph Pung of honours + 8 xps Exposed pung of simples + 2 xpt Exposed pung of terminals + 4 xph Exposed pung of honours + 4 ks Kong of simples + 16 kt Kong of terminals + 32 kh Kong of honours + 32 xks Exposed kong of simples + 8 xkt Exposed kong of terminals + 16 xkh Exposed kong of honours + 16 pd Pair of dragons + 2 pw Pair of winds (either player or prevailing) + 2 b Bonus (flower or season) + 4 + + + p + Point (used for adjusting scores) + 1 + + + d + Penalty (used for adjusting scores) + -1 diff --git a/site/mahjong/index.html b/site/mahjong/index.html index e1dce3f..3ac7778 100644 --- a/site/mahjong/index.html +++ b/site/mahjong/index.html @@ -78,15 +78,21 @@
    -

    -
    -
    +
    +
    +
    +
    +
    +
    + Notation Guide +
    + diff --git a/site/mahjong/main.js b/site/mahjong/main.js index f9ba03c..584bcec 100644 --- a/site/mahjong/main.js +++ b/site/mahjong/main.js @@ -2,13 +2,10 @@ import { Application, Controller } from "https://unpkg.com/@hotwired/stimulus/di import { GameState, getGameState, setGameState, calculateScore } from "./models.js"; function emitGameStateEvent(details) { - // window.setTimeout(() => { - let ev = new CustomEvent("gamestatechanged", { detail: details }); - window.dispatchEvent(ev); - // }, 1); + let ev = new CustomEvent("gamestatechanged", { detail: details }); + window.dispatchEvent(ev); } - window.Stimulus = Application.start(); Stimulus.register("scorecard", class extends Controller { @@ -18,8 +15,6 @@ Stimulus.register("scorecard", class extends Controller { } connect() { - // this._rebuildTable(); - // this.element.classList.remove("hidden"); } handleGameState(ev) { @@ -75,8 +70,8 @@ Stimulus.register("scorecard", class extends Controller { let td = document.createElement("td"); td.textContent = c.s; if (c.w) { + td.textContent += " 🀄"; td.classList.add("winner"); - td.textContent += " 🏆"; } tr.append(td); } @@ -94,7 +89,7 @@ Stimulus.register("scorecard", class extends Controller { this.element.querySelector("table.scorecard").replaceWith(tbl); - this.prevailingTarget.textContent = `Prevailing: ${gameState.prevaling}`; + this.prevailingTarget.textContent = `Prevailing: ${gameState.prevailing}`; } }); @@ -102,7 +97,6 @@ Stimulus.register("newgame", class extends Controller { static targets = ["playerName"]; connect() { - this.element.classList.remove("hidden"); } handleGameState(ev) { @@ -133,10 +127,9 @@ Stimulus.register("newgame", class extends Controller { }); Stimulus.register("endround", class extends Controller { - static targets = ["form", "input", "wind"]; + static targets = ["form", "input", "wind", "prevailing"]; connect() { - // this.element.classList.remove("hidden"); } handleGameState(ev) { @@ -148,6 +141,7 @@ Stimulus.register("endround", class extends Controller { break; case "endround": this._prepForms(); + this._updatePreview(); this.element.classList.remove("hidden"); break; } @@ -155,7 +149,10 @@ Stimulus.register("endround", class extends Controller { updatePreview(ev) { ev.preventDefault(); + this._updatePreview(); + } + _updatePreview() { let scoreExprs = this._readScoreExprs(); let nextRound = getGameState().determineNextRound(scoreExprs); @@ -163,15 +160,23 @@ Stimulus.register("endround", class extends Controller { let previewElem = this.element.querySelector(`.preview[data-player="${i}"]`); previewElem.textContent = nextRound.roundScores[i].score; if (parseInt(i) === nextRound.roundWinner) { - previewElem.textContent += " 🏆"; + previewElem.classList.add("winner"); + previewElem.textContent += " 🀄"; + } else { + previewElem.classList.remove("winner"); } } if (nextRound.windsBump) { - this.windTarget.textContent = `${getGameState().players[nextRound.nextRoundEast].name} will be East next round`; + this.windTarget.textContent = ` will be East next round`; } else { - this.windTarget.textContent = `${getGameState().players[nextRound.nextRoundEast].name} will remain East next round`; + this.windTarget.textContent = ` will remain East next round`; } + let nextWindNameElem = document.createElement("strong"); + nextWindNameElem.textContent = getGameState().players[nextRound.nextRoundEast].name; + this.windTarget.prepend(nextWindNameElem); + + this.prevailingTarget.textContent = `Next prevailing: ${nextRound.nextPrevailing}`; } goBack(ev) { @@ -224,4 +229,13 @@ Stimulus.register("endround", class extends Controller { } return scoreExprs; } -}); \ No newline at end of file +}); + +document.addEventListener("DOMContentLoaded", () => { + let game = getGameState(); + if (game !== null) { + emitGameStateEvent({mode: "startgame"}); + } else { + emitGameStateEvent({mode: "newgame"}); + } +}) \ No newline at end of file diff --git a/site/mahjong/models.js b/site/mahjong/models.js index 48d1762..d49ee27 100644 --- a/site/mahjong/models.js +++ b/site/mahjong/models.js @@ -7,22 +7,24 @@ const windDistributions = [ ]; const scoreTokens = { - 'm': 1, - 'ps': 1, - 'pt': 1, - 'ph': 1, - 'xps': 1, - 'xpt': 1, - 'xph': 1, - 'ks': 1, - 'kt': 1, - 'kh': 1, - 'xks': 1, - 'xkt': 1, - 'xkh': 1, - 'pd': 1, - 'pw': 1, - 'b': 1, + 'm': 20, + 'ps': 4, + 'pt': 8, + 'ph': 8, + 'xps': 2, + 'xpt': 4, + 'xph': 4, + 'ks': 16, + 'kt': 32, + 'kh': 32, + 'xks': 8, + 'xkt': 16, + 'xkh': 16, + 'pd': 2, + 'pw': 2, + 'b': 4, + 'p': 1, + 'd': -1, } function parseTokens(str) { @@ -67,7 +69,7 @@ export class GameState { this.players = players; this.rounds = rounds; this.bumpWind = 0; - this.prevaling = "E"; + this.prevailing = "E"; } static newGame(playerNames) { @@ -78,7 +80,21 @@ export class GameState { } let rounds = []; - return new GameState(players, rounds); + let gs = new GameState(players, rounds); + gs.save(); + return gs; + } + + static load() { + let json = localStorage.getItem('mahjong-scorecard'); + if (!json) { + return null; + } + let o = JSON.parse(json); + let gs = new GameState(o.p, o.r); + gs.bumpWind = o.b; + gs.prevailing = o.w; + return gs; } determineNextRound(playerScoreExprs) { @@ -92,7 +108,7 @@ export class GameState { } let windsBump = roundWinner !== currentEast; - let nextPrevailing = this.prevaling; + let nextPrevailing = this.prevailing; if (windsBump) { nextPrevailing = windDistributions[3][((this.bumpWind + 1) / this.players.length)|0]; } @@ -133,14 +149,24 @@ export class GameState { this.players[pi].wind = windDistribution[i]; } - this.prevaling = nr.nextPrevailing; + this.prevailing = nr.nextPrevailing; if (nr.windsBump) { this.bumpWind++; } + this.save(); + } + + save() { + localStorage.setItem('mahjong-scorecard', JSON.stringify({ + p: this.players, + r: this.rounds, + b: this.bumpWind, + w: this.prevailing, + })); } } -let gameState = new GameState(); +let gameState = GameState.load(); export function getGameState() { return gameState; diff --git a/site/mahjong/style.css b/site/mahjong/style.css index 1940ccb..4f1eed1 100644 --- a/site/mahjong/style.css +++ b/site/mahjong/style.css @@ -1,3 +1,7 @@ +.container { + margin-block-end: 20px; +} + .hidden { display: none; } @@ -18,4 +22,41 @@ gap: 10px; justify-content: space-between; align-items: center; + margin-block-end: 12px; +} + +@media (max-width: 600px) { + .btns { + flex-direction: column; + gap: 20px; + } + + .btns :nth-child(1) { + align-self: start; + } + + .btns :nth-child(2) { + align-self: end; + } +} + +.winner { + color: #9B2318; + font-weight: bold; +} + + +th { + background: #E2E2E2; +} + +@media (prefers-color-scheme: dark) { + th { + background: #474747; + } + + .winner { + color: #F06048; + font-weight: bold; + } } \ No newline at end of file