Finished the scorecard
All checks were successful
/ publish (push) Successful in 50s

This commit is contained in:
Leon Mika 2025-12-21 10:56:49 +11:00
parent 1f8394f23b
commit c5420c97eb
6 changed files with 154 additions and 40 deletions

View file

@ -24,6 +24,7 @@
<li><a href="/gradient-bands/">Gradient Bands</a></li> <li><a href="/gradient-bands/">Gradient Bands</a></li>
<li><a href="/2lcc/">Two-letter Country Codes</a></li> <li><a href="/2lcc/">Two-letter Country Codes</a></li>
<li><a href="/timestamps/">Timestamp Converter</a></li> <li><a href="/timestamps/">Timestamp Converter</a></li>
<li><a href="/mahjong/">Mahjong Scorecard</a></li>
</ul> </ul>
</main> </main>
</body> </body>

View file

@ -32,66 +32,92 @@
<tr> <tr>
<td><code>m</code></td> <td><code>m</code></td>
<td>Mahjong</td> <td>Mahjong</td>
<td>20</td>
</tr> </tr>
<tr> <tr>
<td><code>ps</code></td> <td><code>ps</code></td>
<td>Pung of simples</td> <td>Pung of simples</td>
<td>4</td>
</tr> </tr>
<tr> <tr>
<td><code>pt</code></td> <td><code>pt</code></td>
<td>Pung of terminals</td> <td>Pung of terminals</td>
<td>8</td>
</tr> </tr>
<tr> <tr>
<td><code>ph</code></td> <td><code>ph</code></td>
<td>Pung of honours</td> <td>Pung of honours</td>
<td>8</td>
</tr> </tr>
<tr> <tr>
<td><code>xps</code></td> <td><code>xps</code></td>
<td>Exposed pung of simples</td> <td>Exposed pung of simples</td>
<td>2</td>
</tr> </tr>
<tr> <tr>
<td><code>xpt</code></td> <td><code>xpt</code></td>
<td>Exposed pung of terminals</td> <td>Exposed pung of terminals</td>
<td>4</td>
</tr> </tr>
<tr> <tr>
<td><code>xph</code></td> <td><code>xph</code></td>
<td>Exposed pung of honours</td> <td>Exposed pung of honours</td>
<td>4</td>
</tr> </tr>
<tr> <tr>
<td><code>ks</code></td> <td><code>ks</code></td>
<td>Kong of simples</td> <td>Kong of simples</td>
<td>16</td>
</tr> </tr>
<tr> <tr>
<td><code>kt</code></td> <td><code>kt</code></td>
<td>Kong of terminals</td> <td>Kong of terminals</td>
<td>32</td>
</tr> </tr>
<tr> <tr>
<td><code>kh</code></td> <td><code>kh</code></td>
<td>Kong of honours</td> <td>Kong of honours</td>
<td>32</td>
</tr> </tr>
<tr> <tr>
<td><code>xks</code></td> <td><code>xks</code></td>
<td>Exposed kong of simples</td> <td>Exposed kong of simples</td>
<td>8</td>
</tr> </tr>
<tr> <tr>
<td><code>xkt</code></td> <td><code>xkt</code></td>
<td>Exposed kong of terminals</td> <td>Exposed kong of terminals</td>
<td>16</td>
</tr> </tr>
<tr> <tr>
<td><code>xkh</code></td> <td><code>xkh</code></td>
<td>Exposed kong of honours</td> <td>Exposed kong of honours</td>
<td>16</td>
</tr> </tr>
<tr> <tr>
<td><code>pd</code></td> <td><code>pd</code></td>
<td>Pair of dragons</td> <td>Pair of dragons</td>
<td>2</td>
</tr> </tr>
<tr> <tr>
<td><code>pw</code></td> <td><code>pw</code></td>
<td>Pair of winds (either player or prevailing)</td> <td>Pair of winds (either player or prevailing)</td>
<td>2</td>
</tr> </tr>
<tr> <tr>
<td><code>b</code></td> <td><code>b</code></td>
<td>Bonus (flower or season)</td> <td>Bonus (flower or season)</td>
<td>4</td>
</tr>
<tr>
<td><code>p</code></td>
<td>Point (used for adjusting scores)</td>
<td>1</td>
</tr>
<tr>
<td><code>d</code></td>
<td>Penalty (used for adjusting scores)</td>
<td>-1</td>
</tr> </tr>
</tbody> </tbody>
</table> </table>

View file

@ -78,15 +78,21 @@
<form data-endround-target="form"></form> <form data-endround-target="form"></form>
<p data-endround-target="wind"></p>
<div class="btns"> <div class="btns">
<div></div> <div>
<div data-endround-target="wind"></div>
<div data-endround-target="prevailing"></div>
</div>
<div> <div>
<button data-action="click->endround#goBack">Cancel</button> <button data-action="click->endround#goBack">Cancel</button>
<button data-action="click->endround#nextRound">Next Round</button> <button data-action="click->endround#nextRound">Next Round</button>
</div> </div>
</div> </div>
<div class="btns">
<div></div>
<a href="guide.html" target="_blank">Notation Guide</a>
</div>
</div>
<script src="./main.js" type="module"></script> <script src="./main.js" type="module"></script>
</body> </body>

View file

@ -2,13 +2,10 @@ import { Application, Controller } from "https://unpkg.com/@hotwired/stimulus/di
import { GameState, getGameState, setGameState, calculateScore } from "./models.js"; import { GameState, getGameState, setGameState, calculateScore } from "./models.js";
function emitGameStateEvent(details) { function emitGameStateEvent(details) {
// window.setTimeout(() => {
let ev = new CustomEvent("gamestatechanged", { detail: details }); let ev = new CustomEvent("gamestatechanged", { detail: details });
window.dispatchEvent(ev); window.dispatchEvent(ev);
// }, 1);
} }
window.Stimulus = Application.start(); window.Stimulus = Application.start();
Stimulus.register("scorecard", class extends Controller { Stimulus.register("scorecard", class extends Controller {
@ -18,8 +15,6 @@ Stimulus.register("scorecard", class extends Controller {
} }
connect() { connect() {
// this._rebuildTable();
// this.element.classList.remove("hidden");
} }
handleGameState(ev) { handleGameState(ev) {
@ -75,8 +70,8 @@ Stimulus.register("scorecard", class extends Controller {
let td = document.createElement("td"); let td = document.createElement("td");
td.textContent = c.s; td.textContent = c.s;
if (c.w) { if (c.w) {
td.textContent += " 🀄";
td.classList.add("winner"); td.classList.add("winner");
td.textContent += " 🏆";
} }
tr.append(td); tr.append(td);
} }
@ -94,7 +89,7 @@ Stimulus.register("scorecard", class extends Controller {
this.element.querySelector("table.scorecard").replaceWith(tbl); 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"]; static targets = ["playerName"];
connect() { connect() {
this.element.classList.remove("hidden");
} }
handleGameState(ev) { handleGameState(ev) {
@ -133,10 +127,9 @@ Stimulus.register("newgame", class extends Controller {
}); });
Stimulus.register("endround", class extends Controller { Stimulus.register("endround", class extends Controller {
static targets = ["form", "input", "wind"]; static targets = ["form", "input", "wind", "prevailing"];
connect() { connect() {
// this.element.classList.remove("hidden");
} }
handleGameState(ev) { handleGameState(ev) {
@ -148,6 +141,7 @@ Stimulus.register("endround", class extends Controller {
break; break;
case "endround": case "endround":
this._prepForms(); this._prepForms();
this._updatePreview();
this.element.classList.remove("hidden"); this.element.classList.remove("hidden");
break; break;
} }
@ -155,7 +149,10 @@ Stimulus.register("endround", class extends Controller {
updatePreview(ev) { updatePreview(ev) {
ev.preventDefault(); ev.preventDefault();
this._updatePreview();
}
_updatePreview() {
let scoreExprs = this._readScoreExprs(); let scoreExprs = this._readScoreExprs();
let nextRound = getGameState().determineNextRound(scoreExprs); let nextRound = getGameState().determineNextRound(scoreExprs);
@ -163,15 +160,23 @@ Stimulus.register("endround", class extends Controller {
let previewElem = this.element.querySelector(`.preview[data-player="${i}"]`); let previewElem = this.element.querySelector(`.preview[data-player="${i}"]`);
previewElem.textContent = nextRound.roundScores[i].score; previewElem.textContent = nextRound.roundScores[i].score;
if (parseInt(i) === nextRound.roundWinner) { if (parseInt(i) === nextRound.roundWinner) {
previewElem.textContent += " 🏆"; previewElem.classList.add("winner");
previewElem.textContent += " 🀄";
} else {
previewElem.classList.remove("winner");
} }
} }
if (nextRound.windsBump) { 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 { } 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) { goBack(ev) {
@ -225,3 +230,12 @@ Stimulus.register("endround", class extends Controller {
return scoreExprs; return scoreExprs;
} }
}); });
document.addEventListener("DOMContentLoaded", () => {
let game = getGameState();
if (game !== null) {
emitGameStateEvent({mode: "startgame"});
} else {
emitGameStateEvent({mode: "newgame"});
}
})

View file

@ -7,22 +7,24 @@ const windDistributions = [
]; ];
const scoreTokens = { const scoreTokens = {
'm': 1, 'm': 20,
'ps': 1, 'ps': 4,
'pt': 1, 'pt': 8,
'ph': 1, 'ph': 8,
'xps': 1, 'xps': 2,
'xpt': 1, 'xpt': 4,
'xph': 1, 'xph': 4,
'ks': 1, 'ks': 16,
'kt': 1, 'kt': 32,
'kh': 1, 'kh': 32,
'xks': 1, 'xks': 8,
'xkt': 1, 'xkt': 16,
'xkh': 1, 'xkh': 16,
'pd': 1, 'pd': 2,
'pw': 1, 'pw': 2,
'b': 1, 'b': 4,
'p': 1,
'd': -1,
} }
function parseTokens(str) { function parseTokens(str) {
@ -67,7 +69,7 @@ export class GameState {
this.players = players; this.players = players;
this.rounds = rounds; this.rounds = rounds;
this.bumpWind = 0; this.bumpWind = 0;
this.prevaling = "E"; this.prevailing = "E";
} }
static newGame(playerNames) { static newGame(playerNames) {
@ -78,7 +80,21 @@ export class GameState {
} }
let rounds = []; 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) { determineNextRound(playerScoreExprs) {
@ -92,7 +108,7 @@ export class GameState {
} }
let windsBump = roundWinner !== currentEast; let windsBump = roundWinner !== currentEast;
let nextPrevailing = this.prevaling; let nextPrevailing = this.prevailing;
if (windsBump) { if (windsBump) {
nextPrevailing = windDistributions[3][((this.bumpWind + 1) / this.players.length)|0]; nextPrevailing = windDistributions[3][((this.bumpWind + 1) / this.players.length)|0];
} }
@ -133,14 +149,24 @@ export class GameState {
this.players[pi].wind = windDistribution[i]; this.players[pi].wind = windDistribution[i];
} }
this.prevaling = nr.nextPrevailing; this.prevailing = nr.nextPrevailing;
if (nr.windsBump) { if (nr.windsBump) {
this.bumpWind++; 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() { export function getGameState() {
return gameState; return gameState;

View file

@ -1,3 +1,7 @@
.container {
margin-block-end: 20px;
}
.hidden { .hidden {
display: none; display: none;
} }
@ -18,4 +22,41 @@
gap: 10px; gap: 10px;
justify-content: space-between; justify-content: space-between;
align-items: center; 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;
}
} }