ready for publish
This commit is contained in:
@@ -1,115 +0,0 @@
|
||||
import type { GameStatus, UseScoreType } from "../../src/types";
|
||||
import { useIsMobile } from "./useIsMobile";
|
||||
import { MobileGameOverlay } from "./MobileGameOverlay";
|
||||
|
||||
type GameOverlayProps = {
|
||||
gameStatus: GameStatus;
|
||||
scoreManager: UseScoreType;
|
||||
};
|
||||
|
||||
export const GameOverlay = ({ gameStatus, scoreManager }: GameOverlayProps) => {
|
||||
const isMobile = useIsMobile();
|
||||
if (isMobile) return <MobileGameOverlay gameStatus={gameStatus} scoreManager={scoreManager} />
|
||||
|
||||
if (gameStatus === "started") return null;
|
||||
|
||||
return (
|
||||
<GameOverlayContainer>
|
||||
{gameStatus === "waiting" && <GameWaitingContent />}
|
||||
{gameStatus === "game-over" && <GameOverContent />}
|
||||
{gameStatus === "game-won" && <GameWonContent scoreManager={scoreManager} />}
|
||||
</GameOverlayContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export const GameWaitingContent = () => {
|
||||
return (
|
||||
<div
|
||||
className="text-[#c8d3f5] text-[2em] font-mono text-center select-none"
|
||||
style={{ textShadow: "2px 2px 4px rgba(27, 29, 43, 0.9)" }}
|
||||
>
|
||||
<div className="flex flex-col gap-10">
|
||||
<p className="text-[3rem]">Press <strong className="text-[#82aaff]">New Puzzle</strong> to start</p>
|
||||
<div className="text-[1.2em]">
|
||||
<p className="mb-4"><strong className="text-[#ffc777]">Navigation Mode</strong> (default):</p>
|
||||
<p><strong className="text-[#82aaff]">h</strong>,<strong className="text-[#82aaff]">j</strong>,<strong className="text-[#82aaff]">k</strong>,<strong className="text-[#82aaff]">l</strong> to move around</p>
|
||||
<p className="mt-2"><strong className="text-[#82aaff]">R</strong> or <strong className="text-[#82aaff]">C</strong> to enter edit mode</p>
|
||||
|
||||
<p className="mt-6 mb-4"><strong className="text-[#ffc777]">Edit Mode</strong>:</p>
|
||||
<p>Type <strong className="text-[#82aaff]">1-9</strong> to fill cell</p>
|
||||
<p className="mt-2"><strong className="text-[#82aaff]">Esc</strong> or <strong className="text-[#82aaff]">Enter</strong> to exit edit mode</p>
|
||||
|
||||
<p className="mt-6 text-[0.8em] text-[#828bb8]">
|
||||
Try vim motions: <strong className="text-[#82aaff]">$</strong>, <strong className="text-[#82aaff]">^</strong>, <strong className="text-[#82aaff]">0</strong>, counted moves
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const GameWonContent = ({ scoreManager }: { scoreManager: UseScoreType }) => {
|
||||
const finalScore = scoreManager.finalScore ?? 0;
|
||||
|
||||
return (
|
||||
<div
|
||||
className="text-[#c8d3f5] text-[2em] grid gap-4 font-mono uppercase text-center select-none"
|
||||
style={{ textShadow: "2px 2px 4px rgba(27, 29, 43, 0.9)" }}
|
||||
>
|
||||
<p className="text-[#c3e88d]">🎉 Puzzle Solved!</p>
|
||||
<p>Final Score:</p>
|
||||
<p className="text-[#82aaff] text-[2.5em]">{finalScore.toLocaleString()}</p>
|
||||
<p className="text-[#828bb8] text-[0.65em] normal-case">
|
||||
<span className="text-[#c099ff]">max</span>
|
||||
<span className="text-[#c8d3f5]">(</span>
|
||||
<span className="text-[#ff966c]">0</span>
|
||||
<span className="text-[#c8d3f5]">, </span>
|
||||
<span className="text-[#ff966c]">100000</span>
|
||||
<span className="text-[#c8d3f5]"> - </span>
|
||||
<span className="text-[#82aaff]">timeInSeconds</span>
|
||||
<span className="text-[#c8d3f5]"> * </span>
|
||||
<span className="text-[#ff966c]">10</span>
|
||||
<span className="text-[#c8d3f5]">)</span>
|
||||
</p>
|
||||
<p className="text-[#828bb8]">Click <strong className="text-[#ffc777] normal-case">New Puzzle</strong> to play again</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const GameOverContent = () => {
|
||||
return (
|
||||
<div
|
||||
className="text-[#ff757f] font-mono uppercase text-center select-none"
|
||||
style={{ textShadow: "2px 2px 4px rgba(27, 29, 43, 0.9)" }}
|
||||
>
|
||||
<p className="text-[5em]">Game Over</p>
|
||||
<p className="text-[2em] text-[#828bb8]">Press <strong className="text-[#ffc777] normal-case">i</strong> to play again</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const GameOverlayContainer = ({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) => {
|
||||
return (
|
||||
<div
|
||||
className="
|
||||
absolute inset-0
|
||||
flex justify-center items-center
|
||||
z-40
|
||||
pointer-events-none
|
||||
"
|
||||
style={{
|
||||
backgroundColor: 'rgba(27, 29, 43, 0.75)',
|
||||
backdropFilter: 'blur(1px)'
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
|
||||
@@ -1,96 +0,0 @@
|
||||
import type { GameStatus, UseScoreType } from "../../src/types";
|
||||
|
||||
type MobileGameOverlayProps = {
|
||||
gameStatus: GameStatus;
|
||||
scoreManager: UseScoreType;
|
||||
};
|
||||
|
||||
export const MobileGameOverlay = ({ gameStatus, scoreManager }: MobileGameOverlayProps) => {
|
||||
if (gameStatus === "started") return null;
|
||||
|
||||
return (
|
||||
<GameOverlayContainer>
|
||||
{gameStatus === "waiting" && <GameWaitingContent />}
|
||||
{gameStatus === "game-over" && <GameOverContent />}
|
||||
{gameStatus === "game-won" && <GameWonContent scoreManager={scoreManager} />}
|
||||
</GameOverlayContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export const GameWaitingContent = () => {
|
||||
return (
|
||||
<div
|
||||
className="text-[#c8d3f5] text-[1em] font-mono text-center select-none"
|
||||
style={{ textShadow: "2px 2px 4px rgba(27, 29, 43, 0.9)" }}
|
||||
>
|
||||
<div className="flex flex-col gap-3">
|
||||
<p className="text-[1.5rem]">Tap New Puzzle to start</p>
|
||||
<p>VIM motions available <br /> via on-screen keyboard</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const GameWonContent = ({ scoreManager }: { scoreManager: UseScoreType }) => {
|
||||
const finalScore = scoreManager.finalScore ?? 0;
|
||||
|
||||
return (
|
||||
<div
|
||||
className="text-[#c8d3f5] text-[1em] grid gap-4 font-mono uppercase text-center select-none"
|
||||
style={{ textShadow: "2px 2px 4px rgba(27, 29, 43, 0.9)" }}
|
||||
>
|
||||
<p className="text-[#c3e88d]">🎉 Solved!</p>
|
||||
<p>Final Score:</p>
|
||||
<p className="text-[#82aaff] text-[2.5em]">{finalScore.toLocaleString()}</p>
|
||||
<p className="text-[#828bb8] text-[0.65em] normal-case">
|
||||
<span className="text-[#c099ff]">max</span>
|
||||
<span className="text-[#c8d3f5]">(</span>
|
||||
<span className="text-[#ff966c]">0</span>
|
||||
<span className="text-[#c8d3f5]">, </span>
|
||||
<span className="text-[#ff966c]">100000</span>
|
||||
<span className="text-[#c8d3f5]"> - </span>
|
||||
<span className="text-[#82aaff]">time</span>
|
||||
<span className="text-[#c8d3f5]"> * </span>
|
||||
<span className="text-[#ff966c]">10</span>
|
||||
<span className="text-[#c8d3f5]">)</span>
|
||||
</p>
|
||||
<p className="text-[#828bb8]">Tap to play again</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const GameOverContent = () => {
|
||||
return (
|
||||
<div
|
||||
className="text-[#ff757f] text-[5em] font-mono uppercase text-center select-none"
|
||||
style={{ textShadow: "2px 2px 4px rgba(27, 29, 43, 0.9)" }}
|
||||
>
|
||||
Game Over
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const GameOverlayContainer = ({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) => {
|
||||
return (
|
||||
<div
|
||||
className="
|
||||
absolute inset-0
|
||||
flex justify-center items-center
|
||||
z-40
|
||||
pointer-events-none
|
||||
"
|
||||
style={{
|
||||
backgroundColor: 'rgba(27, 29, 43, 0.75)',
|
||||
backdropFilter: 'blur(1px)'
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
+9
-8
@@ -51,11 +51,14 @@ function initialize() {
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
sudoku.generate = function (difficulty: Difficulty, unique?: boolean): string {
|
||||
let numericDifficulty: number;
|
||||
if (typeof difficulty === "string" || typeof difficulty === "undefined") {
|
||||
difficulty = DIFFICULTY[difficulty as DifficultyName] || DIFFICULTY.easy;
|
||||
numericDifficulty = DIFFICULTY[difficulty as DifficultyName] || DIFFICULTY.easy;
|
||||
} else {
|
||||
numericDifficulty = difficulty;
|
||||
}
|
||||
|
||||
difficulty = sudoku._force_range(difficulty, NR_SQUARES + 1, MIN_GIVENS);
|
||||
numericDifficulty = sudoku._force_range(numericDifficulty, NR_SQUARES + 1, MIN_GIVENS);
|
||||
unique = unique ?? true;
|
||||
|
||||
const blank_board = sudoku.BLANK_BOARD;
|
||||
@@ -74,7 +77,7 @@ sudoku.generate = function (difficulty: Difficulty, unique?: boolean): string {
|
||||
}
|
||||
|
||||
if (
|
||||
single_candidates.length >= difficulty &&
|
||||
single_candidates.length >= numericDifficulty &&
|
||||
sudoku._strip_dups(single_candidates).length >= 8
|
||||
) {
|
||||
let board = "";
|
||||
@@ -90,9 +93,9 @@ sudoku.generate = function (difficulty: Difficulty, unique?: boolean): string {
|
||||
}
|
||||
}
|
||||
|
||||
if (givens_idxs.length > difficulty) {
|
||||
if (givens_idxs.length > numericDifficulty) {
|
||||
givens_idxs = sudoku._shuffle(givens_idxs);
|
||||
for (let i = 0; i < givens_idxs.length - difficulty; ++i) {
|
||||
for (let i = 0; i < givens_idxs.length - numericDifficulty; ++i) {
|
||||
const target = givens_idxs[i];
|
||||
board =
|
||||
board.substring(0, target) +
|
||||
@@ -105,7 +108,7 @@ sudoku.generate = function (difficulty: Difficulty, unique?: boolean): string {
|
||||
}
|
||||
}
|
||||
|
||||
return sudoku.generate(difficulty);
|
||||
return sudoku.generate(numericDifficulty);
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -187,12 +190,10 @@ sudoku._search = function (candidates: Record<string, string>, reverse: boolean)
|
||||
reverse = reverse || false;
|
||||
|
||||
let max_nr_candidates = 0;
|
||||
let max_square = "";
|
||||
for (const s of SQUARES) {
|
||||
const n = candidates[s].length;
|
||||
if (n > max_nr_candidates) {
|
||||
max_nr_candidates = n;
|
||||
max_square = s;
|
||||
}
|
||||
}
|
||||
if (max_nr_candidates === 1) return candidates;
|
||||
|
||||
+5
-19
@@ -1,28 +1,14 @@
|
||||
/* =========================================================
|
||||
🔧 Controls (tweak these)
|
||||
========================================================= */
|
||||
/* :root { */
|
||||
/* --cell-size: 32px; */
|
||||
/* --cell-font-size: 16px; */
|
||||
/* --grid-gap: 2px; */
|
||||
/* --box-separator: 3px; */
|
||||
/* } */
|
||||
|
||||
:root {
|
||||
--cell-size: 48px;
|
||||
--cell-font-size: 32px;
|
||||
--grid-gap: 4px;
|
||||
--box-separator: 6px;
|
||||
--sudoku-size-multiplier: 3;
|
||||
--cell-size: calc(32px * var(--sudoku-size-multiplier));
|
||||
--cell-font-size: calc(16px * var(--sudoku-size-multiplier));
|
||||
--grid-gap: calc(2px * var(--sudoku-size-multiplier));
|
||||
--box-separator: calc(3px * var(--sudoku-size-multiplier));
|
||||
}
|
||||
|
||||
|
||||
/* :root { */
|
||||
/* --cell-size: 96px; */
|
||||
/* --cell-font-size: 48px; */
|
||||
/* --grid-gap: 6px; */
|
||||
/* --box-separator: 9px; */
|
||||
/* } */
|
||||
|
||||
/* =========================================================
|
||||
Container & Grid Base
|
||||
========================================================= */
|
||||
|
||||
@@ -7,7 +7,6 @@ export type UseScoreType = ReturnType<typeof useScore>;
|
||||
export type {
|
||||
Difficulty,
|
||||
DifficultyName,
|
||||
BoardString,
|
||||
} from "./lib/Sudoku";
|
||||
|
||||
export type GamePhase = "idle" | "playing";
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
import { useState, useCallback, useMemo } from "react";
|
||||
import { useState } from "react";
|
||||
import { sudoku } from "../lib/Sudoku";
|
||||
import type { UseCursorType } from "./useCursor";
|
||||
|
||||
export function useBoardState(cursorManager: UseCursorType) {
|
||||
type UseBoardStateOptions = {
|
||||
validateMoves?: boolean;
|
||||
};
|
||||
|
||||
export function useBoardState(cursorManager: UseCursorType, options?: UseBoardStateOptions) {
|
||||
const { cursor, setCursor } = cursorManager;
|
||||
const { validateMoves = false } = options || {};
|
||||
const [board, setBoard] = useState<string>(sudoku.generate("medium"));
|
||||
const [initialBoard, setInitialBoard] = useState<string>(board);
|
||||
const [solution, setSolution] = useState<string | null>(null);
|
||||
@@ -26,8 +31,6 @@ export function useBoardState(cursorManager: UseCursorType) {
|
||||
const isValidPlacement = (testBoard: string, row: number, col: number, digit: string): boolean => {
|
||||
if (digit === ".") return true;
|
||||
|
||||
const idx = row * 9 + col;
|
||||
|
||||
for (let c = 0; c < 9; c++) {
|
||||
const checkIdx = row * 9 + c;
|
||||
if (c !== col && testBoard[checkIdx] === digit) {
|
||||
@@ -131,7 +134,7 @@ export function useBoardState(cursorManager: UseCursorType) {
|
||||
};
|
||||
|
||||
const setDigit = (d: string) => {
|
||||
const i = cursor.r * 9 + cursor.c;
|
||||
const cellIndex = cursor.r * 9 + cursor.c;
|
||||
|
||||
if (!canEditCell(cursor.r, cursor.c)) {
|
||||
window.dispatchEvent(new CustomEvent("sudoku-locked-cell"));
|
||||
@@ -139,10 +142,10 @@ export function useBoardState(cursorManager: UseCursorType) {
|
||||
}
|
||||
|
||||
const arr = board.split("");
|
||||
arr[i] = d;
|
||||
arr[cellIndex] = d;
|
||||
const newBoard = arr.join("");
|
||||
|
||||
if (!isValidPlacement(newBoard, cursor.r, cursor.c, d)) {
|
||||
if (validateMoves && !isValidPlacement(newBoard, cursor.r, cursor.c, d)) {
|
||||
const conflicts = getConflicts(newBoard, cursor.r, cursor.c);
|
||||
window.dispatchEvent(
|
||||
new CustomEvent("sudoku-invalid-move", {
|
||||
@@ -153,11 +156,13 @@ export function useBoardState(cursorManager: UseCursorType) {
|
||||
}
|
||||
|
||||
setBoard(newBoard);
|
||||
window.dispatchEvent(
|
||||
new CustomEvent("sudoku-valid-move", {
|
||||
detail: { row: cursor.r, col: cursor.c, digit: d },
|
||||
})
|
||||
);
|
||||
if (validateMoves) {
|
||||
window.dispatchEvent(
|
||||
new CustomEvent("sudoku-valid-move", {
|
||||
detail: { row: cursor.r, col: cursor.c, digit: d },
|
||||
})
|
||||
);
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
|
||||
@@ -6,14 +6,19 @@ import { useBoardState } from "./useBoardState";
|
||||
import { useKeyBindings } from "./useKeyBindings";
|
||||
import { useScore } from "../useScore";
|
||||
|
||||
export function useGame(platformHook?: unknown) {
|
||||
export type UseGameOptions = {
|
||||
validateMoves?: boolean;
|
||||
};
|
||||
|
||||
export function useGame(platformHook?: unknown, options?: UseGameOptions) {
|
||||
const { validateMoves = false } = options || {};
|
||||
const rendererManager = useRenderer();
|
||||
const { containerRef, renderEmptyBoard, renderBoard, highlightCursor } = rendererManager;
|
||||
const cursorManager = useCursor(rendererManager);
|
||||
const { cursor, moveCursor } = cursorManager;
|
||||
const gameManager = useGameStatus(rendererManager, cursorManager);
|
||||
const { gameStatus, setGameStatus, startGame, stopGame } = gameManager;
|
||||
const boardManager = useBoardState(cursorManager);
|
||||
const boardManager = useBoardState(cursorManager, { validateMoves });
|
||||
const {
|
||||
board,
|
||||
initialBoard,
|
||||
|
||||
Reference in New Issue
Block a user