diff --git a/packages/backend/src/core/MahjongService.ts b/packages/backend/src/core/MahjongService.ts
index 498563ac6b..a10720bf79 100644
--- a/packages/backend/src/core/MahjongService.ts
+++ b/packages/backend/src/core/MahjongService.ts
@@ -345,8 +345,8 @@ export class MahjongService implements OnApplicationShutdown, OnModuleInit {
});
this.endKyoku(room, mj);
break;
- case 'ryukyoku':
- this.globalEventService.publishMahjongRoomStream(room.id, 'ryukyoku', {
+ case 'ryuukyoku':
+ this.globalEventService.publishMahjongRoomStream(room.id, 'ryuukyoku', {
});
this.endKyoku(room, mj);
break;
@@ -387,7 +387,11 @@ export class MahjongService implements OnApplicationShutdown, OnModuleInit {
const aiHouses = [[1, room.user1Ai], [2, room.user2Ai], [3, room.user3Ai], [4, room.user4Ai]].filter(([id, ai]) => ai).map(([id, ai]) => mj.getHouse(id));
- if (res.asking) {
+ if (res.ryuukyoku) {
+ this.endKyoku(room, mj);
+ this.globalEventService.publishMahjongRoomStream(room.id, 'ryuukyoku', {
+ });
+ } else if (res.asking) {
const answers: CallingAnswers = {
pon: null,
cii: null,
diff --git a/packages/frontend/assets/mahjong/99.png b/packages/frontend/assets/mahjong/99.png
new file mode 100644
index 0000000000..2e72339a65
Binary files /dev/null and b/packages/frontend/assets/mahjong/99.png differ
diff --git a/packages/frontend/assets/mahjong/ryuukyoku.png b/packages/frontend/assets/mahjong/ryuukyoku.png
new file mode 100644
index 0000000000..1cdc4cce67
Binary files /dev/null and b/packages/frontend/assets/mahjong/ryuukyoku.png differ
diff --git a/packages/frontend/src/pages/mahjong/room.game.vue b/packages/frontend/src/pages/mahjong/room.game.vue
index 8b8883756d..2f8b7af8cb 100644
--- a/packages/frontend/src/pages/mahjong/room.game.vue
+++ b/packages/frontend/src/pages/mahjong/room.game.vue
@@ -168,6 +168,17 @@ SPDX-License-Identifier: AGPL-3.0-only
+
+
+
+
+
+
Ron
Pon
@@ -259,6 +270,22 @@ const ponSerifHouses = reactive>({ e: false, s: false
const kanSerifHouses = reactive>({ e: false, s: false, w: false, n: false });
const tsumoSerifHouses = reactive>({ e: false, s: false, w: false, n: false });
const riichiSerifHouses = reactive>({ e: false, s: false, w: false, n: false });
+const iTsumoed = ref(false);
+const showKyokuResults = ref(false);
+const kyokuResults = ref;
+} | null>>({
+ e: null,
+ s: null,
+ w: null,
+ n: null,
+});
+const ryuukyokued = ref(false);
/*
if (room.value.isStarted && !room.value.isEnded) {
@@ -387,7 +414,7 @@ function cii(ev: MouseEvent) {
os.popupMenu(patterns.map(pattern => ({
text: pattern.join(' '),
action: () => {
- const index = Mmj.sortTileTypes(pattern).indexOf(targetTile);
+ const index = Mmj.sortTileTypes(pattern).indexOf(mj$type(targetTile));
props.connection!.send('cii', {
pattern: index === 0 ? 'x__' : index === 1 ? '_x_' : '__x',
});
@@ -407,26 +434,6 @@ function skip() {
props.connection!.send('nop', {});
}
-const iTsumoed = ref(false);
-const showKyokuResults = ref(false);
-const kyokuResults = ref;
-} | null>>({
- e: null,
- s: null,
- w: null,
- n: null,
-});
-
-function kyokuEnd() {
- showKyokuResults.value = true;
-}
-
function onStreamDahai(log) {
console.log('onStreamDahai', log);
@@ -617,6 +624,16 @@ function onStreamTsumoHora(log) {
console.log('tsumohora', res);
}
+function onStreamRyuukyoku(log) {
+ console.log('onStreamRyuukyoku', log);
+
+ ryuukyokued.value = true;
+
+ window.setTimeout(() => {
+ showKyokuResults.value = true;
+ }, 1500);
+}
+
function restoreRoom(_room) {
room.value = deepClone(_room);
@@ -635,6 +652,7 @@ onMounted(() => {
props.connection.on('ciied', onStreamCiied);
props.connection.on('ronned', onStreamRonned);
props.connection.on('tsumoHora', onStreamTsumoHora);
+ props.connection.on('ryuukyoku', onStreamRyuukyoku);
}
});
@@ -650,6 +668,7 @@ onActivated(() => {
props.connection.on('ciied', onStreamCiied);
props.connection.on('ronned', onStreamRonned);
props.connection.on('tsumoHora', onStreamTsumoHora);
+ props.connection.on('ryuukyoku', onStreamRyuukyoku);
}
});
@@ -665,6 +684,7 @@ onDeactivated(() => {
props.connection.off('ciied', onStreamCiied);
props.connection.off('ronned', onStreamRonned);
props.connection.off('tsumoHora', onStreamTsumoHora);
+ props.connection.off('ryuukyoku', onStreamRyuukyoku);
}
});
@@ -680,6 +700,7 @@ onUnmounted(() => {
props.connection.off('ciied', onStreamCiied);
props.connection.off('ronned', onStreamRonned);
props.connection.off('tsumoHora', onStreamTsumoHora);
+ props.connection.off('ryuukyoku', onStreamRyuukyoku);
}
});
@@ -969,6 +990,19 @@ onUnmounted(() => {
height: min-content;
}
+.ryuukyokuContainer {
+ position: absolute;
+ top: 0;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ width: 200px;
+ height: min-content;
+ margin: auto;
+ z-index: 100;
+ pointer-events: none;
+}
+
.sideTile {
margin-bottom: -26px;
}
diff --git a/packages/misskey-mahjong/src/engine.master.ts b/packages/misskey-mahjong/src/engine.master.ts
index 64a87d6cc0..8361aaf34f 100644
--- a/packages/misskey-mahjong/src/engine.master.ts
+++ b/packages/misskey-mahjong/src/engine.master.ts
@@ -253,12 +253,12 @@ export class MasterGameEngine {
return tile;
}
- private canRon(house: House, tile: TileId): boolean {
+ private canRon(house: House, tileId: TileId): boolean {
// フリテン
// TODO: ポンされるなどして自分の河にない場合の考慮
- if (this.hoTileTypes[house].includes($type(tile))) return false;
+ if (this.hoTileTypes[house].includes($type(tileId))) return false;
- const horaSets = Common.getHoraSets(this.handTileTypes[house].concat($type(tile)));
+ const horaSets = Common.getHoraSets(this.handTileTypes[house].concat($type(tileId)));
if (horaSets.length === 0) return false; // 完成形じゃない
// TODO
@@ -268,15 +268,15 @@ export class MasterGameEngine {
return true;
}
- private canPon(house: House, tile: TileId): boolean {
- return this.handTileTypes[house].filter(t => t === $type(tile)).length === 2;
+ private canPon(house: House, tileId: TileId): boolean {
+ return this.handTileTypes[house].filter(t => t === $type(tileId)).length === 2;
}
- private canCii(caller: House, callee: House, tile: TileId): boolean {
+ private canCii(caller: House, callee: House, tileId: TileId): boolean {
if (callee !== Common.prevHouse(caller)) return false;
const hand = this.handTileTypes[caller];
return Common.SHUNTU_PATTERNS.some(pattern =>
- pattern.includes($type(tile)) &&
+ pattern.includes($type(tileId)) &&
pattern.filter(t => hand.includes(t)).length >= 2);
}
@@ -325,20 +325,20 @@ export class MasterGameEngine {
this.endKyoku();
}
- public commit_dahai(house: House, tile: TileId, riichi = false) {
+ public commit_dahai(house: House, tileId: TileId, riichi = false) {
if (this.state.turn !== house) throw new Error('Not your turn');
if (riichi) {
const tempHandTiles = [...this.handTileTypes[house]];
- tempHandTiles.splice(tempHandTiles.indexOf($type(tile)), 1);
+ tempHandTiles.splice(tempHandTiles.indexOf($type(tileId)), 1);
if (Common.getHoraTiles(tempHandTiles).length === 0) throw new Error('Not tenpai');
if (this.state.points[house] < 1000) throw new Error('Not enough points');
}
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 (!handTiles.includes(tileId)) throw new Error('No such tile in your hand');
+ handTiles.splice(handTiles.indexOf(tileId), 1);
+ this.state.hoTiles[house].push(tileId);
if (riichi) {
this.state.riichis[house] = true;
@@ -347,24 +347,24 @@ export class MasterGameEngine {
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');
+ if (this.canRon('s', tileId)) canRonHouses.push('s');
+ if (this.canRon('w', tileId)) canRonHouses.push('w');
+ if (this.canRon('n', tileId)) 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');
+ if (this.canRon('e', tileId)) canRonHouses.push('e');
+ if (this.canRon('w', tileId)) canRonHouses.push('w');
+ if (this.canRon('n', tileId)) 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');
+ if (this.canRon('e', tileId)) canRonHouses.push('e');
+ if (this.canRon('s', tileId)) canRonHouses.push('s');
+ if (this.canRon('n', tileId)) 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');
+ if (this.canRon('e', tileId)) canRonHouses.push('e');
+ if (this.canRon('s', tileId)) canRonHouses.push('s');
+ if (this.canRon('w', tileId)) canRonHouses.push('w');
break;
}
@@ -372,18 +372,18 @@ export class MasterGameEngine {
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;
+ case 'e': canPonHouse = this.canPon('s', tileId) ? 's' : this.canPon('w', tileId) ? 'w' : this.canPon('n', tileId) ? 'n' : null; break;
+ case 's': canPonHouse = this.canPon('e', tileId) ? 'e' : this.canPon('w', tileId) ? 'w' : this.canPon('n', tileId) ? 'n' : null; break;
+ case 'w': canPonHouse = this.canPon('e', tileId) ? 'e' : this.canPon('s', tileId) ? 's' : this.canPon('n', tileId) ? 'n' : null; break;
+ case 'n': canPonHouse = this.canPon('e', tileId) ? 'e' : this.canPon('s', tileId) ? 's' : this.canPon('w', tileId) ? 'w' : null; break;
}
let canCiiHouse: House | null = null;
switch (house) {
- case 'e': canCiiHouse = this.canCii('s', house, tile) ? 's' : this.canCii('w', house, tile) ? 'w' : this.canCii('n', house, tile) ? 'n' : null; break;
- case 's': canCiiHouse = this.canCii('e', house, tile) ? 'e' : this.canCii('w', house, tile) ? 'w' : this.canCii('n', house, tile) ? 'n' : null; break;
- case 'w': canCiiHouse = this.canCii('e', house, tile) ? 'e' : this.canCii('s', house, tile) ? 's' : this.canCii('n', house, tile) ? 'n' : null; break;
- case 'n': canCiiHouse = this.canCii('e', house, tile) ? 'e' : this.canCii('s', house, tile) ? 's' : this.canCii('w', house, tile) ? 'w' : null; break;
+ case 'e': canCiiHouse = this.canCii('s', house, tileId) ? 's' : this.canCii('w', house, tileId) ? 'w' : this.canCii('n', house, tileId) ? 'n' : null; break;
+ case 's': canCiiHouse = this.canCii('e', house, tileId) ? 'e' : this.canCii('w', house, tileId) ? 'w' : this.canCii('n', house, tileId) ? 'n' : null; break;
+ case 'w': canCiiHouse = this.canCii('e', house, tileId) ? 'e' : this.canCii('s', house, tileId) ? 's' : this.canCii('n', house, tileId) ? 'n' : null; break;
+ case 'n': canCiiHouse = this.canCii('e', house, tileId) ? 'e' : this.canCii('s', house, tileId) ? 's' : this.canCii('w', house, tileId) ? 'w' : null; break;
}
if (canRonHouses.length > 0 || canPonHouse != null || canCiiHouse != null) {
@@ -422,6 +422,18 @@ export class MasterGameEngine {
};
}
+ // 流局
+ if (this.state.tiles.length === 0) {
+ this.state.turn = null;
+
+ this.endKyoku();
+
+ return {
+ asking: false as const,
+ ryuukyoku: true as const,
+ };
+ }
+
this.state.turn = Common.nextHouse(house);
const tsumoTile = this.tsumo();
@@ -433,11 +445,11 @@ export class MasterGameEngine {
};
}
- public commit_kakan(house: House, tile: TileId) {
- const pon = this.state.huros[house].find(h => h.type === 'pon' && $type(h.tiles[0]) === $type(tile));
+ public commit_kakan(house: House, tileId: TileId) {
+ const pon = this.state.huros[house].find(h => h.type === 'pon' && $type(h.tiles[0]) === $type(tileId));
if (pon == null) throw new Error('No such pon');
- this.state.handTiles[house].splice(this.state.handTiles[house].indexOf(tile), 1);
- const tiles = [tile, ...pon.tiles];
+ this.state.handTiles[house].splice(this.state.handTiles[house].indexOf(tileId), 1);
+ const tiles = [tileId, ...pon.tiles];
this.state.huros[house].push({ type: 'minkan', tiles: tiles, from: pon.from });
this.state.activatedDorasCount++;
@@ -451,14 +463,14 @@ export class MasterGameEngine {
};
}
- public commit_ankan(house: House, tile: TileId) {
- const t1 = this.state.handTiles[house].filter(t => $type(t) === $type(tile)).at(0);
+ public commit_ankan(house: House, tileId: TileId) {
+ const t1 = this.state.handTiles[house].filter(t => $type(t) === $type(tileId)).at(0);
if (t1 == null) throw new Error('No such tile');
- const t2 = this.state.handTiles[house].filter(t => $type(t) === $type(tile)).at(1);
+ const t2 = this.state.handTiles[house].filter(t => $type(t) === $type(tileId)).at(1);
if (t2 == null) throw new Error('No such tile');
- const t3 = this.state.handTiles[house].filter(t => $type(t) === $type(tile)).at(2);
+ const t3 = this.state.handTiles[house].filter(t => $type(t) === $type(tileId)).at(2);
if (t3 == null) throw new Error('No such tile');
- const t4 = this.state.handTiles[house].filter(t => $type(t) === $type(tile)).at(3);
+ const t4 = this.state.handTiles[house].filter(t => $type(t) === $type(tileId)).at(3);
if (t4 == null) throw new Error('No such tile');
this.state.handTiles[house].splice(this.state.handTiles[house].indexOf(t1), 1);
this.state.handTiles[house].splice(this.state.handTiles[house].indexOf(t2), 1);
@@ -660,7 +672,7 @@ export class MasterGameEngine {
this.endKyoku();
return {
- type: 'ryukyoku' as const,
+ type: 'ryuukyoku' as const,
};
} else {
this.state.turn = this.state.nextTurnAfterAsking!;
diff --git a/packages/misskey-mahjong/src/engine.player.ts b/packages/misskey-mahjong/src/engine.player.ts
index 4d571be782..f2ba137eff 100644
--- a/packages/misskey-mahjong/src/engine.player.ts
+++ b/packages/misskey-mahjong/src/engine.player.ts
@@ -152,19 +152,19 @@ export class PlayerGameEngine {
return this.state.riichis[this.myHouse];
}
- public commit_tsumo(house: House, tile: TileId) {
- console.log('commit_tsumo', this.state.turn, house, tile);
+ public commit_tsumo(house: House, tileId: TileId) {
+ console.log('commit_tsumo', this.state.turn, house, tileId);
this.state.tilesCount--;
this.state.turn = house;
if (house === this.myHouse) {
- this.myHandTiles.push(tile);
+ this.myHandTiles.push(tileId);
} else {
this.state.handTiles[house].push(0);
}
}
- public commit_dahai(house: House, tile: TileId, riichi = false) {
- console.log('commit_dahai', this.state.turn, house, tile, riichi);
+ public commit_dahai(house: House, tileId: TileId, riichi = false) {
+ console.log('commit_dahai', this.state.turn, house, tileId, riichi);
if (this.state.turn !== house) throw new PlayerGameEngine.InvalidOperationError();
if (riichi) {
@@ -172,29 +172,29 @@ export class PlayerGameEngine {
}
if (house === this.myHouse) {
- this.myHandTiles.splice(this.myHandTiles.indexOf(tile), 1);
- this.state.hoTiles[this.myHouse].push(tile);
+ this.myHandTiles.splice(this.myHandTiles.indexOf(tileId), 1);
+ this.state.hoTiles[this.myHouse].push(tileId);
} else {
this.state.handTiles[house].pop();
- this.state.hoTiles[house].push(tile);
+ this.state.hoTiles[house].push(tileId);
}
this.state.turn = null;
if (house === this.myHouse) {
} else {
- const canRon = Common.getHoraSets(this.myHandTiles.concat(tile).map(id => $type(id))).length > 0;
- const canPon = !this.isMeRiichi && this.myHandTiles.filter(t => t === tile).length === 2;
- const canKan = !this.isMeRiichi && this.myHandTiles.filter(t => t === tile).length === 3;
+ const canRon = Common.getHoraSets(this.myHandTiles.concat(tileId).map(id => $type(id))).length > 0;
+ const canPon = !this.isMeRiichi && this.myHandTileTypes.filter(t => t === $type(tileId)).length === 2;
+ const canKan = !this.isMeRiichi && this.myHandTileTypes.filter(t => t === $type(tileId)).length === 3;
const canCii = !this.isMeRiichi && house === Common.prevHouse(this.myHouse) &&
Common.SHUNTU_PATTERNS.some(pattern =>
- pattern.includes($type(tile)) &&
+ pattern.includes($type(tileId)) &&
pattern.filter(t => this.myHandTileTypes.includes(t)).length >= 2);
- if (canRon) this.state.canRon = { callee: house };
- if (canPon) this.state.canPon = { callee: house };
- if (canKan) this.state.canKan = { callee: house };
- if (canCii) this.state.canCii = { callee: house };
+ this.state.canRon = canRon ? { callee: house } : null;
+ this.state.canPon = canPon ? { callee: house } : null;
+ this.state.canKan = canKan ? { callee: house } : null;
+ this.state.canCii = canCii ? { callee: house } : null;
}
}