From ae70e9bd6c64bf4d096fa0438e8911c33676c7f1 Mon Sep 17 00:00:00 2001 From: Nervonment Date: Wed, 19 Jun 2024 13:24:29 +0800 Subject: [PATCH 1/3] gridReducer --- app/start/page.js | 67 +++++++++++++++++++++++++++++++--------------- lib/gridReducer.js | 17 ++++++++++++ 2 files changed, 62 insertions(+), 22 deletions(-) create mode 100644 lib/gridReducer.js diff --git a/app/start/page.js b/app/start/page.js index 89abc5d..86443b7 100644 --- a/app/start/page.js +++ b/app/start/page.js @@ -2,25 +2,32 @@ import { Label } from "@/components/ui/label"; import SudokuGrid from "@/components/sudoku_grid"; -import { cn, difficultyDesc, hintColor } from "@/lib/utils"; +import { cn, difficultyDesc } from "@/lib/utils"; import { invoke } from "@tauri-apps/api/tauri"; import { Check, HelpCircle, Lightbulb, Loader2, Redo, RefreshCcw, SquarePen, Tornado, Trash2, Undo, Undo2, X } from "lucide-react"; -import { useCallback, useEffect, useRef, useState } from "react" +import { useCallback, useEffect, useReducer, useRef, useState } from "react" import { Button } from "@/components/ui/button"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"; import { useRouter } from "next/navigation"; import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; +import gridReducer from "@/lib/gridReducer"; export default function Start() { - const [grid, setGrid] = useState(); - const currentCellRef = useRef(); + // const [grid, setGrid] = useState(); + const [grid, dispatchGrid] = useReducer(gridReducer, null); + + + + const [maxCandidates, setMaxCandidates] = useState([]); // 当前局面下所有格的所有候选数 const [markedCandidates, setMarkedCandidates] = useState([]); // 用户标记的所有格的候选数 // 掩码候选数,是上述两者按照各格的交集 const maskedCandidates = markedCandidates.map((row, r) => row.map((candidatesOfCell, c) => candidatesOfCell.map((is, num) => is && maxCandidates[r][c][num]))); + const rowContainsRef = useRef(Array.from({ length: 9 }, (v) => Array.from({ length: 10 }, (v) => false))); const colContainsRef = useRef(Array.from({ length: 9 }, (v) => Array.from({ length: 10 }, (v) => false))); const blkContainsRef = useRef(Array.from({ length: 9 }, (v) => Array.from({ length: 10 }, (v) => false))); + const [hint, setHint] = useState(null); const [noHintReason, setNoHintReason] = useState(""); const [showingHint, setShowingHint] = useState(false); @@ -28,7 +35,7 @@ export default function Start() { const [finished, setFinished] = useState(false); const [usedHint, setUsedHint] = useState(false); - const [usedAssist, setUsedAssist] = useState(false); + // const [usedAssist, setUsedAssist] = useState(false); const markingAssistRef = useRef(false); // 是否开启标记辅助 const beginWithMarksRef = useRef(false); // 是否开启开局标记 @@ -57,12 +64,14 @@ export default function Start() { useEffect(() => { invoke('get_difficulty').then((difficulty) => setDifficulty(difficulty)); - invoke('get_marking_assist').then((markingAssist) => { markingAssistRef.current = markingAssist; setUsedAssist(markingAssist); }); + invoke('get_marking_assist').then((markingAssist) => { markingAssistRef.current = markingAssist; /*setUsedAssist(markingAssist);*/ }); invoke('get_begin_with_marks').then((beginWithMarks) => { beginWithMarksRef.current = beginWithMarks; }); }, []); const init = useCallback((grid) => { - setGrid(grid); + // setGrid(grid); + dispatchGrid({ type: 'set', newGrid: grid }); + setMaxCandidates(getMaxCandidates(grid)); if (beginWithMarksRef.current) { setMarkedCandidates(getMaxCandidates(grid)); @@ -73,7 +82,9 @@ export default function Start() { setTime(0); hideHint(); setUsedHint(false); - setUsedAssist(markingAssistRef.current); + setFinished(false); + + // setUsedAssist(markingAssistRef.current); historyRef.current = []; futureRef.current = []; }, []); @@ -111,9 +122,10 @@ export default function Start() { }, [getPuzzle]); const newPuzzle = () => { - setGrid(null); + // setGrid(null); + dispatchGrid({ type: 'set', newGrid: null }); + getPuzzle(); - setFinished(false); }; const clear = () => { @@ -150,14 +162,20 @@ export default function Start() { invoke('judge_sudoku', { grid: grid.map((row) => row.map((grid) => grid.value)) }) .then(([finished, validCond]) => { setFinished(finished); - setGrid((prev) => { - prev[r][c].value = num; - setMaxCandidates(getMaxCandidates(prev)); - return prev.map((row, r) => row.map((cell, c) => ({ - ...cell, - valid: validCond[r][c], - }))) + setMaxCandidates(getMaxCandidates(grid)); + + dispatchGrid({ + type: 'fill', + r, c, num, + validity: validCond[r][c] }); + // setGrid((prev) => { + // prev[r][c].value = num; + // return prev.map((row, r) => row.map((cell, c) => ({ + // ...cell, + // valid: validCond[r][c], + // }))) + // }); }); } }, [grid, markedCandidates, pushHistory]); @@ -180,7 +198,8 @@ export default function Start() { markedCandidates: markedCandidates.map((row) => row.map((cell) => cell.slice())) }); let { grid: g, markedCandidates: m } = historyRef.current.pop(); - setGrid(g); + // setGrid(g); + dispatchGrid({ type: 'set', newGrid: g }); setMaxCandidates(getMaxCandidates(g)); setMarkedCandidates(m); hideHint(); @@ -194,7 +213,8 @@ export default function Start() { markedCandidates: markedCandidates.map((row) => row.map((cell) => cell.slice())) }); let { grid: g, markedCandidates: m } = futureRef.current.pop(); - setGrid(g); + // setGrid(g); + dispatchGrid({ type: 'set', newGrid: g }); setMaxCandidates(getMaxCandidates(g)); setMarkedCandidates(m); hideHint(); @@ -264,6 +284,9 @@ export default function Start() { }; }, [onKeyDown]); + + const currentCellRef = useRef(); + const handleMouseEnter = (r, c) => { currentCellRef.current = [r, c]; } @@ -344,9 +367,9 @@ export default function Start() { <>

已完成!

- { + {/* { usedAssist ? 已使用辅助 : <> - } + } */} { usedHint ? 已使用提示 : <> } @@ -392,7 +415,7 @@ export default function Start() { + + +

{desc}

+ + + + ) +} export default function Start() { - // const [grid, setGrid] = useState(); const [grid, dispatchGrid] = useReducer(gridReducer, null); + + const currentCellRef = useRef(); + const handleMouseEnter = (r, c) => { + currentCellRef.current = [r, c]; + } + const handleMouseLeave = () => { + currentCellRef.current = null; + } - - - - const [maxCandidates, setMaxCandidates] = useState([]); // 当前局面下所有格的所有候选数 + const getMaxCandidates = useMaxCandidates(); + const maxCandidates = getMaxCandidates(grid); // 当前局面下所有格的所有候选数 const [markedCandidates, setMarkedCandidates] = useState([]); // 用户标记的所有格的候选数 // 掩码候选数,是上述两者按照各格的交集 - const maskedCandidates = markedCandidates.map((row, r) => row.map((candidatesOfCell, c) => candidatesOfCell.map((is, num) => is && maxCandidates[r][c][num]))); + const maskedCandidates = maxCandidates && markedCandidates.map((row, r) => row.map((candidatesOfCell, c) => candidatesOfCell.map((is, num) => is && maxCandidates[r][c][num]))); - const rowContainsRef = useRef(Array.from({ length: 9 }, (v) => Array.from({ length: 10 }, (v) => false))); - const colContainsRef = useRef(Array.from({ length: 9 }, (v) => Array.from({ length: 10 }, (v) => false))); - const blkContainsRef = useRef(Array.from({ length: 9 }, (v) => Array.from({ length: 10 }, (v) => false))); + const { clearHistory, pushHistory, undo, redo } = useHistory(); const [hint, setHint] = useState(null); const [noHintReason, setNoHintReason] = useState(""); const [showingHint, setShowingHint] = useState(false); - const [time, setTime] = useState(0); - const [finished, setFinished] = useState(false); + const [finished, setFinished] = useState(false); + const { timeStr, reset } = useTimer(finished); const [usedHint, setUsedHint] = useState(false); - // const [usedAssist, setUsedAssist] = useState(false); - const markingAssistRef = useRef(false); // 是否开启标记辅助 - const beginWithMarksRef = useRef(false); // 是否开启开局标记 - - const historyRef = useRef([]); - const futureRef = useRef([]); - - const timeMin = parseInt(time / 60); - const timeSec = (() => { - let sec = time % 60; - return sec < 10 ? `0${sec}` : sec; - })(); - const rest = (() => { - let res = 0; - if (grid) - grid.forEach(row => { - row.forEach(cell => { - if (cell.value == 0) - res += 1; - }); - }); - return res; - })(); - - const [difficulty, setDifficulty] = useState(2); - - useEffect(() => { - invoke('get_difficulty').then((difficulty) => setDifficulty(difficulty)); - invoke('get_marking_assist').then((markingAssist) => { markingAssistRef.current = markingAssist; /*setUsedAssist(markingAssist);*/ }); - invoke('get_begin_with_marks').then((beginWithMarks) => { beginWithMarksRef.current = beginWithMarks; }); - }, []); + const { difficulty, markingAssist, beginWithMarks } = useContext(SettingsContext); const init = useCallback((grid) => { - // setGrid(grid); dispatchGrid({ type: 'set', newGrid: grid }); - setMaxCandidates(getMaxCandidates(grid)); - if (beginWithMarksRef.current) { + if (beginWithMarks) { setMarkedCandidates(getMaxCandidates(grid)); - // setMarkedCandidates(Array.from({ length: 9 }, (v) => Array.from({ length: 9 }, (v) => Array.from({ length: 10 }, (v) => true)))); } else { setMarkedCandidates(Array.from({ length: 9 }, (v) => Array.from({ length: 9 }, (v) => Array.from({ length: 10 }, (v) => false)))); } - setTime(0); + reset(); hideHint(); setUsedHint(false); setFinished(false); - - // setUsedAssist(markingAssistRef.current); - historyRef.current = []; - futureRef.current = []; - }, []); + clearHistory(); + }, [getMaxCandidates, beginWithMarks, reset, clearHistory]); const getPuzzle = useCallback(() => { invoke('get_sudoku_puzzle').then((grid) => { @@ -96,35 +84,12 @@ export default function Start() { }); }, [init]); - const getMaxCandidates = (grid) => { - rowContainsRef.current.forEach((row) => row.fill(false)); - colContainsRef.current.forEach((col) => col.fill(false)); - blkContainsRef.current.forEach((blk) => blk.fill(false)); - let rc2b = (r, c) => parseInt(r / 3) * 3 + parseInt(c / 3); - grid.forEach((row, r) => { - row.forEach((cell, c) => { - let num = cell.value; - rowContainsRef.current[r][num] = true; - colContainsRef.current[c][num] = true; - blkContainsRef.current[rc2b(r, c)][num] = true; - }); - }); - return grid.map((row, r) => row.map((cell, c) => ([0, 1, 2, 3, 4, 5, 6, 7, 8, 9].map((num) => ( - grid[r][c].value == 0 - && !rowContainsRef.current[r][num] - && !colContainsRef.current[c][num] - && !blkContainsRef.current[rc2b(r, c)][num] - ))))); - }; - useEffect(() => { getPuzzle(); }, [getPuzzle]); const newPuzzle = () => { - // setGrid(null); dispatchGrid({ type: 'set', newGrid: null }); - getPuzzle(); }; @@ -135,25 +100,6 @@ export default function Start() { } }; - useEffect(() => { - let updateTime = !finished ? - setInterval(() => { - setTime((prev) => prev + 1); - }, 1000) : null; - return () => { - if (updateTime) - clearInterval(updateTime); - }; - }, [finished]); - - const pushHistory = useCallback((grid, markedCandidates) => { - historyRef.current.push({ - grid: grid.map((row) => row.map((cell) => ({ ...cell }))), - markedCandidates: markedCandidates.map((row) => row.map((cell) => cell.slice())) - }); - futureRef.current = []; - }, []); - const fillGrid = useCallback((r, c, num) => { if (grid[r][c].value != num) { pushHistory(grid, markedCandidates); @@ -162,20 +108,12 @@ export default function Start() { invoke('judge_sudoku', { grid: grid.map((row) => row.map((grid) => grid.value)) }) .then(([finished, validCond]) => { setFinished(finished); - setMaxCandidates(getMaxCandidates(grid)); dispatchGrid({ type: 'fill', r, c, num, - validity: validCond[r][c] + validCond: validCond }); - // setGrid((prev) => { - // prev[r][c].value = num; - // return prev.map((row, r) => row.map((cell, c) => ({ - // ...cell, - // valid: validCond[r][c], - // }))) - // }); }); } }, [grid, markedCandidates, pushHistory]); @@ -191,45 +129,29 @@ export default function Start() { }) }; - const undo = useCallback(() => { - if (!finished && historyRef.current.length != 0) { - futureRef.current.push({ - grid: grid.map((row) => row.map((cell) => ({ ...cell }))), - markedCandidates: markedCandidates.map((row) => row.map((cell) => cell.slice())) - }); - let { grid: g, markedCandidates: m } = historyRef.current.pop(); - // setGrid(g); - dispatchGrid({ type: 'set', newGrid: g }); - setMaxCandidates(getMaxCandidates(g)); - setMarkedCandidates(m); - hideHint(); - } - }, [grid, markedCandidates, finished]); + const handleUndo = useCallback(() => { + const { grid: prevGrid, markedCandidates: prevMarkedCandidates } = undo(grid, markedCandidates); + dispatchGrid({ type: 'set', newGrid: prevGrid }); + setMarkedCandidates(prevMarkedCandidates); + hideHint(); + }, [undo, grid, markedCandidates]); - const redo = useCallback(() => { - if (!finished && futureRef.current.length != 0) { - historyRef.current.push({ - grid: grid.map((row) => row.map((cell) => ({ ...cell }))), - markedCandidates: markedCandidates.map((row) => row.map((cell) => cell.slice())) - }); - let { grid: g, markedCandidates: m } = futureRef.current.pop(); - // setGrid(g); - dispatchGrid({ type: 'set', newGrid: g }); - setMaxCandidates(getMaxCandidates(g)); - setMarkedCandidates(m); - hideHint(); - } - }, [grid, markedCandidates, finished]); + const handleRedo = useCallback(() => { + const { grid: nextGrid, markedCandidates: nextMarkedCandidates } = redo(grid, markedCandidates); + dispatchGrid({ type: 'set', newGrid: nextGrid }); + setMarkedCandidates(nextMarkedCandidates); + hideHint(); + }, [redo, grid, markedCandidates]) const onKeyDown = useCallback((event) => { if (!finished && grid && markedCandidates) { // 撤销 if (event.key == 'z') { - undo(); + handleUndo(); } // 重做 else if (event.key == 'x') { - redo(); + handleRedo(); } // 填数或标记候选数 else if (currentCellRef.current) { @@ -251,7 +173,7 @@ export default function Start() { else { // 没有开启标记辅助时,可以标记任意数字 // 开启时,只能标记 maxCandidates[r][c] 中的数字 - if (grid[r][c].value == 0 && (!markingAssistRef.current || maxCandidates[r][c][num] || event.key == ' ')) { + if (grid[r][c].value == 0 && (!markingAssist || maxCandidates[r][c][num] || event.key == ' ')) { if (event.key == ' ') { // 按空格键 if (!markedCandidates[r][c].every((is, num) => num == 0 || !is)) { pushHistory(grid, markedCandidates); @@ -275,7 +197,7 @@ export default function Start() { } } } - }, [grid, finished, maxCandidates, markedCandidates, pushHistory, fillGrid, undo, redo]); + }, [grid, finished, maxCandidates, markedCandidates, pushHistory, fillGrid, handleUndo, handleRedo, markingAssist]); useEffect(() => { window.addEventListener('keydown', onKeyDown); @@ -284,21 +206,11 @@ export default function Start() { }; }, [onKeyDown]); - - const currentCellRef = useRef(); - - const handleMouseEnter = (r, c) => { - currentCellRef.current = [r, c]; - } - const handleMouseLeave = () => { - currentCellRef.current = null; - } - const getHintAndShow = () => { if (!finished && grid) invoke('get_hint', { grid: grid.map((row) => row.map((cell) => cell.value)), - candidates: markingAssistRef.current ? maskedCandidates : markedCandidates + candidates: markingAssist ? maskedCandidates : markedCandidates }).then((hint) => { setUsedHint(true); setHint(hint[0]); @@ -313,7 +225,7 @@ export default function Start() { } const applyHintOption = () => { - if (hint) { + if (hint && grid) { let option = hint.option; if ("Direct" in option) { fillGrid(option.Direct[0], option.Direct[1], option.Direct[2]); @@ -336,7 +248,7 @@ export default function Start() { <> 难度
-

{timeMin}:{timeSec}

+

{timeStr}

{ !finished ? <> -

{rest}

+

{restBlankCount(grid)}

: <>

已完成!

- {/* { - usedAssist ? 已使用辅助 : <> - } */} - { - usedHint ? 已使用提示 : <> - } + {usedHint && 已使用提示}

@@ -385,8 +292,9 @@ export default function Start() { showingHint ? "opacity-100" : "opacity-0" )}> { - showingHint ? - (hint ? + showingHint && + ( + hint ?

@@ -414,8 +322,8 @@ export default function Start() {

: <> ) - ) - : <> + ) }
@@ -472,83 +380,16 @@ export default function Start() { }
- - - - - - -

{showingHint && hint ? "执行" : "提示"}

-
-
-
- - - - - - - -

撤销(Z)

-
-
-
- - - - - - - -

重做(X)

-
-
-
- - - - - - - -

换一道题

-
-
-
- - - - - - - -

清空

-
-
-
- - - - - - - -

返回首页

-
-
-
+ : } + desc={showingHint && hint ? "执行" : "提示"} + onClick={showingHint && hint ? applyHintOption : getHintAndShow} + /> + } desc={"撤销(Z)"} onClick={handleUndo}/> + } desc={"重做(X)"} onClick={handleRedo}/> + } desc={"换一道题"} onClick={newPuzzle}/> + } desc={"清空"} onClick={clear}/> + } desc={"返回首页"} onClick={() => router.replace("/")}/>
) diff --git a/components/SettingsProvider.js b/components/SettingsProvider.js new file mode 100644 index 0000000..4ff514a --- /dev/null +++ b/components/SettingsProvider.js @@ -0,0 +1,14 @@ +'use client' + +import { SettingsContext } from "@/lib/SettingsContext"; +import useSettings from "@/lib/useSettings"; + +export default function SettingsProvider({ children }) { + const settings = useSettings(); + + return ( + + {children} + + ); +} \ No newline at end of file diff --git a/lib/SettingsContext.js b/lib/SettingsContext.js new file mode 100644 index 0000000..b6b9b8b --- /dev/null +++ b/lib/SettingsContext.js @@ -0,0 +1,10 @@ +import { createContext } from "react"; + +export const SettingsContext = createContext({ + difficulty: 1, + markingAssist: true, + beginWithMarks: false, + setDifficulty: () => { }, + setMarkingAssist: () => { }, + setBeginWithMarks: () => { } +}); \ No newline at end of file diff --git a/lib/gridReducer.js b/lib/gridReducer.js index 5d033b8..ef78c29 100644 --- a/lib/gridReducer.js +++ b/lib/gridReducer.js @@ -1,17 +1,17 @@ export default function gridReducer(state, action) { - switch (action.type) { - case 'set': - return action.newGrid; - case 'fill': { - let { r, c, num, validity } = action; - state[r][c].value = num; - return state.map((row) => row.map((cell) => ({ - ...cell, - valid: validity, - }))); - } - - default: - break; + switch (action.type) { + case 'set': + return action.newGrid; + case 'fill': { + let { r, c, num, validCond } = action; + state[r][c].value = num; + return state.map((row, r) => row.map((cell, c) => ({ + ...cell, + valid: validCond[r][c] + }))); } + + default: + break; + } } \ No newline at end of file diff --git a/lib/useHistory.js b/lib/useHistory.js new file mode 100644 index 0000000..c81f38a --- /dev/null +++ b/lib/useHistory.js @@ -0,0 +1,46 @@ +import { useCallback, useRef } from "react"; + +export default function useHistory() { + const historyRef = useRef([]); + const futureRef = useRef([]); + + const clearHistory = useCallback(() => { + historyRef.current = []; + futureRef.current = []; + }, []); + + const pushHistory = useCallback((grid, markedCandidates) => { + historyRef.current.push({ + grid: grid.map((row) => row.map((cell) => ({ ...cell }))), + markedCandidates: markedCandidates.map((row) => row.map((cell) => cell.slice())) + }); + futureRef.current = []; + }, []); + + const undo = useCallback((grid, markedCandidates) => { + if (historyRef.current.length != 0) { + futureRef.current.push({ + grid: grid.map((row) => row.map((cell) => ({ ...cell }))), + markedCandidates: markedCandidates.map((row) => row.map((cell) => cell.slice())) + }); + return historyRef.current.pop(); + } + }, []); + + const redo = useCallback((grid, markedCandidates) => { + if (futureRef.current.length != 0) { + historyRef.current.push({ + grid: grid.map((row) => row.map((cell) => ({ ...cell }))), + markedCandidates: markedCandidates.map((row) => row.map((cell) => cell.slice())) + }); + return futureRef.current.pop(); + } + }, []); + + return { + clearHistory, + pushHistory, + undo, + redo + } +} \ No newline at end of file diff --git a/lib/useMaxCandidates.js b/lib/useMaxCandidates.js new file mode 100644 index 0000000..33fcf17 --- /dev/null +++ b/lib/useMaxCandidates.js @@ -0,0 +1,31 @@ +import { useCallback, useRef } from "react"; + +export function useMaxCandidates() { + const rowContainsRef = useRef(Array.from({ length: 9 }, (v) => Array.from({ length: 10 }, (v) => false))); + const colContainsRef = useRef(Array.from({ length: 9 }, (v) => Array.from({ length: 10 }, (v) => false))); + const blkContainsRef = useRef(Array.from({ length: 9 }, (v) => Array.from({ length: 10 }, (v) => false))); + + const getMaxCandidates = useCallback((grid) => { + rowContainsRef.current.forEach((row) => row.fill(false)); + colContainsRef.current.forEach((col) => col.fill(false)); + blkContainsRef.current.forEach((blk) => blk.fill(false)); + let rc2b = (r, c) => parseInt(r / 3) * 3 + parseInt(c / 3); + if (grid) { + grid.forEach((row, r) => { + row.forEach((cell, c) => { + let num = cell.value; + rowContainsRef.current[r][num] = true; + colContainsRef.current[c][num] = true; + blkContainsRef.current[rc2b(r, c)][num] = true; + }); + }); + return grid.map((row, r) => row.map((cell, c) => ([0, 1, 2, 3, 4, 5, 6, 7, 8, 9].map((num) => ( + grid[r][c].value == 0 + && !rowContainsRef.current[r][num] + && !colContainsRef.current[c][num] + && !blkContainsRef.current[rc2b(r, c)][num] + ))))); + } + }, []); + return getMaxCandidates; +}; \ No newline at end of file diff --git a/lib/useSettings.js b/lib/useSettings.js new file mode 100644 index 0000000..fd68c2e --- /dev/null +++ b/lib/useSettings.js @@ -0,0 +1,32 @@ +import { useEffect, useState } from "react"; +import { invoke } from "@tauri-apps/api/tauri"; + +export default function useSettings() { + const [difficulty, setDifficulty] = useState(2); + const [markingAssist, setMarkingAssist] = useState(false); + const [beginWithMarks, setBeginWithMarks] = useState(false); + + useEffect(() => { + invoke('get_difficulty').then((difficulty) => setDifficulty(difficulty)); + invoke('get_marking_assist').then((markingAssist) => setMarkingAssist(markingAssist)); + invoke('get_begin_with_marks').then((beginWithMarks) => setBeginWithMarks(beginWithMarks)); + }, []); + + return { + difficulty, + markingAssist, + beginWithMarks, + setDifficulty: (difficulty) => { + setDifficulty(difficulty); + invoke('set_difficulty', { newDifficulty: difficulty }); + }, + setMarkingAssist: (markingAssist) => { + setMarkingAssist(markingAssist); + invoke('set_marking_assist', { markingAssist }); + }, + setBeginWithMarks: (beginWithMarks) => { + setBeginWithMarks(beginWithMarks); + invoke('set_begin_with_marks', { beginWithMarks }); + } + } +} \ No newline at end of file diff --git a/lib/useTimer.js b/lib/useTimer.js new file mode 100644 index 0000000..43ad69f --- /dev/null +++ b/lib/useTimer.js @@ -0,0 +1,24 @@ +import { useCallback, useEffect, useState } from "react"; + +export default function useTimer(pause) { + const [time, setTime] = useState(0); + + useEffect(() => { + let updateTime = !pause ? + setInterval(() => { + setTime((prev) => prev + 1); + }, 1000) : null; + return () => { + if (updateTime) + clearInterval(updateTime); + }; + }, [pause]); + + const timeMin = parseInt(time / 60); + const timeSec = (() => { + let sec = time % 60; + return sec < 10 ? `0${sec}` : sec; + })(); + + return { timeStr: `${timeMin}:${timeSec}`, reset: useCallback(() => { setTime(0); }, []) }; +} \ No newline at end of file diff --git a/lib/utils.js b/lib/utils.js index c4d82ed..2762be1 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -12,4 +12,16 @@ export const difficultyDesc = [ "😖困难", "🤯地狱", "🎣钓鱼" -]; \ No newline at end of file +]; + +export function restBlankCount(grid) { + let res = 0; + if (grid) + grid.forEach(row => { + row.forEach(cell => { + if (cell.value == 0) + res += 1; + }); + }); + return res; +} \ No newline at end of file diff --git a/package.json b/package.json index d6b57d5..f065210 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "sudoxide", - "version": "0.3.2", + "version": "0.3.3", "private": true, "scripts": { "dev": "next dev", diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 550e9a9..20b23bc 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -8,7 +8,7 @@ }, "package": { "productName": "Sudoxide", - "version": "0.3.2" + "version": "0.3.3" }, "tauri": { "allowlist": { From 59d0c923d8f755136392f66de827a3f9fc904530 Mon Sep 17 00:00:00 2001 From: Nervonment Date: Wed, 19 Jun 2024 21:26:33 +0800 Subject: [PATCH 3/3] appconfig --- app/start/page.js | 34 ++++--- lib/useHistory.js | 4 +- lib/useSettings.js | 54 ++++++++--- src-tauri/Cargo.toml | 2 +- src-tauri/src/hint.rs | 195 +++++++++++++++++++++++++++++++++++++- src-tauri/src/main.rs | 38 +------- src-tauri/tauri.conf.json | 13 ++- 7 files changed, 267 insertions(+), 73 deletions(-) diff --git a/app/start/page.js b/app/start/page.js index 5ab69f8..a1d706d 100644 --- a/app/start/page.js +++ b/app/start/page.js @@ -35,7 +35,7 @@ function ToolBoxItem({ icon, desc, onClick }) { export default function Start() { const [grid, dispatchGrid] = useReducer(gridReducer, null); - + const currentCellRef = useRef(); const handleMouseEnter = (r, c) => { currentCellRef.current = [r, c]; @@ -130,17 +130,23 @@ export default function Start() { }; const handleUndo = useCallback(() => { - const { grid: prevGrid, markedCandidates: prevMarkedCandidates } = undo(grid, markedCandidates); - dispatchGrid({ type: 'set', newGrid: prevGrid }); - setMarkedCandidates(prevMarkedCandidates); - hideHint(); + const prev = undo(grid, markedCandidates); + if (prev) { + const { grid: prevGrid, markedCandidates: prevMarkedCandidates } = prev; + dispatchGrid({ type: 'set', newGrid: prevGrid }); + setMarkedCandidates(prevMarkedCandidates); + hideHint(); + } }, [undo, grid, markedCandidates]); const handleRedo = useCallback(() => { - const { grid: nextGrid, markedCandidates: nextMarkedCandidates } = redo(grid, markedCandidates); - dispatchGrid({ type: 'set', newGrid: nextGrid }); - setMarkedCandidates(nextMarkedCandidates); - hideHint(); + const next = redo(grid, markedCandidates); + if (next) { + const { grid: nextGrid, markedCandidates: nextMarkedCandidates } = next; + dispatchGrid({ type: 'set', newGrid: nextGrid }); + setMarkedCandidates(nextMarkedCandidates); + hideHint(); + } }, [redo, grid, markedCandidates]) const onKeyDown = useCallback((event) => { @@ -385,11 +391,11 @@ export default function Start() { desc={showingHint && hint ? "执行" : "提示"} onClick={showingHint && hint ? applyHintOption : getHintAndShow} /> - } desc={"撤销(Z)"} onClick={handleUndo}/> - } desc={"重做(X)"} onClick={handleRedo}/> - } desc={"换一道题"} onClick={newPuzzle}/> - } desc={"清空"} onClick={clear}/> - } desc={"返回首页"} onClick={() => router.replace("/")}/> + } desc={"撤销(Z)"} onClick={handleUndo} /> + } desc={"重做(X)"} onClick={handleRedo} /> + } desc={"换一道题"} onClick={newPuzzle} /> + } desc={"清空"} onClick={clear} /> + } desc={"返回首页"} onClick={() => router.replace("/")} /> ) diff --git a/lib/useHistory.js b/lib/useHistory.js index c81f38a..8c1cb08 100644 --- a/lib/useHistory.js +++ b/lib/useHistory.js @@ -18,7 +18,7 @@ export default function useHistory() { }, []); const undo = useCallback((grid, markedCandidates) => { - if (historyRef.current.length != 0) { + if (grid && markedCandidates && historyRef.current.length != 0) { futureRef.current.push({ grid: grid.map((row) => row.map((cell) => ({ ...cell }))), markedCandidates: markedCandidates.map((row) => row.map((cell) => cell.slice())) @@ -28,7 +28,7 @@ export default function useHistory() { }, []); const redo = useCallback((grid, markedCandidates) => { - if (futureRef.current.length != 0) { + if (grid && markedCandidates && futureRef.current.length != 0) { historyRef.current.push({ grid: grid.map((row) => row.map((cell) => ({ ...cell }))), markedCandidates: markedCandidates.map((row) => row.map((cell) => cell.slice())) diff --git a/lib/useSettings.js b/lib/useSettings.js index fd68c2e..a9c2bfb 100644 --- a/lib/useSettings.js +++ b/lib/useSettings.js @@ -1,32 +1,56 @@ import { useEffect, useState } from "react"; import { invoke } from "@tauri-apps/api/tauri"; +import { BaseDirectory, createDir, exists, readTextFile, writeTextFile } from "@tauri-apps/api/fs"; export default function useSettings() { - const [difficulty, setDifficulty] = useState(2); - const [markingAssist, setMarkingAssist] = useState(false); - const [beginWithMarks, setBeginWithMarks] = useState(false); + const [settings, setSettings] = useState({ + difficulty: 1, + markingAssist: true, + beginWithMarks: false + }); useEffect(() => { - invoke('get_difficulty').then((difficulty) => setDifficulty(difficulty)); - invoke('get_marking_assist').then((markingAssist) => setMarkingAssist(markingAssist)); - invoke('get_begin_with_marks').then((beginWithMarks) => setBeginWithMarks(beginWithMarks)); + exists('', { dir: BaseDirectory.AppConfig }) + .then((exist) => { + if (!exist) { + createDir('', { dir: BaseDirectory.AppConfig, recursive: true }); + } + else { + readTextFile('sudoxide_conf.json', { dir: BaseDirectory.AppConfig }) + .then((conf) => { + setSettings(JSON.parse(conf)); + }); + } + }); }, []); + const saveSettings = (settings) => { + writeTextFile('sudoxide_conf.json', JSON.stringify(settings), { dir: BaseDirectory.AppConfig }); + }; + return { - difficulty, - markingAssist, - beginWithMarks, + ...settings, setDifficulty: (difficulty) => { - setDifficulty(difficulty); - invoke('set_difficulty', { newDifficulty: difficulty }); + setSettings((prev) => { + const next = { ...prev, difficulty }; + saveSettings(next); + invoke('set_difficulty', { newDifficulty: difficulty }); + return next; + }) }, setMarkingAssist: (markingAssist) => { - setMarkingAssist(markingAssist); - invoke('set_marking_assist', { markingAssist }); + setSettings((prev) => { + const next = { ...prev, markingAssist }; + saveSettings(next); + return next; + }) }, setBeginWithMarks: (beginWithMarks) => { - setBeginWithMarks(beginWithMarks); - invoke('set_begin_with_marks', { beginWithMarks }); + setSettings((prev) => { + const next = { ...prev, beginWithMarks }; + saveSettings(next); + return next; + }) } } } \ No newline at end of file diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index de90064..2922e1e 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -17,7 +17,7 @@ tauri-build = { version = "1.5.2", features = [] } [dependencies] serde_json = "1.0" serde = { version = "1.0", features = ["derive"] } -tauri = { version = "1.6.5", features = ["updater"] } +tauri = { version = "1.6.5", features = [ "fs-create-dir", "fs-exists", "fs-read-file", "fs-write-file", "updater"] } sudoku = { git = "https://github.com/Nervonment/sudoku.git" } [features] diff --git a/src-tauri/src/hint.rs b/src-tauri/src/hint.rs index decbd24..d68226b 100644 --- a/src-tauri/src/hint.rs +++ b/src-tauri/src/hint.rs @@ -2,10 +2,10 @@ use serde::Serialize; use sudoku::state::full_state::FullState; use sudoku::techniques::{DirectOption, House, ReducingCandidatesOption, Technique}; -pub mod singles; -pub mod locked_candidates; pub mod hidden_subsets; +pub mod locked_candidates; pub mod naked_subsets; +pub mod singles; #[derive(Serialize)] #[serde(remote = "DirectOption")] @@ -81,3 +81,194 @@ fn house_to_string(house: House) -> String { sudoku::techniques::House::Block(idx) => format!("第{}宫", idx + 1), } } + +pub trait To { + fn text_default(&self) -> T; + fn house1(&self) -> T; + fn house2(&self) -> T; + fn cell1(&self) -> T; + fn num_to_fill(&self) -> T; + fn candidate_to_remove(&self) -> T; + fn candidate_to_reserve(&self) -> T; +} + +impl To for String { + fn text_default(&self) -> Segment { + Segment { + text: self.clone(), + color: Color::TextDefault, + } + } + fn house1(&self) -> Segment { + Segment { + text: self.clone(), + color: Color::House1, + } + } + fn house2(&self) -> Segment { + Segment { + text: self.clone(), + color: Color::House2, + } + } + fn cell1(&self) -> Segment { + Segment { + text: self.clone(), + color: Color::Cell1, + } + } + fn num_to_fill(&self) -> Segment { + Segment { + text: self.clone(), + color: Color::NumToFill, + } + } + fn candidate_to_remove(&self) -> Segment { + Segment { + text: self.clone(), + color: Color::CandidateToRemove, + } + } + fn candidate_to_reserve(&self) -> Segment { + Segment { + text: self.clone(), + color: Color::CandidateToReserve, + } + } +} + +impl To for House { + fn text_default(&self) -> Element { + Element { + kind: ElementType::House(self.clone()), + color: Color::TextDefault, + } + } + fn house1(&self) -> Element { + Element { + kind: ElementType::House(self.clone()), + color: Color::House1, + } + } + fn house2(&self) -> Element { + Element { + kind: ElementType::House(self.clone()), + color: Color::House2, + } + } + fn cell1(&self) -> Element { + Element { + kind: ElementType::House(self.clone()), + color: Color::Cell1, + } + } + fn num_to_fill(&self) -> Element { + Element { + kind: ElementType::House(self.clone()), + color: Color::NumToFill, + } + } + fn candidate_to_remove(&self) -> Element { + Element { + kind: ElementType::House(self.clone()), + color: Color::CandidateToRemove, + } + } + fn candidate_to_reserve(&self) -> Element { + Element { + kind: ElementType::House(self.clone()), + color: Color::CandidateToReserve, + } + } +} + +impl To for (usize, usize) { + fn text_default(&self) -> Element { + Element { + kind: ElementType::Cell(self.0, self.1), + color: Color::TextDefault, + } + } + fn house1(&self) -> Element { + Element { + kind: ElementType::Cell(self.0, self.1), + color: Color::House1, + } + } + fn house2(&self) -> Element { + Element { + kind: ElementType::Cell(self.0, self.1), + color: Color::House2, + } + } + fn cell1(&self) -> Element { + Element { + kind: ElementType::Cell(self.0, self.1), + color: Color::Cell1, + } + } + fn num_to_fill(&self) -> Element { + Element { + kind: ElementType::Cell(self.0, self.1), + color: Color::NumToFill, + } + } + fn candidate_to_remove(&self) -> Element { + Element { + kind: ElementType::Cell(self.0, self.1), + color: Color::CandidateToRemove, + } + } + fn candidate_to_reserve(&self) -> Element { + Element { + kind: ElementType::Cell(self.0, self.1), + color: Color::CandidateToReserve, + } + } +} + + +impl To for (usize, usize, i8) { + fn text_default(&self) -> Element { + Element { + kind: ElementType::Candidate(self.0, self.1, self.2), + color: Color::TextDefault, + } + } + fn house1(&self) -> Element { + Element { + kind: ElementType::Candidate(self.0, self.1, self.2), + color: Color::House1, + } + } + fn house2(&self) -> Element { + Element { + kind: ElementType::Candidate(self.0, self.1, self.2), + color: Color::House2, + } + } + fn cell1(&self) -> Element { + Element { + kind: ElementType::Candidate(self.0, self.1, self.2), + color: Color::Cell1, + } + } + fn num_to_fill(&self) -> Element { + Element { + kind: ElementType::Candidate(self.0, self.1, self.2), + color: Color::NumToFill, + } + } + fn candidate_to_remove(&self) -> Element { + Element { + kind: ElementType::Candidate(self.0, self.1, self.2), + color: Color::CandidateToRemove, + } + } + fn candidate_to_reserve(&self) -> Element { + Element { + kind: ElementType::Candidate(self.0, self.1, self.2), + color: Color::CandidateToReserve, + } + } +} \ No newline at end of file diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 904bfad..11af61a 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -29,11 +29,6 @@ fn main() { get_sudoku_puzzle, judge_sudoku, set_difficulty, - get_difficulty, - set_marking_assist, - get_marking_assist, - set_begin_with_marks, - get_begin_with_marks, get_hint ]) .run(tauri::generate_context!()) @@ -43,17 +38,11 @@ fn main() { #[derive(Serialize)] struct Settings { difficulty: u8, - marking_assist: bool, - begin_with_marks: bool, } impl Default for Settings { fn default() -> Self { - Self { - difficulty: 1, - marking_assist: true, - begin_with_marks: false, - } + Self { difficulty: 1 } } } @@ -89,31 +78,6 @@ fn set_difficulty(new_difficulty: u8, settings: State<'_, SettingsState>) { settings.0.lock().unwrap().difficulty = new_difficulty; } -#[tauri::command] -fn get_difficulty(settings: State<'_, SettingsState>) -> Result { - Ok(settings.0.lock().unwrap().difficulty) -} - -#[tauri::command] -fn set_marking_assist(marking_assist: bool, settings: State<'_, SettingsState>) { - settings.0.lock().unwrap().marking_assist = marking_assist; -} - -#[tauri::command] -fn get_marking_assist(settings: State<'_, SettingsState>) -> Result { - Ok(settings.0.lock().unwrap().marking_assist) -} - -#[tauri::command] -fn set_begin_with_marks(begin_with_marks: bool, settings: State<'_, SettingsState>) { - settings.0.lock().unwrap().begin_with_marks = begin_with_marks; -} - -#[tauri::command] -fn get_begin_with_marks(settings: State<'_, SettingsState>) -> Result { - Ok(settings.0.lock().unwrap().begin_with_marks) -} - #[derive(Serialize)] pub enum GetHintResult { Success, diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 20b23bc..501bd38 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -12,7 +12,16 @@ }, "tauri": { "allowlist": { - "all": false + "fs": { + "exists": true, + "createDir": true, + "readFile": true, + "scope": [ + "$APPCONFIG/*", + "$APPCONFIG" + ], + "writeFile": true + } }, "bundle": { "active": true, @@ -29,7 +38,7 @@ "icons/icon.icns", "icons/icon.ico" ], - "identifier": "com.tauri.build", + "identifier": "sudoxide", "longDescription": "", "macOS": { "entitlements": null,