1
0
mirror of https://github.com/misskey-dev/misskey.git synced 2026-06-03 03:44:31 +02:00
This commit is contained in:
syuilo
2024-01-29 10:46:23 +09:00
parent 0111b8736a
commit dad8430040
11 changed files with 880 additions and 775 deletions

View File

@@ -46,3 +46,190 @@ export const TILE_TYPES = [
export type Tile = typeof TILE_TYPES[number];
export type House = 'e' | 's' | 'w' | 'n';
export type Huro = {
type: 'pon';
tile: Tile;
from: House;
} | {
type: 'cii';
tiles: [Tile, Tile, Tile];
from: House;
} | {
type: 'minkan';
tile: Tile;
from: House;
} | {
type: 'ankan';
tile: Tile;
from: House;
};
export const yakuNames = [
'riichi',
'ippatsu',
'tsumo',
'tanyao',
'pinfu',
'iipeko',
'field-wind',
'seat-wind',
'white',
'green',
'red',
'rinshan',
'chankan',
'haitei',
'hotei',
'sanshoku-dojun',
'sanshoku-doko',
'ittsu',
'chanta',
'chitoitsu',
'toitoi',
'sananko',
'honroto',
'sankantsu',
'shosangen',
'double-riichi',
'honitsu',
'junchan',
'ryampeko',
'chinitsu',
'dora',
'red-dora',
] as const;
export const yakumanNames = [
'kokushi',
'kokushi-13',
'suanko',
'suanko-tanki',
'daisangen',
'tsuiso',
'shosushi',
'daisushi',
'ryuiso',
'chinroto',
'sukantsu',
'churen',
'pure-churen',
'tenho',
'chiho',
] as const;
type EnvForCalcYaku = {
house: House;
/**
* 和了る人の手牌(副露した牌は含まない)
*/
handTiles: Tile[];
/**
* 河
*/
hoTiles: Tile[];
/**
* 副露
*/
huros: Huro[];
/**
* ツモ牌
*/
tsumoTile: Tile | null;
/**
* ロン牌
*/
ronTile: Tile | null;
/**
* ドラ表示牌
*/
doraTiles: Tile[];
/**
* 赤ドラ表示牌
*/
redDoraTiles: Tile[];
/**
* 場風
*/
fieldWind: House;
/**
* 自風
*/
seatWind: House;
/**
* リーチしたかどうか
*/
riichi: boolean;
};
const YAKU_DEFINITIONS = [{
name: 'riichi',
fan: 1,
calc: (state: EnvForCalcYaku) => {
return state.riichi;
},
}, {
name: 'red',
fan: 1,
calc: (state: EnvForCalcYaku) => {
return (
(state.handTiles.filter(t => t === 'chun').length >= 3) ||
(state.huros.filter(huro =>
huro.type === 'pon' ? huro.tile === 'chun' :
huro.type === 'ankan' ? huro.tile === 'chun' :
huro.type === 'minkan' ? huro.tile === 'chun' :
false).length >= 3)
);
},
}, {
name: 'white',
fan: 1,
calc: (state: EnvForCalcYaku) => {
return (
(state.handTiles.filter(t => t === 'haku').length >= 3) ||
(state.huros.filter(huro =>
huro.type === 'pon' ? huro.tile === 'haku' :
huro.type === 'ankan' ? huro.tile === 'haku' :
huro.type === 'minkan' ? huro.tile === 'haku' :
false).length >= 3)
);
},
}, {
name: 'green',
fan: 1,
calc: (state: EnvForCalcYaku) => {
return (
(state.handTiles.filter(t => t === 'hatsu').length >= 3) ||
(state.huros.filter(huro =>
huro.type === 'pon' ? huro.tile === 'hatsu' :
huro.type === 'ankan' ? huro.tile === 'hatsu' :
huro.type === 'minkan' ? huro.tile === 'hatsu' :
false).length >= 3)
);
},
}, {
name: 'tanyao',
fan: 1,
calc: (state: EnvForCalcYaku) => {
const yaochuTiles: Tile[] = ['m1', 'm9', 'p1', 'p9', 's1', 's9', 'e', 's', 'w', 'n', 'haku', 'hatsu', 'chun'];
return (
(state.handTiles.filter(t => yaochuTiles.includes(t)).length === 0) &&
(state.huros.filter(huro =>
huro.type === 'pon' ? yaochuTiles.includes(huro.tile) :
huro.type === 'ankan' ? yaochuTiles.includes(huro.tile) :
huro.type === 'minkan' ? yaochuTiles.includes(huro.tile) :
huro.type === 'cii' ? huro.tiles.some(t2 => yaochuTiles.includes(t2)) :
false).length === 0)
);
},
}];

View File

@@ -0,0 +1,455 @@
/*
* SPDX-FileCopyrightText: syuilo and other misskey contributors
* SPDX-License-Identifier: AGPL-3.0-only
*/
import CRC32 from 'crc-32';
import { Tile, House, Huro, TILE_TYPES } from './common.js';
import * as Utils from './utils.js';
import { PlayerState } from './engine.player.js';
export type MasterState = {
user1House: House;
user2House: House;
user3House: House;
user4House: House;
tiles: Tile[];
/**
* 副露した牌を含まない手牌
*/
handTiles: {
e: Tile[];
s: Tile[];
w: Tile[];
n: Tile[];
};
hoTiles: {
e: Tile[];
s: Tile[];
w: Tile[];
n: Tile[];
};
huros: {
e: Huro[];
s: Huro[];
w: Huro[];
n: Huro[];
};
riichis: {
e: boolean;
s: boolean;
w: boolean;
n: boolean;
};
points: {
e: number;
s: number;
w: number;
n: number;
};
turn: House | null;
nextTurnAfterAsking: House | null;
ronAsking: {
/**
* 牌を捨てた人
*/
source: House;
/**
* ロンする権利がある人
*/
targets: House[];
} | null;
ponAsking: {
/**
* 牌を捨てた人
*/
source: House;
/**
* ポンする権利がある人
*/
target: House;
} | null;
ciiAsking: {
/**
* 牌を捨てた人
*/
source: House;
/**
* チーする権利がある人(sourceの下家なのは自明だがプログラム簡略化のため)
*/
target: House;
} | null;
kanAsking: {
/**
* 牌を捨てた人
*/
source: House;
/**
* カンする権利がある人
*/
target: House;
} | null;
};
export class MasterGameEngine {
public state: MasterState;
constructor(state: MasterState) {
this.state = state;
}
public static createInitialState(): MasterState {
const tiles = [...TILE_TYPES.slice(), ...TILE_TYPES.slice(), ...TILE_TYPES.slice(), ...TILE_TYPES.slice()];
tiles.sort(() => Math.random() - 0.5);
const eHandTiles = tiles.splice(0, 14);
const sHandTiles = tiles.splice(0, 13);
const wHandTiles = tiles.splice(0, 13);
const nHandTiles = tiles.splice(0, 13);
return {
user1House: 'e',
user2House: 's',
user3House: 'w',
user4House: 'n',
tiles,
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,
},
points: {
e: 25000,
s: 25000,
w: 25000,
n: 25000,
},
turn: 'e',
nextTurnAfterAsking: null,
ponAsking: null,
ciiAsking: null,
kanAsking: null,
ronAsking: null,
};
}
private tsumo(): Tile {
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;
}
private canRon(house: House, tile: Tile): boolean {
// フリテン
// TODO: ポンされるなどして自分の河にない場合の考慮
if (this.state.hoTiles[house].includes(tile)) return false;
const horaSets = Utils.getHoraSets(this.state.handTiles[house].concat(tile));
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;
}
private canPon(house: House, tile: Tile): boolean {
return this.state.handTiles[house].filter(t => t === tile).length === 2;
}
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 commit_dahai(house: House, tile: Tile, riichi = false) {
if (this.state.turn !== house) throw new Error('Not your turn');
const handTiles = this.state.handTiles[house];
if (!handTiles.includes(tile)) throw new Error('No such tile in your hand');
handTiles.splice(handTiles.indexOf(tile), 1);
this.state.hoTiles[house].push(tile);
if (riichi) {
this.state.riichis[house] = true;
}
const canRonHouses: House[] = [];
switch (house) {
case 'e':
if (this.canRon('s', tile)) canRonHouses.push('s');
if (this.canRon('w', tile)) canRonHouses.push('w');
if (this.canRon('n', tile)) canRonHouses.push('n');
break;
case 's':
if (this.canRon('e', tile)) canRonHouses.push('e');
if (this.canRon('w', tile)) canRonHouses.push('w');
if (this.canRon('n', tile)) canRonHouses.push('n');
break;
case 'w':
if (this.canRon('e', tile)) canRonHouses.push('e');
if (this.canRon('s', tile)) canRonHouses.push('s');
if (this.canRon('n', tile)) canRonHouses.push('n');
break;
case 'n':
if (this.canRon('e', tile)) canRonHouses.push('e');
if (this.canRon('s', tile)) canRonHouses.push('s');
if (this.canRon('w', tile)) canRonHouses.push('w');
break;
}
const canKanHouse: House | null = null;
let canPonHouse: House | null = null;
switch (house) {
case 'e':
canPonHouse = this.canPon('s', tile) ? 's' : this.canPon('w', tile) ? 'w' : this.canPon('n', tile) ? 'n' : null;
break;
case 's':
canPonHouse = this.canPon('e', tile) ? 'e' : this.canPon('w', tile) ? 'w' : this.canPon('n', tile) ? 'n' : null;
break;
case 'w':
canPonHouse = this.canPon('e', tile) ? 'e' : this.canPon('s', tile) ? 's' : this.canPon('n', tile) ? 'n' : null;
break;
case 'n':
canPonHouse = this.canPon('e', tile) ? 'e' : this.canPon('s', tile) ? 's' : this.canPon('w', tile) ? 'w' : null;
break;
}
const canCiiHouse: House | null = null;
// TODO
//let canCii: boolean = false;
//if (house === 'e') {
// canCii = this.state.sHandTiles...
//} else if (house === 's') {
// canCii = this.state.wHandTiles...
//} else if (house === 'w') {
// canCii = this.state.nHandTiles...
//} else if (house === 'n') {
// canCii = this.state.eHandTiles...
//}
if (canRonHouses.length > 0 || canPonHouse != null) {
if (canRonHouses.length > 0) {
this.state.ronAsking = {
source: house,
targets: canRonHouses,
};
}
if (canKanHouse != null) {
this.state.kanAsking = {
source: house,
target: canKanHouse,
};
}
if (canPonHouse != null) {
this.state.ponAsking = {
source: house,
target: canPonHouse,
};
}
if (canCiiHouse != null) {
this.state.ciiAsking = {
source: house,
target: canCiiHouse,
};
}
this.state.turn = null;
this.state.nextTurnAfterAsking = Utils.nextHouse(house);
return {
asking: true,
canRonHouses: canRonHouses,
canKanHouse: canKanHouse,
canPonHouse: canPonHouse,
canCiiHouse: canCiiHouse,
};
}
this.state.turn = Utils.nextHouse(house);
const tsumoTile = this.tsumo();
return {
asking: false,
tsumoTile: tsumoTile,
};
}
public commit_resolveCallAndRonInterruption(answers: {
pon: boolean;
cii: boolean;
kan: boolean;
ron: House[];
}) {
if (this.state.ponAsking == null && this.state.ciiAsking == null && this.state.kanAsking == null && this.state.ronAsking == null) throw new Error();
const clearAsking = () => {
this.state.ponAsking = null;
this.state.ciiAsking = null;
this.state.kanAsking = null;
this.state.ronAsking = null;
};
if (this.state.ronAsking != null && answers.ron.length > 0) {
// TODO
return;
}
if (this.state.kanAsking != null && answers.kan) {
const source = this.state.kanAsking.source;
const target = this.state.kanAsking.target;
const tile = this.state.hoTiles[source].pop()!;
this.state.huros[target].push({ type: 'minkan', tile, from: source });
clearAsking();
this.state.turn = target;
// TODO
return;
}
if (this.state.ponAsking != null && answers.pon) {
const source = this.state.ponAsking.source;
const target = this.state.ponAsking.target;
const tile = this.state.hoTiles[source].pop()!;
this.state.handTiles[target].splice(this.state.handTiles[target].indexOf(tile), 1);
this.state.handTiles[target].splice(this.state.handTiles[target].indexOf(tile), 1);
this.state.huros[target].push({ type: 'pon', tile, from: source });
clearAsking();
this.state.turn = target;
return {
type: 'ponned',
source,
target,
tile,
};
}
if (this.state.ciiAsking != null && answers.cii) {
const source = this.state.ciiAsking.source;
const target = this.state.ciiAsking.target;
const tile = this.state.hoTiles[source].pop()!;
this.state.huros[target].push({ type: 'cii', tile, from: source });
clearAsking();
this.state.turn = target;
return {
type: 'ciied',
source,
target,
tile,
};
}
clearAsking();
this.state.turn = this.state.nextTurnAfterAsking;
this.state.nextTurnAfterAsking = null;
const tile = this.tsumo();
return {
type: 'tsumo',
house: this.state.turn,
tile,
};
}
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,
tilesCount: this.state.tiles.length,
handTiles: {
e: house === 'e' ? this.state.handTiles.e : this.state.handTiles.e.map(() => null),
s: house === 's' ? this.state.handTiles.s : this.state.handTiles.s.map(() => null),
w: house === 'w' ? this.state.handTiles.w : this.state.handTiles.w.map(() => null),
n: house === 'n' ? this.state.handTiles.n : this.state.handTiles.n.map(() => null),
},
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,
},
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
}
}

View File

@@ -0,0 +1,177 @@
/*
* SPDX-FileCopyrightText: syuilo and other misskey contributors
* SPDX-License-Identifier: AGPL-3.0-only
*/
import CRC32 from 'crc-32';
import { Tile, House, Huro, TILE_TYPES } from './common.js';
import * as Utils from './utils.js';
export type PlayerState = {
user1House: House;
user2House: House;
user3House: House;
user4House: House;
tilesCount: number;
/**
* 副露した牌を含まない手牌
*/
handTiles: {
e: Tile[] | null[];
s: Tile[] | null[];
w: Tile[] | null[];
n: Tile[] | null[];
};
hoTiles: {
e: Tile[];
s: Tile[];
w: Tile[];
n: Tile[];
};
huros: {
e: Huro[];
s: Huro[];
w: Huro[];
n: Huro[];
};
riichis: {
e: boolean;
s: boolean;
w: boolean;
n: boolean;
};
points: {
e: number;
s: number;
w: number;
n: number;
};
latestDahaiedTile: Tile | null;
turn: House | null;
canPonSource: House | null;
canCiiSource: House | null;
canKanSource: House | null;
canRonSource: House | null;
canCiiTo: House | null;
canKanTo: House | null;
canRonTo: House | null;
};
export class PlayerGameEngine {
/**
* このエラーが発生したときはdesyncが疑われる
*/
public static InvalidOperationError = class extends Error {};
private myUserNumber: 1 | 2 | 3 | 4;
public state: PlayerState;
constructor(myUserNumber: PlayerGameEngine['myUserNumber'], state: PlayerState) {
this.myUserNumber = myUserNumber;
this.state = state;
}
public get myHouse(): House {
switch (this.myUserNumber) {
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 get myHandTiles(): Tile[] {
return this.state.handTiles[this.myHouse] as Tile[];
}
public get isMeRiichi(): boolean {
return this.state.riichis[this.myHouse];
}
public commit_tsumo(house: House, tile: Tile) {
console.log('commit_tsumo', this.state.turn, house, tile);
this.state.turn = house;
if (house === this.myHouse) {
this.myHandTiles.push(tile);
} else {
this.state.handTiles[house].push(null);
}
}
public commit_dahai(house: House, tile: Tile, riichi = false) {
console.log('commit_dahai', this.state.turn, house, tile, riichi);
if (this.state.turn !== house) throw new PlayerGameEngine.InvalidOperationError();
if (riichi) {
this.state.riichis[house] = true;
}
if (house === this.myHouse) {
this.myHandTiles.splice(this.myHandTiles.indexOf(tile), 1);
this.state.hoTiles[this.myHouse].push(tile);
} else {
this.state.handTiles[house].pop();
this.state.hoTiles[house].push(tile);
}
this.state.turn = null;
if (house === this.myHouse) {
} else {
const canRon = Utils.getHoraSets(this.myHandTiles.concat(tile)).length > 0;
const canPon = this.myHandTiles.filter(t => t === tile).length === 2;
// TODO: canCii
if (canRon) this.state.canRonSource = house;
if (canPon) this.state.canPonSource = house;
}
}
/**
* ロンします
* @param source 牌を捨てた人
* @param target ロンした人
*/
public commit_ron(source: House, target: House) {
this.state.canRonSource = null;
const lastTile = this.state.hoTiles[source].pop();
if (lastTile == null) throw new PlayerGameEngine.InvalidOperationError();
if (target === this.myHouse) {
this.myHandTiles.push(lastTile);
} else {
this.state.handTiles[target].push(null);
}
this.state.turn = null;
}
/**
* ポンします
* @param source 牌を捨てた人
* @param target ポンした人
*/
public commit_pon(source: House, target: House) {
this.state.canPonSource = null;
const lastTile = this.state.hoTiles[source].pop();
if (lastTile == null) throw new PlayerGameEngine.InvalidOperationError();
if (target === this.myHouse) {
this.myHandTiles.splice(this.myHandTiles.indexOf(lastTile), 1);
this.myHandTiles.splice(this.myHandTiles.indexOf(lastTile), 1);
} else {
this.state.handTiles[target].unshift();
this.state.handTiles[target].unshift();
}
this.state.huros[target].push({ type: 'pon', tile: lastTile, from: source });
this.state.turn = target;
}
public commit_nop() {
this.state.canRonSource = null;
this.state.canPonSource = null;
}
}

View File

@@ -1,723 +0,0 @@
/*
* SPDX-FileCopyrightText: syuilo and other misskey contributors
* SPDX-License-Identifier: AGPL-3.0-only
*/
import CRC32 from 'crc-32';
import { Tile, House, TILE_TYPES } from './common.js';
import * as Utils from './utils.js';
type Huro = {
type: 'pon';
tile: Tile;
from: House;
} | {
type: 'cii';
tiles: [Tile, Tile, Tile];
from: House;
} | {
type: 'kan';
tile: Tile;
from: House;
} | {
type: 'kakan';
tile: Tile;
from: House;
} | {
type: 'ankan';
tile: Tile;
from: House;
};
export type MasterState = {
user1House: House;
user2House: House;
user3House: House;
user4House: House;
tiles: Tile[];
eHandTiles: Tile[];
sHandTiles: Tile[];
wHandTiles: Tile[];
nHandTiles: Tile[];
eHoTiles: Tile[];
sHoTiles: Tile[];
wHoTiles: Tile[];
nHoTiles: Tile[];
eHuros: Huro[];
sHuros: Huro[];
wHuros: Huro[];
nHuros: Huro[];
eRiichi: boolean;
sRiichi: boolean;
wRiichi: boolean;
nRiichi: boolean;
ePoints: number;
sPoints: number;
wPoints: number;
nPoints: number;
turn: House | null;
nextTurnAfterAsking: House | null;
ronAsking: {
/**
* 牌を捨てた人
*/
source: House;
/**
* ロンする権利がある人
*/
targets: House[];
} | null;
ponAsking: {
/**
* 牌を捨てた人
*/
source: House;
/**
* ポンする権利がある人
*/
target: House;
} | null;
ciiAsking: {
/**
* 牌を捨てた人
*/
source: House;
/**
* チーする権利がある人(sourceの下家なのは自明だがプログラム簡略化のため)
*/
target: House;
} | null;
kanAsking: {
/**
* 牌を捨てた人
*/
source: House;
/**
* カンする権利がある人
*/
target: House;
} | null;
};
export class MasterGameEngine {
public state: MasterState;
constructor(state: MasterState) {
this.state = state;
}
public static createInitialState(): MasterState {
const tiles = [...TILE_TYPES.slice(), ...TILE_TYPES.slice(), ...TILE_TYPES.slice(), ...TILE_TYPES.slice()];
tiles.sort(() => Math.random() - 0.5);
const eHandTiles = tiles.splice(0, 14);
const sHandTiles = tiles.splice(0, 13);
const wHandTiles = tiles.splice(0, 13);
const nHandTiles = tiles.splice(0, 13);
return {
user1House: 'e',
user2House: 's',
user3House: 'w',
user4House: 'n',
tiles,
eHandTiles,
sHandTiles,
wHandTiles,
nHandTiles,
eHoTiles: [],
sHoTiles: [],
wHoTiles: [],
nHoTiles: [],
eHuros: [],
sHuros: [],
wHuros: [],
nHuros: [],
eRiichi: false,
sRiichi: false,
wRiichi: false,
nRiichi: false,
ePoints: 25000,
sPoints: 25000,
wPoints: 25000,
nPoints: 25000,
turn: 'e',
nextTurnAfterAsking: null,
ponAsking: null,
ciiAsking: null,
kanAsking: null,
ronAsking: null,
};
}
private tsumo(): Tile {
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.getHandTilesOf(this.state.turn).push(tile);
return tile;
}
private canRon(house: House, tile: Tile): boolean {
// フリテン
// TODO: ポンされるなどして自分の河にない場合の考慮
if (this.getHoTilesOf(house).includes(tile)) return false;
const horaSets = Utils.getHoraSets(this.getHandTilesOf(house).concat(tile));
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;
}
private canPon(house: House, tile: Tile): boolean {
return this.getHandTilesOf(house).filter(t => t === tile).length === 2;
}
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 getHandTilesOf(house: House): Tile[] {
switch (house) {
case 'e': return this.state.eHandTiles;
case 's': return this.state.sHandTiles;
case 'w': return this.state.wHandTiles;
case 'n': return this.state.nHandTiles;
default: throw new Error(`unrecognized house: ${house}`);
}
}
public getHoTilesOf(house: House): Tile[] {
switch (house) {
case 'e': return this.state.eHoTiles;
case 's': return this.state.sHoTiles;
case 'w': return this.state.wHoTiles;
case 'n': return this.state.nHoTiles;
default: throw new Error(`unrecognized house: ${house}`);
}
}
public getHurosOf(house: House): Huro[] {
switch (house) {
case 'e': return this.state.eHuros;
case 's': return this.state.sHuros;
case 'w': return this.state.wHuros;
case 'n': return this.state.nHuros;
default: throw new Error(`unrecognized house: ${house}`);
}
}
public getPointsOf(house: House): number {
switch (house) {
case 'e': return this.state.ePoints;
case 's': return this.state.sPoints;
case 'w': return this.state.wPoints;
case 'n': return this.state.nPoints;
default: throw new Error(`unrecognized house: ${house}`);
}
}
public setPointsOf(house: House, points: number) {
switch (house) {
case 'e': this.state.ePoints = points; break;
case 's': this.state.sPoints = points; break;
case 'w': this.state.wPoints = points; break;
case 'n': this.state.nPoints = points; break;
default: throw new Error(`unrecognized house: ${house}`);
}
}
public isRiichiHouse(house: House): boolean {
switch (house) {
case 'e': return this.state.eRiichi;
case 's': return this.state.sRiichi;
case 'w': return this.state.wRiichi;
case 'n': return this.state.nRiichi;
default: throw new Error(`unrecognized house: ${house}`);
}
}
public op_dahai(house: House, tile: Tile, riichi = false) {
if (this.state.turn !== house) throw new Error('Not your turn');
const handTiles = this.getHandTilesOf(house);
if (!handTiles.includes(tile)) throw new Error('No such tile in your hand');
handTiles.splice(handTiles.indexOf(tile), 1);
this.getHoTilesOf(house).push(tile);
if (riichi) {
switch (house) {
case 'e': this.state.eRiichi = true; break;
case 's': this.state.sRiichi = true; break;
case 'w': this.state.wRiichi = true; break;
case 'n': this.state.nRiichi = true; break;
}
}
const canRonHouses: House[] = [];
switch (house) {
case 'e':
if (this.canRon('s', tile)) canRonHouses.push('s');
if (this.canRon('w', tile)) canRonHouses.push('w');
if (this.canRon('n', tile)) canRonHouses.push('n');
break;
case 's':
if (this.canRon('e', tile)) canRonHouses.push('e');
if (this.canRon('w', tile)) canRonHouses.push('w');
if (this.canRon('n', tile)) canRonHouses.push('n');
break;
case 'w':
if (this.canRon('e', tile)) canRonHouses.push('e');
if (this.canRon('s', tile)) canRonHouses.push('s');
if (this.canRon('n', tile)) canRonHouses.push('n');
break;
case 'n':
if (this.canRon('e', tile)) canRonHouses.push('e');
if (this.canRon('s', tile)) canRonHouses.push('s');
if (this.canRon('w', tile)) canRonHouses.push('w');
break;
}
const canKanHouse: House | null = null;
let canPonHouse: House | null = null;
switch (house) {
case 'e':
canPonHouse = this.canPon('s', tile) ? 's' : this.canPon('w', tile) ? 'w' : this.canPon('n', tile) ? 'n' : null;
break;
case 's':
canPonHouse = this.canPon('e', tile) ? 'e' : this.canPon('w', tile) ? 'w' : this.canPon('n', tile) ? 'n' : null;
break;
case 'w':
canPonHouse = this.canPon('e', tile) ? 'e' : this.canPon('s', tile) ? 's' : this.canPon('n', tile) ? 'n' : null;
break;
case 'n':
canPonHouse = this.canPon('e', tile) ? 'e' : this.canPon('s', tile) ? 's' : this.canPon('w', tile) ? 'w' : null;
break;
}
const canCiiHouse: House | null = null;
// TODO
//let canCii: boolean = false;
//if (house === 'e') {
// canCii = this.state.sHandTiles...
//} else if (house === 's') {
// canCii = this.state.wHandTiles...
//} else if (house === 'w') {
// canCii = this.state.nHandTiles...
//} else if (house === 'n') {
// canCii = this.state.eHandTiles...
//}
if (canRonHouses.length > 0 || canPonHouse != null) {
if (canRonHouses.length > 0) {
this.state.ronAsking = {
source: house,
targets: canRonHouses,
};
}
if (canKanHouse != null) {
this.state.kanAsking = {
source: house,
target: canKanHouse,
};
}
if (canPonHouse != null) {
this.state.ponAsking = {
source: house,
target: canPonHouse,
};
}
if (canCiiHouse != null) {
this.state.ciiAsking = {
source: house,
target: canCiiHouse,
};
}
this.state.turn = null;
this.state.nextTurnAfterAsking = Utils.nextHouse(house);
return {
asking: true,
canRonHouses: canRonHouses,
canKanHouse: canKanHouse,
canPonHouse: canPonHouse,
canCiiHouse: canCiiHouse,
};
}
this.state.turn = Utils.nextHouse(house);
const tsumoTile = this.tsumo();
return {
asking: false,
tsumoTile: tsumoTile,
};
}
public op_resolveCallAndRonInterruption(answers: {
pon: boolean;
cii: boolean;
kan: boolean;
ron: House[];
}) {
if (this.state.ponAsking == null && this.state.ciiAsking == null && this.state.kanAsking == null && this.state.ronAsking == null) throw new Error();
const clearAsking = () => {
this.state.ponAsking = null;
this.state.ciiAsking = null;
this.state.kanAsking = null;
this.state.ronAsking = null;
};
if (this.state.ronAsking != null && answers.ron.length > 0) {
// TODO
return;
}
if (this.state.kanAsking != null && answers.kan) {
const source = this.state.kanAsking.source;
const target = this.state.kanAsking.target;
const tile = this.getHoTilesOf(source).pop()!;
this.getHurosOf(target).push({ type: 'kan', tile, from: source });
clearAsking();
this.state.turn = target;
// TODO
return;
}
if (this.state.ponAsking != null && answers.pon) {
const source = this.state.ponAsking.source;
const target = this.state.ponAsking.target;
const tile = this.getHoTilesOf(source).pop()!;
this.getHandTilesOf(target).splice(this.getHandTilesOf(target).indexOf(tile), 1);
this.getHandTilesOf(target).splice(this.getHandTilesOf(target).indexOf(tile), 1);
this.getHurosOf(target).push({ type: 'pon', tile, from: source });
clearAsking();
this.state.turn = target;
return {
type: 'ponned',
source,
target,
tile,
};
}
if (this.state.ciiAsking != null && answers.cii) {
const source = this.state.ciiAsking.source;
const target = this.state.ciiAsking.target;
const tile = this.getHoTilesOf(source).pop()!;
this.getHurosOf(target).push({ type: 'cii', tile, from: source });
clearAsking();
this.state.turn = target;
return {
type: 'ciied',
source,
target,
tile,
};
}
clearAsking();
this.state.turn = this.state.nextTurnAfterAsking;
this.state.nextTurnAfterAsking = null;
const tile = this.tsumo();
return {
type: 'tsumo',
house: this.state.turn,
tile,
};
}
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,
tilesCount: this.state.tiles.length,
eHandTiles: house === 'e' ? this.state.eHandTiles : this.state.eHandTiles.map(() => null),
sHandTiles: house === 's' ? this.state.sHandTiles : this.state.sHandTiles.map(() => null),
wHandTiles: house === 'w' ? this.state.wHandTiles : this.state.wHandTiles.map(() => null),
nHandTiles: house === 'n' ? this.state.nHandTiles : this.state.nHandTiles.map(() => null),
eHoTiles: this.state.eHoTiles,
sHoTiles: this.state.sHoTiles,
wHoTiles: this.state.wHoTiles,
nHoTiles: this.state.nHoTiles,
eHuros: this.state.eHuros,
sHuros: this.state.sHuros,
wHuros: this.state.wHuros,
nHuros: this.state.nHuros,
eRiichi: this.state.eRiichi,
sRiichi: this.state.sRiichi,
wRiichi: this.state.wRiichi,
nRiichi: this.state.nRiichi,
ePoints: this.state.ePoints,
sPoints: this.state.sPoints,
wPoints: this.state.wPoints,
nPoints: this.state.nPoints,
latestDahaiedTile: null,
turn: this.state.turn,
};
}
public calcCrc32ForUser1(): number {
// TODO
}
public calcCrc32ForUser2(): number {
// TODO
}
public calcCrc32ForUser3(): number {
// TODO
}
public calcCrc32ForUser4(): number {
// TODO
}
}
export type PlayerState = {
user1House: House;
user2House: House;
user3House: House;
user4House: House;
tilesCount: number;
eHandTiles: Tile[] | null[];
sHandTiles: Tile[] | null[];
wHandTiles: Tile[] | null[];
nHandTiles: Tile[] | null[];
eHoTiles: Tile[];
sHoTiles: Tile[];
wHoTiles: Tile[];
nHoTiles: Tile[];
eHuros: Huro[];
sHuros: Huro[];
wHuros: Huro[];
nHuros: Huro[];
eRiichi: boolean;
sRiichi: boolean;
wRiichi: boolean;
nRiichi: boolean;
ePoints: number;
sPoints: number;
wPoints: number;
nPoints: number;
latestDahaiedTile: Tile | null;
turn: House | null;
canPonSource: House | null;
canCiiSource: House | null;
canKanSource: House | null;
canRonSource: House | null;
canCiiTo: House | null;
canKanTo: House | null;
canRonTo: House | null;
};
export class PlayerGameEngine {
/**
* このエラーが発生したときはdesyncが疑われる
*/
public static InvalidOperationError = class extends Error {};
private myUserNumber: 1 | 2 | 3 | 4;
public state: PlayerState;
constructor(myUserNumber: PlayerGameEngine['myUserNumber'], state: PlayerState) {
this.myUserNumber = myUserNumber;
this.state = state;
}
public get myHouse(): House {
switch (this.myUserNumber) {
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 get myHandTiles(): Tile[] {
switch (this.myHouse) {
case 'e': return this.state.eHandTiles as Tile[];
case 's': return this.state.sHandTiles as Tile[];
case 'w': return this.state.wHandTiles as Tile[];
case 'n': return this.state.nHandTiles as Tile[];
}
}
public get myHoTiles(): Tile[] {
switch (this.myHouse) {
case 'e': return this.state.eHoTiles;
case 's': return this.state.sHoTiles;
case 'w': return this.state.wHoTiles;
case 'n': return this.state.nHoTiles;
}
}
public get isMeRiichi(): boolean {
switch (this.myHouse) {
case 'e': return this.state.eRiichi;
case 's': return this.state.sRiichi;
case 'w': return this.state.wRiichi;
case 'n': return this.state.nRiichi;
}
}
public getHandTilesOf(house: House) {
switch (house) {
case 'e': return this.state.eHandTiles;
case 's': return this.state.sHandTiles;
case 'w': return this.state.wHandTiles;
case 'n': return this.state.nHandTiles;
default: throw new Error(`unrecognized house: ${house}`);
}
}
public getHoTilesOf(house: House): Tile[] {
switch (house) {
case 'e': return this.state.eHoTiles;
case 's': return this.state.sHoTiles;
case 'w': return this.state.wHoTiles;
case 'n': return this.state.nHoTiles;
default: throw new Error(`unrecognized house: ${house}`);
}
}
public getHurosOf(house: House): Huro[] {
switch (house) {
case 'e': return this.state.eHuros;
case 's': return this.state.sHuros;
case 'w': return this.state.wHuros;
case 'n': return this.state.nHuros;
default: throw new Error(`unrecognized house: ${house}`);
}
}
public op_tsumo(house: House, tile: Tile) {
console.log('op_tsumo', this.state.turn, house, tile);
this.state.turn = house;
if (house === this.myHouse) {
this.myHandTiles.push(tile);
} else {
this.getHandTilesOf(house).push(null);
}
}
public op_dahai(house: House, tile: Tile, riichi = false) {
console.log('op_dahai', this.state.turn, house, tile, riichi);
if (this.state.turn !== house) throw new PlayerGameEngine.InvalidOperationError();
if (riichi) {
switch (house) {
case 'e': this.state.eRiichi = true; break;
case 's': this.state.sRiichi = true; break;
case 'w': this.state.wRiichi = true; break;
case 'n': this.state.nRiichi = true; break;
}
}
if (house === this.myHouse) {
this.myHandTiles.splice(this.myHandTiles.indexOf(tile), 1);
this.myHoTiles.push(tile);
} else {
this.getHandTilesOf(house).pop();
this.getHoTilesOf(house).push(tile);
}
this.state.turn = null;
if (house === this.myHouse) {
} else {
const canRon = Utils.getHoraSets(this.myHandTiles.concat(tile)).length > 0;
const canPon = this.myHandTiles.filter(t => t === tile).length === 2;
// TODO: canCii
if (canRon) this.state.canRonSource = house;
if (canPon) this.state.canPonSource = house;
}
}
/**
* ロンします
* @param source 牌を捨てた人
* @param target ロンした人
*/
public op_ron(source: House, target: House) {
this.state.canRonSource = null;
const lastTile = this.getHoTilesOf(source).pop();
if (lastTile == null) throw new PlayerGameEngine.InvalidOperationError();
if (target === this.myHouse) {
this.myHandTiles.push(lastTile);
} else {
this.getHandTilesOf(target).push(null);
}
this.state.turn = null;
}
/**
* ポンします
* @param source 牌を捨てた人
* @param target ポンした人
*/
public op_pon(source: House, target: House) {
this.state.canPonSource = null;
const lastTile = this.getHoTilesOf(source).pop();
if (lastTile == null) throw new PlayerGameEngine.InvalidOperationError();
if (target === this.myHouse) {
this.myHandTiles.splice(this.myHandTiles.indexOf(lastTile), 1);
this.myHandTiles.splice(this.myHandTiles.indexOf(lastTile), 1);
} else {
this.getHandTilesOf(target).unshift();
this.getHandTilesOf(target).unshift();
}
this.getHurosOf(target).push({ type: 'pon', tile: lastTile, from: source });
this.state.turn = target;
}
public op_nop() {
this.state.canRonSource = null;
this.state.canPonSource = null;
}
}
const YAKU_DEFINITIONS = [{
name: 'riichi',
fan: 1,
calc: (state: PlayerState, ctx: { tsumoTile: Tile; ronTile: Tile; }) => {
const house = state.turn;
return house === 'e' ? state.eRiichi : house === 's' ? state.sRiichi : house === 'w' ? state.wRiichi : state.nRiichi;
},
}];

View File

@@ -3,7 +3,9 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
export * as Engine from './engine.js';
export * as Serializer from './serializer.js';
export * as Common from './common.js';
export * as Utils from './utils.js';
export { MasterGameEngine, MasterState } from './engine.master.js';
export { PlayerGameEngine, PlayerState } from './engine.player.js';

View File

@@ -3,7 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Tile } from './engine.js';
import { Tile } from './engine.player.js';
export type Log = {
time: number;