Compare commits
No commits in common. "c5420c97eb779ac50f856faa8f281fffd4b9f2e4" and "e74906e0c46eaf7c5cb422b43d4c778f323f410c" have entirely different histories.
c5420c97eb
...
e74906e0c4
|
|
@ -24,7 +24,6 @@
|
||||||
<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>
|
||||||
|
|
|
||||||
|
|
@ -1,126 +0,0 @@
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>Clocks - Tools</title>
|
|
||||||
<link
|
|
||||||
rel="stylesheet"
|
|
||||||
href="https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.min.css"
|
|
||||||
>
|
|
||||||
<link rel="stylesheet" href="style.css">
|
|
||||||
</head>
|
|
||||||
<body class="container">
|
|
||||||
<header>
|
|
||||||
<hgroup>
|
|
||||||
<h1>Mahjong Score Card</h1>
|
|
||||||
<p>Score entry guide</p>
|
|
||||||
</hgroup>
|
|
||||||
</header>
|
|
||||||
<div>
|
|
||||||
<p>Use the following notation to enter player scores.</p>
|
|
||||||
|
|
||||||
<table>
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Notation</th>
|
|
||||||
<th>Meaning</th>
|
|
||||||
<th>Score</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td><code>m</code></td>
|
|
||||||
<td>Mahjong</td>
|
|
||||||
<td>20</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td><code>ps</code></td>
|
|
||||||
<td>Pung of simples</td>
|
|
||||||
<td>4</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td><code>pt</code></td>
|
|
||||||
<td>Pung of terminals</td>
|
|
||||||
<td>8</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td><code>ph</code></td>
|
|
||||||
<td>Pung of honours</td>
|
|
||||||
<td>8</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td><code>xps</code></td>
|
|
||||||
<td>Exposed pung of simples</td>
|
|
||||||
<td>2</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td><code>xpt</code></td>
|
|
||||||
<td>Exposed pung of terminals</td>
|
|
||||||
<td>4</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td><code>xph</code></td>
|
|
||||||
<td>Exposed pung of honours</td>
|
|
||||||
<td>4</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td><code>ks</code></td>
|
|
||||||
<td>Kong of simples</td>
|
|
||||||
<td>16</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td><code>kt</code></td>
|
|
||||||
<td>Kong of terminals</td>
|
|
||||||
<td>32</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td><code>kh</code></td>
|
|
||||||
<td>Kong of honours</td>
|
|
||||||
<td>32</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td><code>xks</code></td>
|
|
||||||
<td>Exposed kong of simples</td>
|
|
||||||
<td>8</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td><code>xkt</code></td>
|
|
||||||
<td>Exposed kong of terminals</td>
|
|
||||||
<td>16</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td><code>xkh</code></td>
|
|
||||||
<td>Exposed kong of honours</td>
|
|
||||||
<td>16</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td><code>pd</code></td>
|
|
||||||
<td>Pair of dragons</td>
|
|
||||||
<td>2</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td><code>pw</code></td>
|
|
||||||
<td>Pair of winds (either player or prevailing)</td>
|
|
||||||
<td>2</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td><code>b</code></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>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
|
|
@ -1,99 +0,0 @@
|
||||||
<!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">
|
|
||||||
</head>
|
|
||||||
<body class="container">
|
|
||||||
<header>
|
|
||||||
<hgroup>
|
|
||||||
<h1>Mahjong Score Card</h1>
|
|
||||||
</hgroup>
|
|
||||||
</header>
|
|
||||||
<div id="main-app" class="hidden"
|
|
||||||
data-controller="scorecard"
|
|
||||||
data-action="gamestatechanged@window->scorecard#handleGameState">
|
|
||||||
<table class="scorecard">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>AB (E)</th>
|
|
||||||
<th>CD (S)</th>
|
|
||||||
<th>EF (W)</th>
|
|
||||||
<th>GH (N)</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td>20 🏆</td>
|
|
||||||
<td>15</td>
|
|
||||||
<td>12</td>
|
|
||||||
<td>13</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>20 🏆</td>
|
|
||||||
<td>15</td>
|
|
||||||
<td>12</td>
|
|
||||||
<td>13</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
<div class="btns">
|
|
||||||
<div data-scorecard-target="prevailing">Prevailing: E</div>
|
|
||||||
<div>
|
|
||||||
<button data-action="click->scorecard#endGame">End Game</button>
|
|
||||||
<button data-action="click->scorecard#endRound">End Round</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div id="player-entry" class="hidden" data-controller="newgame"
|
|
||||||
data-action="gamestatechanged@window->newgame#handleGameState">
|
|
||||||
<h3>New Game</h3>
|
|
||||||
<label for="player-1">Player 1 (East)</label>
|
|
||||||
<input type="text" data-newgame-target="playerName">
|
|
||||||
<label for="player-2">Player 2</label>
|
|
||||||
<input type="text" data-newgame-target="playerName">
|
|
||||||
<label for="player-3">Player 3</label>
|
|
||||||
<input type="text" data-newgame-target="playerName">
|
|
||||||
<label for="player-4">Player 4</label>
|
|
||||||
<input type="text" data-newgame-target="playerName">
|
|
||||||
<label for="player-5">Player 5</label>
|
|
||||||
<input type="text" data-newgame-target="playerName">
|
|
||||||
|
|
||||||
<div class="btns">
|
|
||||||
<div></div>
|
|
||||||
<div>
|
|
||||||
<button data-action="click->newgame#startGame">Start Game</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div id="end-round" class="hidden" data-controller="endround"
|
|
||||||
data-action="gamestatechanged@window->endround#handleGameState">
|
|
||||||
<h3>End Round</h3>
|
|
||||||
|
|
||||||
<form data-endround-target="form"></form>
|
|
||||||
|
|
||||||
<div class="btns">
|
|
||||||
<div>
|
|
||||||
<div data-endround-target="wind"></div>
|
|
||||||
<div data-endround-target="prevailing"></div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<button data-action="click->endround#goBack">Cancel</button>
|
|
||||||
<button data-action="click->endround#nextRound">Next Round</button>
|
|
||||||
</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>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
|
|
@ -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"});
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
@ -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;
|
|
||||||
}
|
|
||||||
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Loading…
Reference in a new issue