@@ -49,6 +49,61 @@ function useContainerSize() {
49
49
return containerSize ;
50
50
}
51
51
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
+
52
107
const ALL_FACTS = [
53
108
// 1
54
109
`Zak King <a href="mailto:[email protected] ">[email protected] </a>` ,
@@ -100,8 +155,7 @@ const ALL_FACTS = [
100
155
] ;
101
156
102
157
function getFactsForGridSize ( gridSize : number ) {
103
- const needed = gridSize * gridSize ;
104
- return ALL_FACTS . slice ( 0 , needed ) ;
158
+ return ALL_FACTS . slice ( 0 , gridSize * gridSize ) ;
105
159
}
106
160
107
161
function getPuzzleImages ( gridSize : number ) {
@@ -110,36 +164,20 @@ function getPuzzleImages(gridSize: number) {
110
164
for ( let row = 0 ; row < gridSize ; row ++ ) {
111
165
for ( let col = 0 ; col < gridSize ; col ++ ) {
112
166
const x = col + 1 ;
113
- const y = gridSize - row ;
167
+ const y = row + 1 ;
114
168
paths . push ( `${ basePath } /image${ x } x${ y } .jpeg` ) ;
115
169
}
116
170
}
117
171
return paths ;
118
172
}
119
173
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
-
138
174
export default function PuzzleClient ( ) {
139
175
const gridSize = useGridSize ( ) ;
140
176
const containerSize = useContainerSize ( ) ;
177
+
141
178
const resumeFacts = useMemo ( ( ) => getFactsForGridSize ( gridSize ) , [ gridSize ] ) ;
142
179
const puzzleImages = useMemo ( ( ) => getPuzzleImages ( gridSize ) , [ gridSize ] ) ;
180
+
143
181
const [ tiles , setTiles ] = useState < ( number | null ) [ ] > ( [ ] ) ;
144
182
const [ tileOffsets , setTileOffsets ] = useState < { x : number ; y : number } [ ] > (
145
183
[ ]
@@ -154,38 +192,29 @@ export default function PuzzleClient() {
154
192
155
193
// "You win!" state
156
194
const [ isWon , setIsWon ] = useState ( false ) ;
157
-
158
195
const tileSize = containerSize / gridSize ;
159
196
160
- // On initial mount or if puzzleImages changes, shuffle puzzle
161
197
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 ) ;
169
199
setTiles ( arr ) ;
170
200
setTileOffsets ( arr . map ( ( ) => ( { x : 0 , y : 0 } ) ) ) ;
171
201
setIsDraggingTile ( arr . map ( ( ) => false ) ) ;
172
- setIsWon ( false ) ; // reset if grid changes
202
+ setIsWon ( false ) ;
173
203
} , [ puzzleImages , gridSize ] ) ;
174
204
175
205
// Whenever tiles change, check if puzzle is solved
176
206
useEffect ( ( ) => {
177
207
if ( ! isWon && isPuzzleSolved ( tiles ) ) {
178
208
setIsWon ( true ) ;
179
- // you could also play a sound, call an API, log an event, etc.
180
209
}
181
210
} , [ tiles , isWon ] ) ;
182
211
183
- /** Convert puzzle index => [row, col] */
212
+ // Convert puzzle index => [row, col]
184
213
function getRowCol ( i : number ) {
185
214
return [ Math . floor ( i / gridSize ) , i % gridSize ] as const ;
186
215
}
187
216
188
- /** True if slots differ by exactly one row or col => adjacency. */
217
+ // True if slots differ by exactly one row or col => adjacency.
189
218
function isAdjacent ( i : number , j : number ) {
190
219
const [ r1 , c1 ] = getRowCol ( i ) ;
191
220
const [ r2 , c2 ] = getRowCol ( j ) ;
@@ -195,7 +224,7 @@ export default function PuzzleClient() {
195
224
) ;
196
225
}
197
226
198
- /** Swap puzzle slots i and j, also resetting offsets/drag states. */
227
+ // Swap puzzle slots i and j, also resetting offsets/drag states
199
228
function swapTiles ( i : number , j : number ) {
200
229
setTiles ( ( prev ) => {
201
230
const copy = [ ...prev ] ;
@@ -423,7 +452,7 @@ export default function PuzzleClient() {
423
452
{ isWon && (
424
453
< >
425
454
< Confetti
426
- // just let it fill parent
455
+ // Just let it fill parent
427
456
style = { {
428
457
position : "fixed" ,
429
458
top : 0 ,
@@ -433,7 +462,7 @@ export default function PuzzleClient() {
433
462
} }
434
463
/>
435
464
< 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 >
437
466
</ div >
438
467
</ >
439
468
) }
@@ -449,8 +478,8 @@ export default function PuzzleClient() {
449
478
{ /* Bottom layer: NxN facts */ }
450
479
{ resumeFacts . map ( ( fact , i ) => {
451
480
if ( fact === null ) return null ;
452
-
453
- const [ row , col ] = getRowCol ( i ) ;
481
+ const row = Math . floor ( i / gridSize ) ;
482
+ const col = i % gridSize ;
454
483
const baseX = col * tileSize ;
455
484
const baseY = row * tileSize ;
456
485
@@ -480,11 +509,9 @@ export default function PuzzleClient() {
480
509
481
510
{ /* Tiles (images) */ }
482
511
{ tiles . map ( ( tile , i ) => {
483
- if ( tile === null ) {
484
- return null ;
485
- }
512
+ if ( tile === null ) return null ; // Blank spot
486
513
487
- const [ row , col ] = getRowCol ( i ) ;
514
+ const [ row , col ] = [ Math . floor ( i / gridSize ) , i % gridSize ] ;
488
515
const baseX = col * tileSize ;
489
516
const baseY = row * tileSize ;
490
517
const off = tileOffsets [ i ] || { x : 0 , y : 0 } ;
@@ -524,6 +551,7 @@ export default function PuzzleClient() {
524
551
fill
525
552
className = "object-cover pointer-events-none"
526
553
/>
554
+ < div className = "text-center text-md absolute" > { tile } </ div >
527
555
</ div >
528
556
) ;
529
557
} ) }
0 commit comments