1
This commit is contained in:
@@ -3,7 +3,6 @@ import type {
|
||||
PuzzleBoardSnapshot,
|
||||
PuzzleCellPosition,
|
||||
PuzzleGridSize,
|
||||
PuzzleLeaderboardEntry,
|
||||
PuzzleMergedGroupState,
|
||||
PuzzlePieceState,
|
||||
PuzzleRunSnapshot,
|
||||
@@ -75,11 +74,11 @@ function buildInitialPositions(gridSize: PuzzleGridSize, seed: number) {
|
||||
);
|
||||
ensureBoardIsNotSolved(shuffled, gridSize);
|
||||
const pieces = buildPiecesFromPositions(gridSize, shuffled);
|
||||
if (!hasAnyCorrectNeighborPair(pieces)) {
|
||||
if (!hasAnyOriginalNeighborPair(pieces)) {
|
||||
return shuffled;
|
||||
}
|
||||
}
|
||||
return positions.slice().reverse();
|
||||
return buildOriginalNeighborFreePositions(gridSize, seed) ?? positions;
|
||||
}
|
||||
|
||||
function boardCellKey(row: number, col: number) {
|
||||
@@ -90,48 +89,6 @@ function clampElapsedMs(value: number) {
|
||||
return Math.max(1_000, Math.round(value));
|
||||
}
|
||||
|
||||
function rankLeaderboardEntries(
|
||||
entries: Omit<PuzzleLeaderboardEntry, 'rank'>[],
|
||||
): PuzzleLeaderboardEntry[] {
|
||||
return entries
|
||||
.map((entry) => ({ ...entry }))
|
||||
.sort((left, right) => left.elapsedMs - right.elapsedMs)
|
||||
.map((entry, index) => ({
|
||||
...entry,
|
||||
rank: index + 1,
|
||||
}));
|
||||
}
|
||||
|
||||
// V1 本地榜单只用于单次游玩闭环展示;正式榜单后续迁移到 SpacetimeDB 表或 view。
|
||||
function buildLocalLeaderboardEntries(
|
||||
elapsedMs: number,
|
||||
playerNickname: string,
|
||||
levelIndex: number,
|
||||
gridSize: PuzzleGridSize,
|
||||
): PuzzleLeaderboardEntry[] {
|
||||
const normalizedElapsedMs = clampElapsedMs(elapsedMs);
|
||||
const baseOffsetMs = gridSize === 3 ? 4_000 : 8_000;
|
||||
return rankLeaderboardEntries([
|
||||
{
|
||||
nickname: playerNickname.trim() || '玩家',
|
||||
elapsedMs: normalizedElapsedMs,
|
||||
isCurrentPlayer: true,
|
||||
},
|
||||
{
|
||||
nickname: '星桥旅人',
|
||||
elapsedMs: normalizedElapsedMs + baseOffsetMs + levelIndex * 700,
|
||||
},
|
||||
{
|
||||
nickname: '月港拼图手',
|
||||
elapsedMs: Math.max(1_000, normalizedElapsedMs - baseOffsetMs / 2),
|
||||
},
|
||||
{
|
||||
nickname: '雾灯收藏家',
|
||||
elapsedMs: normalizedElapsedMs + baseOffsetMs * 2 + levelIndex * 900,
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
function neighborCells(row: number, col: number): PuzzleCellPosition[] {
|
||||
return [
|
||||
row > 0 ? { row: row - 1, col } : null,
|
||||
@@ -179,6 +136,119 @@ function hasAnyCorrectNeighborPair(pieces: PuzzlePieceState[]) {
|
||||
);
|
||||
}
|
||||
|
||||
function areOriginalNeighbors(left: PuzzlePieceState, right: PuzzlePieceState) {
|
||||
return (
|
||||
Math.abs(right.correctRow - left.correctRow) +
|
||||
Math.abs(right.correctCol - left.correctCol) ===
|
||||
1
|
||||
);
|
||||
}
|
||||
|
||||
function hasAnyOriginalNeighborPair(pieces: PuzzlePieceState[]) {
|
||||
const piecesByCell = new Map(
|
||||
pieces.map((piece) => [boardCellKey(piece.currentRow, piece.currentCol), piece]),
|
||||
);
|
||||
return pieces.some((piece) =>
|
||||
neighborCells(piece.currentRow, piece.currentCol).some((neighbor) => {
|
||||
const neighborPiece = piecesByCell.get(boardCellKey(neighbor.row, neighbor.col));
|
||||
return Boolean(neighborPiece && areOriginalNeighbors(piece, neighborPiece));
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
function seededOrderKey(seed: number, value: number) {
|
||||
let state = (seed ^ Math.imul(value, 2654435761)) >>> 0;
|
||||
state ^= state >>> 16;
|
||||
state = Math.imul(state, 2246822507) >>> 0;
|
||||
state ^= state >>> 13;
|
||||
state = Math.imul(state, 3266489909) >>> 0;
|
||||
return (state ^ (state >>> 16)) >>> 0;
|
||||
}
|
||||
|
||||
function buildOriginalNeighborFreePositions(
|
||||
gridSize: PuzzleGridSize,
|
||||
seed: number,
|
||||
) {
|
||||
const total = gridSize * gridSize;
|
||||
const pieceOrder = Array.from({ length: total }, (_, index) => index).sort(
|
||||
(left, right) =>
|
||||
seededOrderKey(seed ^ 0xa0761d64, left) -
|
||||
seededOrderKey(seed ^ 0xa0761d64, right),
|
||||
);
|
||||
const cellOrder = Array.from({ length: total }, (_, index) => ({
|
||||
row: Math.floor(index / gridSize),
|
||||
col: index % gridSize,
|
||||
})).sort(
|
||||
(left, right) =>
|
||||
seededOrderKey(seed ^ 0xe7037ed1, left.row * 16 + left.col) -
|
||||
seededOrderKey(seed ^ 0xe7037ed1, right.row * 16 + right.col),
|
||||
);
|
||||
const placements: Array<PuzzleCellPosition | null> = Array.from(
|
||||
{ length: total },
|
||||
() => null,
|
||||
);
|
||||
const usedCells = new Set<string>();
|
||||
|
||||
const placePiece = (depth: number): boolean => {
|
||||
const pieceIndex = pieceOrder[depth];
|
||||
if (pieceIndex === undefined) {
|
||||
return true;
|
||||
}
|
||||
|
||||
for (const cell of cellOrder) {
|
||||
const cellKey = boardCellKey(cell.row, cell.col);
|
||||
if (usedCells.has(cellKey)) {
|
||||
continue;
|
||||
}
|
||||
if (
|
||||
cell.row === Math.floor(pieceIndex / gridSize) &&
|
||||
cell.col === pieceIndex % gridSize
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
if (
|
||||
violatesOriginalNeighborFreeRule(gridSize, pieceIndex, cell, placements)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
placements[pieceIndex] = cell;
|
||||
usedCells.add(cellKey);
|
||||
if (placePiece(depth + 1)) {
|
||||
return true;
|
||||
}
|
||||
usedCells.delete(cellKey);
|
||||
placements[pieceIndex] = null;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
return placePiece(0) && placements.every(Boolean)
|
||||
? (placements as PuzzleCellPosition[])
|
||||
: null;
|
||||
}
|
||||
|
||||
function violatesOriginalNeighborFreeRule(
|
||||
gridSize: PuzzleGridSize,
|
||||
pieceIndex: number,
|
||||
cell: PuzzleCellPosition,
|
||||
placements: Array<PuzzleCellPosition | null>,
|
||||
) {
|
||||
return placements.some((placedCell, placedIndex) => {
|
||||
if (!placedCell) {
|
||||
return false;
|
||||
}
|
||||
const originalNeighbors =
|
||||
Math.abs(Math.floor(pieceIndex / gridSize) - Math.floor(placedIndex / gridSize)) +
|
||||
Math.abs((pieceIndex % gridSize) - (placedIndex % gridSize)) ===
|
||||
1;
|
||||
const currentNeighbors =
|
||||
Math.abs(cell.row - placedCell.row) + Math.abs(cell.col - placedCell.col) ===
|
||||
1;
|
||||
return originalNeighbors && currentNeighbors;
|
||||
});
|
||||
}
|
||||
|
||||
function resolveMergedGroups(
|
||||
pieces: PuzzlePieceState[],
|
||||
): PuzzleMergedGroupState[] {
|
||||
@@ -306,15 +376,6 @@ function applyNextBoard(
|
||||
const elapsedMs = justCleared
|
||||
? clampElapsedMs(nowMs - run.currentLevel.startedAtMs)
|
||||
: (run.currentLevel.elapsedMs ?? null);
|
||||
const leaderboardEntries =
|
||||
justCleared && elapsedMs
|
||||
? buildLocalLeaderboardEntries(
|
||||
elapsedMs,
|
||||
run.currentLevel.authorDisplayName,
|
||||
run.currentLevel.levelIndex,
|
||||
run.currentLevel.gridSize,
|
||||
)
|
||||
: run.currentLevel.leaderboardEntries;
|
||||
return {
|
||||
...run,
|
||||
clearedLevelCount: nextClearedLevelCount,
|
||||
@@ -324,9 +385,9 @@ function applyNextBoard(
|
||||
status,
|
||||
clearedAtMs,
|
||||
elapsedMs,
|
||||
leaderboardEntries,
|
||||
leaderboardEntries: justCleared ? [] : run.currentLevel.leaderboardEntries,
|
||||
},
|
||||
leaderboardEntries,
|
||||
leaderboardEntries: justCleared ? [] : run.leaderboardEntries,
|
||||
recommendedNextProfileId:
|
||||
status === 'cleared'
|
||||
? buildLocalNextProfileId(run.entryProfileId, nextClearedLevelCount + 1)
|
||||
|
||||
Reference in New Issue
Block a user