This commit is contained in:
parent
1f8394f23b
commit
c5420c97eb
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
@ -224,4 +229,13 @@ 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"});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Loading…
Reference in a new issue