Skip to content

Commit cfd268a

Browse files
committed
add difficulty selection
1 parent 1263c37 commit cfd268a

3 files changed

Lines changed: 57 additions & 11 deletions

File tree

src/App.tsx

Lines changed: 45 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@ import { useEffect, useMemo, useRef, useState, type CSSProperties } from "react"
22
import {
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

3233
export 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+
};

src/game/constants.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
export const BOARD_SIZE = 9;
22
export const BLOCK_SIZE = 3;
33
export const CELL_SIZE = 42;
4-
export const CLUE_PIECES = 10;
4+
export const DIFFICULTY_CLUE_PIECES = {
5+
easy: 14,
6+
medium: 10,
7+
hard: 6,
8+
} as const;
9+
export const DEFAULT_DIFFICULTY = "medium";
510
export const GENERATION_TIMEOUT_MS = 100;
611
export const GENERATION_MAX_RETRIES = 20;

src/game/puzzle.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { BLOCK_SIZE, BOARD_SIZE, CLUE_PIECES } from "./constants";
1+
import { BLOCK_SIZE, BOARD_SIZE } from "./constants";
22
import type { Cell, PieceCell, PlacedPiece, RandomFillArrangement } from "./types";
33
import { createEmptyBoard, rotatePieceCells, shuffle } from "./utils";
44

@@ -55,9 +55,12 @@ const getPieceCellsFromBoard = (
5555
}));
5656
};
5757

58-
export const createPuzzleFromArrangement = (arrangement: RandomFillArrangement) => {
58+
export const createPuzzleFromArrangement = (
59+
arrangement: RandomFillArrangement,
60+
cluePieceCount: number,
61+
) => {
5962
const allPieces = Object.values(arrangement.placedPieces);
60-
const cluePieces = shuffle(allPieces).slice(0, CLUE_PIECES);
63+
const cluePieces = shuffle(allPieces).slice(0, cluePieceCount);
6164
const cluePieceIds = new Set(cluePieces.map((piece) => piece.pieceInstanceId));
6265
const fixedPieceIds = new Set(cluePieceIds);
6366
const sudokuSolution = createSudokuSolution();

0 commit comments

Comments
 (0)