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; }