This commit is contained in:
syuilo 2024-02-02 20:56:39 +09:00
parent 9ea29fe84c
commit 7cdaa10d46
9 changed files with 303 additions and 303 deletions

View file

@ -428,8 +428,7 @@ export class MahjongService implements OnApplicationShutdown, OnModuleInit {
if (aiHouses.includes(res.canPonHouse)) {
// TODO: ちゃんと思考するようにする
//answers.pon = Math.random() < 0.25;
answers.pon = false;
answers.pon = Math.random() < 0.25;
}
if (aiHouses.includes(res.canCiiHouse)) {
// TODO: ちゃんと思考するようにする
@ -438,8 +437,7 @@ export class MahjongService implements OnApplicationShutdown, OnModuleInit {
}
if (aiHouses.includes(res.canKanHouse)) {
// TODO: ちゃんと思考するようにする
//answers.kan = Math.random() < 0.25;
answers.kan = false;
answers.kan = Math.random() < 0.25;
}
for (const h of res.canRonHouses) {
if (aiHouses.includes(h)) {
@ -501,7 +499,7 @@ export class MahjongService implements OnApplicationShutdown, OnModuleInit {
const room = await this.getRoom(roomId);
if (room == null) return;
if (room.gameState == null) return;
if (!Mahjong.Utils.isTile(tile)) return;
if (!Mahjong.Common.isTile(tile)) return;
const engine = new Mahjong.MasterGameEngine(room.gameState);
const myHouse = getHouseOfUserId(room, engine, user.id);
@ -629,7 +627,7 @@ export class MahjongService implements OnApplicationShutdown, OnModuleInit {
if (engine.state.riichis[house]) {
// リーチ時はアガリ牌でない限りツモ切り
const handTiles = engine.state.handTiles[house];
const horaSets = Mahjong.Utils.getHoraSets(handTiles);
const horaSets = Mahjong.Common.getHoraSets(handTiles);
if (horaSets.length === 0) {
setTimeout(() => {
this.dahai(room, engine, house, handTiles.at(-1));

Binary file not shown.

Before

Width:  |  Height:  |  Size: 318 KiB

After

Width:  |  Height:  |  Size: 177 KiB

View file

@ -10,20 +10,20 @@ SPDX-License-Identifier: AGPL-3.0-only
<div style="text-align: center;">
<div :class="$style.centerPanelTickerToi">
<div style="position: absolute; left: 10px; bottom: 5px;">
<span :class="$style.centerPanelHouse">{{ Mahjong.Utils.prevHouse(Mahjong.Utils.prevHouse(engine.myHouse)) === 'e' ? i18n.ts._mahjong.east : Mahjong.Utils.prevHouse(Mahjong.Utils.prevHouse(engine.myHouse)) === 's' ? i18n.ts._mahjong.south : Mahjong.Utils.prevHouse(Mahjong.Utils.prevHouse(engine.myHouse)) === 'w' ? i18n.ts._mahjong.west : i18n.ts._mahjong.north }}</span>
<span :class="$style.centerPanelPoint">{{ engine.state.points[Mahjong.Utils.prevHouse(Mahjong.Utils.prevHouse(engine.myHouse))] }}</span>
<span :class="$style.centerPanelHouse">{{ Mahjong.Common.prevHouse(Mahjong.Common.prevHouse(engine.myHouse)) === 'e' ? i18n.ts._mahjong.east : Mahjong.Common.prevHouse(Mahjong.Common.prevHouse(engine.myHouse)) === 's' ? i18n.ts._mahjong.south : Mahjong.Common.prevHouse(Mahjong.Common.prevHouse(engine.myHouse)) === 'w' ? i18n.ts._mahjong.west : i18n.ts._mahjong.north }}</span>
<span :class="$style.centerPanelPoint">{{ engine.state.points[Mahjong.Common.prevHouse(Mahjong.Common.prevHouse(engine.myHouse))] }}</span>
</div>
</div>
<div :class="$style.centerPanelTickerKami">
<div style="position: absolute; left: 10px; bottom: 5px;">
<span :class="$style.centerPanelHouse">{{ Mahjong.Utils.prevHouse(engine.myHouse) === 'e' ? i18n.ts._mahjong.east : Mahjong.Utils.prevHouse(engine.myHouse) === 's' ? i18n.ts._mahjong.south : Mahjong.Utils.prevHouse(engine.myHouse) === 'w' ? i18n.ts._mahjong.west : i18n.ts._mahjong.north }}</span>
<span :class="$style.centerPanelPoint">{{ engine.state.points[Mahjong.Utils.prevHouse(engine.myHouse)] }}</span>
<span :class="$style.centerPanelHouse">{{ Mahjong.Common.prevHouse(engine.myHouse) === 'e' ? i18n.ts._mahjong.east : Mahjong.Common.prevHouse(engine.myHouse) === 's' ? i18n.ts._mahjong.south : Mahjong.Common.prevHouse(engine.myHouse) === 'w' ? i18n.ts._mahjong.west : i18n.ts._mahjong.north }}</span>
<span :class="$style.centerPanelPoint">{{ engine.state.points[Mahjong.Common.prevHouse(engine.myHouse)] }}</span>
</div>
</div>
<div :class="$style.centerPanelTickerSimo">
<div style="position: absolute; left: 10px; bottom: 5px;">
<span :class="$style.centerPanelHouse">{{ Mahjong.Utils.nextHouse(engine.myHouse) === 'e' ? i18n.ts._mahjong.east : Mahjong.Utils.nextHouse(engine.myHouse) === 's' ? i18n.ts._mahjong.south : Mahjong.Utils.nextHouse(engine.myHouse) === 'w' ? i18n.ts._mahjong.west : i18n.ts._mahjong.north }}</span>
<span :class="$style.centerPanelPoint">{{ engine.state.points[Mahjong.Utils.nextHouse(engine.myHouse)] }}</span>
<span :class="$style.centerPanelHouse">{{ Mahjong.Common.nextHouse(engine.myHouse) === 'e' ? i18n.ts._mahjong.east : Mahjong.Common.nextHouse(engine.myHouse) === 's' ? i18n.ts._mahjong.south : Mahjong.Common.nextHouse(engine.myHouse) === 'w' ? i18n.ts._mahjong.west : i18n.ts._mahjong.north }}</span>
<span :class="$style.centerPanelPoint">{{ engine.state.points[Mahjong.Common.nextHouse(engine.myHouse)] }}</span>
</div>
</div>
<div :class="$style.centerPanelTickerMe">
@ -32,23 +32,26 @@ SPDX-License-Identifier: AGPL-3.0-only
<span :class="$style.centerPanelPoint">{{ engine.state.points[engine.myHouse] }}</span>
</div>
</div>
<div>
<div>{{ engine.state.tilesCount }}</div>
</div>
</div>
</div>
<div :class="$style.handTilesOfToimen">
<div v-for="tile in engine.state.handTiles[Mahjong.Utils.prevHouse(Mahjong.Utils.prevHouse(engine.myHouse))]" style="display: inline-block;">
<div v-for="tile in engine.state.handTiles[Mahjong.Common.prevHouse(Mahjong.Common.prevHouse(engine.myHouse))]" style="display: inline-block;">
<img :src="`/client-assets/mahjong/tile-back.png`" :class="$style.handTileImgOfToimen"/>
</div>
</div>
<div :class="$style.handTilesOfKamitya">
<div v-for="tile in engine.state.handTiles[Mahjong.Utils.prevHouse(engine.myHouse)]" :class="$style.sideTile">
<div v-for="tile in engine.state.handTiles[Mahjong.Common.prevHouse(engine.myHouse)]" :class="$style.sideTile">
<img :src="`/client-assets/mahjong/tile-side.png`" style="display: inline-block; width: 32px;"/>
</div>
</div>
<div :class="$style.handTilesOfSimotya">
<div v-for="tile in engine.state.handTiles[Mahjong.Utils.nextHouse(engine.myHouse)]" :class="$style.sideTile">
<div v-for="tile in engine.state.handTiles[Mahjong.Common.nextHouse(engine.myHouse)]" :class="$style.sideTile">
<img :src="`/client-assets/mahjong/tile-side.png`" style="display: inline-block; width: 32px; scale: -1 1;"/>
</div>
</div>
@ -56,21 +59,21 @@ SPDX-License-Identifier: AGPL-3.0-only
<div :class="$style.hoTilesContainer">
<div :class="$style.hoTilesContainerOfToimen">
<div :class="$style.hoTilesOfToimen">
<div v-for="(tile, i) in engine.state.hoTiles[Mahjong.Utils.prevHouse(Mahjong.Utils.prevHouse(engine.myHouse))]" :class="$style.hoTile" :style="{ zIndex: engine.state.hoTiles[Mahjong.Utils.prevHouse(Mahjong.Utils.prevHouse(engine.myHouse))].length - i }">
<div v-for="(tile, i) in engine.state.hoTiles[Mahjong.Common.prevHouse(Mahjong.Common.prevHouse(engine.myHouse))]" :class="$style.hoTile" :style="{ zIndex: engine.state.hoTiles[Mahjong.Common.prevHouse(Mahjong.Common.prevHouse(engine.myHouse))].length - i }">
<XTile :tile="tile" variation="2" :doras="engine.doras"/>
</div>
</div>
</div>
<div :class="$style.hoTilesContainerOfKamitya">
<div :class="$style.hoTilesOfKamitya">
<div v-for="tile in engine.state.hoTiles[Mahjong.Utils.prevHouse(engine.myHouse)]" :class="$style.hoTile">
<div v-for="tile in engine.state.hoTiles[Mahjong.Common.prevHouse(engine.myHouse)]" :class="$style.hoTile">
<XTile :tile="tile" variation="4" :doras="engine.doras"/>
</div>
</div>
</div>
<div :class="$style.hoTilesContainerOfSimotya">
<div :class="$style.hoTilesOfSimotya">
<div v-for="(tile, i) in engine.state.hoTiles[Mahjong.Utils.nextHouse(engine.myHouse)]" :class="$style.hoTile" :style="{ zIndex: engine.state.hoTiles[Mahjong.Utils.nextHouse(engine.myHouse)].length - i }">
<div v-for="(tile, i) in engine.state.hoTiles[Mahjong.Common.nextHouse(engine.myHouse)]" :class="$style.hoTile" :style="{ zIndex: engine.state.hoTiles[Mahjong.Common.nextHouse(engine.myHouse)].length - i }">
<XTile :tile="tile" variation="5" :doras="engine.doras"/>
</div>
</div>
@ -86,7 +89,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<div :class="$style.handTilesOfMe">
<div
v-for="tile in Mahjong.Utils.sortTiles((isMyTurn && iTsumoed) ? engine.myHandTiles.slice(0, engine.myHandTiles.length - 1) : engine.myHandTiles)"
v-for="tile in Mahjong.Common.sortTiles((isMyTurn && iTsumoed) ? engine.myHandTiles.slice(0, engine.myHandTiles.length - 1) : engine.myHandTiles)"
:class="[$style.myTile, { [$style.myTileNonSelectable]: selectableTiles != null && !selectableTiles.includes(tile), [$style.myTileDora]: engine.doras.includes(tile) }]"
@click="chooseTile(tile, $event)"
>
@ -116,25 +119,25 @@ SPDX-License-Identifier: AGPL-3.0-only
<div :class="$style.serifContainer">
<div :class="$style.serifContainerOfToimen">
<img v-if="ronSerifHouses[Mahjong.Utils.prevHouse(Mahjong.Utils.prevHouse(engine.myHouse))]" :src="`/client-assets/mahjong/ron.png`" style="display: block; width: 100%;"/>
<img v-else-if="ciiSerifHouses[Mahjong.Utils.prevHouse(Mahjong.Utils.prevHouse(engine.myHouse))]" :src="`/client-assets/mahjong/cii.png`" style="display: block; width: 100%;"/>
<img v-else-if="ponSerifHouses[Mahjong.Utils.prevHouse(Mahjong.Utils.prevHouse(engine.myHouse))]" :src="`/client-assets/mahjong/pon.png`" style="display: block; width: 100%;"/>
<img v-else-if="kanSerifHouses[Mahjong.Utils.prevHouse(Mahjong.Utils.prevHouse(engine.myHouse))]" :src="`/client-assets/mahjong/kan.png`" style="display: block; width: 100%;"/>
<img v-else-if="tsumoSerifHouses[Mahjong.Utils.prevHouse(Mahjong.Utils.prevHouse(engine.myHouse))]" :src="`/client-assets/mahjong/tsumo.png`" style="display: block; width: 100%;"/>
<img v-if="ronSerifHouses[Mahjong.Common.prevHouse(Mahjong.Common.prevHouse(engine.myHouse))]" :src="`/client-assets/mahjong/ron.png`" style="display: block; width: 100%;"/>
<img v-else-if="ciiSerifHouses[Mahjong.Common.prevHouse(Mahjong.Common.prevHouse(engine.myHouse))]" :src="`/client-assets/mahjong/cii.png`" style="display: block; width: 100%;"/>
<img v-else-if="ponSerifHouses[Mahjong.Common.prevHouse(Mahjong.Common.prevHouse(engine.myHouse))]" :src="`/client-assets/mahjong/pon.png`" style="display: block; width: 100%;"/>
<img v-else-if="kanSerifHouses[Mahjong.Common.prevHouse(Mahjong.Common.prevHouse(engine.myHouse))]" :src="`/client-assets/mahjong/kan.png`" style="display: block; width: 100%;"/>
<img v-else-if="tsumoSerifHouses[Mahjong.Common.prevHouse(Mahjong.Common.prevHouse(engine.myHouse))]" :src="`/client-assets/mahjong/tsumo.png`" style="display: block; width: 100%;"/>
</div>
<div :class="$style.serifContainerOfKamitya">
<img v-if="ronSerifHouses[Mahjong.Utils.prevHouse(engine.myHouse)]" :src="`/client-assets/mahjong/ron.png`" style="display: block; width: 100%;"/>
<img v-else-if="ciiSerifHouses[Mahjong.Utils.prevHouse(engine.myHouse)]" :src="`/client-assets/mahjong/cii.png`" style="display: block; width: 100%;"/>
<img v-else-if="ponSerifHouses[Mahjong.Utils.prevHouse(engine.myHouse)]" :src="`/client-assets/mahjong/pon.png`" style="display: block; width: 100%;"/>
<img v-else-if="kanSerifHouses[Mahjong.Utils.prevHouse(engine.myHouse)]" :src="`/client-assets/mahjong/kan.png`" style="display: block; width: 100%;"/>
<img v-else-if="tsumoSerifHouses[Mahjong.Utils.prevHouse(engine.myHouse)]" :src="`/client-assets/mahjong/tsumo.png`" style="display: block; width: 100%;"/>
<img v-if="ronSerifHouses[Mahjong.Common.prevHouse(engine.myHouse)]" :src="`/client-assets/mahjong/ron.png`" style="display: block; width: 100%;"/>
<img v-else-if="ciiSerifHouses[Mahjong.Common.prevHouse(engine.myHouse)]" :src="`/client-assets/mahjong/cii.png`" style="display: block; width: 100%;"/>
<img v-else-if="ponSerifHouses[Mahjong.Common.prevHouse(engine.myHouse)]" :src="`/client-assets/mahjong/pon.png`" style="display: block; width: 100%;"/>
<img v-else-if="kanSerifHouses[Mahjong.Common.prevHouse(engine.myHouse)]" :src="`/client-assets/mahjong/kan.png`" style="display: block; width: 100%;"/>
<img v-else-if="tsumoSerifHouses[Mahjong.Common.prevHouse(engine.myHouse)]" :src="`/client-assets/mahjong/tsumo.png`" style="display: block; width: 100%;"/>
</div>
<div :class="$style.serifContainerOfSimotya">
<img v-if="ronSerifHouses[Mahjong.Utils.nextHouse(engine.myHouse)]" :src="`/client-assets/mahjong/ron.png`" style="display: block; width: 100%;"/>
<img v-else-if="ciiSerifHouses[Mahjong.Utils.nextHouse(engine.myHouse)]" :src="`/client-assets/mahjong/cii.png`" style="display: block; width: 100%;"/>
<img v-else-if="ponSerifHouses[Mahjong.Utils.nextHouse(engine.myHouse)]" :src="`/client-assets/mahjong/pon.png`" style="display: block; width: 100%;"/>
<img v-else-if="kanSerifHouses[Mahjong.Utils.nextHouse(engine.myHouse)]" :src="`/client-assets/mahjong/kan.png`" style="display: block; width: 100%;"/>
<img v-else-if="tsumoSerifHouses[Mahjong.Utils.nextHouse(engine.myHouse)]" :src="`/client-assets/mahjong/tsumo.png`" style="display: block; width: 100%;"/>
<img v-if="ronSerifHouses[Mahjong.Common.nextHouse(engine.myHouse)]" :src="`/client-assets/mahjong/ron.png`" style="display: block; width: 100%;"/>
<img v-else-if="ciiSerifHouses[Mahjong.Common.nextHouse(engine.myHouse)]" :src="`/client-assets/mahjong/cii.png`" style="display: block; width: 100%;"/>
<img v-else-if="ponSerifHouses[Mahjong.Common.nextHouse(engine.myHouse)]" :src="`/client-assets/mahjong/pon.png`" style="display: block; width: 100%;"/>
<img v-else-if="kanSerifHouses[Mahjong.Common.nextHouse(engine.myHouse)]" :src="`/client-assets/mahjong/kan.png`" style="display: block; width: 100%;"/>
<img v-else-if="tsumoSerifHouses[Mahjong.Common.nextHouse(engine.myHouse)]" :src="`/client-assets/mahjong/tsumo.png`" style="display: block; width: 100%;"/>
</div>
<div :class="$style.serifContainerOfMe">
<img v-if="ronSerifHouses[engine.myHouse]" :src="`/client-assets/mahjong/ron.png`" style="display: block; width: 100%;"/>
@ -190,7 +193,7 @@ const isMyTurn = computed(() => {
});
const canHora = computed(() => {
return Mahjong.Utils.getHoraSets(engine.value.myHandTiles).length > 0;
return Mahjong.Common.getHoraSets(engine.value.myHandTiles).length > 0;
});
const selectableTiles = ref<Mahjong.Common.Tile[] | null>(null);
@ -201,7 +204,7 @@ const kanSerifHouses = reactive<Record<Mahjong.Common.House, boolean>>({ e: fals
const tsumoSerifHouses = reactive<Record<Mahjong.Common.House, boolean>>({ e: false, s: false, w: false, n: false });
/*
console.log(Mahjong.Utils.getTilesForRiichi([
console.log(Mahjong.Common.getTilesForRiichi([
'm1',
'm2',
'm2',
@ -219,7 +222,7 @@ console.log(Mahjong.Utils.getTilesForRiichi([
]));
*/
/*
console.log(Mahjong.Utils.getHoraSets([
console.log(Mahjong.Common.getHoraSets([
'm3',
'm3',
'm4',
@ -298,8 +301,8 @@ function riichi() {
if (!isMyTurn.value) return;
riichiSelect = true;
selectableTiles.value = Mahjong.Utils.getTilesForRiichi(engine.value.myHandTiles);
console.log(Mahjong.Utils.getTilesForRiichi(engine.value.myHandTiles));
selectableTiles.value = Mahjong.Common.getTilesForRiichi(engine.value.myHandTiles);
console.log(Mahjong.Common.getTilesForRiichi(engine.value.myHandTiles));
}
function kakan() {
@ -403,10 +406,10 @@ function onStreamDahaiAndTsumo(log) {
triggerRef(engine);
window.setTimeout(() => {
engine.value.commit_tsumo(Mahjong.Utils.nextHouse(log.dahaiHouse), log.tsumoTile);
engine.value.commit_tsumo(Mahjong.Common.nextHouse(log.dahaiHouse), log.tsumoTile);
triggerRef(engine);
if (Mahjong.Utils.nextHouse(log.dahaiHouse) === engine.value.myHouse) {
if (Mahjong.Common.nextHouse(log.dahaiHouse) === engine.value.myHouse) {
iTsumoed.value = true;
}
}, 100);

View file

@ -360,3 +360,249 @@ export function calcTsumoHoraPointDeltas(house: House, fans: number): Record<Hou
return deltas;
}
export function isTile(tile: string): tile is Tile {
return TILE_TYPES.includes(tile as Tile);
}
export function sortTiles(tiles: Tile[]): Tile[] {
tiles.sort((a, b) => {
const aIndex = TILE_TYPES.indexOf(a);
const bIndex = TILE_TYPES.indexOf(b);
return aIndex - bIndex;
});
return tiles;
}
export function nextHouse(house: House): House {
switch (house) {
case 'e': return 's';
case 's': return 'w';
case 'w': return 'n';
case 'n': return 'e';
default: throw new Error(`unrecognized house: ${house}`);
}
}
export function prevHouse(house: House): House {
switch (house) {
case 'e': return 'n';
case 's': return 'e';
case 'w': return 's';
case 'n': return 'w';
default: throw new Error(`unrecognized house: ${house}`);
}
}
type HoraSet = {
head: Tile;
mentsus: [Tile, Tile, Tile][];
};
export const SHUNTU_PATTERNS: [Tile, Tile, Tile][] = [
['m1', 'm2', 'm3'],
['m2', 'm3', 'm4'],
['m3', 'm4', 'm5'],
['m4', 'm5', 'm6'],
['m5', 'm6', 'm7'],
['m6', 'm7', 'm8'],
['m7', 'm8', 'm9'],
['p1', 'p2', 'p3'],
['p2', 'p3', 'p4'],
['p3', 'p4', 'p5'],
['p4', 'p5', 'p6'],
['p5', 'p6', 'p7'],
['p6', 'p7', 'p8'],
['p7', 'p8', 'p9'],
['s1', 's2', 's3'],
['s2', 's3', 's4'],
['s3', 's4', 's5'],
['s4', 's5', 's6'],
['s5', 's6', 's7'],
['s6', 's7', 's8'],
['s7', 's8', 's9'],
];
const SHUNTU_PATTERN_IDS = [
'm123',
'm234',
'm345',
'm456',
'm567',
'm678',
'm789',
'p123',
'p234',
'p345',
'p456',
'p567',
'p678',
'p789',
's123',
's234',
's345',
's456',
's567',
's678',
's789',
] as const;
/**
*
* @param handTiles
* @returns
*/
export function getHoraSets(handTiles: Tile[]): HoraSet[] {
const horaSets: HoraSet[] = [];
const headSet: Tile[] = [];
const countMap = new Map<Tile, number>();
for (const tile of handTiles) {
const count = (countMap.get(tile) ?? 0) + 1;
countMap.set(tile, count);
if (count === 2) {
headSet.push(tile);
}
}
for (const head of headSet) {
const tempHandTiles = [...handTiles];
tempHandTiles.splice(tempHandTiles.indexOf(head), 1);
tempHandTiles.splice(tempHandTiles.indexOf(head), 1);
const kotsuTileSet: Tile[] = []; // インデックスアクセスしたいため配列だが実態はSet
for (const [t, c] of countMap.entries()) {
if (t === head) continue; // 同じ牌種は4枚しかないので、頭と同じ牌種は刻子になりえない
if (c >= 3) {
kotsuTileSet.push(t);
}
}
let kotsuPatterns: Tile[][];
if (kotsuTileSet.length === 0) {
kotsuPatterns = [
[],
];
} else if (kotsuTileSet.length === 1) {
kotsuPatterns = [
[],
[kotsuTileSet[0]],
];
} else if (kotsuTileSet.length === 2) {
kotsuPatterns = [
[],
[kotsuTileSet[0]],
[kotsuTileSet[1]],
[kotsuTileSet[0], kotsuTileSet[1]],
];
} else if (kotsuTileSet.length === 3) {
kotsuPatterns = [
[],
[kotsuTileSet[0]],
[kotsuTileSet[1]],
[kotsuTileSet[2]],
[kotsuTileSet[0], kotsuTileSet[1]],
[kotsuTileSet[0], kotsuTileSet[2]],
[kotsuTileSet[1], kotsuTileSet[2]],
[kotsuTileSet[0], kotsuTileSet[1], kotsuTileSet[2]],
];
} else if (kotsuTileSet.length === 4) {
kotsuPatterns = [
[],
[kotsuTileSet[0]],
[kotsuTileSet[1]],
[kotsuTileSet[2]],
[kotsuTileSet[3]],
[kotsuTileSet[0], kotsuTileSet[1]],
[kotsuTileSet[0], kotsuTileSet[2]],
[kotsuTileSet[0], kotsuTileSet[3]],
[kotsuTileSet[1], kotsuTileSet[2]],
[kotsuTileSet[1], kotsuTileSet[3]],
[kotsuTileSet[2], kotsuTileSet[3]],
[kotsuTileSet[0], kotsuTileSet[1], kotsuTileSet[2]],
[kotsuTileSet[0], kotsuTileSet[1], kotsuTileSet[3]],
[kotsuTileSet[0], kotsuTileSet[2], kotsuTileSet[3]],
[kotsuTileSet[1], kotsuTileSet[2], kotsuTileSet[3]],
[kotsuTileSet[0], kotsuTileSet[1], kotsuTileSet[2], kotsuTileSet[3]],
];
} else {
throw new Error('arienai');
}
for (const kotsuPattern of kotsuPatterns) {
const tempHandTilesWithoutKotsu = [...tempHandTiles];
for (const kotsuTile of kotsuPattern) {
tempHandTilesWithoutKotsu.splice(tempHandTilesWithoutKotsu.indexOf(kotsuTile), 1);
tempHandTilesWithoutKotsu.splice(tempHandTilesWithoutKotsu.indexOf(kotsuTile), 1);
tempHandTilesWithoutKotsu.splice(tempHandTilesWithoutKotsu.indexOf(kotsuTile), 1);
}
tempHandTilesWithoutKotsu.sort((a, b) => {
const aIndex = TILE_TYPES.indexOf(a);
const bIndex = TILE_TYPES.indexOf(b);
return aIndex - bIndex;
});
const tempHandTilesWithoutKotsuAndShuntsu: (Tile | null)[] = [...tempHandTilesWithoutKotsu];
const shuntsus: [Tile, Tile, Tile][] = [];
while (tempHandTilesWithoutKotsuAndShuntsu.length > 0) {
let isShuntu = false;
for (const shuntuPattern of SHUNTU_PATTERNS) {
if (
tempHandTilesWithoutKotsuAndShuntsu[0] === shuntuPattern[0] &&
tempHandTilesWithoutKotsuAndShuntsu.includes(shuntuPattern[1]) &&
tempHandTilesWithoutKotsuAndShuntsu.includes(shuntuPattern[2])
) {
shuntsus.push(shuntuPattern);
tempHandTilesWithoutKotsuAndShuntsu.splice(0, 1);
tempHandTilesWithoutKotsuAndShuntsu.splice(tempHandTilesWithoutKotsuAndShuntsu.indexOf(shuntuPattern[1]), 1);
tempHandTilesWithoutKotsuAndShuntsu.splice(tempHandTilesWithoutKotsuAndShuntsu.indexOf(shuntuPattern[2]), 1);
isShuntu = true;
break;
}
}
if (!isShuntu) tempHandTilesWithoutKotsuAndShuntsu.splice(0, 1);
}
if (shuntsus.length * 3 === tempHandTilesWithoutKotsu.length) { // アガリ形
horaSets.push({
head,
mentsus: [...kotsuPattern.map(t => [t, t, t] as [Tile, Tile, Tile]), ...shuntsus],
});
}
}
}
return horaSets;
}
/**
*
* @param handTiles
*/
export function getHoraTiles(handTiles: Tile[]): Tile[] {
return TILE_TYPES.filter(tile => {
const tempHandTiles = [...handTiles, tile];
const horaSets = getHoraSets(tempHandTiles);
return horaSets.length > 0;
});
}
// TODO: 国士無双判定関数
// TODO: 七対子判定関数
export function getTilesForRiichi(handTiles: Tile[]): Tile[] {
return handTiles.filter(tile => {
const tempHandTiles = [...handTiles];
tempHandTiles.splice(tempHandTiles.indexOf(tile), 1);
const horaTiles = getHoraTiles(tempHandTiles);
return horaTiles.length > 0;
});
}
export function nextTileForDora(tile: Tile): Tile {
return NEXT_TILE_FOR_DORA_MAP[tile];
}

View file

@ -6,7 +6,6 @@
import CRC32 from 'crc-32';
import { Tile, House, Huro, TILE_TYPES, YAKU_DEFINITIONS } from './common.js';
import * as Common from './common.js';
import * as Utils from './utils.js';
import { PlayerState } from './engine.player.js';
export type MasterState = {
@ -116,7 +115,7 @@ export class MasterGameEngine {
}
public get doras(): Tile[] {
return this.state.kingTiles.slice(0, this.state.activatedDorasCount).map(t => Utils.nextTileForDora(t));
return this.state.kingTiles.slice(0, this.state.activatedDorasCount).map(t => Common.nextTileForDora(t));
}
public static createInitialState(): MasterState {
@ -199,7 +198,7 @@ export class MasterGameEngine {
// TODO: ポンされるなどして自分の河にない場合の考慮
if (this.state.hoTiles[house].includes(tile)) return false;
const horaSets = Utils.getHoraSets(this.state.handTiles[house].concat(tile));
const horaSets = Common.getHoraSets(this.state.handTiles[house].concat(tile));
if (horaSets.length === 0) return false; // 完成形じゃない
// TODO
@ -213,8 +212,12 @@ export class MasterGameEngine {
return this.state.handTiles[house].filter(t => t === tile).length === 2;
}
private canCii(house: House, tile: Tile): boolean {
// TODO
private canCii(caller: House, callee: House, tile: Tile): boolean {
if (callee !== Common.prevHouse(caller)) return false;
const hand = this.state.handTiles[caller];
return Common.SHUNTU_PATTERNS.some(pattern =>
pattern.includes(tile) &&
pattern.filter(t => hand.includes(t)).length >= 2);
}
public getHouse(index: 1 | 2 | 3 | 4): House {
@ -266,7 +269,7 @@ export class MasterGameEngine {
if (riichi) {
const tempHandTiles = [...this.state.handTiles[house]];
tempHandTiles.splice(tempHandTiles.indexOf(tile), 1);
if (Utils.getHoraTiles(tempHandTiles).length === 0) throw new Error('Not tenpai');
if (Common.getHoraTiles(tempHandTiles).length === 0) throw new Error('Not tenpai');
if (this.state.points[house] < 1000) throw new Error('Not enough points');
}
@ -360,7 +363,7 @@ export class MasterGameEngine {
};
}
this.state.turn = null;
this.state.nextTurnAfterAsking = Utils.nextHouse(house);
this.state.nextTurnAfterAsking = Common.nextHouse(house);
return {
asking: true as const,
canRonHouses: canRonHouses,
@ -370,7 +373,7 @@ export class MasterGameEngine {
};
}
this.state.turn = Utils.nextHouse(house);
this.state.turn = Common.nextHouse(house);
const tsumoTile = this.tsumo();

View file

@ -6,7 +6,6 @@
import CRC32 from 'crc-32';
import { Tile, House, Huro, TILE_TYPES, YAKU_DEFINITIONS } from './common.js';
import * as Common from './common.js';
import * as Utils from './utils.js';
export type PlayerState = {
user1House: House;
@ -97,7 +96,7 @@ export class PlayerGameEngine {
}
public get doras(): Tile[] {
return this.state.doraIndicateTiles.map(t => Utils.nextTileForDora(t));
return this.state.doraIndicateTiles.map(t => Common.nextTileForDora(t));
}
public commit_tsumo(house: House, tile: Tile) {
@ -131,7 +130,7 @@ export class PlayerGameEngine {
if (house === this.myHouse) {
} else {
const canRon = Utils.getHoraSets(this.myHandTiles.concat(tile)).length > 0;
const canRon = Common.getHoraSets(this.myHandTiles.concat(tile)).length > 0;
const canPon = this.myHandTiles.filter(t => t === tile).length === 2;
// TODO: canCii
@ -243,7 +242,7 @@ export class PlayerGameEngine {
if (this.state.riichis[this.myHouse]) return false;
if (this.state.points[this.myHouse] < 1000) return false;
if (!this.isMenzen) return false;
if (Utils.getTilesForRiichi(this.myHandTiles).length === 0) return false;
if (Common.getTilesForRiichi(this.myHandTiles).length === 0) return false;
return true;
}
}

View file

@ -5,7 +5,6 @@
export * as Serializer from './serializer.js';
export * as Common from './common.js';
export * as Utils from './utils.js';
export { MasterGameEngine } from './engine.master.js';
export type { MasterState } from './engine.master.js';

View file

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

View file

@ -1,248 +0,0 @@
/*
* SPDX-FileCopyrightText: syuilo and other misskey contributors
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { House, NEXT_TILE_FOR_DORA_MAP, TILE_TYPES, Tile } from './common.js';
export function isTile(tile: string): tile is Tile {
return TILE_TYPES.includes(tile as Tile);
}
export function sortTiles(tiles: Tile[]): Tile[] {
tiles.sort((a, b) => {
const aIndex = TILE_TYPES.indexOf(a);
const bIndex = TILE_TYPES.indexOf(b);
return aIndex - bIndex;
});
return tiles;
}
export function nextHouse(house: House): House {
switch (house) {
case 'e': return 's';
case 's': return 'w';
case 'w': return 'n';
case 'n': return 'e';
default: throw new Error(`unrecognized house: ${house}`);
}
}
export function prevHouse(house: House): House {
switch (house) {
case 'e': return 'n';
case 's': return 'e';
case 'w': return 's';
case 'n': return 'w';
default: throw new Error(`unrecognized house: ${house}`);
}
}
type HoraSet = {
head: Tile;
mentsus: [Tile, Tile, Tile][];
};
export const SHUNTU_PATTERNS: [Tile, Tile, Tile][] = [
['m1', 'm2', 'm3'],
['m2', 'm3', 'm4'],
['m3', 'm4', 'm5'],
['m4', 'm5', 'm6'],
['m5', 'm6', 'm7'],
['m6', 'm7', 'm8'],
['m7', 'm8', 'm9'],
['p1', 'p2', 'p3'],
['p2', 'p3', 'p4'],
['p3', 'p4', 'p5'],
['p4', 'p5', 'p6'],
['p5', 'p6', 'p7'],
['p6', 'p7', 'p8'],
['p7', 'p8', 'p9'],
['s1', 's2', 's3'],
['s2', 's3', 's4'],
['s3', 's4', 's5'],
['s4', 's5', 's6'],
['s5', 's6', 's7'],
['s6', 's7', 's8'],
['s7', 's8', 's9'],
];
const SHUNTU_PATTERN_IDS = [
'm123',
'm234',
'm345',
'm456',
'm567',
'm678',
'm789',
'p123',
'p234',
'p345',
'p456',
'p567',
'p678',
'p789',
's123',
's234',
's345',
's456',
's567',
's678',
's789',
] as const;
/**
*
* @param handTiles
* @returns
*/
export function getHoraSets(handTiles: Tile[]): HoraSet[] {
const horaSets: HoraSet[] = [];
const headSet: Tile[] = [];
const countMap = new Map<Tile, number>();
for (const tile of handTiles) {
const count = (countMap.get(tile) ?? 0) + 1;
countMap.set(tile, count);
if (count === 2) {
headSet.push(tile);
}
}
for (const head of headSet) {
const tempHandTiles = [...handTiles];
tempHandTiles.splice(tempHandTiles.indexOf(head), 1);
tempHandTiles.splice(tempHandTiles.indexOf(head), 1);
const kotsuTileSet: Tile[] = []; // インデックスアクセスしたいため配列だが実態はSet
for (const [t, c] of countMap.entries()) {
if (t === head) continue; // 同じ牌種は4枚しかないので、頭と同じ牌種は刻子になりえない
if (c >= 3) {
kotsuTileSet.push(t);
}
}
let kotsuPatterns: Tile[][];
if (kotsuTileSet.length === 0) {
kotsuPatterns = [
[],
];
} else if (kotsuTileSet.length === 1) {
kotsuPatterns = [
[],
[kotsuTileSet[0]],
];
} else if (kotsuTileSet.length === 2) {
kotsuPatterns = [
[],
[kotsuTileSet[0]],
[kotsuTileSet[1]],
[kotsuTileSet[0], kotsuTileSet[1]],
];
} else if (kotsuTileSet.length === 3) {
kotsuPatterns = [
[],
[kotsuTileSet[0]],
[kotsuTileSet[1]],
[kotsuTileSet[2]],
[kotsuTileSet[0], kotsuTileSet[1]],
[kotsuTileSet[0], kotsuTileSet[2]],
[kotsuTileSet[1], kotsuTileSet[2]],
[kotsuTileSet[0], kotsuTileSet[1], kotsuTileSet[2]],
];
} else if (kotsuTileSet.length === 4) {
kotsuPatterns = [
[],
[kotsuTileSet[0]],
[kotsuTileSet[1]],
[kotsuTileSet[2]],
[kotsuTileSet[3]],
[kotsuTileSet[0], kotsuTileSet[1]],
[kotsuTileSet[0], kotsuTileSet[2]],
[kotsuTileSet[0], kotsuTileSet[3]],
[kotsuTileSet[1], kotsuTileSet[2]],
[kotsuTileSet[1], kotsuTileSet[3]],
[kotsuTileSet[2], kotsuTileSet[3]],
[kotsuTileSet[0], kotsuTileSet[1], kotsuTileSet[2]],
[kotsuTileSet[0], kotsuTileSet[1], kotsuTileSet[3]],
[kotsuTileSet[0], kotsuTileSet[2], kotsuTileSet[3]],
[kotsuTileSet[1], kotsuTileSet[2], kotsuTileSet[3]],
[kotsuTileSet[0], kotsuTileSet[1], kotsuTileSet[2], kotsuTileSet[3]],
];
} else {
throw new Error('arienai');
}
for (const kotsuPattern of kotsuPatterns) {
const tempHandTilesWithoutKotsu = [...tempHandTiles];
for (const kotsuTile of kotsuPattern) {
tempHandTilesWithoutKotsu.splice(tempHandTilesWithoutKotsu.indexOf(kotsuTile), 1);
tempHandTilesWithoutKotsu.splice(tempHandTilesWithoutKotsu.indexOf(kotsuTile), 1);
tempHandTilesWithoutKotsu.splice(tempHandTilesWithoutKotsu.indexOf(kotsuTile), 1);
}
tempHandTilesWithoutKotsu.sort((a, b) => {
const aIndex = TILE_TYPES.indexOf(a);
const bIndex = TILE_TYPES.indexOf(b);
return aIndex - bIndex;
});
const tempHandTilesWithoutKotsuAndShuntsu: (Tile | null)[] = [...tempHandTilesWithoutKotsu];
const shuntsus: [Tile, Tile, Tile][] = [];
while (tempHandTilesWithoutKotsuAndShuntsu.length > 0) {
let isShuntu = false;
for (const shuntuPattern of SHUNTU_PATTERNS) {
if (
tempHandTilesWithoutKotsuAndShuntsu[0] === shuntuPattern[0] &&
tempHandTilesWithoutKotsuAndShuntsu.includes(shuntuPattern[1]) &&
tempHandTilesWithoutKotsuAndShuntsu.includes(shuntuPattern[2])
) {
shuntsus.push(shuntuPattern);
tempHandTilesWithoutKotsuAndShuntsu.splice(0, 1);
tempHandTilesWithoutKotsuAndShuntsu.splice(tempHandTilesWithoutKotsuAndShuntsu.indexOf(shuntuPattern[1]), 1);
tempHandTilesWithoutKotsuAndShuntsu.splice(tempHandTilesWithoutKotsuAndShuntsu.indexOf(shuntuPattern[2]), 1);
isShuntu = true;
break;
}
}
if (!isShuntu) tempHandTilesWithoutKotsuAndShuntsu.splice(0, 1);
}
if (shuntsus.length * 3 === tempHandTilesWithoutKotsu.length) { // アガリ形
horaSets.push({
head,
mentsus: [...kotsuPattern.map(t => [t, t, t] as [Tile, Tile, Tile]), ...shuntsus],
});
}
}
}
return horaSets;
}
/**
*
* @param handTiles
*/
export function getHoraTiles(handTiles: Tile[]): Tile[] {
return TILE_TYPES.filter(tile => {
const tempHandTiles = [...handTiles, tile];
const horaSets = getHoraSets(tempHandTiles);
return horaSets.length > 0;
});
}
export function getTilesForRiichi(handTiles: Tile[]): Tile[] {
return handTiles.filter(tile => {
const tempHandTiles = [...handTiles];
tempHandTiles.splice(tempHandTiles.indexOf(tile), 1);
const horaTiles = getHoraTiles(tempHandTiles);
return horaTiles.length > 0;
});
}
export function nextTileForDora(tile: Tile): Tile {
return NEXT_TILE_FOR_DORA_MAP[tile];
}