198 lines
5.6 KiB
TypeScript
198 lines
5.6 KiB
TypeScript
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);
|
|
});
|
|
});
|