feat: 实现麻将游戏结算系统与自摸胡功能

新增结算类型枚举和分数变更记录模型
补全响应裁决器与结算服务,支持点炮胡、自摸胡和明杠结算
扩展座位模型,增加已胡状态和分数字段
完善胡牌评估器,支持自摸胡判断
前端原型页增加分数显示和已胡状态
更新SPRINT文档记录当前进度
This commit is contained in:
hujun
2026-03-20 13:58:16 +08:00
parent 48da7d4990
commit 36dcfb7d31
24 changed files with 1349 additions and 53 deletions

View File

@@ -30,7 +30,9 @@ type SelfSeatView = {
seatNo: number
playerId: string
nickname: string
won: boolean
lackSuit: string | null
score: number
handTiles: string[]
discardTiles: string[]
}
@@ -40,7 +42,9 @@ type PublicSeatView = {
playerId: string
nickname: string
ai: boolean
won: boolean
lackSuit: string | null
score: number
handCount: number
discardTiles: string[]
}
@@ -125,7 +129,11 @@ const actionScopeLabelMap: Record<string, string> = {
const canSelectLack = computed(() => game.value?.phase === 'LACK_SELECTION')
const canDiscard = computed(
() => game.value?.phase === 'PLAYING' && game.value.selfSeat.playerId === currentUserId.value && game.value.currentSeatNo === game.value.selfSeat.seatNo
() =>
game.value?.phase === 'PLAYING' &&
!game.value.selfSeat.won &&
game.value.selfSeat.playerId === currentUserId.value &&
game.value.currentSeatNo === game.value.selfSeat.seatNo
)
const publicSeats = computed(() => game.value?.seats ?? [])
const privateActionCandidates = computed(() => privateAction.value?.candidates ?? [])
@@ -330,7 +338,7 @@ async function refreshGameState() {
})
}
async function submitAction(actionType: string, tile?: string) {
async function submitAction(actionType: string, tile?: string, sourceSeatNo?: number | null) {
if (!game.value) {
return
}
@@ -341,7 +349,8 @@ async function submitAction(actionType: string, tile?: string) {
body: JSON.stringify({
userId: currentUserId.value,
actionType,
tile: tile ?? null
tile: tile ?? null,
sourceSeatNo: sourceSeatNo ?? null
})
})
info.value = actionType === 'DISCARD' ? `已打出 ${tile}` : `已提交动作 ${actionType}`
@@ -355,6 +364,11 @@ function selectLack() {
function discard(tile: string) {
return submitAction('DISCARD', tile)
}
function submitCandidateAction(actionType: string, tile: string | null) {
const sourceSeatNo = privateAction.value?.sourceSeatNo ?? null
return submitAction(actionType, tile ?? undefined, sourceSeatNo)
}
</script>
<template>
@@ -512,7 +526,11 @@ function discard(tile: string) {
<div class="self-card">
<div class="section-title">
<strong>我的手牌</strong>
<span class="mini-pill">当前回合 {{ game.currentSeatNo }}</span>
<div class="mini-tags">
<span class="mini-pill">当前回合 {{ game.currentSeatNo }}</span>
<span class="mini-tag">积分 {{ game.selfSeat.score }}</span>
<span class="mini-tag">{{ game.selfSeat.won ? '已胡' : '未胡' }}</span>
</div>
</div>
<div class="tile-grid">
<button
@@ -549,13 +567,15 @@ function discard(tile: string) {
<span v-if="privateAction.sourceSeatNo !== null" class="mini-tag">来源座位 {{ privateAction.sourceSeatNo }}</span>
</div>
<div class="candidate-list" v-if="privateActionCandidates.length > 0">
<span
<button
v-for="(candidate, index) in privateActionCandidates"
:key="`${candidate.actionType}-${candidate.tile ?? 'none'}-${index}`"
class="candidate-chip"
type="button"
@click="submitCandidateAction(candidate.actionType, candidate.tile)"
>
{{ candidate.actionType }}<template v-if="candidate.tile"> · {{ candidate.tile }}</template>
</span>
</button>
</div>
<span v-if="privateAction.actionScope === 'RESPONSE'" class="message-copy">
当前原型页已能识别响应候选消息后续会继续补正式动作面板和真实响应流程
@@ -579,6 +599,8 @@ function discard(tile: string) {
</div>
<div class="mini-tags">
<span class="mini-tag">{{ seat.ai ? 'AI' : '真人' }}</span>
<span class="mini-tag">{{ seat.won ? '已胡' : '在局' }}</span>
<span class="mini-tag">积分 {{ seat.score }}</span>
<span class="mini-tag">手牌 {{ seat.handCount }}</span>
<span class="mini-tag">{{ seat.lackSuit ?? '未定缺' }}</span>
</div>