clone

a daily shikaku puzzle for phones: one seeded puzzle per date, solved by dragging rectangles

main
rect.run / lib / shikaku / types.ts
types.ts 3.7K
1export const CURRENT_STORAGE_VERSION = 1;23export type PuzzleStatus = "in_progress" | "solved" | "gave_up";4export type ArchivedPuzzleStatus = "solved" | "gave_up" | "expired_unsolved";56export interface CellSelection {7 clueId: number;8 cells: string[];9 value: number;10}1112export interface UserSettings {13 theme: "system" | "light" | "dark";14 reducedMotion: boolean;15 hapticsEnabled: boolean;16 handedness: "left" | "right";17 showRegionHints: boolean;18}1920export interface GridPoint {21 row: number;22 col: number;23}2425export interface PuzzleGiven extends GridPoint {26 id: number;27 value: number;28}2930export interface RectanglePlacement extends GridPoint {31 width: number;32 height: number;33}3435export interface PlacedRectangle extends RectanglePlacement {36 clueId: number;37 value: number;38}3940export interface ShikakuPuzzle {41 id: string;42 date: string;43 streakIndex: number;44 width: number;45 height: number;46 givens: PuzzleGiven[];47 solution?: PlacedRectangle[];48 difficulty: "easy" | "medium" | "hard";49 seed: string;50}5152export interface PuzzleProgress {53 date: string;54 puzzleId: string;55 startedAt: string;56 updatedAt: string;57 status: PuzzleStatus;58 elapsedMs: number;59 mistakes: number;60 selections: CellSelection[];61 notes: Record<string, string>;62 lastViewedAt: string;63 clientVersion: number;64}6566export interface PuzzleHistoryEntry {67 date: string;68 puzzleId: string;69 status: ArchivedPuzzleStatus;70 completedAt?: string;71 elapsedMs?: number;72 mistakes: number;73 score?: number;74}7576export interface PlayerStats {77 played: number;78 solved: number;79 currentStreak: number;80 bestStreak: number;81 averageSolveMs?: number;82 lastPlayedDate?: string;83}8485export function createDefaultSettings(): UserSettings {86 return {87 theme: "system",88 reducedMotion: false,89 hapticsEnabled: true,90 handedness: "right",91 showRegionHints: true,92 };93}9495export function parsePuzzleProgress(value: unknown): PuzzleProgress | null {96 if (!isRecord(value)) return null;97 if (!isString(value.date) || !isString(value.puzzleId)) return null;98 if (!isString(value.startedAt) || !isString(value.updatedAt)) return null;99 if (!isPuzzleStatus(value.status)) return null;100 if (101 typeof value.elapsedMs !== "number" || typeof value.mistakes !== "number"102 ) {103 return null;104 }105 if (!Array.isArray(value.selections) || !isRecord(value.notes)) return null;106 if (!isString(value.lastViewedAt)) return null;107108 return {109 date: value.date,110 puzzleId: value.puzzleId,111 startedAt: value.startedAt,112 updatedAt: value.updatedAt,113 status: value.status,114 elapsedMs: value.elapsedMs,115 mistakes: value.mistakes,116 selections: value.selections.map((selection) => {117 if (118 !isRecord(selection) || !Array.isArray(selection.cells) ||119 typeof selection.value !== "number"120 ) {121 return { clueId: -1, cells: [], value: 0 };122 }123 return {124 clueId: typeof selection.clueId === "number" ? selection.clueId : -1,125 cells: selection.cells.filter(isString),126 value: selection.value,127 };128 }),129 notes: Object.fromEntries(130 Object.entries(value.notes).filter((entry): entry is [string, string] =>131 typeof entry[1] === "string"132 ),133 ),134 lastViewedAt: value.lastViewedAt,135 clientVersion: typeof value.clientVersion === "number"136 ? value.clientVersion137 : CURRENT_STORAGE_VERSION,138 };139}140141function isRecord(value: unknown): value is Record<string, unknown> {142 return typeof value === "object" && value !== null;143}144145function isString(value: unknown): value is string {146 return typeof value === "string";147}148149function isPuzzleStatus(value: unknown): value is PuzzleStatus {150 return value === "in_progress" || value === "solved" || value === "gave_up";151}