@@ -2,7 +2,8 @@ import { useEffect, useMemo, useRef, useState, type CSSProperties } from "react"
22import {
33 BOARD_SIZE ,
44 CELL_SIZE ,
5- CLUE_PIECES ,
5+ DEFAULT_DIFFICULTY ,
6+ DIFFICULTY_CLUE_PIECES ,
67 GENERATION_MAX_RETRIES ,
78 GENERATION_TIMEOUT_MS ,
89} from "./game/constants" ;
@@ -30,6 +31,8 @@ import {
3031} from "./game/utils" ;
3132
3233export const App = ( ) => {
34+ type Difficulty = keyof typeof DIFFICULTY_CLUE_PIECES ;
35+
3336 const [ board , setBoard ] = useState < Cell [ ] [ ] > ( createEmptyBoard ) ;
3437 const [ placedPieces , setPlacedPieces ] = useState < Record < string , PlacedPiece > > ( { } ) ;
3538 const [ trayPieces , setTrayPieces ] = useState < PlacedPiece [ ] > ( [ ] ) ;
@@ -38,6 +41,7 @@ export const App = () => {
3841 const [ activeDrag , setActiveDrag ] = useState < ActiveDrag | null > ( null ) ;
3942 const [ preview , setPreview ] = useState < PreviewState | null > ( null ) ;
4043 const [ isGenerating , setIsGenerating ] = useState ( false ) ;
44+ const [ difficulty , setDifficulty ] = useState < Difficulty > ( DEFAULT_DIFFICULTY ) ;
4145 const [ hoveredPieceId , setHoveredPieceId ] = useState < string | null > ( null ) ;
4246 const [ , setMessage ] = useState ( "Generate a fresh board and recover the missing tetrominoes." ) ;
4347 const generationToken = useRef ( 0 ) ;
@@ -48,6 +52,7 @@ export const App = () => {
4852 ) ;
4953
5054 const isSolved = solutionBoard !== null && boardEquals ( board , solutionBoard ) ;
55+ const cluePieces = DIFFICULTY_CLUE_PIECES [ difficulty ] ;
5156 const duplicateNumberKeys = useMemo ( ( ) => findDuplicateNumberKeys ( board ) , [ board ] ) ;
5257 const fireworks = useMemo ( ( ) => {
5358 const palette = [ "#ffd166" , "#ff6b6b" , "#7bdff2" , "#cdb4db" , "#9bf6b0" , "#f9c74f" ] ;
@@ -101,9 +106,10 @@ export const App = () => {
101106 } ) ;
102107 } ;
103108
104- const startGeneration = ( ) => {
109+ const startGeneration = ( targetDifficulty : Difficulty = difficulty ) => {
105110 const token = generationToken . current + 1 ;
106111 generationToken . current = token ;
112+ const targetCluePieces = DIFFICULTY_CLUE_PIECES [ targetDifficulty ] ;
107113
108114 setIsGenerating ( true ) ;
109115 setSolutionBoard ( null ) ;
@@ -134,7 +140,7 @@ export const App = () => {
134140 return ;
135141 }
136142
137- const puzzle = createPuzzleFromArrangement ( arrangement ) ;
143+ const puzzle = createPuzzleFromArrangement ( arrangement , targetCluePieces ) ;
138144
139145 finish ( ) ;
140146 setBoard ( puzzle . board ) ;
@@ -146,7 +152,7 @@ export const App = () => {
146152 setPreview ( null ) ;
147153 setIsGenerating ( false ) ;
148154 setMessage (
149- `${ CLUE_PIECES } clue pieces are fixed on the board. Rebuild the rest from the tray.` ,
155+ `${ targetCluePieces } clue pieces are fixed on the board. Rebuild the rest from the tray.` ,
150156 ) ;
151157 } ;
152158
@@ -482,6 +488,15 @@ export const App = () => {
482488 ] . join ( " " ) ;
483489 } ;
484490
491+ const handleDifficultyChange = ( nextDifficulty : Difficulty ) => {
492+ if ( nextDifficulty === difficulty ) {
493+ return ;
494+ }
495+
496+ setDifficulty ( nextDifficulty ) ;
497+ startGeneration ( nextDifficulty ) ;
498+ } ;
499+
485500 return (
486501 < div
487502 style = { {
@@ -536,7 +551,7 @@ export const App = () => {
536551 A 9x9 puzzle of rotating pieces.
537552 </ h1 >
538553 < p style = { { marginTop : 0 , maxWidth : "66ch" , opacity : 0.84 , lineHeight : 1.6 } } >
539- The board starts with a generated tetromino layout. { CLUE_PIECES } pieces stay fixed as
554+ The board starts with a generated tetromino layout. { cluePieces } pieces stay fixed as
540555 clues, and the rest are moved into the tray for you to rotate and drag back into place.
541556 </ p >
542557 </ header >
@@ -642,7 +657,20 @@ export const App = () => {
642657 </ div >
643658
644659 < div style = { { display : "flex" , gap : "12px" , marginTop : "18px" , flexWrap : "wrap" } } >
645- < button type = "button" onClick = { startGeneration } style = { buttonStyle } >
660+ < label style = { { display : "flex" , alignItems : "center" , gap : "8px" , fontWeight : 600 } } >
661+ Difficulty
662+ < select
663+ value = { difficulty }
664+ onChange = { ( event ) => handleDifficultyChange ( event . target . value as Difficulty ) }
665+ style = { selectStyle }
666+ disabled = { isGenerating }
667+ >
668+ < option value = "easy" > Easy (14 clues)</ option >
669+ < option value = "medium" > Medium (10 clues)</ option >
670+ < option value = "hard" > Hard (6 clues)</ option >
671+ </ select >
672+ </ label >
673+ < button type = "button" onClick = { ( ) => startGeneration ( ) } style = { buttonStyle } >
646674 { isGenerating ? "Generating..." : "New puzzle" }
647675 </ button >
648676 </ div >
@@ -840,7 +868,7 @@ export const App = () => {
840868 >
841869 Solved!
842870 </ h2 >
843- < button type = "button" onClick = { startGeneration } style = { buttonStyle } >
871+ < button type = "button" onClick = { ( ) => startGeneration ( ) } style = { buttonStyle } >
844872 Retry
845873 </ button >
846874 </ div >
@@ -868,3 +896,13 @@ const trayButtonStyle: CSSProperties = {
868896 gap : "12px" ,
869897 textAlign : "left" ,
870898} ;
899+
900+ const selectStyle : CSSProperties = {
901+ border : "1px solid rgba(255, 255, 255, 0.28)" ,
902+ borderRadius : "999px" ,
903+ padding : "10px 14px" ,
904+ fontWeight : 700 ,
905+ color : "#f8f9fa" ,
906+ background : "rgba(15, 20, 37, 0.65)" ,
907+ cursor : "pointer" ,
908+ } ;
0 commit comments