1
Some checks failed
CI / verify (push) Has been cancelled

This commit is contained in:
2026-05-02 20:43:41 +08:00
parent 543ccf2509
commit 5831703156
36 changed files with 799 additions and 254 deletions

View File

@@ -10,6 +10,7 @@ import type {
PuzzleRuntimeLevelSnapshot,
SwapPuzzlePiecesRequest,
} from '../../../packages/shared/src/contracts/puzzleRuntimeSession';
import type { PuzzleDraftLevel } from '../../../packages/shared/src/contracts/puzzleAgentDraft';
import type { PuzzleWorkSummary } from '../../../packages/shared/src/contracts/puzzleWorkSummary';
const LOCAL_PUZZLE_RUN_ID_PREFIX = 'local-puzzle-run-';
@@ -53,10 +54,6 @@ function resolvePuzzleLevelConfig(levelIndex: number): PuzzleLevelConfig {
}
}
function resolvePuzzleGridSize(clearedLevelCount: number): PuzzleGridSize {
return resolvePuzzleLevelConfig(clearedLevelCount + 1).gridSize;
}
const PUZZLE_INITIAL_SHUFFLE_ATTEMPTS = 64;
function buildLocalPuzzleRunId(profileId: string) {
@@ -724,20 +721,88 @@ function buildLocalLevelName(previousLevelName: string, levelIndex: number) {
}
// 本地兜底只保证单次游玩闭环:通关后立即重建下一关棋盘,不写回后端。
function buildFallbackLocalLevel(run: PuzzleRunSnapshot): PuzzleRunSnapshot {
function resolveWorkLevelIndexById(
levels: PuzzleDraftLevel[] | undefined,
levelId: string | null | undefined,
) {
if (!levelId) {
return -1;
}
return levels?.findIndex((level) => level.levelId === levelId) ?? -1;
}
function resolveWorkLevelById(
levels: PuzzleDraftLevel[] | undefined,
levelId: string | null | undefined,
) {
const levelIndex = resolveWorkLevelIndexById(levels, levelId);
return levelIndex >= 0 ? (levels?.[levelIndex] ?? null) : null;
}
function resolveNextSameWorkLevel(
work: PuzzleWorkSummary | null | undefined,
currentLevel: PuzzleRuntimeLevelSnapshot,
) {
const levels = work?.levels;
if (!levels?.length) {
return null;
}
const currentLevelIndexById = resolveWorkLevelIndexById(
levels,
currentLevel.levelId,
);
const nextLevelIndex =
currentLevelIndexById >= 0
? currentLevelIndexById + 1
: currentLevel.levelIndex;
return levels[nextLevelIndex] ?? null;
}
function applyLocalNextLevelHandoff(
run: PuzzleRunSnapshot,
work: PuzzleWorkSummary | null | undefined,
currentLevel: PuzzleRuntimeLevelSnapshot,
) {
const nextLevel = resolveNextSameWorkLevel(work, currentLevel);
return {
...run,
nextLevelMode: nextLevel ? ('sameWork' as const) : ('none' as const),
nextLevelProfileId: nextLevel ? currentLevel.profileId : null,
nextLevelId: nextLevel?.levelId ?? null,
recommendedNextProfileId: nextLevel ? currentLevel.profileId : null,
recommendedNextWorks: [],
};
}
function buildFallbackLocalLevel(
run: PuzzleRunSnapshot,
work?: PuzzleWorkSummary | null,
target?: { profileId?: string; levelId?: string | null },
): PuzzleRunSnapshot {
const currentLevel = run.currentLevel;
if (!currentLevel || currentLevel.status !== 'cleared') {
return run;
}
const nextLevelIndex = run.currentLevelIndex + 1;
const gridSize = resolvePuzzleGridSize(run.clearedLevelCount);
const gridSize = resolvePuzzleLevelConfig(nextLevelIndex).gridSize;
const nextProfileId =
run.recommendedNextProfileId ??
target?.profileId?.trim() ||
run.nextLevelProfileId ||
run.recommendedNextProfileId ||
buildLocalNextProfileId(run.entryProfileId, nextLevelIndex);
const nextLevel =
resolveWorkLevelById(work?.levels, target?.levelId ?? run.nextLevelId) ??
resolveNextSameWorkLevel(work, currentLevel);
const startedAtMs = Date.now();
const nextLevelName =
nextLevel?.levelName ??
buildLocalLevelName(currentLevel.levelName, nextLevelIndex);
const nextCoverImageSrc =
nextLevel?.coverImageSrc ?? currentLevel.coverImageSrc;
return {
const nextRun: PuzzleRunSnapshot = {
...run,
currentLevelIndex: nextLevelIndex,
currentGridSize: gridSize,
@@ -749,10 +814,10 @@ function buildFallbackLocalLevel(run: PuzzleRunSnapshot): PuzzleRunSnapshot {
...currentLevel,
runId: run.runId,
levelIndex: nextLevelIndex,
levelId: null,
levelId: nextLevel?.levelId ?? null,
gridSize,
profileId: nextProfileId,
levelName: buildLocalLevelName(currentLevel.levelName, nextLevelIndex),
levelName: nextLevelName,
board: buildInitialBoard(
gridSize,
run.runId,
@@ -763,28 +828,32 @@ function buildFallbackLocalLevel(run: PuzzleRunSnapshot): PuzzleRunSnapshot {
startedAtMs,
clearedAtMs: null,
elapsedMs: null,
coverImageSrc: nextCoverImageSrc,
...buildLevelTimerFields(nextLevelIndex),
leaderboardEntries: [],
},
recommendedNextProfileId: null,
nextLevelMode: 'none',
nextLevelProfileId: null,
nextLevelId: null,
recommendedNextWorks: [],
leaderboardEntries: [],
};
if (!nextRun.currentLevel) {
return nextRun;
}
return applyLocalNextLevelHandoff(nextRun, work, nextRun.currentLevel);
}
export function startLocalPuzzleRun(
item: PuzzleWorkSummary,
levelId?: string | null,
): PuzzleRunSnapshot {
const gridSize = resolvePuzzleGridSize(0);
const gridSize = resolvePuzzleLevelConfig(1).gridSize;
const runId = buildLocalPuzzleRunId(item.profileId);
const startedAtMs = Date.now();
const firstLevel = item.levels?.[0] ?? null;
const requestedLevelIndex = resolveWorkLevelIndexById(item.levels, levelId);
const currentLevelIndex = requestedLevelIndex >= 0 ? requestedLevelIndex : 0;
const firstLevel = item.levels?.[currentLevelIndex] ?? null;
const firstLevelName = firstLevel?.levelName || item.levelName;
const firstCoverImageSrc = firstLevel?.coverImageSrc ?? item.coverImageSrc;
const secondLevel = item.levels?.[1] ?? null;
const nextSameWorkLevel = item.levels?.[currentLevelIndex + 1] ?? null;
return {
runId,
entryProfileId: item.profileId,
@@ -811,10 +880,10 @@ export function startLocalPuzzleRun(
...buildLevelTimerFields(1),
leaderboardEntries: [],
},
recommendedNextProfileId: null,
nextLevelMode: secondLevel ? 'sameWork' : 'none',
nextLevelProfileId: secondLevel ? item.profileId : null,
nextLevelId: secondLevel?.levelId ?? null,
recommendedNextProfileId: nextSameWorkLevel ? item.profileId : null,
nextLevelMode: nextSameWorkLevel ? 'sameWork' : 'none',
nextLevelProfileId: nextSameWorkLevel ? item.profileId : null,
nextLevelId: nextSameWorkLevel?.levelId ?? null,
recommendedNextWorks: [],
leaderboardEntries: [],
};
@@ -823,6 +892,7 @@ export function startLocalPuzzleRun(
export function swapLocalPuzzlePieces(
run: PuzzleRunSnapshot,
payload: SwapPuzzlePiecesRequest,
work?: PuzzleWorkSummary | null,
): PuzzleRunSnapshot {
const timedRun = withResolvedTimer(run);
const currentLevel = timedRun.currentLevel;
@@ -843,10 +913,13 @@ export function swapLocalPuzzlePieces(
second.currentRow = firstPosition.row;
second.currentCol = firstPosition.col;
return applyNextBoard(
const nextRun = applyNextBoard(
timedRun,
rebuildBoardSnapshot(currentLevel.gridSize, pieces),
);
return nextRun.currentLevel?.status === 'cleared'
? syncLocalPuzzleRunHandoff(nextRun, work)
: nextRun;
}
function dragSinglePiece(
@@ -968,6 +1041,7 @@ function dragGroup(
export function dragLocalPuzzlePiece(
run: PuzzleRunSnapshot,
payload: DragPuzzlePieceRequest,
work?: PuzzleWorkSummary | null,
): PuzzleRunSnapshot {
const timedRun = withResolvedTimer(run);
const currentLevel = timedRun.currentLevel;
@@ -1003,16 +1077,32 @@ export function dragLocalPuzzlePiece(
dragSinglePiece(pieces, moving, payload.targetRow, payload.targetCol);
}
return applyNextBoard(
const nextRun = applyNextBoard(
timedRun,
rebuildBoardSnapshot(currentLevel.gridSize, pieces),
);
return nextRun.currentLevel?.status === 'cleared'
? syncLocalPuzzleRunHandoff(nextRun, work)
: nextRun;
}
export function advanceLocalPuzzleLevel(
run: PuzzleRunSnapshot,
work?: PuzzleWorkSummary | null,
target?: { profileId?: string; levelId?: string | null },
): PuzzleRunSnapshot {
return buildFallbackLocalLevel(run);
return buildFallbackLocalLevel(run, work, target);
}
export function syncLocalPuzzleRunHandoff(
run: PuzzleRunSnapshot,
work: PuzzleWorkSummary | null | undefined,
): PuzzleRunSnapshot {
const currentLevel = run.currentLevel;
if (!currentLevel) {
return run;
}
return applyLocalNextLevelHandoff(run, work, currentLevel);
}
export function restartLocalPuzzleLevel(

View File

@@ -1,5 +1,6 @@
import type {
DragPuzzlePieceRequest,
AdvancePuzzleNextLevelRequest,
PuzzleRunResponse,
StartPuzzleRunRequest,
SubmitPuzzleLeaderboardRequest,
@@ -101,11 +102,21 @@ export async function dragPuzzlePieceOrGroup(
/**
* 进入推荐出的下一关。
*/
export async function advancePuzzleNextLevel(runId: string) {
export async function advancePuzzleNextLevel(
runId: string,
payload: AdvancePuzzleNextLevelRequest = {},
) {
const targetProfileId = payload.targetProfileId?.trim() ?? '';
return requestJson<PuzzleRunResponse>(
`${PUZZLE_RUNTIME_API_BASE}/${encodeURIComponent(runId)}/next-level`,
{
method: 'POST',
...(targetProfileId
? {
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ targetProfileId }),
}
: {}),
},
'进入下一关失败',
{