Merge branch 'codex/backend-rewrite-spacetimedb' of http://82.157.175.59:3000/GenarrativeAI/Genarrative into codex/backend-rewrite-spacetimedb
This commit is contained in:
69
src/services/puzzle-runtime/puzzleLocalRuntime.test.ts
Normal file
69
src/services/puzzle-runtime/puzzleLocalRuntime.test.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
import { describe, expect, test } from 'vitest';
|
||||
|
||||
import type { PuzzleWorkSummary } from '../../../packages/shared/src/contracts/puzzleWorkSummary';
|
||||
import {
|
||||
advanceLocalPuzzleLevel,
|
||||
dragLocalPuzzlePiece,
|
||||
startLocalPuzzleRun,
|
||||
} from './puzzleLocalRuntime';
|
||||
|
||||
const baseWork: PuzzleWorkSummary = {
|
||||
workId: 'work-1',
|
||||
profileId: 'profile-1',
|
||||
ownerUserId: 'user-1',
|
||||
sourceSessionId: null,
|
||||
authorDisplayName: '测试作者',
|
||||
levelName: '测试拼图',
|
||||
summary: '服务层测试用拼图。',
|
||||
themeTags: ['测试', '拼图'],
|
||||
coverImageSrc: '/generated-puzzle-assets/test.png',
|
||||
coverAssetId: null,
|
||||
publicationStatus: 'published',
|
||||
updatedAt: '2026-04-25T00:00:00.000Z',
|
||||
publishedAt: '2026-04-25T00:00:00.000Z',
|
||||
playCount: 0,
|
||||
publishReady: true,
|
||||
};
|
||||
|
||||
function solveCurrentLevel(run: ReturnType<typeof startLocalPuzzleRun>) {
|
||||
let nextRun = run;
|
||||
for (let index = 0; index < 12; index += 1) {
|
||||
const currentLevel = nextRun.currentLevel;
|
||||
if (!currentLevel || currentLevel.status === 'cleared') {
|
||||
return nextRun;
|
||||
}
|
||||
|
||||
const misplacedPiece = currentLevel.board.pieces.find(
|
||||
(piece) =>
|
||||
piece.currentRow !== piece.correctRow ||
|
||||
piece.currentCol !== piece.correctCol,
|
||||
);
|
||||
if (!misplacedPiece) {
|
||||
return nextRun;
|
||||
}
|
||||
|
||||
nextRun = dragLocalPuzzlePiece(nextRun, {
|
||||
pieceId: misplacedPiece.pieceId,
|
||||
targetRow: misplacedPiece.correctRow,
|
||||
targetCol: misplacedPiece.correctCol,
|
||||
});
|
||||
}
|
||||
return nextRun;
|
||||
}
|
||||
|
||||
describe('puzzleLocalRuntime', () => {
|
||||
test('通关后提供下一关入口并能推进到新棋盘', () => {
|
||||
const clearedRun = solveCurrentLevel(startLocalPuzzleRun(baseWork));
|
||||
|
||||
expect(clearedRun.currentLevel?.status).toBe('cleared');
|
||||
expect(clearedRun.recommendedNextProfileId).toBe('profile-1::local-level-2');
|
||||
|
||||
const nextRun = advanceLocalPuzzleLevel(clearedRun);
|
||||
|
||||
expect(nextRun.currentLevelIndex).toBe(2);
|
||||
expect(nextRun.currentLevel?.status).toBe('playing');
|
||||
expect(nextRun.currentLevel?.levelName).toBe('测试拼图 · 第 2 关');
|
||||
expect(nextRun.currentLevel?.board.allTilesResolved).toBe(false);
|
||||
expect(nextRun.recommendedNextProfileId).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -89,17 +89,66 @@ function applyNextBoard(
|
||||
return run;
|
||||
}
|
||||
const status = nextBoard.allTilesResolved ? 'cleared' : 'playing';
|
||||
const nextClearedLevelCount =
|
||||
status === 'cleared' && run.currentLevel.status !== 'cleared'
|
||||
? run.clearedLevelCount + 1
|
||||
: run.clearedLevelCount;
|
||||
return {
|
||||
...run,
|
||||
clearedLevelCount:
|
||||
status === 'cleared' && run.currentLevel.status !== 'cleared'
|
||||
? run.clearedLevelCount + 1
|
||||
: run.clearedLevelCount,
|
||||
clearedLevelCount: nextClearedLevelCount,
|
||||
currentLevel: {
|
||||
...run.currentLevel,
|
||||
board: nextBoard,
|
||||
status,
|
||||
},
|
||||
recommendedNextProfileId:
|
||||
status === 'cleared'
|
||||
? buildLocalNextProfileId(run.entryProfileId, nextClearedLevelCount + 1)
|
||||
: run.recommendedNextProfileId,
|
||||
};
|
||||
}
|
||||
|
||||
function buildLocalNextProfileId(entryProfileId: string, levelIndex: number) {
|
||||
return `${entryProfileId}::local-level-${levelIndex}`;
|
||||
}
|
||||
|
||||
// 第一版单机玩法没有后端推荐池,本地沿用当前作品图片并生成可推进的临时关卡名。
|
||||
function buildLocalLevelName(previousLevelName: string, levelIndex: number) {
|
||||
return `${previousLevelName.replace(/ · 第 \d+ 关$/, '')} · 第 ${levelIndex} 关`;
|
||||
}
|
||||
|
||||
// 本地运行态只保证单次游玩闭环:通关后立即重建下一关棋盘,不写回后端。
|
||||
function buildNextLocalLevel(run: PuzzleRunSnapshot): PuzzleRunSnapshot {
|
||||
const currentLevel = run.currentLevel;
|
||||
if (!currentLevel || currentLevel.status !== 'cleared') {
|
||||
return run;
|
||||
}
|
||||
|
||||
const nextLevelIndex = run.currentLevelIndex + 1;
|
||||
const gridSize = resolvePuzzleGridSize(run.clearedLevelCount);
|
||||
const nextProfileId =
|
||||
run.recommendedNextProfileId ??
|
||||
buildLocalNextProfileId(run.entryProfileId, nextLevelIndex);
|
||||
|
||||
return {
|
||||
...run,
|
||||
currentLevelIndex: nextLevelIndex,
|
||||
currentGridSize: gridSize,
|
||||
playedProfileIds: run.playedProfileIds.includes(nextProfileId)
|
||||
? run.playedProfileIds
|
||||
: [...run.playedProfileIds, nextProfileId],
|
||||
previousLevelTags: currentLevel.themeTags,
|
||||
currentLevel: {
|
||||
...currentLevel,
|
||||
runId: run.runId,
|
||||
levelIndex: nextLevelIndex,
|
||||
gridSize,
|
||||
profileId: nextProfileId,
|
||||
levelName: buildLocalLevelName(currentLevel.levelName, nextLevelIndex),
|
||||
board: buildInitialBoard(gridSize),
|
||||
status: 'playing',
|
||||
},
|
||||
recommendedNextProfileId: null,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -191,5 +240,5 @@ export function dragLocalPuzzlePiece(
|
||||
}
|
||||
|
||||
export function advanceLocalPuzzleLevel(run: PuzzleRunSnapshot): PuzzleRunSnapshot {
|
||||
return run;
|
||||
return buildNextLocalLevel(run);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user