This commit is contained in:
syuilo
2024-01-27 17:50:41 +09:00
parent 0f2991cbaf
commit ab404d491d
20 changed files with 1036 additions and 493 deletions

View File

@@ -4,51 +4,8 @@
*/
import CRC32 from 'crc-32';
export const TILE_TYPES = [
'bamboo1',
'bamboo2',
'bamboo3',
'bamboo4',
'bamboo5',
'bamboo6',
'bamboo7',
'bamboo8',
'bamboo9',
'character1',
'character2',
'character3',
'character4',
'character5',
'character6',
'character7',
'character8',
'character9',
'circle1',
'circle2',
'circle3',
'circle4',
'circle5',
'circle6',
'circle7',
'circle8',
'circle9',
'wind-east',
'wind-south',
'wind-west',
'wind-north',
'dragon-red',
'dragon-green',
'dragon-white',
] as const;
export type Tile = typeof TILE_TYPES[number];
export function isTile(tile: string): tile is Tile {
return TILE_TYPES.includes(tile as Tile);
}
export type House = 'e' | 's' | 'w' | 'n';
import { Tile, House, TILE_TYPES } from './common.js';
import * as Utils from './utils.js';
export type MasterState = {
user1House: House;
@@ -81,34 +38,56 @@ export type MasterState = {
wPoints: number;
nPoints: number;
turn: House | null;
ponAsking: {
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 Common {
public static nextHouse(house: House): House {
switch (house) {
case 'e':
return 's';
case 's':
return 'w';
case 'w':
return 'n';
case 'n':
return 'e';
}
}
public static checkYaku(tiles: Tile[]) {
}
}
export class MasterGameEngine {
public state: MasterState;
@@ -117,7 +96,7 @@ export class MasterGameEngine {
}
public static createInitialState(): MasterState {
const tiles = TILE_TYPES.slice();
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);
@@ -161,62 +140,64 @@ export class MasterGameEngine {
};
}
private (): Tile {
private tsumo(): Tile {
const tile = this.state.tiles.pop();
switch (this.state.turn) {
case 'e':
this.state.eHandTiles.push(tile);
break;
case 's':
this.state.sHandTiles.push(tile);
break;
case 'w':
this.state.wHandTiles.push(tile);
break;
case 'n':
this.state.nHandTiles.push(tile);
break;
}
if (tile == null) throw new Error('No tiles left');
this.getHandTilesOf(this.state.turn).push(tile);
return tile;
}
public op_dahai(house: House, tile: Tile) {
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);
const canRonHouses: House[] = [];
switch (house) {
case 'e':
if (!this.state.eHandTiles.includes(tile)) throw new Error('Invalid tile');
this.state.eHandTiles.splice(this.state.eHandTiles.indexOf(tile), 1);
this.state.eHoTiles.push(tile);
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.state.sHandTiles.includes(tile)) throw new Error('Invalid tile');
this.state.sHandTiles.splice(this.state.sHandTiles.indexOf(tile), 1);
this.state.sHoTiles.push(tile);
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.state.wHandTiles.includes(tile)) throw new Error('Invalid tile');
this.state.wHandTiles.splice(this.state.wHandTiles.indexOf(tile), 1);
this.state.wHoTiles.push(tile);
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.state.nHandTiles.includes(tile)) throw new Error('Invalid tile');
this.state.nHandTiles.splice(this.state.nHandTiles.indexOf(tile), 1);
this.state.nHoTiles.push(tile);
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;
if (house === 'e') {
canPonHouse = this.canPon('s', tile) ? 's' : this.canPon('w', tile) ? 'w' : this.canPon('n', tile) ? 'n' : null;
} else if (house === 's') {
canPonHouse = this.canPon('e', tile) ? 'e' : this.canPon('w', tile) ? 'w' : this.canPon('n', tile) ? 'n' : null;
} else if (house === 'w') {
canPonHouse = this.canPon('e', tile) ? 'e' : this.canPon('s', tile) ? 's' : this.canPon('n', tile) ? 'n' : null;
} else if (house === 'n') {
canPonHouse = this.canPon('e', tile) ? 'e' : this.canPon('s', tile) ? 's' : this.canPon('w', tile) ? 'w' : 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') {
@@ -229,96 +210,237 @@ export class MasterGameEngine {
// canCii = this.state.eHandTiles...
//}
if (canPonHouse) {
this.state.ponAsking = {
source: house,
target: canPonHouse,
};
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,
};
}
return {
asking: true,
canRonHouses: canRonHouses,
canKanHouse: canKanHouse,
canPonHouse: canPonHouse,
canCiiHouse: canCiiHouse,
};
}
this.state.turn = Common.nextHouse(house);
this.state.turn = Utils.nextHouse(house);
const tsumoTile = this.();
const tsumoTile = this.tsumo();
return {
tsumo: tsumoTile,
asking: false,
tsumoTile: tsumoTile,
};
}
public op_pon(house: House) {
if (this.state.ponAsking == null) throw new Error('No one is asking for pon');
if (this.state.ponAsking.target !== house) throw new Error('Not you');
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 source = this.state.ponAsking.source;
const target = this.state.ponAsking.target;
this.state.ponAsking = null;
const clearAsking = () => {
this.state.ponAsking = null;
this.state.ciiAsking = null;
this.state.kanAsking = null;
this.state.ronAsking = null;
};
let tile: Tile;
switch (source) {
case 'e':
tile = this.state.eHoTiles.pop();
break;
case 's':
tile = this.state.sHoTiles.pop();
break;
case 'w':
tile = this.state.wHoTiles.pop();
break;
case 'n':
tile = this.state.nHoTiles.pop();
break;
default: throw new Error('Invalid source');
if (this.state.ronAsking != null && answers.ron.length > 0) {
// TODO
return;
}
switch (target) {
case 'e':
this.state.ePonnedTiles.push({ tile, from: source });
break;
case 's':
this.state.sPonnedTiles.push({ tile, from: source });
break;
case 'w':
this.state.wPonnedTiles.push({ tile, from: source });
break;
case 'n':
this.state.nPonnedTiles.push({ tile, from: source });
break;
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.getKannedTilesOf(target).push({ tile, from: source });
clearAsking();
this.state.turn = target;
// TODO
return;
}
this.state.turn = target;
}
if (this.state.ponAsking != null && answers.pon) {
const source = this.state.ponAsking.source;
const target = this.state.ponAsking.target;
public op_noOnePon() {
if (this.state.ponAsking == null) throw new Error('No one is asking for pon');
const tile = this.getHoTilesOf(source).pop();
this.getPonnedTilesOf(target).push({ tile, from: source });
this.state.ponAsking = null;
this.state.turn = Common.nextHouse(this.state.turn);
clearAsking();
this.state.turn = target;
return {
type: 'ponned',
house: this.state.turn,
tile,
};
}
const tile = this.();
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.getCiiedTilesOf(target).push({ tile, from: source });
clearAsking();
this.state.turn = target;
return {
type: 'ciied',
house: this.state.turn,
tile,
};
}
clearAsking();
this.state.turn = Utils.nextHouse(this.state.turn);
const tile = this.tsumo();
return {
type: 'tsumo',
house: this.state.turn,
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; // 完成形じゃない
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 {
switch (house) {
case 'e':
return this.state.eHandTiles.filter(t => t === tile).length === 2;
case 's':
return this.state.sHandTiles.filter(t => t === tile).length === 2;
case 'w':
return this.state.wHandTiles.filter(t => t === tile).length === 2;
case 'n':
return this.state.nHandTiles.filter(t => t === tile).length === 2;
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;
}
}
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;
}
}
public getPonnedTilesOf(house: House): { tile: Tile; from: House; }[] {
switch (house) {
case 'e': return this.state.ePonnedTiles;
case 's': return this.state.sPonnedTiles;
case 'w': return this.state.wPonnedTiles;
case 'n': return this.state.nPonnedTiles;
}
}
public getCiiedTilesOf(house: House): { tiles: [Tile, Tile, Tile]; from: House; }[] {
switch (house) {
case 'e': return this.state.eCiiedTiles;
case 's': return this.state.sCiiedTiles;
case 'w': return this.state.wCiiedTiles;
case 'n': return this.state.nCiiedTiles;
}
}
public getKannedTilesOf(house: House): { tile: Tile; from: House; }[] {
switch (house) {
case 'e': return this.state.ePonnedTiles;
case 's': return this.state.sPonnedTiles;
case 'w': return this.state.wPonnedTiles;
case 'n': return this.state.nPonnedTiles;
}
}
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,
ePonnedTiles: this.state.ePonnedTiles,
sPonnedTiles: this.state.sPonnedTiles,
wPonnedTiles: this.state.wPonnedTiles,
nPonnedTiles: this.state.nPonnedTiles,
eCiiedTiles: this.state.eCiiedTiles,
sCiiedTiles: this.state.sCiiedTiles,
wCiiedTiles: this.state.wCiiedTiles,
nCiiedTiles: this.state.nCiiedTiles,
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
}
@@ -368,6 +490,10 @@ export type PlayerState = {
nPoints: number;
latestDahaiedTile: Tile | null;
turn: House | null;
canPonTo: House | null;
canCiiTo: House | null;
canKanTo: House | null;
canRonTo: House | null;
};
export class PlayerGameEngine {
@@ -411,113 +537,104 @@ export class PlayerGameEngine {
}
}
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;
}
}
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;
}
}
public getPonnedTilesOf(house: House): { tile: Tile; from: House; }[] {
switch (house) {
case 'e': return this.state.ePonnedTiles;
case 's': return this.state.sPonnedTiles;
case 'w': return this.state.wPonnedTiles;
case 'n': return this.state.nPonnedTiles;
}
}
public getCiiedTilesOf(house: House): { tiles: [Tile, Tile, Tile]; from: House; }[] {
switch (house) {
case 'e': return this.state.eCiiedTiles;
case 's': return this.state.sCiiedTiles;
case 'w': return this.state.wCiiedTiles;
case 'n': return this.state.nCiiedTiles;
}
}
public getKannedTilesOf(house: House): { tile: Tile; from: House; }[] {
switch (house) {
case 'e': return this.state.ePonnedTiles;
case 's': return this.state.sPonnedTiles;
case 'w': return this.state.wPonnedTiles;
case 'n': return this.state.nPonnedTiles;
}
}
public op_tsumo(house: House, tile: Tile) {
if (house === this.myHouse) {
this.myHandTiles.push(tile);
} else {
switch (house) {
case 'e':
this.state.eHandTiles.push(null);
break;
case 's':
this.state.sHandTiles.push(null);
break;
case 'w':
this.state.wHandTiles.push(null);
break;
case 'n':
this.state.nHandTiles.push(null);
break;
}
this.getHandTilesOf(house).push(null);
}
}
public op_dahai(house: House, tile: Tile) {
console.log(this.state.turn, house, tile);
if (this.state.turn !== house) throw new PlayerGameEngine.InvalidOperationError();
if (house === this.myHouse) {
this.myHandTiles.splice(this.myHandTiles.indexOf(tile), 1);
this.myHoTiles.push(tile);
} else {
switch (house) {
case 'e':
this.state.eHandTiles.pop();
this.state.eHoTiles.push(tile);
break;
case 's':
this.state.sHandTiles.pop();
this.state.sHoTiles.push(tile);
break;
case 'w':
this.state.wHandTiles.pop();
this.state.wHoTiles.push(tile);
break;
case 'n':
this.state.nHandTiles.pop();
this.state.nHoTiles.push(tile);
break;
}
this.getHandTilesOf(house).pop();
this.getHoTilesOf(house).push(tile);
}
this.state.turn = Utils.nextHouse(this.state.turn);
if (house === this.myHouse) {
this.state.turn = null;
} else {
const canPon = this.myHandTiles.filter(t => t === tile).length === 2;
// TODO: canCii
return {
canPon,
};
if (canPon) this.state.canPonTo = house;
}
}
public op_pon(source: House, target: House) {
let tile: Tile;
this.state.canPonTo = null;
switch (source) {
case 'e': {
const lastTile = this.state.eHoTiles.pop();
if (lastTile == null) throw new PlayerGameEngine.InvalidOperationError();
tile = lastTile;
break;
}
case 's': {
const lastTile = this.state.sHoTiles.pop();
if (lastTile == null) throw new PlayerGameEngine.InvalidOperationError();
tile = lastTile;
break;
}
case 'w': {
const lastTile = this.state.wHoTiles.pop();
if (lastTile == null) throw new PlayerGameEngine.InvalidOperationError();
tile = lastTile;
break;
}
case 'n': {
const lastTile = this.state.nHoTiles.pop();
if (lastTile == null) throw new PlayerGameEngine.InvalidOperationError();
tile = lastTile;
break;
}
default: throw new Error('Invalid source');
}
switch (target) {
case 'e':
this.state.ePonnedTiles.push({ tile, from: source });
break;
case 's':
this.state.sPonnedTiles.push({ tile, from: source });
break;
case 'w':
this.state.wPonnedTiles.push({ tile, from: source });
break;
case 'n':
this.state.nPonnedTiles.push({ tile, from: source });
break;
}
const lastTile = this.getHoTilesOf(source).pop();
if (lastTile == null) throw new PlayerGameEngine.InvalidOperationError();
this.getPonnedTilesOf(target).push({ tile: lastTile, from: source });
this.state.turn = target;
}
public op_nop() {
this.state.canPonTo = 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;
},
}];