refactor: 收口拼图 runtime 状态合并

This commit is contained in:
2026-06-04 05:24:16 +08:00
parent d44560f330
commit 4069fd5859
6 changed files with 284 additions and 39 deletions

View File

@@ -589,6 +589,7 @@ import {
buildPuzzleResultProfileId,
buildPuzzleResultWorkId,
} from './platformPuzzleIdentityModel';
import { mergePuzzleServiceRuntimeState } from './platformPuzzleRuntimeStateModel';
import {
type PlatformPuzzleRuntimeAuthMode,
resolvePlatformRecommendRuntimeAuthPlan,
@@ -1250,45 +1251,6 @@ function CreationResultRecoveryPanel({
);
}
function mergePuzzleServiceRuntimeState(
currentRun: PuzzleRunSnapshot,
serviceRun: PuzzleRunSnapshot,
): PuzzleRunSnapshot {
if (!currentRun.currentLevel || !serviceRun.currentLevel) {
return currentRun;
}
const serviceLevel = serviceRun.currentLevel;
const leaderboardEntries =
serviceLevel.leaderboardEntries.length > 0
? serviceLevel.leaderboardEntries
: serviceRun.leaderboardEntries;
// 中文注释:拼块布局和通关状态由前端即时裁决;后端快照只合并榜单与下一关 handoff。
return {
...currentRun,
runId: serviceRun.runId,
entryProfileId: serviceRun.entryProfileId,
clearedLevelCount: Math.max(
currentRun.clearedLevelCount,
serviceRun.clearedLevelCount,
),
recommendedNextProfileId: serviceRun.recommendedNextProfileId,
nextLevelMode: serviceRun.nextLevelMode,
nextLevelProfileId: serviceRun.nextLevelProfileId,
nextLevelId: serviceRun.nextLevelId,
recommendedNextWorks: serviceRun.recommendedNextWorks,
leaderboardEntries,
currentLevel: {
...currentRun.currentLevel,
leaderboardEntries:
leaderboardEntries.length > 0
? leaderboardEntries
: currentRun.currentLevel.leaderboardEntries,
},
};
}
export function PlatformEntryFlowShellImpl({
selectionStage,
setSelectionStage,

View File

@@ -0,0 +1,197 @@
import { describe, expect, test } from 'vitest';
import type {
PuzzleLeaderboardEntry,
PuzzleRunSnapshot,
PuzzleRuntimeLevelSnapshot,
} from '../../../packages/shared/src/contracts/puzzleRuntimeSession';
import { mergePuzzleServiceRuntimeState } from './platformPuzzleRuntimeStateModel';
const currentLeaderboard: PuzzleLeaderboardEntry[] = [
{
rank: 1,
nickname: '本地玩家',
elapsedMs: 12000,
isCurrentPlayer: true,
},
];
const serviceLevelLeaderboard: PuzzleLeaderboardEntry[] = [
{
rank: 1,
nickname: '服务端玩家',
elapsedMs: 9000,
},
];
const serviceRunLeaderboard: PuzzleLeaderboardEntry[] = [
{
rank: 2,
nickname: '全局玩家',
elapsedMs: 15000,
},
];
function buildPuzzleLevel(
overrides: Partial<PuzzleRuntimeLevelSnapshot> = {},
): PuzzleRuntimeLevelSnapshot {
return {
runId: 'run-current',
levelIndex: 0,
levelId: 'level-1',
gridSize: 3,
profileId: 'puzzle-profile-current',
levelName: '星桥机关',
authorDisplayName: '玩家',
themeTags: ['星桥'],
coverImageSrc: '/cover.png',
board: {
rows: 3,
cols: 3,
pieces: [],
mergedGroups: [],
selectedPieceId: null,
allTilesResolved: true,
},
status: 'cleared',
startedAtMs: 1000,
clearedAtMs: 13000,
elapsedMs: 12000,
timeLimitMs: 120000,
remainingMs: 108000,
pausedAccumulatedMs: 0,
pauseStartedAtMs: null,
freezeAccumulatedMs: 0,
freezeStartedAtMs: null,
freezeUntilMs: null,
leaderboardEntries: currentLeaderboard,
...overrides,
};
}
function buildPuzzleRun(
overrides: Partial<PuzzleRunSnapshot> = {},
): PuzzleRunSnapshot {
return {
runId: 'run-current',
entryProfileId: 'puzzle-profile-current',
clearedLevelCount: 1,
currentLevelIndex: 0,
currentGridSize: 3,
playedProfileIds: ['puzzle-profile-current'],
previousLevelTags: ['星桥'],
currentLevel: buildPuzzleLevel(),
recommendedNextProfileId: null,
nextLevelMode: 'sameWork',
nextLevelProfileId: null,
nextLevelId: null,
recommendedNextWorks: [],
leaderboardEntries: currentLeaderboard,
...overrides,
};
}
describe('platformPuzzleRuntimeStateModel', () => {
test('keeps current run when either current level is missing', () => {
const currentRun = buildPuzzleRun({ currentLevel: null });
expect(
mergePuzzleServiceRuntimeState(currentRun, buildPuzzleRun()),
).toBe(currentRun);
const serviceRun = buildPuzzleRun({ currentLevel: null });
const playableCurrentRun = buildPuzzleRun();
expect(
mergePuzzleServiceRuntimeState(playableCurrentRun, serviceRun),
).toBe(playableCurrentRun);
});
test('merges service leaderboard and next-level handoff without replacing local level state', () => {
const currentRun = buildPuzzleRun({
clearedLevelCount: 2,
currentLevel: buildPuzzleLevel({
runId: 'run-current',
status: 'cleared',
board: {
rows: 3,
cols: 3,
pieces: [
{
pieceId: 'piece-local',
correctRow: 0,
correctCol: 0,
currentRow: 0,
currentCol: 0,
mergedGroupId: null,
},
],
mergedGroups: [],
selectedPieceId: 'piece-local',
allTilesResolved: true,
},
}),
});
const serviceRun = buildPuzzleRun({
runId: 'run-service',
entryProfileId: 'puzzle-profile-service',
clearedLevelCount: 1,
recommendedNextProfileId: 'next-recommended',
nextLevelMode: 'similarWorks',
nextLevelProfileId: 'next-profile',
nextLevelId: 'next-level',
recommendedNextWorks: [
{
profileId: 'next-profile',
levelName: '月桥机关',
authorDisplayName: '推荐作者',
themeTags: ['月桥'],
coverImageSrc: '/next-cover.png',
similarityScore: 0.91,
},
],
currentLevel: buildPuzzleLevel({
runId: 'run-service-level',
status: 'playing',
leaderboardEntries: serviceLevelLeaderboard,
}),
});
const merged = mergePuzzleServiceRuntimeState(currentRun, serviceRun);
expect(merged.runId).toBe('run-service');
expect(merged.entryProfileId).toBe('puzzle-profile-service');
expect(merged.clearedLevelCount).toBe(2);
expect(merged.recommendedNextProfileId).toBe('next-recommended');
expect(merged.nextLevelMode).toBe('similarWorks');
expect(merged.nextLevelProfileId).toBe('next-profile');
expect(merged.nextLevelId).toBe('next-level');
expect(merged.recommendedNextWorks).toEqual(serviceRun.recommendedNextWorks);
expect(merged.leaderboardEntries).toEqual(serviceLevelLeaderboard);
expect(merged.currentLevel?.status).toBe('cleared');
expect(merged.currentLevel?.board.pieces).toEqual(
currentRun.currentLevel?.board.pieces,
);
expect(merged.currentLevel?.leaderboardEntries).toEqual(
serviceLevelLeaderboard,
);
});
test('falls back to service run leaderboard, then current level leaderboard', () => {
const currentRun = buildPuzzleRun();
const serviceRun = buildPuzzleRun({
currentLevel: buildPuzzleLevel({ leaderboardEntries: [] }),
leaderboardEntries: serviceRunLeaderboard,
});
expect(
mergePuzzleServiceRuntimeState(currentRun, serviceRun).currentLevel
?.leaderboardEntries,
).toEqual(serviceRunLeaderboard);
expect(
mergePuzzleServiceRuntimeState(currentRun, {
...serviceRun,
leaderboardEntries: [],
}).currentLevel?.leaderboardEntries,
).toEqual(currentLeaderboard);
});
});

View File

@@ -0,0 +1,40 @@
import type { PuzzleRunSnapshot } from '../../../packages/shared/src/contracts/puzzleRuntimeSession';
export function mergePuzzleServiceRuntimeState(
currentRun: PuzzleRunSnapshot,
serviceRun: PuzzleRunSnapshot,
): PuzzleRunSnapshot {
if (!currentRun.currentLevel || !serviceRun.currentLevel) {
return currentRun;
}
const serviceLevel = serviceRun.currentLevel;
const leaderboardEntries =
serviceLevel.leaderboardEntries.length > 0
? serviceLevel.leaderboardEntries
: serviceRun.leaderboardEntries;
// 中文注释:拼块布局和通关状态由前端即时裁决;后端快照只合并榜单与下一关 handoff。
return {
...currentRun,
runId: serviceRun.runId,
entryProfileId: serviceRun.entryProfileId,
clearedLevelCount: Math.max(
currentRun.clearedLevelCount,
serviceRun.clearedLevelCount,
),
recommendedNextProfileId: serviceRun.recommendedNextProfileId,
nextLevelMode: serviceRun.nextLevelMode,
nextLevelProfileId: serviceRun.nextLevelProfileId,
nextLevelId: serviceRun.nextLevelId,
recommendedNextWorks: serviceRun.recommendedNextWorks,
leaderboardEntries,
currentLevel: {
...currentRun.currentLevel,
leaderboardEntries:
leaderboardEntries.length > 0
? leaderboardEntries
: currentRun.currentLevel.leaderboardEntries,
},
};
}