wip
This commit is contained in:
		
							parent
							
								
									7a9434414d
								
							
						
					
					
						commit
						2d6f9b083f
					
				|  | @ -280,28 +280,18 @@ export class MahjongService implements OnApplicationShutdown, OnModuleInit { | |||
| 
 | ||||
| 		room.gameState = Mmj.MasterGameEngine.createInitialState(); | ||||
| 		room.isStarted = true; | ||||
| 
 | ||||
| 		await this.saveRoom(room); | ||||
| 
 | ||||
| 		this.globalEventService.publishMahjongRoomStream(room.id, 'started', { room: room }); | ||||
| 
 | ||||
| 		return room; | ||||
| 		this.kyokuStarted(room); | ||||
| 	} | ||||
| 
 | ||||
| 	@bindThis | ||||
| 	public async packRoom(room: Room, me: MiUser) { | ||||
| 		if (room.gameState) { | ||||
| 			const mj = new Mmj.MasterGameEngine(room.gameState); | ||||
| 			const myIndex = room.user1Id === me.id ? 1 : room.user2Id === me.id ? 2 : room.user3Id === me.id ? 3 : 4; | ||||
| 			return { | ||||
| 				...room, | ||||
| 				gameState: mj.createPlayerState(myIndex), | ||||
| 			}; | ||||
| 		} else { | ||||
| 			return { | ||||
| 				...room, | ||||
| 			}; | ||||
| 		} | ||||
| 	private kyokuStarted(room: Room) { | ||||
| 		const mj = new Mmj.MasterGameEngine(room.gameState); | ||||
| 
 | ||||
| 		this.waitForTurn(room, mj.turn, mj); | ||||
| 	} | ||||
| 
 | ||||
| 	@bindThis | ||||
|  | @ -379,6 +369,17 @@ export class MahjongService implements OnApplicationShutdown, OnModuleInit { | |||
| 		}, 2000); | ||||
| 	} | ||||
| 
 | ||||
| 	@bindThis | ||||
| 	private async nextKyoku(room: Room, mj: Mmj.MasterGameEngine) { | ||||
| 		const res = mj.commit_nextKyoku(); | ||||
| 		room.gameState = mj.getState(); | ||||
| 		await this.saveRoom(room); | ||||
| 		this.globalEventService.publishMahjongRoomStream(room.id, 'nextKyoku', { | ||||
| 			room: room, | ||||
| 		}); | ||||
| 		this.kyokuStarted(room); | ||||
| 	} | ||||
| 
 | ||||
| 	@bindThis | ||||
| 	private async dahai(room: Room, mj: Mmj.MasterGameEngine, house: Mmj.House, tile: Mmj.TileId, riichi = false) { | ||||
| 		const res = mj.commit_dahai(house, tile, riichi); | ||||
|  | @ -551,6 +552,8 @@ export class MahjongService implements OnApplicationShutdown, OnModuleInit { | |||
| 		await this.saveRoom(room); | ||||
| 
 | ||||
| 		this.globalEventService.publishMahjongRoomStream(room.id, 'tsumoHora', { house: myHouse, handTiles: res.handTiles, tsumoTile: res.tsumoTile }); | ||||
| 
 | ||||
| 		this.endKyoku(room, mj); | ||||
| 	} | ||||
| 
 | ||||
| 	@bindThis | ||||
|  | @ -700,6 +703,27 @@ export class MahjongService implements OnApplicationShutdown, OnModuleInit { | |||
| 		await this.redisClient.del(`mahjong:gameTurnWaiting:${roomId}`); | ||||
| 	} | ||||
| 
 | ||||
| 	@bindThis | ||||
| 	public packState(room: Room, me: MiUser) { | ||||
| 		const mj = new Mmj.MasterGameEngine(room.gameState); | ||||
| 		const myIndex = room.user1Id === me.id ? 1 : room.user2Id === me.id ? 2 : room.user3Id === me.id ? 3 : 4; | ||||
| 		return mj.createPlayerState(myIndex); | ||||
| 	} | ||||
| 
 | ||||
| 	@bindThis | ||||
| 	public async packRoom(room: Room, me: MiUser) { | ||||
| 		if (room.gameState) { | ||||
| 			return { | ||||
| 				...room, | ||||
| 				gameState: this.packState(room, me), | ||||
| 			}; | ||||
| 		} else { | ||||
| 			return { | ||||
| 				...room, | ||||
| 			}; | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	@bindThis | ||||
| 	public dispose(): void { | ||||
| 	} | ||||
|  |  | |||
|  | @ -40,6 +40,11 @@ class MahjongRoomChannel extends Channel { | |||
| 			this.send('started', { | ||||
| 				room: packed, | ||||
| 			}); | ||||
| 		} else if (message.type === 'nextKyoku') { | ||||
| 			const packed = this.mahjongService.packState(message.body.room, this.user!); | ||||
| 			this.send('nextKyoku', { | ||||
| 				state: packed, | ||||
| 			}); | ||||
| 		} else { | ||||
| 			this.send(message.type, message.body); | ||||
| 		} | ||||
|  |  | |||
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 68 KiB | 
|  | @ -40,12 +40,12 @@ import { computed } from 'vue'; | |||
| import * as Mmj from 'misskey-mahjong'; | ||||
| 
 | ||||
| //#region syntax suger | ||||
| function mj$(tileId: Mmj.TileId): Mmj.TileInstance { | ||||
| 	return Mmj.findTileByIdOrFail(tileId); | ||||
| function mj$(tid: Mmj.TileId): Mmj.TileInstance { | ||||
| 	return Mmj.findTileByIdOrFail(tid); | ||||
| } | ||||
| 
 | ||||
| function mj$type(tileId: Mmj.TileId): Mmj.TileType { | ||||
| 	return mj$(tileId).t; | ||||
| function mj$type(tid: Mmj.TileId): Mmj.TileType { | ||||
| 	return mj$(tid).t; | ||||
| } | ||||
| //#endregion | ||||
| 
 | ||||
|  |  | |||
|  | @ -168,6 +168,17 @@ SPDX-License-Identifier: AGPL-3.0-only | |||
| 			</div> | ||||
| 		</div> | ||||
| 
 | ||||
| 		<div :class="$style.startTextContainer"> | ||||
| 			<Transition | ||||
| 				:enterActiveClass="$style.transition_serif_enterActive" | ||||
| 				:leaveActiveClass="$style.transition_serif_leaveActive" | ||||
| 				:enterFromClass="$style.transition_serif_enterFrom" | ||||
| 				:leaveToClass="$style.transition_serif_leaveTo" | ||||
| 			> | ||||
| 				<img v-if="startTextShowing" :src="`/client-assets/mahjong/kaisi.png`" style="display: block; width: 100%;"/> | ||||
| 			</Transition> | ||||
| 		</div> | ||||
| 
 | ||||
| 		<div :class="$style.ryuukyokuContainer"> | ||||
| 			<Transition | ||||
| 				:enterActiveClass="$style.transition_serif_enterActive" | ||||
|  | @ -210,6 +221,7 @@ SPDX-License-Identifier: AGPL-3.0-only | |||
| 				<div>{{ res.pointDeltas.e }} / {{ res.pointDeltas.s }} / {{ res.pointDeltas.w }} / {{ res.pointDeltas.n }}</div> | ||||
| 			</div> | ||||
| 		</div> | ||||
| 		<MkButton primary @click="confirmKyokuResult">OK</MkButton> | ||||
| 	</div> | ||||
| </div> | ||||
| </template> | ||||
|  | @ -235,12 +247,12 @@ import * as os from '@/os.js'; | |||
| import { confetti } from '@/scripts/confetti.js'; | ||||
| 
 | ||||
| //#region syntax suger | ||||
| function mj$(tileId: Mmj.TileId): Mmj.TileInstance { | ||||
| 	return Mmj.findTileByIdOrFail(tileId); | ||||
| function mj$(tid: Mmj.TileId): Mmj.TileInstance { | ||||
| 	return Mmj.findTileByIdOrFail(tid); | ||||
| } | ||||
| 
 | ||||
| function mj$type(tileId: Mmj.TileId): Mmj.TileType { | ||||
| 	return mj$(tileId).t; | ||||
| function mj$type(tid: Mmj.TileId): Mmj.TileType { | ||||
| 	return mj$(tid).t; | ||||
| } | ||||
| //#endregion | ||||
| 
 | ||||
|  | @ -256,11 +268,11 @@ const myUserNumber = computed(() => room.value.user1Id === $i.id ? 1 : room.valu | |||
| const mj = shallowRef(new Mmj.PlayerGameEngine(myUserNumber.value, room.value.gameState)); | ||||
| 
 | ||||
| const isMyTurn = computed(() => { | ||||
| 	return mj.value.state.turn === mj.value.myHouse; | ||||
| 	return mj.value.turn === mj.value.myHouse; | ||||
| }); | ||||
| 
 | ||||
| const canHora = computed(() => { | ||||
| 	return Mmj.getHoraSets(mj.value.myHandTiles).length > 0; | ||||
| 	return Mmj.getHoraSets(mj.value.myHandTileTypes).length > 0; | ||||
| }); | ||||
| 
 | ||||
| const selectableTiles = ref<Mmj.TileType[] | null>(null); | ||||
|  | @ -286,6 +298,7 @@ const kyokuResults = ref<Record<Mmj.House, { | |||
| 	n: null, | ||||
| }); | ||||
| const ryuukyokued = ref(false); | ||||
| const startTextShowing = ref(false); | ||||
| 
 | ||||
| /* | ||||
| if (room.value.isStarted && !room.value.isEnded) { | ||||
|  | @ -329,7 +342,7 @@ if (!props.room.isEnded) { | |||
| */ | ||||
| 
 | ||||
| function houseToUser(house: Mmj.House) { | ||||
| 	return room.value.gameState.user1House === house ? room.value.user1 : room.value.gameState.user2House === house ? room.value.user2 : room.value.gameState.user3House === house ? room.value.user3 : room.value.user4; | ||||
| 	return mj.value.user1House === house ? room.value.user1 : mj.value.user2House === house ? room.value.user2 : mj.value.user3House === house ? room.value.user3 : room.value.user4; | ||||
| } | ||||
| 
 | ||||
| let riichiSelect = false; | ||||
|  | @ -434,6 +447,10 @@ function skip() { | |||
| 	props.connection!.send('nop', {}); | ||||
| } | ||||
| 
 | ||||
| function confirmKyokuResult() { | ||||
| 	props.connection!.send('confirmNextKyoku', {}); | ||||
| } | ||||
| 
 | ||||
| function onStreamDahai(log) { | ||||
| 	console.log('onStreamDahai', log); | ||||
| 
 | ||||
|  | @ -602,6 +619,10 @@ function onStreamRonned(log) { | |||
| 
 | ||||
| 	for (const caller of log.callers) { | ||||
| 		ronSerifHouses[caller] = true; | ||||
| 
 | ||||
| 		window.setTimeout(() => { | ||||
| 			ronSerifHouses[caller] = false; | ||||
| 		}, 2000); | ||||
| 	} | ||||
| 
 | ||||
| 	console.log('ronned', res); | ||||
|  | @ -620,6 +641,9 @@ function onStreamTsumoHora(log) { | |||
| 	}, 1500); | ||||
| 
 | ||||
| 	tsumoSerifHouses[log.house] = true; | ||||
| 	window.setTimeout(() => { | ||||
| 		tsumoSerifHouses[log.house] = false; | ||||
| 	}, 2000); | ||||
| 
 | ||||
| 	console.log('tsumohora', res); | ||||
| } | ||||
|  | @ -634,6 +658,23 @@ function onStreamRyuukyoku(log) { | |||
| 	}, 1500); | ||||
| } | ||||
| 
 | ||||
| function onStreamNextKyoku(log) { | ||||
| 	console.log('onStreamNextKyoku', log); | ||||
| 
 | ||||
| 	const res = mj.value.commit_nextKyoku(log.state); | ||||
| 	triggerRef(mj); | ||||
| 
 | ||||
| 	iTsumoed.value = false; | ||||
| 	showKyokuResults.value = false; | ||||
| 	kyokuResults.value = { | ||||
| 		e: null, | ||||
| 		s: null, | ||||
| 		w: null, | ||||
| 		n: null, | ||||
| 	}; | ||||
| 	ryuukyokued.value = false; | ||||
| } | ||||
| 
 | ||||
| function restoreRoom(_room) { | ||||
| 	room.value = deepClone(_room); | ||||
| 
 | ||||
|  | @ -653,6 +694,7 @@ onMounted(() => { | |||
| 		props.connection.on('ronned', onStreamRonned); | ||||
| 		props.connection.on('tsumoHora', onStreamTsumoHora); | ||||
| 		props.connection.on('ryuukyoku', onStreamRyuukyoku); | ||||
| 		props.connection.on('nextKyoku', onStreamNextKyoku); | ||||
| 	} | ||||
| }); | ||||
| 
 | ||||
|  | @ -669,6 +711,7 @@ onActivated(() => { | |||
| 		props.connection.on('ronned', onStreamRonned); | ||||
| 		props.connection.on('tsumoHora', onStreamTsumoHora); | ||||
| 		props.connection.on('ryuukyoku', onStreamRyuukyoku); | ||||
| 		props.connection.on('nextKyoku', onStreamNextKyoku); | ||||
| 	} | ||||
| }); | ||||
| 
 | ||||
|  | @ -685,6 +728,7 @@ onDeactivated(() => { | |||
| 		props.connection.off('ronned', onStreamRonned); | ||||
| 		props.connection.off('tsumoHora', onStreamTsumoHora); | ||||
| 		props.connection.off('ryuukyoku', onStreamRyuukyoku); | ||||
| 		props.connection.off('nextKyoku', onStreamNextKyoku); | ||||
| 	} | ||||
| }); | ||||
| 
 | ||||
|  | @ -701,6 +745,7 @@ onUnmounted(() => { | |||
| 		props.connection.off('ronned', onStreamRonned); | ||||
| 		props.connection.off('tsumoHora', onStreamTsumoHora); | ||||
| 		props.connection.off('ryuukyoku', onStreamRyuukyoku); | ||||
| 		props.connection.off('nextKyoku', onStreamNextKyoku); | ||||
| 	} | ||||
| }); | ||||
| </script> | ||||
|  |  | |||
|  | @ -92,14 +92,14 @@ export const TILE_ID_MAP = new Map<TileId, TileInstance>([ | |||
| 	/* eslint-enable no-multi-spaces */ | ||||
| ]); | ||||
| 
 | ||||
| export function findTileByIdOrFail(tileId: TileId): TileInstance { | ||||
| 	const tile = TILE_ID_MAP.get(tileId); | ||||
| 	if (tile == null) throw new Error(`tile not found: ${tileId}`); | ||||
| export function findTileByIdOrFail(tid: TileId): TileInstance { | ||||
| 	const tile = TILE_ID_MAP.get(tid); | ||||
| 	if (tile == null) throw new Error(`tile not found: ${tid}`); | ||||
| 	return tile; | ||||
| } | ||||
| 
 | ||||
| export function findTileById(tileId: TileId): TileInstance | null { | ||||
| 	return TILE_ID_MAP.get(tileId) ?? null; | ||||
| export function findTileById(tid: TileId): TileInstance | null { | ||||
| 	return TILE_ID_MAP.get(tid) ?? null; | ||||
| } | ||||
| 
 | ||||
| export type House = 'e' | 's' | 'w' | 'n'; | ||||
|  | @ -300,6 +300,13 @@ export const YAKU_DEFINITIONS = [{ | |||
| 	calc: (state: EnvForCalcYaku) => { | ||||
| 		return state.tsumoTile != null; | ||||
| 	}, | ||||
| }, { | ||||
| 	name: 'ippatsu', | ||||
| 	fan: 1, | ||||
| 	isYakuman: false, | ||||
| 	calc: (state: EnvForCalcYaku) => { | ||||
| 
 | ||||
| 	}, | ||||
| }, { | ||||
| 	name: 'red', | ||||
| 	fan: 1, | ||||
|  |  | |||
|  | @ -9,12 +9,12 @@ import * as Common from './common.js'; | |||
| import { PlayerState } from './engine.player.js'; | ||||
| 
 | ||||
| //#region syntax suger
 | ||||
| function $(tileId: TileId): Common.TileInstance { | ||||
| 	return Common.findTileByIdOrFail(tileId); | ||||
| function $(tid: TileId): Common.TileInstance { | ||||
| 	return Common.findTileByIdOrFail(tid); | ||||
| } | ||||
| 
 | ||||
| function $type(tileId: TileId): TileType { | ||||
| 	return $(tileId).t; | ||||
| function $type(tid: TileId): TileType { | ||||
| 	return $(tid).t; | ||||
| } | ||||
| //#endregion
 | ||||
| 
 | ||||
|  | @ -26,7 +26,7 @@ export type MasterState = { | |||
| 
 | ||||
| 	round: 'e' | 's' | 'w' | 'n'; | ||||
| 	kyoku: number; | ||||
| 
 | ||||
| 	turnCount: number; | ||||
| 	tiles: TileId[]; | ||||
| 	kingTiles: TileId[]; | ||||
| 	activatedDorasCount: number; | ||||
|  | @ -59,6 +59,12 @@ export type MasterState = { | |||
| 		w: boolean; | ||||
| 		n: boolean; | ||||
| 	}; | ||||
| 	ippatsus: { | ||||
| 		e: boolean; | ||||
| 		s: boolean; | ||||
| 		w: boolean; | ||||
| 		n: boolean; | ||||
| 	}; | ||||
| 	points: { | ||||
| 		e: number; | ||||
| 		s: number; | ||||
|  | @ -176,6 +182,10 @@ export class MasterGameEngine { | |||
| 		return this.state.user4House; | ||||
| 	} | ||||
| 
 | ||||
| 	public get turn(): House | null { | ||||
| 		return this.state.turn; | ||||
| 	} | ||||
| 
 | ||||
| 	public static createInitialState(): MasterState { | ||||
| 		const ikasama: TileId[] = [125, 129, 9, 56, 57, 61, 77, 81, 85, 133, 134, 135, 121, 122]; | ||||
| 
 | ||||
|  | @ -201,6 +211,7 @@ export class MasterGameEngine { | |||
| 			user4House: 'n', | ||||
| 			round: 'e', | ||||
| 			kyoku: 1, | ||||
| 			turnCount: 0, | ||||
| 			tiles, | ||||
| 			kingTiles, | ||||
| 			activatedDorasCount: 1, | ||||
|  | @ -253,12 +264,12 @@ export class MasterGameEngine { | |||
| 		return tile; | ||||
| 	} | ||||
| 
 | ||||
| 	private canRon(house: House, tileId: TileId): boolean { | ||||
| 	private canRon(house: House, tid: TileId): boolean { | ||||
| 		// フリテン
 | ||||
| 		// TODO: ポンされるなどして自分の河にない場合の考慮
 | ||||
| 		if (this.hoTileTypes[house].includes($type(tileId))) return false; | ||||
| 		if (this.hoTileTypes[house].includes($type(tid))) return false; | ||||
| 
 | ||||
| 		const horaSets = Common.getHoraSets(this.handTileTypes[house].concat($type(tileId))); | ||||
| 		const horaSets = Common.getHoraSets(this.handTileTypes[house].concat($type(tid))); | ||||
| 		if (horaSets.length === 0) return false; // 完成形じゃない
 | ||||
| 
 | ||||
| 		// TODO
 | ||||
|  | @ -268,15 +279,19 @@ export class MasterGameEngine { | |||
| 		return true; | ||||
| 	} | ||||
| 
 | ||||
| 	private canPon(house: House, tileId: TileId): boolean { | ||||
| 		return this.handTileTypes[house].filter(t => t === $type(tileId)).length === 2; | ||||
| 	private canPon(house: House, tid: TileId): boolean { | ||||
| 		return this.handTileTypes[house].filter(t => t === $type(tid)).length === 2; | ||||
| 	} | ||||
| 
 | ||||
| 	private canCii(caller: House, callee: House, tileId: TileId): boolean { | ||||
| 	private canDaiminkan(caller: House, tid: TileId): boolean { | ||||
| 		return this.handTileTypes[caller].filter(t => t === $type(tid)).length === 3; | ||||
| 	} | ||||
| 
 | ||||
| 	private canCii(caller: House, callee: House, tid: TileId): boolean { | ||||
| 		if (callee !== Common.prevHouse(caller)) return false; | ||||
| 		const hand = this.handTileTypes[caller]; | ||||
| 		return Common.SHUNTU_PATTERNS.some(pattern => | ||||
| 			pattern.includes($type(tileId)) && | ||||
| 			pattern.includes($type(tid)) && | ||||
| 			pattern.filter(t => hand.includes(t)).length >= 2); | ||||
| 	} | ||||
| 
 | ||||
|  | @ -325,68 +340,104 @@ export class MasterGameEngine { | |||
| 		this.endKyoku(); | ||||
| 	} | ||||
| 
 | ||||
| 	public commit_dahai(house: House, tileId: TileId, riichi = false) { | ||||
| 	public startTransaction() { | ||||
| 		const newState = structuredClone(this.state); | ||||
| 		return { | ||||
| 			state: newState, | ||||
| 			commit: () => { | ||||
| 				this.state = newState; | ||||
| 			}, | ||||
| 		}; | ||||
| 	} | ||||
| 
 | ||||
| 	public commit_nextKyoku() { | ||||
| 		const newState = MasterGameEngine.createInitialState(); | ||||
| 		newState.kyoku = this.state.kyoku + 1; | ||||
| 		newState.points = this.state.points; | ||||
| 		newState.turn = 'e'; | ||||
| 		newState.user1House = Common.nextHouse(this.state.user1House); | ||||
| 		newState.user2House = Common.nextHouse(this.state.user2House); | ||||
| 		newState.user3House = Common.nextHouse(this.state.user3House); | ||||
| 		newState.user4House = Common.nextHouse(this.state.user4House); | ||||
| 		this.state = newState; | ||||
| 	} | ||||
| 
 | ||||
| 	public commit_dahai(house: House, tid: TileId, riichi = false) { | ||||
| 		const { state, commit } = this.startTransaction(); | ||||
| 
 | ||||
| 		if (this.state.turn !== house) throw new Error('Not your turn'); | ||||
| 
 | ||||
| 		if (riichi) { | ||||
| 			if (this.state.riichis[house]) throw new Error('Already riichi'); | ||||
| 			const tempHandTiles = [...this.handTileTypes[house]]; | ||||
| 			tempHandTiles.splice(tempHandTiles.indexOf($type(tileId)), 1); | ||||
| 			tempHandTiles.splice(tempHandTiles.indexOf($type(tid)), 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(tileId)) throw new Error('No such tile in your hand'); | ||||
| 		handTiles.splice(handTiles.indexOf(tileId), 1); | ||||
| 		this.state.hoTiles[house].push(tileId); | ||||
| 		if (!handTiles.includes(tid)) throw new Error('No such tile in your hand'); | ||||
| 		handTiles.splice(handTiles.indexOf(tid), 1); | ||||
| 		this.state.hoTiles[house].push(tid); | ||||
| 
 | ||||
| 		if (this.state.riichis[house]) { | ||||
| 			this.state.ippatsus[house] = false; | ||||
| 		} | ||||
| 
 | ||||
| 		if (riichi) { | ||||
| 			this.state.riichis[house] = true; | ||||
| 			this.state.ippatsus[house] = true; | ||||
| 		} | ||||
| 
 | ||||
| 		const canRonHouses: House[] = []; | ||||
| 		switch (house) { | ||||
| 			case 'e': | ||||
| 				if (this.canRon('s', tileId)) canRonHouses.push('s'); | ||||
| 				if (this.canRon('w', tileId)) canRonHouses.push('w'); | ||||
| 				if (this.canRon('n', tileId)) canRonHouses.push('n'); | ||||
| 				if (this.canRon('s', tid)) canRonHouses.push('s'); | ||||
| 				if (this.canRon('w', tid)) canRonHouses.push('w'); | ||||
| 				if (this.canRon('n', tid)) canRonHouses.push('n'); | ||||
| 				break; | ||||
| 			case 's': | ||||
| 				if (this.canRon('e', tileId)) canRonHouses.push('e'); | ||||
| 				if (this.canRon('w', tileId)) canRonHouses.push('w'); | ||||
| 				if (this.canRon('n', tileId)) canRonHouses.push('n'); | ||||
| 				if (this.canRon('e', tid)) canRonHouses.push('e'); | ||||
| 				if (this.canRon('w', tid)) canRonHouses.push('w'); | ||||
| 				if (this.canRon('n', tid)) canRonHouses.push('n'); | ||||
| 				break; | ||||
| 			case 'w': | ||||
| 				if (this.canRon('e', tileId)) canRonHouses.push('e'); | ||||
| 				if (this.canRon('s', tileId)) canRonHouses.push('s'); | ||||
| 				if (this.canRon('n', tileId)) canRonHouses.push('n'); | ||||
| 				if (this.canRon('e', tid)) canRonHouses.push('e'); | ||||
| 				if (this.canRon('s', tid)) canRonHouses.push('s'); | ||||
| 				if (this.canRon('n', tid)) canRonHouses.push('n'); | ||||
| 				break; | ||||
| 			case 'n': | ||||
| 				if (this.canRon('e', tileId)) canRonHouses.push('e'); | ||||
| 				if (this.canRon('s', tileId)) canRonHouses.push('s'); | ||||
| 				if (this.canRon('w', tileId)) canRonHouses.push('w'); | ||||
| 				if (this.canRon('e', tid)) canRonHouses.push('e'); | ||||
| 				if (this.canRon('s', tid)) canRonHouses.push('s'); | ||||
| 				if (this.canRon('w', tid)) canRonHouses.push('w'); | ||||
| 				break; | ||||
| 		} | ||||
| 
 | ||||
| 		const canKanHouse: House | null = null; | ||||
| 		let canKanHouse: House | null = null; | ||||
| 		switch (house) { | ||||
| 			case 'e': canKanHouse = this.canDaiminkan('s', tid) ? 's' : this.canDaiminkan('w', tid) ? 'w' : this.canDaiminkan('n', tid) ? 'n' : null; break; | ||||
| 			case 's': canKanHouse = this.canDaiminkan('e', tid) ? 'e' : this.canDaiminkan('w', tid) ? 'w' : this.canDaiminkan('n', tid) ? 'n' : null; break; | ||||
| 			case 'w': canKanHouse = this.canDaiminkan('e', tid) ? 'e' : this.canDaiminkan('s', tid) ? 's' : this.canDaiminkan('n', tid) ? 'n' : null; break; | ||||
| 			case 'n': canKanHouse = this.canDaiminkan('e', tid) ? 'e' : this.canDaiminkan('s', tid) ? 's' : this.canDaiminkan('w', tid) ? 'w' : null; break; | ||||
| 		} | ||||
| 
 | ||||
| 		let canPonHouse: House | null = null; | ||||
| 		switch (house) { | ||||
| 			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; | ||||
| 			case 'e': canPonHouse = this.canPon('s', tid) ? 's' : this.canPon('w', tid) ? 'w' : this.canPon('n', tid) ? 'n' : null; break; | ||||
| 			case 's': canPonHouse = this.canPon('e', tid) ? 'e' : this.canPon('w', tid) ? 'w' : this.canPon('n', tid) ? 'n' : null; break; | ||||
| 			case 'w': canPonHouse = this.canPon('e', tid) ? 'e' : this.canPon('s', tid) ? 's' : this.canPon('n', tid) ? 'n' : null; break; | ||||
| 			case 'n': canPonHouse = this.canPon('e', tid) ? 'e' : this.canPon('s', tid) ? 's' : this.canPon('w', tid) ? 'w' : null; break; | ||||
| 		} | ||||
| 
 | ||||
| 		let canCiiHouse: House | null = null; | ||||
| 		switch (house) { | ||||
| 			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; | ||||
| 			case 'e': canCiiHouse = this.canCii('s', house, tid) ? 's' : this.canCii('w', house, tid) ? 'w' : this.canCii('n', house, tid) ? 'n' : null; break; | ||||
| 			case 's': canCiiHouse = this.canCii('e', house, tid) ? 'e' : this.canCii('w', house, tid) ? 'w' : this.canCii('n', house, tid) ? 'n' : null; break; | ||||
| 			case 'w': canCiiHouse = this.canCii('e', house, tid) ? 'e' : this.canCii('s', house, tid) ? 's' : this.canCii('n', house, tid) ? 'n' : null; break; | ||||
| 			case 'n': canCiiHouse = this.canCii('e', house, tid) ? 'e' : this.canCii('s', house, tid) ? 's' : this.canCii('w', house, tid) ? 'w' : null; break; | ||||
| 		} | ||||
| 
 | ||||
| 		if (canRonHouses.length > 0 || canPonHouse != null || canCiiHouse != null) { | ||||
| 		if (canRonHouses.length > 0 || canKanHouse != null || canPonHouse != null || canCiiHouse != null) { | ||||
| 			if (canRonHouses.length > 0) { | ||||
| 				this.state.askings.ron = { | ||||
| 					callee: house, | ||||
|  | @ -445,13 +496,18 @@ export class MasterGameEngine { | |||
| 		}; | ||||
| 	} | ||||
| 
 | ||||
| 	public commit_kakan(house: House, tileId: TileId) { | ||||
| 		const pon = this.state.huros[house].find(h => h.type === 'pon' && $type(h.tiles[0]) === $type(tileId)); | ||||
| 	public commit_kakan(house: House, tid: TileId) { | ||||
| 		const pon = this.state.huros[house].find(h => h.type === 'pon' && $type(h.tiles[0]) === $type(tid)); | ||||
| 		if (pon == null) throw new Error('No such pon'); | ||||
| 		this.state.handTiles[house].splice(this.state.handTiles[house].indexOf(tileId), 1); | ||||
| 		const tiles = [tileId, ...pon.tiles]; | ||||
| 		this.state.handTiles[house].splice(this.state.handTiles[house].indexOf(tid), 1); | ||||
| 		const tiles = [tid, ...pon.tiles]; | ||||
| 		this.state.huros[house].push({ type: 'minkan', tiles: tiles, from: pon.from }); | ||||
| 
 | ||||
| 		this.state.ippatsus.e = false; | ||||
| 		this.state.ippatsus.s = false; | ||||
| 		this.state.ippatsus.w = false; | ||||
| 		this.state.ippatsus.n = false; | ||||
| 
 | ||||
| 		this.state.activatedDorasCount++; | ||||
| 
 | ||||
| 		const rinsyan = this.tsumo(); | ||||
|  | @ -463,14 +519,14 @@ export class MasterGameEngine { | |||
| 		}; | ||||
| 	} | ||||
| 
 | ||||
| 	public commit_ankan(house: House, tileId: TileId) { | ||||
| 		const t1 = this.state.handTiles[house].filter(t => $type(t) === $type(tileId)).at(0); | ||||
| 	public commit_ankan(house: House, tid: TileId) { | ||||
| 		const t1 = this.state.handTiles[house].filter(t => $type(t) === $type(tid)).at(0); | ||||
| 		if (t1 == null) throw new Error('No such tile'); | ||||
| 		const t2 = this.state.handTiles[house].filter(t => $type(t) === $type(tileId)).at(1); | ||||
| 		const t2 = this.state.handTiles[house].filter(t => $type(t) === $type(tid)).at(1); | ||||
| 		if (t2 == null) throw new Error('No such tile'); | ||||
| 		const t3 = this.state.handTiles[house].filter(t => $type(t) === $type(tileId)).at(2); | ||||
| 		const t3 = this.state.handTiles[house].filter(t => $type(t) === $type(tid)).at(2); | ||||
| 		if (t3 == null) throw new Error('No such tile'); | ||||
| 		const t4 = this.state.handTiles[house].filter(t => $type(t) === $type(tileId)).at(3); | ||||
| 		const t4 = this.state.handTiles[house].filter(t => $type(t) === $type(tid)).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); | ||||
|  | @ -479,6 +535,11 @@ export class MasterGameEngine { | |||
| 		const tiles = [t1, t2, t3, t4]; | ||||
| 		this.state.huros[house].push({ type: 'ankan', tiles: tiles }); | ||||
| 
 | ||||
| 		this.state.ippatsus.e = false; | ||||
| 		this.state.ippatsus.s = false; | ||||
| 		this.state.ippatsus.w = false; | ||||
| 		this.state.ippatsus.n = false; | ||||
| 
 | ||||
| 		this.state.activatedDorasCount++; | ||||
| 
 | ||||
| 		const rinsyan = this.tsumo(); | ||||
|  | @ -567,6 +628,11 @@ export class MasterGameEngine { | |||
| 			const tiles = [tile, t1, t2, t3]; | ||||
| 			this.state.huros[kan.caller].push({ type: 'minkan', tiles: tiles, from: kan.callee }); | ||||
| 
 | ||||
| 			this.state.ippatsus.e = false; | ||||
| 			this.state.ippatsus.s = false; | ||||
| 			this.state.ippatsus.w = false; | ||||
| 			this.state.ippatsus.n = false; | ||||
| 
 | ||||
| 			this.state.activatedDorasCount++; | ||||
| 
 | ||||
| 			const rinsyan = this.tsumo(); | ||||
|  | @ -594,6 +660,11 @@ export class MasterGameEngine { | |||
| 			const tiles = [tile, t1, t2]; | ||||
| 			this.state.huros[pon.caller].push({ type: 'pon', tiles: tiles, from: pon.callee }); | ||||
| 
 | ||||
| 			this.state.ippatsus.e = false; | ||||
| 			this.state.ippatsus.s = false; | ||||
| 			this.state.ippatsus.w = false; | ||||
| 			this.state.ippatsus.n = false; | ||||
| 
 | ||||
| 			this.state.turn = pon.caller; | ||||
| 
 | ||||
| 			return { | ||||
|  | @ -654,6 +725,11 @@ export class MasterGameEngine { | |||
| 
 | ||||
| 			this.state.huros[cii.caller].push({ type: 'cii', tiles: tiles, from: cii.callee }); | ||||
| 
 | ||||
| 			this.state.ippatsus.e = false; | ||||
| 			this.state.ippatsus.s = false; | ||||
| 			this.state.ippatsus.w = false; | ||||
| 			this.state.ippatsus.n = false; | ||||
| 
 | ||||
| 			this.state.turn = cii.caller; | ||||
| 
 | ||||
| 			return { | ||||
|  | @ -699,6 +775,7 @@ export class MasterGameEngine { | |||
| 			user4House: this.state.user4House, | ||||
| 			round: this.state.round, | ||||
| 			kyoku: this.state.kyoku, | ||||
| 			turnCount: this.state.turnCount, | ||||
| 			tilesCount: this.state.tiles.length, | ||||
| 			doraIndicateTiles: this.state.kingTiles.slice(0, this.state.activatedDorasCount), | ||||
| 			handTiles: { | ||||
|  | @ -756,3 +833,7 @@ export class MasterGameEngine { | |||
| 		return structuredClone(this.state); | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| function commit_dahai(state: MasterState): MasterState { | ||||
| 
 | ||||
| } | ||||
|  |  | |||
|  | @ -8,12 +8,12 @@ import { TileType, House, Huro, TileId, YAKU_DEFINITIONS } from './common.js'; | |||
| import * as Common from './common.js'; | ||||
| 
 | ||||
| //#region syntax suger
 | ||||
| function $(tileId: TileId): Common.TileInstance { | ||||
| 	return Common.findTileByIdOrFail(tileId); | ||||
| function $(tid: TileId): Common.TileInstance { | ||||
| 	return Common.findTileByIdOrFail(tid); | ||||
| } | ||||
| 
 | ||||
| function $type(tileId: TileId): TileType { | ||||
| 	return $(tileId).t; | ||||
| function $type(tid: TileId): TileType { | ||||
| 	return $(tid).t; | ||||
| } | ||||
| //#endregion
 | ||||
| 
 | ||||
|  | @ -26,6 +26,7 @@ export type PlayerState = { | |||
| 	round: 'e' | 's' | 'w' | 'n'; | ||||
| 	kyoku: number; | ||||
| 
 | ||||
| 	turnCount: number; | ||||
| 	tilesCount: number; | ||||
| 	doraIndicateTiles: TileId[]; | ||||
| 
 | ||||
|  | @ -111,6 +112,10 @@ export class PlayerGameEngine { | |||
| 		return this.state.huros; | ||||
| 	} | ||||
| 
 | ||||
| 	public get turnCount(): number { | ||||
| 		return this.state.turnCount; | ||||
| 	} | ||||
| 
 | ||||
| 	public get tilesCount(): number { | ||||
| 		return this.state.tilesCount; | ||||
| 	} | ||||
|  | @ -131,6 +136,26 @@ export class PlayerGameEngine { | |||
| 		return this.state.canCii; | ||||
| 	} | ||||
| 
 | ||||
| 	public get turn(): House | null { | ||||
| 		return this.state.turn; | ||||
| 	} | ||||
| 
 | ||||
| 	public get user1House(): House { | ||||
| 		return this.state.user1House; | ||||
| 	} | ||||
| 
 | ||||
| 	public get user2House(): House { | ||||
| 		return this.state.user2House; | ||||
| 	} | ||||
| 
 | ||||
| 	public get user3House(): House { | ||||
| 		return this.state.user3House; | ||||
| 	} | ||||
| 
 | ||||
| 	public get user4House(): House { | ||||
| 		return this.state.user4House; | ||||
| 	} | ||||
| 
 | ||||
| 	public get myHouse(): House { | ||||
| 		switch (this.myUserNumber) { | ||||
| 			case 1: return this.state.user1House; | ||||
|  | @ -152,19 +177,23 @@ export class PlayerGameEngine { | |||
| 		return this.state.riichis[this.myHouse]; | ||||
| 	} | ||||
| 
 | ||||
| 	public commit_tsumo(house: House, tileId: TileId) { | ||||
| 		console.log('commit_tsumo', this.state.turn, house, tileId); | ||||
| 	public commit_nextKyoku(state: PlayerState) { | ||||
| 		this.state = state; | ||||
| 	} | ||||
| 
 | ||||
| 	public commit_tsumo(house: House, tid: TileId) { | ||||
| 		console.log('commit_tsumo', this.state.turn, house, tid); | ||||
| 		this.state.tilesCount--; | ||||
| 		this.state.turn = house; | ||||
| 		if (house === this.myHouse) { | ||||
| 			this.myHandTiles.push(tileId); | ||||
| 			this.myHandTiles.push(tid); | ||||
| 		} else { | ||||
| 			this.state.handTiles[house].push(0); | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	public commit_dahai(house: House, tileId: TileId, riichi = false) { | ||||
| 		console.log('commit_dahai', this.state.turn, house, tileId, riichi); | ||||
| 	public commit_dahai(house: House, tid: TileId, riichi = false) { | ||||
| 		console.log('commit_dahai', this.state.turn, house, tid, riichi); | ||||
| 		if (this.state.turn !== house) throw new PlayerGameEngine.InvalidOperationError(); | ||||
| 
 | ||||
| 		if (riichi) { | ||||
|  | @ -172,23 +201,23 @@ export class PlayerGameEngine { | |||
| 		} | ||||
| 
 | ||||
| 		if (house === this.myHouse) { | ||||
| 			this.myHandTiles.splice(this.myHandTiles.indexOf(tileId), 1); | ||||
| 			this.state.hoTiles[this.myHouse].push(tileId); | ||||
| 			this.myHandTiles.splice(this.myHandTiles.indexOf(tid), 1); | ||||
| 			this.state.hoTiles[this.myHouse].push(tid); | ||||
| 		} else { | ||||
| 			this.state.handTiles[house].pop(); | ||||
| 			this.state.hoTiles[house].push(tileId); | ||||
| 			this.state.hoTiles[house].push(tid); | ||||
| 		} | ||||
| 
 | ||||
| 		this.state.turn = null; | ||||
| 
 | ||||
| 		if (house === this.myHouse) { | ||||
| 		} else { | ||||
| 			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 canRon = Common.getHoraSets(this.myHandTiles.concat(tid).map(id => $type(id))).length > 0; | ||||
| 			const canPon = !this.isMeRiichi && this.myHandTileTypes.filter(t => t === $type(tid)).length === 2; | ||||
| 			const canKan = !this.isMeRiichi && this.myHandTileTypes.filter(t => t === $type(tid)).length === 3; | ||||
| 			const canCii = !this.isMeRiichi && house === Common.prevHouse(this.myHouse) && | ||||
| 				Common.SHUNTU_PATTERNS.some(pattern => | ||||
| 					pattern.includes($type(tileId)) && | ||||
| 					pattern.includes($type(tid)) && | ||||
| 					pattern.filter(t => this.myHandTileTypes.includes(t)).length >= 2); | ||||
| 
 | ||||
| 			this.state.canRon = canRon ? { callee: house } : null; | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue