diff --git a/site/index.html b/site/index.html index 444f37f..c71b5cc 100644 --- a/site/index.html +++ b/site/index.html @@ -24,7 +24,6 @@
  • Gradient Bands
  • Two-letter Country Codes
  • Timestamp Converter
  • -
  • Mahjong Scorecard
  • diff --git a/site/mahjong/guide.html b/site/mahjong/guide.html deleted file mode 100644 index 0609f8f..0000000 --- a/site/mahjong/guide.html +++ /dev/null @@ -1,126 +0,0 @@ - - - - - - Clocks - Tools - - - - -
    -
    -

    Mahjong Score Card

    -

    Score entry guide

    -
    -
    -
    -

    Use the following notation to enter player scores.

    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    NotationMeaningScore
    mMahjong20
    psPung of simples4
    ptPung of terminals8
    phPung of honours8
    xpsExposed pung of simples2
    xptExposed pung of terminals4
    xphExposed pung of honours4
    ksKong of simples16
    ktKong of terminals32
    khKong of honours32
    xksExposed kong of simples8
    xktExposed kong of terminals16
    xkhExposed kong of honours16
    pdPair of dragons2
    pwPair of winds (either player or prevailing)2
    bBonus (flower or season)4
    pPoint (used for adjusting scores)1
    dPenalty (used for adjusting scores)-1
    -
    - - \ No newline at end of file diff --git a/site/mahjong/index.html b/site/mahjong/index.html deleted file mode 100644 index 3ac7778..0000000 --- a/site/mahjong/index.html +++ /dev/null @@ -1,99 +0,0 @@ - - - - - - Mahjong Score Card - Tools - - - - -
    -
    -

    Mahjong Score Card

    -
    -
    - - - - - - - \ No newline at end of file diff --git a/site/mahjong/main.js b/site/mahjong/main.js deleted file mode 100644 index 584bcec..0000000 --- a/site/mahjong/main.js +++ /dev/null @@ -1,241 +0,0 @@ -import { Application, Controller } from "https://unpkg.com/@hotwired/stimulus/dist/stimulus.js"; -import { GameState, getGameState, setGameState, calculateScore } from "./models.js"; - -function emitGameStateEvent(details) { - let ev = new CustomEvent("gamestatechanged", { detail: details }); - window.dispatchEvent(ev); -} - -window.Stimulus = Application.start(); - -Stimulus.register("scorecard", class extends Controller { - static targets = ["prevailing"]; - - initialize() { - } - - connect() { - } - - handleGameState(ev) { - switch (ev.detail.mode) { - case "newgame": - case "endround": - this.element.classList.add("hidden"); - break; - case "startgame": - case "nextround": - this._rebuildTable(); - this.element.classList.remove("hidden"); - break; - } - } - - endRound(ev) { - ev.preventDefault(); - emitGameStateEvent({mode: "endround"}); - } - - endGame(ev) { - ev.preventDefault(); - - if (!confirm("Are you sure you want to end the game?")) { - return; - } - - emitGameStateEvent({mode: "newgame"}); - } - - _rebuildTable() { - let gameState = getGameState(); - - let tbl = document.createElement("table"); - tbl.classList.add("scorecard"); - - let thead = document.createElement("thead"); - let thr = document.createElement("tr"); - let headRows = gameState.players.map(player => { - let th = document.createElement("th"); - th.textContent = player.name + " (" + player.wind + ")"; - return th; - }); - thr.append(...headRows); - thead.append(thr); - tbl.append(thead); - - let tbody = document.createElement("tbody"); - for (let r of gameState.rounds) { - let tr = document.createElement("tr"); - for (let c of r) { - let td = document.createElement("td"); - td.textContent = c.s; - if (c.w) { - td.textContent += " 🀄"; - td.classList.add("winner"); - } - tr.append(td); - } - tbody.append(tr); - } - tbl.append(tbody); - - let tfoot = document.createElement("tfoot"); - for (let t of gameState.playerTotals()) { - let td = document.createElement("td"); - td.textContent = t; - tfoot.append(td); - } - tbl.append(tfoot); - - this.element.querySelector("table.scorecard").replaceWith(tbl); - - this.prevailingTarget.textContent = `Prevailing: ${gameState.prevailing}`; - } -}); - -Stimulus.register("newgame", class extends Controller { - static targets = ["playerName"]; - - connect() { - } - - handleGameState(ev) { - switch (ev.detail.mode) { - case "startgame": - case "endround": - case "nextround": - this.element.classList.add("hidden"); - break; - case "newgame": - this.element.classList.remove("hidden"); - break; - } - } - - startGame() { - let players = []; - this.playerNameTargets.forEach(el => { - if (el.value != "") { - players.push(el.value); - } - }); - - setGameState(GameState.newGame(players)); - this.element.classList.add("hidden"); - emitGameStateEvent({mode: "startgame"}); - } -}); - -Stimulus.register("endround", class extends Controller { - static targets = ["form", "input", "wind", "prevailing"]; - - connect() { - } - - handleGameState(ev) { - switch (ev.detail.mode) { - case "startgame": - case "nextround": - case "newgame": - this.element.classList.add("hidden"); - break; - case "endround": - this._prepForms(); - this._updatePreview(); - this.element.classList.remove("hidden"); - break; - } - } - - updatePreview(ev) { - ev.preventDefault(); - this._updatePreview(); - } - - _updatePreview() { - let scoreExprs = this._readScoreExprs(); - let nextRound = getGameState().determineNextRound(scoreExprs); - - for (let i in nextRound.roundScores) { - let previewElem = this.element.querySelector(`.preview[data-player="${i}"]`); - previewElem.textContent = nextRound.roundScores[i].score; - if (parseInt(i) === nextRound.roundWinner) { - previewElem.classList.add("winner"); - previewElem.textContent += " 🀄"; - } else { - previewElem.classList.remove("winner"); - } - } - - if (nextRound.windsBump) { - this.windTarget.textContent = ` will be East next round`; - } else { - 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) { - emitGameStateEvent({mode: "startgame"}); - } - - nextRound(ev) { - ev.preventDefault(); - - let scoreExprs = this._readScoreExprs(); - getGameState().startNextRound(scoreExprs); - - emitGameStateEvent({mode: "nextround"}); - } - - _prepForms() { - let newFormElems = getGameState().players.map((player, i) => { - let outerDiv = document.createElement("div"); - - let label = document.createElement("label"); - label.textContent = player.name + " (" + player.wind + ")"; - label.setAttribute("for", `player-${i}`); - - let div = document.createElement("div"); - div.classList.add("score-input-group"); - - let input = document.createElement("input"); - input.name = `player-${i}`; - input.dataset["endroundTarget"] = "input"; - input.dataset["action"] = "keyup->endround#updatePreview"; - input.dataset["player"] = i; - - let preview = document.createElement("div"); - preview.classList.add("preview"); - preview.textContent = "0"; - preview.dataset["player"] = i; - - div.append(input, preview); - outerDiv.append(label, div); - return outerDiv; - }); - this.formTarget.replaceChildren(...newFormElems); - } - - _readScoreExprs() { - let scoreExprs = []; - for (let playerIndex in getGameState().players) { - let thisScoreExpr = this.formTarget.querySelector(`input[data-player="${playerIndex}"]`).value; - scoreExprs.push(thisScoreExpr); - } - return scoreExprs; - } -}); - -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 deleted file mode 100644 index d49ee27..0000000 --- a/site/mahjong/models.js +++ /dev/null @@ -1,177 +0,0 @@ -const windDistributions = [ - ["E"], - ["E", "W"], - ["E", "S", "N"], - ["E", "S", "W", "N"], - ["E", "S", "W", "N", "X"], -]; - -const scoreTokens = { - '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) { - // Split on whitespace and filter out empty strings - const tokens = str.split(/\s+/).filter(token => token.length > 0); - - const pattern = /^([0-9]*)([a-z]+)$/; - const result = []; - - for (const token of tokens) { - const match = token.match(pattern); - if (match) { - result.push({ - number: match[1] ? parseInt(match[1], 10) : null, - letters: match[2] - }); - } - } - - return result; -} - -export function calculateScore(str) { - const tokens = parseTokens(str); - let o = {score: 0, winner: false}; - - for (const token of tokens) { - if (!(token.letters in scoreTokens)) { - continue; - } - o.score += scoreTokens[token.letters] * (token.number || 1); - if (token.letters === 'm') { - o.winner = true; - } - } - return o; -} - - -export class GameState { - constructor(players, rounds) { - this.players = players; - this.rounds = rounds; - this.bumpWind = 0; - this.prevailing = "E"; - } - - static newGame(playerNames) { - let players = playerNames.map(name => { return { name: name } }); - let windDistribution = windDistributions[players.length - 1]; - for (let i = 0; i < players.length; i++) { - players[i].wind = windDistribution[i]; - } - - let 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) { - let roundScores = playerScoreExprs.map(calculateScore); - let roundWinner = roundScores.findIndex(rs => rs.winner); - - let currentEast = this.players.findIndex(p => p.wind === 'E'); - let nextRoundEast = currentEast; - if (roundWinner !== currentEast) { - nextRoundEast = (currentEast + 1) % this.players.length; - } - - let windsBump = roundWinner !== currentEast; - let nextPrevailing = this.prevailing; - if (windsBump) { - nextPrevailing = windDistributions[3][((this.bumpWind + 1) / this.players.length)|0]; - } - - return { - roundScores: roundScores, - roundWinner: roundWinner, - nextRoundEast: nextRoundEast, - windsBump: roundWinner !== currentEast, - nextPrevailing: nextPrevailing, - } - } - - playerTotals() { - let scores = this.players.map(p => 0); - for (let i = 0; i < this.rounds.length; i++) { - for (let j = 0; j < this.rounds[i].length; j++) { - scores[j] += this.rounds[i][j].s; - } - } - - return scores; - } - - startNextRound(playerScoreExprs) { - let nr = this.determineNextRound(playerScoreExprs); - - this.rounds.push(nr.roundScores.map((s, i) => { - if (i === nr.roundWinner) { - return { s: s.score, w: true }; - } - return { s: s.score }; - })); - - let windDistribution = windDistributions[this.players.length - 1]; - for (let i = 0; i < this.players.length; i++) { - let pi = (nr.nextRoundEast + i) % this.players.length; - this.players[pi].wind = windDistribution[i]; - } - - 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 = GameState.load(); - -export function getGameState() { - return gameState; -} - -export function setGameState(gs) { - gameState = gs; -} \ No newline at end of file diff --git a/site/mahjong/style.css b/site/mahjong/style.css deleted file mode 100644 index 4f1eed1..0000000 --- a/site/mahjong/style.css +++ /dev/null @@ -1,62 +0,0 @@ -.container { - margin-block-end: 20px; -} - -.hidden { - display: none; -} - -.main-table { - table-layout: fixed; -} - -.score-input-group { - display: grid; - gap: 15px; - grid-template-columns: 2fr 1fr; - align-items: baseline; -} - -.btns { - display: flex; - 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