puzzle单机实现

This commit is contained in:
2026-04-24 20:41:24 +08:00
parent 88bd4f36e9
commit 0940a1945c
6 changed files with 716 additions and 152 deletions

View File

@@ -58,11 +58,11 @@ import {
} from '../../services/puzzle-agent';
import { getPuzzleGalleryDetail } from '../../services/puzzle-gallery';
import {
advancePuzzleNextLevel,
dragPuzzlePieceOrGroup,
startPuzzleRun,
swapPuzzlePieces,
} from '../../services/puzzle-runtime';
advanceLocalPuzzleLevel,
dragLocalPuzzlePiece,
startLocalPuzzleRun,
swapLocalPuzzlePieces,
} from '../../services/puzzle-runtime/puzzleLocalRuntime';
import { listPuzzleWorks } from '../../services/puzzle-works';
import { deleteRpgEntryWorldProfile } from '../../services/rpg-entry';
import { getRpgEntryWorldGalleryDetailByCode } from '../../services/rpg-entry/rpgEntryLibraryClient';
@@ -1024,8 +1024,9 @@ export function PlatformEntryFlowShellImpl({
setPuzzleError(null);
try {
const { run } = await startPuzzleRun({ profileId });
setPuzzleRun(run);
const { item } = await getPuzzleGalleryDetail(profileId);
setSelectedPuzzleDetail(item);
setPuzzleRun(startLocalPuzzleRun(item));
setSelectionStage('puzzle-runtime');
} catch (error) {
setPuzzleError(resolvePuzzleErrorMessage(error, '启动拼图玩法失败。'));
@@ -1060,28 +1061,19 @@ export function PlatformEntryFlowShellImpl({
);
const swapPuzzlePiecesInRun = useCallback(
async (payload: { firstPieceId: string; secondPieceId: string }) => {
(payload: { firstPieceId: string; secondPieceId: string }) => {
if (!puzzleRun || isPuzzleBusy) {
return;
}
setIsPuzzleBusy(true);
setPuzzleError(null);
try {
const { run } = await swapPuzzlePieces(puzzleRun.runId, payload);
setPuzzleRun(run);
} catch (error) {
setPuzzleError(resolvePuzzleErrorMessage(error, '交换拼图块失败。'));
} finally {
setIsPuzzleBusy(false);
}
setPuzzleRun(swapLocalPuzzlePieces(puzzleRun, payload));
},
[isPuzzleBusy, puzzleRun, resolvePuzzleErrorMessage],
[isPuzzleBusy, puzzleRun],
);
const dragPuzzlePiece = useCallback(
async (payload: {
(payload: {
pieceId: string;
targetRow: number;
targetCol: number;
@@ -1090,19 +1082,10 @@ export function PlatformEntryFlowShellImpl({
return;
}
setIsPuzzleBusy(true);
setPuzzleError(null);
try {
const { run } = await dragPuzzlePieceOrGroup(puzzleRun.runId, payload);
setPuzzleRun(run);
} catch (error) {
setPuzzleError(resolvePuzzleErrorMessage(error, '拖动拼图块失败。'));
} finally {
setIsPuzzleBusy(false);
}
setPuzzleRun(dragLocalPuzzlePiece(puzzleRun, payload));
},
[isPuzzleBusy, puzzleRun, resolvePuzzleErrorMessage],
[isPuzzleBusy, puzzleRun],
);
const advancePuzzleLevel = useCallback(async () => {
@@ -1110,18 +1093,9 @@ export function PlatformEntryFlowShellImpl({
return;
}
setIsPuzzleBusy(true);
setPuzzleError(null);
try {
const { run } = await advancePuzzleNextLevel(puzzleRun.runId);
setPuzzleRun(run);
} catch (error) {
setPuzzleError(resolvePuzzleErrorMessage(error, '进入下一关失败。'));
} finally {
setIsPuzzleBusy(false);
}
}, [isPuzzleBusy, puzzleRun, resolvePuzzleErrorMessage]);
setPuzzleRun(advanceLocalPuzzleLevel(puzzleRun));
}, [isPuzzleBusy, puzzleRun]);
const leaveAgentWorkspace = useCallback(() => {
enterCreateTab();

View File

@@ -0,0 +1,195 @@
import type {
DragPuzzlePieceRequest,
PuzzleBoardSnapshot,
PuzzleGridSize,
PuzzlePieceState,
PuzzleRunSnapshot,
SwapPuzzlePiecesRequest,
} from '../../../packages/shared/src/contracts/puzzleRuntimeSession';
import type { PuzzleWorkSummary } from '../../../packages/shared/src/contracts/puzzleWorkSummary';
function resolvePuzzleGridSize(clearedLevelCount: number): PuzzleGridSize {
return clearedLevelCount >= 3 ? 4 : 3;
}
function buildInitialPositions(gridSize: PuzzleGridSize) {
const positions = Array.from({ length: gridSize * gridSize }, (_, index) => ({
row: Math.floor(index / gridSize),
col: index % gridSize,
}));
return positions.slice(1).concat(positions.slice(0, 1));
}
function rebuildBoardSnapshot(
gridSize: PuzzleGridSize,
pieces: PuzzlePieceState[],
): PuzzleBoardSnapshot {
const resolvedPieceIds = new Set(
pieces
.filter(
(piece) =>
piece.currentRow === piece.correctRow &&
piece.currentCol === piece.correctCol,
)
.map((piece) => piece.pieceId),
);
const allTilesResolved = resolvedPieceIds.size === pieces.length;
return {
rows: gridSize,
cols: gridSize,
pieces: pieces.map((piece) => ({
...piece,
mergedGroupId: resolvedPieceIds.has(piece.pieceId)
? 'resolved-main'
: null,
})),
mergedGroups: resolvedPieceIds.size
? [
{
groupId: 'resolved-main',
pieceIds: Array.from(resolvedPieceIds),
occupiedCells: pieces
.filter((piece) => resolvedPieceIds.has(piece.pieceId))
.map((piece) => ({
row: piece.currentRow,
col: piece.currentCol,
})),
},
]
: [],
selectedPieceId: null,
allTilesResolved,
};
}
function buildInitialBoard(gridSize: PuzzleGridSize): PuzzleBoardSnapshot {
const shuffledPositions = buildInitialPositions(gridSize);
const pieces = Array.from({ length: gridSize * gridSize }, (_, index) => {
const correctRow = Math.floor(index / gridSize);
const correctCol = index % gridSize;
const current = shuffledPositions[index];
return {
pieceId: `piece-${index}`,
correctRow,
correctCol,
currentRow: current.row,
currentCol: current.col,
mergedGroupId: null,
};
});
return rebuildBoardSnapshot(gridSize, pieces);
}
function applyNextBoard(
run: PuzzleRunSnapshot,
nextBoard: PuzzleBoardSnapshot,
): PuzzleRunSnapshot {
if (!run.currentLevel) {
return run;
}
const status = nextBoard.allTilesResolved ? 'cleared' : 'playing';
return {
...run,
clearedLevelCount:
status === 'cleared' && run.currentLevel.status !== 'cleared'
? run.clearedLevelCount + 1
: run.clearedLevelCount,
currentLevel: {
...run.currentLevel,
board: nextBoard,
status,
},
};
}
export function startLocalPuzzleRun(item: PuzzleWorkSummary): PuzzleRunSnapshot {
const gridSize = resolvePuzzleGridSize(0);
return {
runId: `local-puzzle-run-${item.profileId}-${Date.now()}`,
entryProfileId: item.profileId,
clearedLevelCount: 0,
currentLevelIndex: 1,
currentGridSize: gridSize,
playedProfileIds: [item.profileId],
previousLevelTags: item.themeTags,
currentLevel: {
runId: `local-puzzle-run-${item.profileId}`,
levelIndex: 1,
gridSize,
profileId: item.profileId,
levelName: item.levelName,
authorDisplayName: item.authorDisplayName,
themeTags: item.themeTags,
coverImageSrc: item.coverImageSrc,
board: buildInitialBoard(gridSize),
status: 'playing',
},
recommendedNextProfileId: null,
};
}
export function swapLocalPuzzlePieces(
run: PuzzleRunSnapshot,
payload: SwapPuzzlePiecesRequest,
): PuzzleRunSnapshot {
const currentLevel = run.currentLevel;
if (!currentLevel || currentLevel.status === 'cleared') {
return run;
}
const pieces = currentLevel.board.pieces.map((piece) => ({ ...piece }));
const first = pieces.find((piece) => piece.pieceId === payload.firstPieceId);
const second = pieces.find((piece) => piece.pieceId === payload.secondPieceId);
if (!first || !second) {
return run;
}
const firstPosition = { row: first.currentRow, col: first.currentCol };
first.currentRow = second.currentRow;
first.currentCol = second.currentCol;
second.currentRow = firstPosition.row;
second.currentCol = firstPosition.col;
return applyNextBoard(run, rebuildBoardSnapshot(currentLevel.gridSize, pieces));
}
export function dragLocalPuzzlePiece(
run: PuzzleRunSnapshot,
payload: DragPuzzlePieceRequest,
): PuzzleRunSnapshot {
const currentLevel = run.currentLevel;
if (!currentLevel || currentLevel.status === 'cleared') {
return run;
}
if (
payload.targetRow < 0 ||
payload.targetCol < 0 ||
payload.targetRow >= currentLevel.gridSize ||
payload.targetCol >= currentLevel.gridSize
) {
return run;
}
const pieces = currentLevel.board.pieces.map((piece) => ({ ...piece }));
const moving = pieces.find((piece) => piece.pieceId === payload.pieceId);
if (!moving) {
return run;
}
const occupying = pieces.find(
(piece) =>
piece.pieceId !== payload.pieceId &&
piece.currentRow === payload.targetRow &&
piece.currentCol === payload.targetCol,
);
const source = { row: moving.currentRow, col: moving.currentCol };
moving.currentRow = payload.targetRow;
moving.currentCol = payload.targetCol;
if (occupying) {
occupying.currentRow = source.row;
occupying.currentCol = source.col;
}
return applyNextBoard(run, rebuildBoardSnapshot(currentLevel.gridSize, pieces));
}
export function advanceLocalPuzzleLevel(run: PuzzleRunSnapshot): PuzzleRunSnapshot {
return run;
}