Files
misskey/packages/misskey-mahjong/src/engine.master.ts
syuilo 166aeb631e wip
2024-02-06 18:21:58 +09:00

930 lines
25 KiB
TypeScript

/*
* SPDX-FileCopyrightText: syuilo and other misskey contributors
* SPDX-License-Identifier: AGPL-3.0-only
*/
import CRC32 from 'crc-32';
import { TileType, House, Huro, TILE_TYPES, YAKU_DEFINITIONS, TileId } from './common.js';
import * as Common from './common.js';
import { PlayerState } from './engine.player.js';
//#region syntax suger
function $(tid: TileId): Common.TileInstance {
return Common.findTileByIdOrFail(tid);
}
function $type(tid: TileId): TileType {
return $(tid).t;
}
//#endregion
class StateManager {
public state: MasterState;
private commitCallback: (state: MasterState) => void;
constructor(state: MasterState, commitCallback: (state: MasterState) => void) {
this.state = structuredClone(state);
this.commitCallback = commitCallback;
}
public commit() {
this.commitCallback(this.state);
}
public get doras(): TileType[] {
return this.state.kingTiles.slice(0, this.state.activatedDorasCount)
.map(id => Common.nextTileForDora($type(id)));
}
public get handTiles(): Record<House, TileId[]> {
return this.state.handTiles;
}
public get handTileTypes(): Record<House, TileType[]> {
return {
e: this.state.handTiles.e.map(id => $type(id)),
s: this.state.handTiles.s.map(id => $type(id)),
w: this.state.handTiles.w.map(id => $type(id)),
n: this.state.handTiles.n.map(id => $type(id)),
};
}
public get hoTileTypes(): Record<House, TileType[]> {
return {
e: this.state.hoTiles.e.map(id => $type(id)),
s: this.state.hoTiles.s.map(id => $type(id)),
w: this.state.hoTiles.w.map(id => $type(id)),
n: this.state.hoTiles.n.map(id => $type(id)),
};
}
public get riichis(): Record<House, boolean> {
return this.state.riichis;
}
public get askings(): MasterState['askings'] {
return this.state.askings;
}
public get user1House(): House {
return this.state.user1House;
}
public get user2House(): House {
return this.state.user2House;
}
public get user3House(): House {
return this.state.user3House;
}
public get user4House(): House {
return this.state.user4House;
}
public get turn(): House | null {
return this.state.turn;
}
public canRon(house: House, tid: TileId): boolean {
// フリテン
// TODO: ポンされるなどして自分の河にない場合の考慮
if (this.hoTileTypes[house].includes($type(tid))) return false;
const horaSets = Common.getHoraSets(this.handTileTypes[house].concat($type(tid)));
if (horaSets.length === 0) return false; // 完成形じゃない
// TODO
//const yakus = YAKU_DEFINITIONS.filter(yaku => yaku.calc(this.state, { tsumoTile: null, ronTile: tile }));
//if (yakus.length === 0) return false; // 役がない
return true;
}
public canPon(house: House, tid: TileId): boolean {
return this.handTileTypes[house].filter(t => t === $type(tid)).length === 2;
}
public canDaiminkan(caller: House, tid: TileId): boolean {
return this.handTileTypes[caller].filter(t => t === $type(tid)).length === 3;
}
public canCii(caller: House, callee: House, tid: TileId): boolean {
if (callee !== Common.prevHouse(caller)) return false;
const hand = this.handTileTypes[caller];
return Common.SHUNTU_PATTERNS.some(pattern =>
pattern.includes($type(tid)) &&
pattern.filter(t => hand.includes(t)).length >= 2);
}
public tsumo(): TileId {
const tile = this.state.tiles.pop();
if (tile == null) throw new Error('No tiles left');
if (this.state.turn == null) throw new Error('Not your turn');
this.state.handTiles[this.state.turn].push(tile);
return tile;
}
}
export type MasterState = {
user1House: House;
user2House: House;
user3House: House;
user4House: House;
round: 'e' | 's' | 'w' | 'n';
kyoku: number;
turnCount: number;
tiles: TileId[];
kingTiles: TileId[];
activatedDorasCount: number;
/**
* 副露した牌を含まない手牌
*/
handTiles: {
e: TileId[];
s: TileId[];
w: TileId[];
n: TileId[];
};
hoTiles: {
e: TileId[];
s: TileId[];
w: TileId[];
n: TileId[];
};
huros: {
e: Huro[];
s: Huro[];
w: Huro[];
n: Huro[];
};
riichis: {
e: boolean;
s: boolean;
w: boolean;
n: boolean;
};
ippatsus: {
e: boolean;
s: boolean;
w: boolean;
n: boolean;
};
points: {
e: number;
s: number;
w: number;
n: number;
};
turn: House | null;
nextTurnAfterAsking: House | null;
askings: {
ron: {
/**
* 牌を捨てた人
*/
callee: House;
/**
* ロンする権利がある人
*/
callers: House[];
} | null;
pon: {
/**
* 牌を捨てた人
*/
callee: House;
/**
* ポンする権利がある人
*/
caller: House;
} | null;
cii: {
/**
* 牌を捨てた人
*/
callee: House;
/**
* チーする権利がある人(calleeの下家なのは自明だがプログラム簡略化のため)
*/
caller: House;
} | null;
kan: {
/**
* 牌を捨てた人
*/
callee: House;
/**
* カンする権利がある人
*/
caller: House;
} | null;
};
};
export class MasterGameEngine {
private state: MasterState;
constructor(state: MasterState) {
this.state = state;
}
public get doras(): TileType[] {
return this.state.kingTiles.slice(0, this.state.activatedDorasCount)
.map(id => Common.nextTileForDora($type(id)));
}
public get handTiles(): Record<House, TileId[]> {
return this.state.handTiles;
}
public get handTileTypes(): Record<House, TileType[]> {
return {
e: this.state.handTiles.e.map(id => $type(id)),
s: this.state.handTiles.s.map(id => $type(id)),
w: this.state.handTiles.w.map(id => $type(id)),
n: this.state.handTiles.n.map(id => $type(id)),
};
}
public get hoTileTypes(): Record<House, TileType[]> {
return {
e: this.state.hoTiles.e.map(id => $type(id)),
s: this.state.hoTiles.s.map(id => $type(id)),
w: this.state.hoTiles.w.map(id => $type(id)),
n: this.state.hoTiles.n.map(id => $type(id)),
};
}
public get riichis(): Record<House, boolean> {
return this.state.riichis;
}
public get askings(): MasterState['askings'] {
return this.state.askings;
}
public get user1House(): House {
return this.state.user1House;
}
public get user2House(): House {
return this.state.user2House;
}
public get user3House(): House {
return this.state.user3House;
}
public get user4House(): House {
return this.state.user4House;
}
public get turn(): House | null {
return this.state.turn;
}
public static createInitialState(): MasterState {
const ikasama: TileId[] = [125, 129, 9, 56, 57, 61, 77, 81, 85, 133, 134, 135, 121, 122];
const tiles = [...Common.TILE_ID_MAP.keys()];
tiles.sort(() => Math.random() - 0.5);
for (const tile of ikasama) {
const index = tiles.indexOf(tile);
tiles.splice(index, 1);
}
//const eHandTiles = tiles.splice(0, 14);
const eHandTiles = ikasama;
const sHandTiles = tiles.splice(0, 13);
const wHandTiles = tiles.splice(0, 13);
const nHandTiles = tiles.splice(0, 13);
const kingTiles = tiles.splice(0, 14);
return {
user1House: 'e',
user2House: 's',
user3House: 'w',
user4House: 'n',
round: 'e',
kyoku: 1,
turnCount: 0,
tiles,
kingTiles,
activatedDorasCount: 1,
handTiles: {
e: eHandTiles,
s: sHandTiles,
w: wHandTiles,
n: nHandTiles,
},
hoTiles: {
e: [],
s: [],
w: [],
n: [],
},
huros: {
e: [],
s: [],
w: [],
n: [],
},
riichis: {
e: false,
s: false,
w: false,
n: false,
},
ippatsus: {
e: false,
s: false,
w: false,
n: false,
},
points: {
e: 25000,
s: 25000,
w: 25000,
n: 25000,
},
turn: 'e',
nextTurnAfterAsking: null,
askings: {
ron: null,
pon: null,
cii: null,
kan: null,
},
};
}
public getHouse(index: 1 | 2 | 3 | 4): House {
switch (index) {
case 1: return this.state.user1House;
case 2: return this.state.user2House;
case 3: return this.state.user3House;
case 4: return this.state.user4House;
}
}
public startTransaction() {
return new StateManager(this.state, (newState) => {
this.state = newState;
});
}
public commit_nextKyoku() {
const newState = MasterGameEngine.createInitialState();
newState.kyoku = this.state.kyoku + 1;
newState.points = this.state.points;
newState.turn = 'e';
newState.user1House = Common.nextHouse(this.state.user1House);
newState.user2House = Common.nextHouse(this.state.user2House);
newState.user3House = Common.nextHouse(this.state.user3House);
newState.user4House = Common.nextHouse(this.state.user4House);
this.state = newState;
}
public commit_dahai(house: House, tid: TileId, riichi = false) {
const tx = this.startTransaction();
if (tx.state.turn !== house) throw new Error('Not your turn');
if (riichi) {
if (tx.state.riichis[house]) throw new Error('Already riichi');
const tempHandTiles = [...tx.handTileTypes[house]];
tempHandTiles.splice(tempHandTiles.indexOf($type(tid)), 1);
if (Common.getHoraTiles(tempHandTiles).length === 0) throw new Error('Not tenpai');
if (tx.state.points[house] < 1000) throw new Error('Not enough points');
}
const handTiles = tx.state.handTiles[house];
if (!handTiles.includes(tid)) throw new Error('No such tile in your hand');
handTiles.splice(handTiles.indexOf(tid), 1);
tx.state.hoTiles[house].push(tid);
if (tx.state.riichis[house]) {
tx.state.ippatsus[house] = false;
}
if (riichi) {
tx.state.riichis[house] = true;
tx.state.ippatsus[house] = true;
}
const canRonHouses: House[] = [];
switch (house) {
case 'e':
if (tx.canRon('s', tid)) canRonHouses.push('s');
if (tx.canRon('w', tid)) canRonHouses.push('w');
if (tx.canRon('n', tid)) canRonHouses.push('n');
break;
case 's':
if (tx.canRon('e', tid)) canRonHouses.push('e');
if (tx.canRon('w', tid)) canRonHouses.push('w');
if (tx.canRon('n', tid)) canRonHouses.push('n');
break;
case 'w':
if (tx.canRon('e', tid)) canRonHouses.push('e');
if (tx.canRon('s', tid)) canRonHouses.push('s');
if (tx.canRon('n', tid)) canRonHouses.push('n');
break;
case 'n':
if (tx.canRon('e', tid)) canRonHouses.push('e');
if (tx.canRon('s', tid)) canRonHouses.push('s');
if (tx.canRon('w', tid)) canRonHouses.push('w');
break;
}
let canKanHouse: House | null = null;
switch (house) {
case 'e': canKanHouse = tx.canDaiminkan('s', tid) ? 's' : tx.canDaiminkan('w', tid) ? 'w' : tx.canDaiminkan('n', tid) ? 'n' : null; break;
case 's': canKanHouse = tx.canDaiminkan('e', tid) ? 'e' : tx.canDaiminkan('w', tid) ? 'w' : tx.canDaiminkan('n', tid) ? 'n' : null; break;
case 'w': canKanHouse = tx.canDaiminkan('e', tid) ? 'e' : tx.canDaiminkan('s', tid) ? 's' : tx.canDaiminkan('n', tid) ? 'n' : null; break;
case 'n': canKanHouse = tx.canDaiminkan('e', tid) ? 'e' : tx.canDaiminkan('s', tid) ? 's' : tx.canDaiminkan('w', tid) ? 'w' : null; break;
}
let canPonHouse: House | null = null;
switch (house) {
case 'e': canPonHouse = tx.canPon('s', tid) ? 's' : tx.canPon('w', tid) ? 'w' : tx.canPon('n', tid) ? 'n' : null; break;
case 's': canPonHouse = tx.canPon('e', tid) ? 'e' : tx.canPon('w', tid) ? 'w' : tx.canPon('n', tid) ? 'n' : null; break;
case 'w': canPonHouse = tx.canPon('e', tid) ? 'e' : tx.canPon('s', tid) ? 's' : tx.canPon('n', tid) ? 'n' : null; break;
case 'n': canPonHouse = tx.canPon('e', tid) ? 'e' : tx.canPon('s', tid) ? 's' : tx.canPon('w', tid) ? 'w' : null; break;
}
let canCiiHouse: House | null = null;
switch (house) {
case 'e': canCiiHouse = tx.canCii('s', house, tid) ? 's' : tx.canCii('w', house, tid) ? 'w' : tx.canCii('n', house, tid) ? 'n' : null; break;
case 's': canCiiHouse = tx.canCii('e', house, tid) ? 'e' : tx.canCii('w', house, tid) ? 'w' : tx.canCii('n', house, tid) ? 'n' : null; break;
case 'w': canCiiHouse = tx.canCii('e', house, tid) ? 'e' : tx.canCii('s', house, tid) ? 's' : tx.canCii('n', house, tid) ? 'n' : null; break;
case 'n': canCiiHouse = tx.canCii('e', house, tid) ? 'e' : tx.canCii('s', house, tid) ? 's' : tx.canCii('w', house, tid) ? 'w' : null; break;
}
if (canRonHouses.length > 0 || canKanHouse != null || canPonHouse != null || canCiiHouse != null) {
if (canRonHouses.length > 0) {
tx.state.askings.ron = {
callee: house,
callers: canRonHouses,
};
}
if (canKanHouse != null) {
tx.state.askings.kan = {
callee: house,
caller: canKanHouse,
};
}
if (canPonHouse != null) {
tx.state.askings.pon = {
callee: house,
caller: canPonHouse,
};
}
if (canCiiHouse != null) {
tx.state.askings.cii = {
callee: house,
caller: canCiiHouse,
};
}
tx.state.turn = null;
tx.state.nextTurnAfterAsking = Common.nextHouse(house);
tx.commit();
return {
asking: true as const,
canRonHouses: canRonHouses,
canKanHouse: canKanHouse,
canPonHouse: canPonHouse,
canCiiHouse: canCiiHouse,
};
}
// 流局
if (tx.state.tiles.length === 0) {
tx.state.turn = null;
tx.commit();
return {
asking: false as const,
ryuukyoku: true as const,
};
}
tx.state.turn = Common.nextHouse(house);
const tsumoTile = tx.tsumo();
tx.commit();
return {
asking: false as const,
tsumoTile: tsumoTile,
next: tx.state.turn,
};
}
public commit_kakan(house: House, tid: TileId) {
const tx = this.startTransaction();
const pon = tx.state.huros[house].find(h => h.type === 'pon' && $type(h.tiles[0]) === $type(tid));
if (pon == null) throw new Error('No such pon');
tx.state.handTiles[house].splice(tx.state.handTiles[house].indexOf(tid), 1);
const tiles = [tid, ...pon.tiles];
tx.state.huros[house].push({ type: 'minkan', tiles: tiles, from: pon.from });
tx.state.ippatsus.e = false;
tx.state.ippatsus.s = false;
tx.state.ippatsus.w = false;
tx.state.ippatsus.n = false;
tx.state.activatedDorasCount++;
const rinsyan = tx.tsumo();
tx.commit();
return {
rinsyan,
tiles,
from: pon.from,
};
}
public commit_ankan(house: House, tid: TileId) {
const tx = this.startTransaction();
const t1 = tx.state.handTiles[house].filter(t => $type(t) === $type(tid)).at(0);
if (t1 == null) throw new Error('No such tile');
const t2 = tx.state.handTiles[house].filter(t => $type(t) === $type(tid)).at(1);
if (t2 == null) throw new Error('No such tile');
const t3 = tx.state.handTiles[house].filter(t => $type(t) === $type(tid)).at(2);
if (t3 == null) throw new Error('No such tile');
const t4 = tx.state.handTiles[house].filter(t => $type(t) === $type(tid)).at(3);
if (t4 == null) throw new Error('No such tile');
tx.state.handTiles[house].splice(tx.state.handTiles[house].indexOf(t1), 1);
tx.state.handTiles[house].splice(tx.state.handTiles[house].indexOf(t2), 1);
tx.state.handTiles[house].splice(tx.state.handTiles[house].indexOf(t3), 1);
tx.state.handTiles[house].splice(tx.state.handTiles[house].indexOf(t4), 1);
const tiles = [t1, t2, t3, t4];
tx.state.huros[house].push({ type: 'ankan', tiles: tiles });
tx.state.ippatsus.e = false;
tx.state.ippatsus.s = false;
tx.state.ippatsus.w = false;
tx.state.ippatsus.n = false;
tx.state.activatedDorasCount++;
const rinsyan = tx.tsumo();
tx.commit();
return {
rinsyan,
tiles,
};
}
/**
* ツモ和了
* @param house
*/
public commit_tsumoHora(house: House) {
const tx = this.startTransaction();
if (tx.state.turn !== house) throw new Error('Not your turn');
const yakus = YAKU_DEFINITIONS.filter(yaku => yaku.calc({
house: house,
handTiles: tx.handTileTypes[house],
huros: tx.state.huros[house],
tsumoTile: tx.handTileTypes[house].at(-1)!,
ronTile: null,
riichi: tx.state.riichis[house],
ippatsu: tx.state.ippatsus[house],
}));
const doraCount =
Common.calcOwnedDoraCount(tx.handTileTypes[house], tx.state.huros[house], tx.doras) +
Common.calcRedDoraCount(tx.state.handTiles[house], tx.state.huros[house]);
const fans = yakus.map(yaku => yaku.fan).reduce((a, b) => a + b, 0) + doraCount;
const pointDeltas = Common.calcTsumoHoraPointDeltas(house, fans);
tx.state.points.e += pointDeltas.e;
tx.state.points.s += pointDeltas.s;
tx.state.points.w += pointDeltas.w;
tx.state.points.n += pointDeltas.n;
console.log('yakus', house, yakus);
tx.commit();
return {
handTiles: tx.state.handTiles[house],
tsumoTile: tx.state.handTiles[house].at(-1)!,
};
}
public commit_resolveCallingInterruption(answers: {
pon: boolean;
cii: false | 'x__' | '_x_' | '__x';
kan: boolean;
ron: House[];
}) {
const tx = this.startTransaction();
if (tx.state.askings.pon == null && tx.state.askings.cii == null && tx.state.askings.kan == null && tx.state.askings.ron == null) throw new Error();
const pon = tx.state.askings.pon;
const cii = tx.state.askings.cii;
const kan = tx.state.askings.kan;
const ron = tx.state.askings.ron;
tx.state.askings.pon = null;
tx.state.askings.cii = null;
tx.state.askings.kan = null;
tx.state.askings.ron = null;
if (ron != null && answers.ron.length > 0) {
const callers = answers.ron;
const callee = ron.callee;
for (const house of callers) {
const yakus = YAKU_DEFINITIONS.filter(yaku => yaku.calc({
house: house,
handTiles: tx.handTileTypes[house],
huros: tx.state.huros[house],
tsumoTile: null,
ronTile: tx.hoTileTypes[callee].at(-1)!,
riichi: tx.state.riichis[house],
ippatsu: tx.state.ippatsus[house],
}));
const doraCount =
Common.calcOwnedDoraCount(tx.handTileTypes[house], tx.state.huros[house], tx.doras) +
Common.calcRedDoraCount(tx.state.handTiles[house], tx.state.huros[house]);
const fans = yakus.map(yaku => yaku.fan).reduce((a, b) => a + b, 0) + doraCount;
const point = Common.fanToPoint(fans, house === 'e');
tx.state.points[callee] -= point;
tx.state.points[house] += point;
console.log('fans point', fans, point);
console.log('yakus', house, yakus);
}
tx.commit();
return {
type: 'ronned' as const,
callers: ron.callers,
callee: ron.callee,
turn: null,
};
} else if (kan != null && answers.kan) {
// 大明槓
const tile = tx.state.hoTiles[kan.callee].pop()!;
const t1 = tx.state.handTiles[kan.caller].filter(t => $type(t) === $type(tile)).at(0);
if (t1 == null) throw new Error('No such tile');
const t2 = tx.state.handTiles[kan.caller].filter(t => $type(t) === $type(tile)).at(1);
if (t2 == null) throw new Error('No such tile');
const t3 = tx.state.handTiles[kan.caller].filter(t => $type(t) === $type(tile)).at(2);
if (t3 == null) throw new Error('No such tile');
tx.state.handTiles[kan.caller].splice(tx.state.handTiles[kan.caller].indexOf(t1), 1);
tx.state.handTiles[kan.caller].splice(tx.state.handTiles[kan.caller].indexOf(t2), 1);
tx.state.handTiles[kan.caller].splice(tx.state.handTiles[kan.caller].indexOf(t3), 1);
const tiles = [tile, t1, t2, t3];
tx.state.huros[kan.caller].push({ type: 'minkan', tiles: tiles, from: kan.callee });
tx.state.ippatsus.e = false;
tx.state.ippatsus.s = false;
tx.state.ippatsus.w = false;
tx.state.ippatsus.n = false;
tx.state.activatedDorasCount++;
const rinsyan = tx.tsumo();
tx.state.turn = kan.caller;
tx.commit();
return {
type: 'kanned' as const,
caller: kan.caller,
callee: kan.callee,
tiles: tiles,
rinsyan,
turn: tx.state.turn,
};
} else if (pon != null && answers.pon) {
const tile = tx.state.hoTiles[pon.callee].pop()!;
const t1 = tx.state.handTiles[pon.caller].filter(t => $type(t) === $type(tile)).at(0);
if (t1 == null) throw new Error('No such tile');
const t2 = tx.state.handTiles[pon.caller].filter(t => $type(t) === $type(tile)).at(1);
if (t2 == null) throw new Error('No such tile');
tx.state.handTiles[pon.caller].splice(tx.state.handTiles[pon.caller].indexOf(t1), 1);
tx.state.handTiles[pon.caller].splice(tx.state.handTiles[pon.caller].indexOf(t2), 1);
const tiles = [tile, t1, t2];
tx.state.huros[pon.caller].push({ type: 'pon', tiles: tiles, from: pon.callee });
tx.state.ippatsus.e = false;
tx.state.ippatsus.s = false;
tx.state.ippatsus.w = false;
tx.state.ippatsus.n = false;
tx.state.turn = pon.caller;
tx.commit();
return {
type: 'ponned' as const,
caller: pon.caller,
callee: pon.callee,
tiles: tiles,
turn: tx.state.turn,
};
} else if (cii != null && answers.cii) {
const tile = tx.state.hoTiles[cii.callee].pop()!;
let tiles: [TileId, TileId, TileId];
switch (answers.cii) {
case 'x__': {
const a = Common.NEXT_TILE_FOR_SHUNTSU[$type(tile)];
if (a == null) throw new Error();
const b = Common.NEXT_TILE_FOR_SHUNTSU[a];
if (b == null) throw new Error();
const aTile = tx.state.handTiles[cii.caller].find(t => $type(t) === a);
if (aTile == null) throw new Error();
const bTile = tx.state.handTiles[cii.caller].find(t => $type(t) === b);
if (bTile == null) throw new Error();
tx.state.handTiles[cii.caller].splice(tx.state.handTiles[cii.caller].indexOf(aTile), 1);
tx.state.handTiles[cii.caller].splice(tx.state.handTiles[cii.caller].indexOf(bTile), 1);
tiles = [tile, aTile, bTile];
break;
}
case '_x_': {
const a = Common.PREV_TILE_FOR_SHUNTSU[$type(tile)];
if (a == null) throw new Error();
const b = Common.NEXT_TILE_FOR_SHUNTSU[$type(tile)];
if (b == null) throw new Error();
const aTile = tx.state.handTiles[cii.caller].find(t => $type(t) === a);
if (aTile == null) throw new Error();
const bTile = tx.state.handTiles[cii.caller].find(t => $type(t) === b);
if (bTile == null) throw new Error();
tx.state.handTiles[cii.caller].splice(tx.state.handTiles[cii.caller].indexOf(aTile), 1);
tx.state.handTiles[cii.caller].splice(tx.state.handTiles[cii.caller].indexOf(bTile), 1);
tiles = [aTile, tile, bTile];
break;
}
case '__x': {
const a = Common.PREV_TILE_FOR_SHUNTSU[$type(tile)];
if (a == null) throw new Error();
const b = Common.PREV_TILE_FOR_SHUNTSU[a];
if (b == null) throw new Error();
const aTile = tx.state.handTiles[cii.caller].find(t => $type(t) === a);
if (aTile == null) throw new Error();
const bTile = tx.state.handTiles[cii.caller].find(t => $type(t) === b);
if (bTile == null) throw new Error();
tx.state.handTiles[cii.caller].splice(tx.state.handTiles[cii.caller].indexOf(aTile), 1);
tx.state.handTiles[cii.caller].splice(tx.state.handTiles[cii.caller].indexOf(bTile), 1);
tiles = [bTile, aTile, tile];
break;
}
}
tx.state.huros[cii.caller].push({ type: 'cii', tiles: tiles, from: cii.callee });
tx.state.ippatsus.e = false;
tx.state.ippatsus.s = false;
tx.state.ippatsus.w = false;
tx.state.ippatsus.n = false;
tx.state.turn = cii.caller;
tx.commit();
return {
type: 'ciied' as const,
caller: cii.caller,
callee: cii.callee,
tiles: tiles,
turn: tx.state.turn,
};
} else if (tx.state.tiles.length === 0) {
// 流局
tx.state.turn = null;
tx.state.nextTurnAfterAsking = null;
tx.commit();
return {
type: 'ryuukyoku' as const,
};
} else {
tx.state.turn = tx.state.nextTurnAfterAsking!;
tx.state.nextTurnAfterAsking = null;
const tile = tx.tsumo();
tx.commit();
return {
type: 'tsumo' as const,
house: tx.state.turn,
tile,
turn: tx.state.turn,
};
}
}
public createPlayerState(index: 1 | 2 | 3 | 4): PlayerState {
const house = this.getHouse(index);
return {
user1House: this.state.user1House,
user2House: this.state.user2House,
user3House: this.state.user3House,
user4House: this.state.user4House,
round: this.state.round,
kyoku: this.state.kyoku,
turnCount: this.state.turnCount,
tilesCount: this.state.tiles.length,
doraIndicateTiles: this.state.kingTiles.slice(0, this.state.activatedDorasCount),
handTiles: {
e: house === 'e' ? this.state.handTiles.e : this.state.handTiles.e.map(() => 0),
s: house === 's' ? this.state.handTiles.s : this.state.handTiles.s.map(() => 0),
w: house === 'w' ? this.state.handTiles.w : this.state.handTiles.w.map(() => 0),
n: house === 'n' ? this.state.handTiles.n : this.state.handTiles.n.map(() => 0),
},
hoTiles: {
e: this.state.hoTiles.e,
s: this.state.hoTiles.s,
w: this.state.hoTiles.w,
n: this.state.hoTiles.n,
},
huros: {
e: this.state.huros.e,
s: this.state.huros.s,
w: this.state.huros.w,
n: this.state.huros.n,
},
riichis: {
e: this.state.riichis.e,
s: this.state.riichis.s,
w: this.state.riichis.w,
n: this.state.riichis.n,
},
ippatsus: {
e: this.state.ippatsus.e,
s: this.state.ippatsus.s,
w: this.state.ippatsus.w,
n: this.state.ippatsus.n,
},
points: {
e: this.state.points.e,
s: this.state.points.s,
w: this.state.points.w,
n: this.state.points.n,
},
latestDahaiedTile: null,
turn: this.state.turn,
};
}
public calcCrc32ForUser1(): number {
// TODO
}
public calcCrc32ForUser2(): number {
// TODO
}
public calcCrc32ForUser3(): number {
// TODO
}
public calcCrc32ForUser4(): number {
// TODO
}
public getState(): MasterState {
return structuredClone(this.state);
}
}
function commit_dahai(state: MasterState): MasterState {
}