diff --git a/packages/frontend/.eslintrc.cjs b/packages/frontend/.eslintrc.cjs index 20f88dc078..669b57d736 100644 --- a/packages/frontend/.eslintrc.cjs +++ b/packages/frontend/.eslintrc.cjs @@ -22,8 +22,7 @@ module.exports = { }, ], // window の禁止理由: グローバルスコープと衝突し、予期せぬ結果を招くため - // e の禁止理由: error や event など、複数のキーワードの頭文字であり分かりにくいため - 'id-denylist': ['error', 'window', 'e'], + 'id-denylist': ['error', 'window'], 'no-shadow': ['warn'], 'vue/attributes-order': ['error', { 'alphabetical': false, diff --git a/packages/frontend/assets/mahjong/cii.png b/packages/frontend/assets/mahjong/cii.png new file mode 100644 index 0000000000..d2da0a111d Binary files /dev/null and b/packages/frontend/assets/mahjong/cii.png differ diff --git a/packages/frontend/assets/mahjong/kan.png b/packages/frontend/assets/mahjong/kan.png new file mode 100644 index 0000000000..3442df0004 Binary files /dev/null and b/packages/frontend/assets/mahjong/kan.png differ diff --git a/packages/frontend/assets/mahjong/pon.png b/packages/frontend/assets/mahjong/pon.png new file mode 100644 index 0000000000..204e7fcd52 Binary files /dev/null and b/packages/frontend/assets/mahjong/pon.png differ diff --git a/packages/frontend/assets/mahjong/ron.png b/packages/frontend/assets/mahjong/ron.png index 8bd40d6dca..6e1f889865 100644 Binary files a/packages/frontend/assets/mahjong/ron.png and b/packages/frontend/assets/mahjong/ron.png differ diff --git a/packages/frontend/assets/mahjong/tsumo.png b/packages/frontend/assets/mahjong/tsumo.png new file mode 100644 index 0000000000..14577818ab Binary files /dev/null and b/packages/frontend/assets/mahjong/tsumo.png differ diff --git a/packages/frontend/src/pages/mahjong/room.game.vue b/packages/frontend/src/pages/mahjong/room.game.vue index cdc63d7af8..55fb295117 100644 --- a/packages/frontend/src/pages/mahjong/room.game.vue +++ b/packages/frontend/src/pages/mahjong/room.game.vue @@ -9,20 +9,28 @@ SPDX-License-Identifier: AGPL-3.0-only
- {{ 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 }} - {{ engine.state.points[Mahjong.Utils.prevHouse(Mahjong.Utils.prevHouse(engine.myHouse))] }} +
+ {{ 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 }} + {{ engine.state.points[Mahjong.Utils.prevHouse(Mahjong.Utils.prevHouse(engine.myHouse))] }} +
- {{ 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 }} - {{ engine.state.points[Mahjong.Utils.prevHouse(engine.myHouse)] }} +
+ {{ 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 }} + {{ engine.state.points[Mahjong.Utils.prevHouse(engine.myHouse)] }} +
- {{ 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 }} - {{ engine.state.points[Mahjong.Utils.nextHouse(engine.myHouse)] }} +
+ {{ 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 }} + {{ engine.state.points[Mahjong.Utils.nextHouse(engine.myHouse)] }} +
- {{ engine.myHouse === 'e' ? i18n.ts._mahjong.east : engine.myHouse === 's' ? i18n.ts._mahjong.south : engine.myHouse === 'w' ? i18n.ts._mahjong.west : i18n.ts._mahjong.north }} - {{ engine.state.points[engine.myHouse] }} +
+ {{ engine.myHouse === 'e' ? i18n.ts._mahjong.east : engine.myHouse === 's' ? i18n.ts._mahjong.south : engine.myHouse === 'w' ? i18n.ts._mahjong.west : i18n.ts._mahjong.north }} + {{ engine.state.points[engine.myHouse] }} +
@@ -49,28 +57,28 @@ SPDX-License-Identifier: AGPL-3.0-only
- +
- +
- +
- +
@@ -79,7 +87,7 @@ SPDX-License-Identifier: AGPL-3.0-only
@@ -88,7 +96,7 @@ SPDX-License-Identifier: AGPL-3.0-only
@@ -99,23 +107,57 @@ SPDX-License-Identifier: AGPL-3.0-only
- - - + + +
+ +
+
+ + + + + +
+
+ + + + + +
+
+ + + + + +
+
+ + + + + +
+
+ +
+ Ron + Pon + Skip + Tsumo + Riichi +
- Ron - Pon - Skip - Tsumo - Riichi
diff --git a/packages/frontend/src/pages/mahjong/tile.vue b/packages/frontend/src/pages/mahjong/tile.vue index 6a8428c073..1a18a0140f 100644 --- a/packages/frontend/src/pages/mahjong/tile.vue +++ b/packages/frontend/src/pages/mahjong/tile.vue @@ -4,23 +4,31 @@ SPDX-License-Identifier: AGPL-3.0-only --> diff --git a/packages/misskey-mahjong/src/common.ts b/packages/misskey-mahjong/src/common.ts index 31a3594fb0..5e9ffa2b87 100644 --- a/packages/misskey-mahjong/src/common.ts +++ b/packages/misskey-mahjong/src/common.ts @@ -64,6 +64,43 @@ export type Huro = { from: House | null; // null で加槓 }; +export const NEXT_TILE_FOR_DORA_MAP: Record = { + m1: 'm2', + m2: 'm3', + m3: 'm4', + m4: 'm5', + m5: 'm6', + m6: 'm7', + m7: 'm8', + m8: 'm9', + m9: 'm1', + p1: 'p2', + p2: 'p3', + p3: 'p4', + p4: 'p5', + p5: 'p6', + p6: 'p7', + p7: 'p8', + p8: 'p9', + p9: 'p1', + s1: 's2', + s2: 's3', + s3: 's4', + s4: 's5', + s5: 's6', + s6: 's7', + s7: 's8', + s8: 's9', + s9: 's1', + e: 's', + s: 'w', + w: 'n', + n: 'e', + haku: 'hatsu', + hatsu: 'chun', + chun: 'haku', +}; + export const yakuNames = [ 'riichi', 'ippatsu', @@ -245,3 +282,45 @@ export const YAKU_DEFINITIONS = [{ ); }, }]; + +export function fanToPoint(fan: number, isParent: boolean): number { + let point; + + if (fan >= 13) { + point = 32000; + } else if (fan >= 11) { + point = 24000; + } else if (fan >= 8) { + point = 16000; + } else if (fan >= 6) { + point = 12000; + } else if (fan >= 4) { + point = 8000; + } else if (fan >= 3) { + point = 4000; + } else if (fan >= 2) { + point = 2000; + } else { + point = 1000; + } + + if (isParent) { + point *= 1.5; + } + + return point; +} + +export function calcOwnedDoraCount(handTiles: Tile[], huros: Huro[], doras: Tile[]): number { + let count = 0; + for (const t of handTiles) { + if (doras.includes(t)) count++; + } + for (const huro of huros) { + if (huro.type === 'pon' && doras.includes(huro.tile)) count += 3; + if (huro.type === 'cii') count += huro.tiles.filter(t => doras.includes(t)).length; + if (huro.type === 'minkan' && doras.includes(huro.tile)) count += 4; + if (huro.type === 'ankan' && doras.includes(huro.tile)) count += 4; + } + return count; +} diff --git a/packages/misskey-mahjong/src/engine.master.ts b/packages/misskey-mahjong/src/engine.master.ts index 67ec730791..7fbc9bcd69 100644 --- a/packages/misskey-mahjong/src/engine.master.ts +++ b/packages/misskey-mahjong/src/engine.master.ts @@ -5,6 +5,7 @@ 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'; @@ -18,6 +19,8 @@ export type MasterState = { kyoku: number; tiles: Tile[]; + kingTiles: Tile[]; + activatedDorasCount: number; /** * 副露した牌を含まない手牌 @@ -112,8 +115,12 @@ export class MasterGameEngine { this.state = state; } + public get doras(): Tile[] { + return this.state.kingTiles.slice(0, this.state.activatedDorasCount).map(t => Utils.nextTileForDora(t)); + } + public static createInitialState(): MasterState { - const ikasama: Tile[] = ['haku', 'm2', 'm3', 'p5', 'p6', 'p7', 's2', 's3', 's4', 'chun', 'chun', 'chun', 'n', 'n']; + const ikasama: Tile[] = ['haku', 'hatsu', 'm3', 'p5', 'p6', 'p7', 's2', 's3', 's4', 'chun', 'chun', 'chun', 'n', 'n']; const tiles = [...TILE_TYPES.slice(), ...TILE_TYPES.slice(), ...TILE_TYPES.slice(), ...TILE_TYPES.slice()]; tiles.sort(() => Math.random() - 0.5); @@ -128,6 +135,7 @@ export class MasterGameEngine { const sHandTiles = tiles.splice(0, 13); const wHandTiles = tiles.splice(0, 13); const nHandTiles = tiles.splice(0, 13); + const kingTiles = tiles.splice(0, 14); return { user1House: 'e', @@ -137,6 +145,8 @@ export class MasterGameEngine { round: 'e', kyoku: 1, tiles, + kingTiles, + activatedDorasCount: 1, handTiles: { e: eHandTiles, s: sHandTiles, @@ -219,6 +229,11 @@ export class MasterGameEngine { newState.points = this.state.points; } + /** + * ロン + * @param callers ロンする人 + * @param callee ロンされる人 + */ private ron(callers: House[], callee: House) { for (const house of callers) { const yakus = YAKU_DEFINITIONS.filter(yaku => yaku.calc({ @@ -229,6 +244,12 @@ export class MasterGameEngine { ronTile: this.state.hoTiles[callee].at(-1)!, riichi: this.state.riichis[house], })); + const doraCount = Common.calcOwnedDoraCount(this.state.handTiles[house], this.state.huros[house], this.doras); + const fans = yakus.map(yaku => yaku.fan).reduce((a, b) => a + b, 0) + doraCount; + const point = Common.fanToPoint(fans, house === 'e'); + this.state.points[callee] -= point; + this.state.points[house] += point; + console.log('fans point', fans, point); console.log('yakus', house, yakus); } @@ -365,7 +386,42 @@ export class MasterGameEngine { public commit_hora(house: House) { if (this.state.turn !== house) throw new Error('Not your turn'); - const yakus = Utils.getYakus(this.state.handTiles[house], null); + const isParent = house === 'e'; + + const yakus = YAKU_DEFINITIONS.filter(yaku => yaku.calc({ + house: house, + handTiles: this.state.handTiles[house], + huros: this.state.huros[house], + tsumoTile: this.state.handTiles[house].at(-1)!, + ronTile: null, + riichi: this.state.riichis[house], + })); + const doraCount = Common.calcOwnedDoraCount(this.state.handTiles[house], this.state.huros[house], this.doras); + const fans = yakus.map(yaku => yaku.fan).reduce((a, b) => a + b, 0) + doraCount; + const point = Common.fanToPoint(fans, isParent); + this.state.points[house] += point; + if (isParent) { + const childPoint = Math.ceil(point / 3); + this.state.points.s -= childPoint; + this.state.points.w -= childPoint; + this.state.points.n -= childPoint; + } else { + const parentPoint = Math.ceil(point / 2); + this.state.points.e -= parentPoint; + const otherPoint = Math.ceil(point / 4); + if (house === 's') { + this.state.points.w -= otherPoint; + this.state.points.n -= otherPoint; + } else if (house === 'w') { + this.state.points.s -= otherPoint; + this.state.points.n -= otherPoint; + } else if (house === 'n') { + this.state.points.s -= otherPoint; + this.state.points.w -= otherPoint; + } + } + console.log('fans point', fans, point); + console.log('yakus', house, yakus); this.endKyoku(); } @@ -402,6 +458,8 @@ export class MasterGameEngine { const tile = this.state.hoTiles[kan.callee].pop()!; this.state.huros[kan.caller].push({ type: 'minkan', tile, from: kan.callee }); + this.state.activatedDorasCount++; + const rinsyan = this.tsumo(); this.state.turn = kan.caller; @@ -476,6 +534,7 @@ export class MasterGameEngine { round: this.state.round, kyoku: this.state.kyoku, tilesCount: this.state.tiles.length, + doraIndicateTiles: this.state.kingTiles.slice(0, this.state.activatedDorasCount), handTiles: { e: house === 'e' ? this.state.handTiles.e : this.state.handTiles.e.map(() => null), s: house === 's' ? this.state.handTiles.s : this.state.handTiles.s.map(() => null), diff --git a/packages/misskey-mahjong/src/engine.player.ts b/packages/misskey-mahjong/src/engine.player.ts index 415c962928..51babc5832 100644 --- a/packages/misskey-mahjong/src/engine.player.ts +++ b/packages/misskey-mahjong/src/engine.player.ts @@ -5,6 +5,7 @@ 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 = { @@ -17,6 +18,7 @@ export type PlayerState = { kyoku: number; tilesCount: number; + doraIndicateTiles: Tile[]; /** * 副露した牌を含まない手牌 @@ -94,6 +96,10 @@ export class PlayerGameEngine { return this.state.riichis[this.myHouse]; } + public get doras(): Tile[] { + return this.state.doraIndicateTiles.map(t => Utils.nextTileForDora(t)); + } + public commit_tsumo(house: House, tile: Tile) { console.log('commit_tsumo', this.state.turn, house, tile); this.state.tilesCount--; @@ -161,7 +167,19 @@ export class PlayerGameEngine { this.state.canRonSource = null; - // TODO: ロンした人の手牌情報を貰う必要がある + const yakusMap: Record = { + e: [] as { name: string; fan: number; }[], + s: [] as { name: string; fan: number; }[], + w: [] as { name: string; fan: number; }[], + n: [] as { name: string; fan: number; }[], + }; + + const doraCountsMap: Record = { + e: 0, + s: 0, + w: 0, + n: 0, + }; for (const house of callers) { const yakus = YAKU_DEFINITIONS.filter(yaku => yaku.calc({ @@ -172,8 +190,20 @@ export class PlayerGameEngine { ronTile: this.state.hoTiles[callee].at(-1)!, riichi: this.state.riichis[house], })); + const doraCount = Common.calcOwnedDoraCount(handTiles[house], this.state.huros[house], this.doras); + const fans = yakus.map(yaku => yaku.fan).reduce((a, b) => a + b, 0) + doraCount; + const point = Common.fanToPoint(fans, house === 'e'); + this.state.points[callee] -= point; + this.state.points[house] += point; + yakusMap[house] = yakus.map(yaku => ({ name: yaku.name, fan: yaku.fan })); + doraCountsMap[house] = doraCount; console.log('yakus', house, yakus); } + + return { + yakusMap, + doraCountsMap, + }; } /** diff --git a/packages/misskey-mahjong/src/utils.ts b/packages/misskey-mahjong/src/utils.ts index bed483548e..54af6937e5 100644 --- a/packages/misskey-mahjong/src/utils.ts +++ b/packages/misskey-mahjong/src/utils.ts @@ -3,7 +3,7 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { House, TILE_TYPES, Tile } from './common.js'; +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); @@ -242,3 +242,7 @@ export function getTilesForRiichi(handTiles: Tile[]): Tile[] { return horaTiles.length > 0; }); } + +export function nextTileForDora(tile: Tile): Tile { + return NEXT_TILE_FOR_DORA_MAP[tile]; +}