Skip to content

Commit 7e4bad0

Browse files
committed
legal moves scramble
1 parent 96a15c5 commit 7e4bad0

File tree

1 file changed

+71
-43
lines changed

1 file changed

+71
-43
lines changed

src/app/PuzzleClient.tsx

Lines changed: 71 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,61 @@ function useContainerSize() {
4949
return containerSize;
5050
}
5151

52+
// --------------------------------------------------------------------------
53+
// 1) A random "legal moves" scramble function
54+
// --------------------------------------------------------------------------
55+
function randomLegalScramble(gridSize: number, steps = 200): (number | null)[] {
56+
const total = gridSize * gridSize;
57+
58+
// Start with a solved arrangement: 0..(total-2), null
59+
const puzzle: (number | null)[] = Array.from(
60+
{ length: total - 1 },
61+
(_, i) => i
62+
);
63+
puzzle.push(null);
64+
65+
// Helper to get row, col in 0-based from a puzzle index
66+
function rc(i: number) {
67+
return [Math.floor(i / gridSize), i % gridSize];
68+
}
69+
70+
let blankIndex = puzzle.indexOf(null);
71+
72+
// Make `steps` random moves
73+
for (let step = 0; step < steps; step++) {
74+
const [blankRow, blankCol] = rc(blankIndex);
75+
const neighbors: number[] = [];
76+
77+
// up
78+
if (blankRow > 0) neighbors.push(blankIndex - gridSize);
79+
// down
80+
if (blankRow < gridSize - 1) neighbors.push(blankIndex + gridSize);
81+
// left
82+
if (blankCol > 0) neighbors.push(blankIndex - 1);
83+
// right
84+
if (blankCol < gridSize - 1) neighbors.push(blankIndex + 1);
85+
86+
// pick a random neighbor
87+
const n = neighbors[Math.floor(Math.random() * neighbors.length)];
88+
89+
// swap puzzle[n] and puzzle[blankIndex]
90+
[puzzle[n], puzzle[blankIndex]] = [puzzle[blankIndex], puzzle[n]];
91+
92+
blankIndex = n;
93+
}
94+
95+
return puzzle;
96+
}
97+
98+
/** Helper to detect puzzle completion. */
99+
function isPuzzleSolved(tiles: (number | null)[]) {
100+
// The solution is [0, 1, 2, ..., lastIndex-1, null]
101+
for (let i = 0; i < tiles.length - 1; i++) {
102+
if (tiles[i] !== i) return false;
103+
}
104+
return tiles[tiles.length - 1] === null;
105+
}
106+
52107
const ALL_FACTS = [
53108
// 1
54109
`Zak King <a href="mailto:[email protected]">[email protected]</a>`,
@@ -100,8 +155,7 @@ const ALL_FACTS = [
100155
];
101156

102157
function getFactsForGridSize(gridSize: number) {
103-
const needed = gridSize * gridSize;
104-
return ALL_FACTS.slice(0, needed);
158+
return ALL_FACTS.slice(0, gridSize * gridSize);
105159
}
106160

107161
function getPuzzleImages(gridSize: number) {
@@ -110,36 +164,20 @@ function getPuzzleImages(gridSize: number) {
110164
for (let row = 0; row < gridSize; row++) {
111165
for (let col = 0; col < gridSize; col++) {
112166
const x = col + 1;
113-
const y = gridSize - row;
167+
const y = row + 1;
114168
paths.push(`${basePath}/image${x}x${y}.jpeg`);
115169
}
116170
}
117171
return paths;
118172
}
119173

120-
function shuffleArray<T>(arr: T[]): T[] {
121-
for (let i = arr.length - 1; i > 0; i--) {
122-
const j = Math.floor(Math.random() * (i + 1));
123-
[arr[i], arr[j]] = [arr[j], arr[i]];
124-
}
125-
return arr;
126-
}
127-
128-
/** Helper to detect puzzle completion. */
129-
function isPuzzleSolved(tiles: (number | null)[]) {
130-
// The solution is [0, 1, 2, ..., lastIndex-1, null]
131-
// i.e., each tile's index should match its value, except last is null
132-
for (let i = 0; i < tiles.length - 1; i++) {
133-
if (tiles[i] !== i) return false;
134-
}
135-
return tiles[tiles.length - 1] === null;
136-
}
137-
138174
export default function PuzzleClient() {
139175
const gridSize = useGridSize();
140176
const containerSize = useContainerSize();
177+
141178
const resumeFacts = useMemo(() => getFactsForGridSize(gridSize), [gridSize]);
142179
const puzzleImages = useMemo(() => getPuzzleImages(gridSize), [gridSize]);
180+
143181
const [tiles, setTiles] = useState<(number | null)[]>([]);
144182
const [tileOffsets, setTileOffsets] = useState<{ x: number; y: number }[]>(
145183
[]
@@ -154,38 +192,29 @@ export default function PuzzleClient() {
154192

155193
// "You win!" state
156194
const [isWon, setIsWon] = useState(false);
157-
158195
const tileSize = containerSize / gridSize;
159196

160-
// On initial mount or if puzzleImages changes, shuffle puzzle
161197
useEffect(() => {
162-
const total = puzzleImages.length;
163-
const arr: (number | null)[] = Array.from(
164-
{ length: total - 1 },
165-
(_, i) => i
166-
);
167-
arr.push(null);
168-
shuffleArray(arr);
198+
const arr = randomLegalScramble(gridSize);
169199
setTiles(arr);
170200
setTileOffsets(arr.map(() => ({ x: 0, y: 0 })));
171201
setIsDraggingTile(arr.map(() => false));
172-
setIsWon(false); // reset if grid changes
202+
setIsWon(false);
173203
}, [puzzleImages, gridSize]);
174204

175205
// Whenever tiles change, check if puzzle is solved
176206
useEffect(() => {
177207
if (!isWon && isPuzzleSolved(tiles)) {
178208
setIsWon(true);
179-
// you could also play a sound, call an API, log an event, etc.
180209
}
181210
}, [tiles, isWon]);
182211

183-
/** Convert puzzle index => [row, col] */
212+
// Convert puzzle index => [row, col]
184213
function getRowCol(i: number) {
185214
return [Math.floor(i / gridSize), i % gridSize] as const;
186215
}
187216

188-
/** True if slots differ by exactly one row or col => adjacency. */
217+
// True if slots differ by exactly one row or col => adjacency.
189218
function isAdjacent(i: number, j: number) {
190219
const [r1, c1] = getRowCol(i);
191220
const [r2, c2] = getRowCol(j);
@@ -195,7 +224,7 @@ export default function PuzzleClient() {
195224
);
196225
}
197226

198-
/** Swap puzzle slots i and j, also resetting offsets/drag states. */
227+
// Swap puzzle slots i and j, also resetting offsets/drag states
199228
function swapTiles(i: number, j: number) {
200229
setTiles((prev) => {
201230
const copy = [...prev];
@@ -423,7 +452,7 @@ export default function PuzzleClient() {
423452
{isWon && (
424453
<>
425454
<Confetti
426-
// just let it fill parent
455+
// Just let it fill parent
427456
style={{
428457
position: "fixed",
429458
top: 0,
@@ -433,7 +462,7 @@ export default function PuzzleClient() {
433462
}}
434463
/>
435464
<div className="absolute top-0 left-0 w-full h-full bg-black bg-opacity-50 z-50 flex flex-col items-center justify-center text-center">
436-
<h1 className="text-4xl text-white mb-4">OMG you solved it</h1>
465+
<h1 className="text-4xl text-white mb-4">OMG you solved it!</h1>
437466
</div>
438467
</>
439468
)}
@@ -449,8 +478,8 @@ export default function PuzzleClient() {
449478
{/* Bottom layer: NxN facts */}
450479
{resumeFacts.map((fact, i) => {
451480
if (fact === null) return null;
452-
453-
const [row, col] = getRowCol(i);
481+
const row = Math.floor(i / gridSize);
482+
const col = i % gridSize;
454483
const baseX = col * tileSize;
455484
const baseY = row * tileSize;
456485

@@ -480,11 +509,9 @@ export default function PuzzleClient() {
480509

481510
{/* Tiles (images) */}
482511
{tiles.map((tile, i) => {
483-
if (tile === null) {
484-
return null;
485-
}
512+
if (tile === null) return null; // Blank spot
486513

487-
const [row, col] = getRowCol(i);
514+
const [row, col] = [Math.floor(i / gridSize), i % gridSize];
488515
const baseX = col * tileSize;
489516
const baseY = row * tileSize;
490517
const off = tileOffsets[i] || { x: 0, y: 0 };
@@ -524,6 +551,7 @@ export default function PuzzleClient() {
524551
fill
525552
className="object-cover pointer-events-none"
526553
/>
554+
<div className="text-center text-md absolute">{tile}</div>
527555
</div>
528556
);
529557
})}

0 commit comments

Comments
 (0)