@@ -99,6 +102,10 @@ function unready() {
props.connection.send('ready', false);
}
+function addCpu() {
+ props.connection.send('addAi', {});
+}
+
function onChangeReadyStates(states) {
room.value.user1Ready = states.user1;
room.value.user2Ready = states.user2;
@@ -106,10 +113,40 @@ function onChangeReadyStates(states) {
room.value.user4Ready = states.user4;
}
+function onJoined(x) {
+ switch (x.index) {
+ case 1:
+ room.value.user1 = x.user;
+ room.value.user1Ai = x.user == null;
+ room.value.user1Ready = room.value.user1Ai;
+ break;
+ case 2:
+ room.value.user2 = x.user;
+ room.value.user2Ai = x.user == null;
+ room.value.user2Ready = room.value.user2Ai;
+ break;
+ case 3:
+ room.value.user3 = x.user;
+ room.value.user3Ai = x.user == null;
+ room.value.user3Ready = room.value.user3Ai;
+ break;
+ case 4:
+ room.value.user4 = x.user;
+ room.value.user4Ai = x.user == null;
+ room.value.user4Ready = room.value.user4Ai;
+ break;
+
+ default:
+ break;
+ }
+}
+
props.connection.on('changeReadyStates', onChangeReadyStates);
+props.connection.on('joined', onJoined);
onUnmounted(() => {
props.connection.off('changeReadyStates', onChangeReadyStates);
+ props.connection.off('joined', onJoined);
});
diff --git a/packages/misskey-js/src/autogen/apiClientJSDoc.ts b/packages/misskey-js/src/autogen/apiClientJSDoc.ts
index 5897fa74ed..8e95da1514 100644
--- a/packages/misskey-js/src/autogen/apiClientJSDoc.ts
+++ b/packages/misskey-js/src/autogen/apiClientJSDoc.ts
@@ -1,6 +1,6 @@
/*
* version: 2024.2.0-beta.7
- * generatedAt: 2024-01-26T05:23:04.911Z
+ * generatedAt: 2024-01-26T07:53:15.923Z
*/
import type { SwitchCaseResponseType } from '../api.js';
diff --git a/packages/misskey-js/src/autogen/endpoint.ts b/packages/misskey-js/src/autogen/endpoint.ts
index 8cf669cab6..24ed297b56 100644
--- a/packages/misskey-js/src/autogen/endpoint.ts
+++ b/packages/misskey-js/src/autogen/endpoint.ts
@@ -1,6 +1,6 @@
/*
* version: 2024.2.0-beta.7
- * generatedAt: 2024-01-26T05:23:04.909Z
+ * generatedAt: 2024-01-26T07:53:15.921Z
*/
import type {
diff --git a/packages/misskey-js/src/autogen/entities.ts b/packages/misskey-js/src/autogen/entities.ts
index 41a6f6c7a7..5e43ef831d 100644
--- a/packages/misskey-js/src/autogen/entities.ts
+++ b/packages/misskey-js/src/autogen/entities.ts
@@ -1,6 +1,6 @@
/*
* version: 2024.2.0-beta.7
- * generatedAt: 2024-01-26T05:23:04.908Z
+ * generatedAt: 2024-01-26T07:53:15.919Z
*/
import { operations } from './types.js';
diff --git a/packages/misskey-js/src/autogen/models.ts b/packages/misskey-js/src/autogen/models.ts
index 01a5cddbb8..eb0070f9b6 100644
--- a/packages/misskey-js/src/autogen/models.ts
+++ b/packages/misskey-js/src/autogen/models.ts
@@ -1,6 +1,6 @@
/*
* version: 2024.2.0-beta.7
- * generatedAt: 2024-01-26T05:23:04.907Z
+ * generatedAt: 2024-01-26T07:53:15.918Z
*/
import { components } from './types.js';
diff --git a/packages/misskey-js/src/autogen/types.ts b/packages/misskey-js/src/autogen/types.ts
index c4595ef4f2..02752c24cb 100644
--- a/packages/misskey-js/src/autogen/types.ts
+++ b/packages/misskey-js/src/autogen/types.ts
@@ -3,7 +3,7 @@
/*
* version: 2024.2.0-beta.7
- * generatedAt: 2024-01-26T05:23:04.825Z
+ * generatedAt: 2024-01-26T07:53:15.837Z
*/
/**
@@ -4595,6 +4595,7 @@ export type components = {
user2Ready: boolean;
user3Ready: boolean;
user4Ready: boolean;
+ timeLimitForEachTurn: number;
};
};
responses: never;
diff --git a/packages/misskey-mahjong/package.json b/packages/misskey-mahjong/package.json
index 36348665ac..a7942b6475 100644
--- a/packages/misskey-mahjong/package.json
+++ b/packages/misskey-mahjong/package.json
@@ -2,6 +2,7 @@
"type": "module",
"name": "misskey-mahjong",
"version": "0.0.1",
+ "types": "./built/dts/index.d.ts",
"exports": {
".": {
"import": "./built/esm/index.js",
diff --git a/packages/misskey-mahjong/src/common.ts b/packages/misskey-mahjong/src/common.ts
new file mode 100644
index 0000000000..bf7d15491e
--- /dev/null
+++ b/packages/misskey-mahjong/src/common.ts
@@ -0,0 +1,48 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+// NOTE: アガリ形の判定に使われるため並び順が重要
+// 具体的には、文字列としてソートした際に同じ牌種の1~9が順に並んでいる必要がある
+// また、字牌は最後にある必要がある
+export const TILE_TYPES = [
+ 'm1',
+ 'm2',
+ 'm3',
+ 'm4',
+ 'm5',
+ 'm6',
+ 'm7',
+ 'm8',
+ 'm9',
+ 'p1',
+ 'p2',
+ 'p3',
+ 'p4',
+ 'p5',
+ 'p6',
+ 'p7',
+ 'p8',
+ 'p9',
+ 's1',
+ 's2',
+ 's3',
+ 's4',
+ 's5',
+ 's6',
+ 's7',
+ 's8',
+ 's9',
+ 'e',
+ 's',
+ 'w',
+ 'n',
+ 'haku',
+ 'hatsu',
+ 'chun',
+] as const;
+
+export type Tile = typeof TILE_TYPES[number];
+
+export type House = 'e' | 's' | 'w' | 'n';
diff --git a/packages/misskey-mahjong/src/engine.ts b/packages/misskey-mahjong/src/engine.ts
index 5dd50817d5..3bab8e8390 100644
--- a/packages/misskey-mahjong/src/engine.ts
+++ b/packages/misskey-mahjong/src/engine.ts
@@ -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;
+ },
+}];
diff --git a/packages/misskey-mahjong/src/index.ts b/packages/misskey-mahjong/src/index.ts
index 1fc4505942..c2cc36da3f 100644
--- a/packages/misskey-mahjong/src/index.ts
+++ b/packages/misskey-mahjong/src/index.ts
@@ -5,3 +5,5 @@
export * as Engine from './engine.js';
export * as Serializer from './serializer.js';
+export * as Common from './common.js';
+export * as Utils from './utils.js';
diff --git a/packages/misskey-mahjong/src/serializer.ts b/packages/misskey-mahjong/src/serializer.ts
index 7444812b2b..94be1c6947 100644
--- a/packages/misskey-mahjong/src/serializer.ts
+++ b/packages/misskey-mahjong/src/serializer.ts
@@ -15,40 +15,40 @@ export type Log = {
export type SerializedLog = number[];
export const TILE_MAP: Record = {
- 'bamboo1': 1,
- 'bamboo2': 2,
- 'bamboo3': 3,
- 'bamboo4': 4,
- 'bamboo5': 5,
- 'bamboo6': 6,
- 'bamboo7': 7,
- 'bamboo8': 8,
- 'bamboo9': 9,
- 'character1': 10,
- 'character2': 11,
- 'character3': 12,
- 'character4': 13,
- 'character5': 14,
- 'character6': 15,
- 'character7': 16,
- 'character8': 17,
- 'character9': 18,
- 'circle1': 19,
- 'circle2': 20,
- 'circle3': 21,
- 'circle4': 22,
- 'circle5': 23,
- 'circle6': 24,
- 'circle7': 25,
- 'circle8': 26,
- 'circle9': 27,
- 'wind-east': 28,
- 'wind-south': 29,
- 'wind-west': 30,
- 'wind-north': 31,
- 'dragon-red': 32,
- 'dragon-green': 33,
- 'dragon-white': 34,
+ 'm1': 1,
+ 'm2': 2,
+ 'm3': 3,
+ 'm4': 4,
+ 'm5': 5,
+ 'm6': 6,
+ 'm7': 7,
+ 'm8': 8,
+ 'm9': 9,
+ 'p1': 10,
+ 'p2': 11,
+ 'p3': 12,
+ 'p4': 13,
+ 'p5': 14,
+ 'p6': 15,
+ 'p7': 16,
+ 'p8': 17,
+ 'p9': 18,
+ 's1': 19,
+ 's2': 20,
+ 's3': 21,
+ 's4': 22,
+ 's5': 23,
+ 's6': 24,
+ 's7': 25,
+ 's8': 26,
+ 's9': 27,
+ 'e': 28,
+ 's': 29,
+ 'w': 30,
+ 'n': 31,
+ 'haku': 32,
+ 'hatsu': 33,
+ 'chun': 34,
};
export function serializeTile(tile: Tile): number {
diff --git a/packages/misskey-mahjong/src/utils.ts b/packages/misskey-mahjong/src/utils.ts
new file mode 100644
index 0000000000..72ec2cfb2c
--- /dev/null
+++ b/packages/misskey-mahjong/src/utils.ts
@@ -0,0 +1,235 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { House, 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';
+ }
+}
+
+export function prevHouse(house: House): House {
+ switch (house) {
+ case 'e': return 'n';
+ case 's': return 'e';
+ case 'w': return 's';
+ case 'n': return 'w';
+ }
+}
+
+type HoraSet = {
+ head: Tile;
+ mentsus: [Tile, Tile, Tile][];
+};
+
+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();
+ 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 tempTempHandTilesWithoutKotsuAndShuntsu: (Tile | null)[] = [...tempHandTilesWithoutKotsu];
+
+ const shuntsus: [Tile, Tile, Tile][] = [];
+ let i = 0;
+ while (i < tempHandTilesWithoutKotsu.length) {
+ const headThree = tempHandTilesWithoutKotsu.slice(i, i + 3);
+ if (headThree.length !== 3) break;
+
+ for (const shuntuPattern of SHUNTU_PATTERNS) {
+ if (headThree[0] === shuntuPattern[0] && headThree[1] === shuntuPattern[1] && headThree[2] === shuntuPattern[2]) {
+ shuntsus.push(shuntuPattern);
+ tempTempHandTilesWithoutKotsuAndShuntsu[i] = null;
+ tempTempHandTilesWithoutKotsuAndShuntsu[i + 1] = null;
+ tempTempHandTilesWithoutKotsuAndShuntsu[i + 2] = null;
+ i += 3;
+ break;
+ }
+ }
+
+ i++;
+ }
+
+ const tempHandTilesWithoutKotsuAndShuntsu = tempTempHandTilesWithoutKotsuAndShuntsu.filter(t => t != null) as Tile[];
+
+ if (tempHandTilesWithoutKotsuAndShuntsu.length === 0) { // アガリ形
+ 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;
+ });
+}
diff --git a/scripts/dev.mjs b/scripts/dev.mjs
index d8f369af3f..d5e4c23516 100644
--- a/scripts/dev.mjs
+++ b/scripts/dev.mjs
@@ -46,7 +46,7 @@ await execa('pnpm', ['--filter', 'misskey-bubble-game', 'build:tsc'], {
stderr: process.stderr,
});
-await execa('pnpm', ['--filter', 'misskey-mahjong', 'build'], {
+await execa('pnpm', ['--filter', 'misskey-mahjong', 'build:tsc'], {
cwd: _dirname + '/../',
stdout: process.stdout,
stderr: process.stderr,