diff --git a/.gitignore b/.gitignore index f56a4d91..b50038aa 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,6 @@ .DS_Store .idea tailwind-full.config.js -pnpm-lock.yaml \ No newline at end of file +pnpm-lock.yaml +package-lock.json +.README.analysis.md \ No newline at end of file diff --git a/README.md b/README.md deleted file mode 100644 index 974e07a2..00000000 --- a/README.md +++ /dev/null @@ -1,10 +0,0 @@ -# [sudoku](https://sudoku.jonasgeiler.com) - -This is a very simple sudoku game built with Svelte and TailwindCSS. - -Have fun! 😉 - -> [!WARNING] -> Unfortunately not all features are done yet. Specifically: -> - Undoing/redoing moves -> - Creating your own sudoku games \ No newline at end of file diff --git a/package.json b/package.json index 505e46f3..a51e9926 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,9 @@ "build": "rollup -c --environment NODE_ENV:production", "postbuild": "node scripts/postbuild.js", "predev": "rimraf dist", - "dev": "rollup -c -w", + "dev": "node scripts/dev.js", + "dev:watch": "rollup -c -w", + "dev:serve": "sirv dist --dev", "start": "sirv dist" }, "devDependencies": { @@ -17,7 +19,7 @@ "postcss": "8.4.31", "postcss-clean": "1.1.0", "postcss-import": "13.0.0", - "postcss-load-config": "3.0.0", + "postcss-load-config": "^2.1.2", "reaver": "2.0.0", "rimraf": "3.0.2", "rollup": "2.33.3", @@ -31,6 +33,10 @@ }, "dependencies": { "@mattflow/sudoku-solver": "2.2.0", + "axios": "^1.13.2", + "cheerio": "^1.1.0", + "cors": "^2.8.5", + "express": "^5.2.1", "fake-sudoku-puzzle-generator": "1.2.1", "sirv-cli": "1.0.0", "tailwindcss": "1.9.6" diff --git a/scripts/dev.js b/scripts/dev.js new file mode 100644 index 00000000..d4ed2d0e --- /dev/null +++ b/scripts/dev.js @@ -0,0 +1,68 @@ +const { spawn } = require('child_process'); +const scripts = ['dev:watch', 'dev:serve']; +const children = []; +let shuttingDown = false; + +function getSpawnCommand(script) { + const nodeExec = process.env.npm_node_execpath || process.execPath; + const npmCli = process.env.npm_execpath; + + if (npmCli) { + return { + command: nodeExec, + args: [npmCli, 'run', script], + }; + } + + const npmCommand = process.platform === 'win32' ? 'npm.cmd' : 'npm'; + return { + command: npmCommand, + args: ['run', script], + }; +} + +function start(script) { + const { command, args } = getSpawnCommand(script); + const child = spawn(command, args, { + stdio: 'inherit', + env: process.env, + shell: false, + }); + + children.push(child); + + child.on('exit', (code, signal) => { + if (!shuttingDown) { + const exitCode = typeof code === 'number' ? code : signal ? 1 : 0; + shutdown(exitCode); + } + }); + + child.on('error', (error) => { + console.error(`[dev] Failed to run ${script}:`, error); + if (!shuttingDown) { + shutdown(1); + } + }); +} + +function shutdown(code = 0) { + if (!shuttingDown) { + shuttingDown = true; + for (const child of children) { + if (!child.killed) { + child.kill(); + } + } + } + + // Allow child processes a brief moment to exit gracefully. + setTimeout(() => { + process.exit(code); + }, 100); +} + +process.on('SIGINT', () => shutdown(0)); +process.on('SIGTERM', () => shutdown(0)); + +scripts.forEach(start); diff --git a/src/components/Controls/ActionBar/Actions.svelte b/src/components/Controls/ActionBar/Actions.svelte index 2fdaee54..4aa6906e 100644 --- a/src/components/Controls/ActionBar/Actions.svelte +++ b/src/components/Controls/ActionBar/Actions.svelte @@ -7,6 +7,7 @@ import { settings } from '@sudoku/stores/settings'; import { keyboardDisabled } from '@sudoku/stores/keyboard'; import { gamePaused } from '@sudoku/stores/game'; + import { modal } from '@sudoku/stores/modal'; $: hintsAvailable = $hints > 0; @@ -17,6 +18,16 @@ } userGrid.applyHint($cursor); + }else{ + modal.show('nohints', { + title: 'No Hints Left', + text: 'You have used all available hints for this puzzle.', + button: 'Continue', + callback: () => { + // console.log('User acknowledged no hints'); + // 可以在这里添加其他逻辑 + } + }); } } @@ -34,8 +45,8 @@ - - + {/each} + +
+ + +
+ + {/if} + + + + + + {#if data.showSequence} +
+
Current Sequence:
+ {getCurrentSequence()} +
+ {/if} + + + + \ No newline at end of file diff --git a/src/components/Modal/Types/Nohints.svelte b/src/components/Modal/Types/Nohints.svelte new file mode 100644 index 00000000..f0505780 --- /dev/null +++ b/src/components/Modal/Types/Nohints.svelte @@ -0,0 +1,24 @@ + + +
+

{data.title || 'No Hints Available'}

+ + {#if data.text} +

{data.text}

+ {/if} + +
+ +
+
+ diff --git a/src/components/Modal/Types/Solver.svelte b/src/components/Modal/Types/Solver.svelte new file mode 100644 index 00000000..81d0a319 --- /dev/null +++ b/src/components/Modal/Types/Solver.svelte @@ -0,0 +1,398 @@ + + + + + + \ No newline at end of file diff --git a/src/components/Modal/Types/index.js b/src/components/Modal/Types/index.js index 54b8810a..5654a92d 100644 --- a/src/components/Modal/Types/index.js +++ b/src/components/Modal/Types/index.js @@ -5,6 +5,9 @@ import confirm from './Confirm.svelte'; import prompt from './Prompt.svelte'; import welcome from './Welcome.svelte'; import gameover from './GameOver.svelte'; +import nohints from './Nohints.svelte' +import custom from './Custom.svelte' +import solver from './Solver.svelte' export default { share, @@ -13,5 +16,8 @@ export default { confirm, prompt, welcome, - gameover + gameover, + nohints, + custom, + solver } \ No newline at end of file diff --git a/src/main.js b/src/main.js index 3b5b5314..09aed200 100644 --- a/src/main.js +++ b/src/main.js @@ -7,9 +7,10 @@ const app = new App({ export default app; -// TODO: Warn when hint not possible + + // TODO: Undo/Redo // TODO: Import sudoku // TODO: Creator mode // TODO: Bug hunt -// TODO: Announce \ No newline at end of file +// TODO: Announce diff --git a/src/node_modules/@sudoku/game.js b/src/node_modules/@sudoku/game.js index 16d1226e..73ceb331 100644 --- a/src/node_modules/@sudoku/game.js +++ b/src/node_modules/@sudoku/game.js @@ -4,7 +4,6 @@ import { gamePaused } from './stores/game'; import { grid } from './stores/grid'; import { timer } from './stores/timer'; import { hints } from './stores/hints'; - /** * Start new game with a generated sudoku * @@ -20,6 +19,17 @@ export function startNew(diff) { location.hash = ''; } +export function createNew(data){ + difficulty.set(data.difficulty) + grid.updateFromSequence(data.sequence) + cursor.reset(); + timer.reset(); + hints.reset(); + + location.hash = ''; +} + + /** * Start new game with a custom sudoku * @@ -53,5 +63,6 @@ export default { startNew, startCustom, pause: pauseGame, - resume: resumeGame + resume: resumeGame, + createNew }; \ No newline at end of file diff --git a/src/node_modules/@sudoku/stores/grid.js b/src/node_modules/@sudoku/stores/grid.js index f803807b..933766ba 100644 --- a/src/node_modules/@sudoku/stores/grid.js +++ b/src/node_modules/@sudoku/stores/grid.js @@ -35,6 +35,30 @@ function createGrid() { getSencode(gridStore) { return encodeSudoku(gridStore); }, + + // 新增:直接从 sequence 更新 grid + updateFromSequence: (sequence) => { + grid.update(() => { + + const newGrid = []; + + for (let y = 0; y < SUDOKU_SIZE; y++) { + newGrid[y] = []; + for (let x = 0; x < SUDOKU_SIZE; x++) { + const cellIndex = y * SUDOKU_SIZE + x; + const sequenceChar = sequence.charAt(cellIndex); + + if (sequenceChar && sequenceChar !== '0' && sequenceChar !== '.') { + newGrid[y][x] = parseInt(sequenceChar, 10); + } else { + newGrid[y][x] = 0; + } + } + } + + return newGrid; + }); + } }; } @@ -85,6 +109,7 @@ function createUserGrid() { return $userGrid; }); }, + }; } diff --git a/src/node_modules/@sudoku/stores/solver.js b/src/node_modules/@sudoku/stores/solver.js new file mode 100644 index 00000000..fa867997 --- /dev/null +++ b/src/node_modules/@sudoku/stores/solver.js @@ -0,0 +1,117 @@ +class SudokuParser { + constructor() { + this.baseUrl = 'https://www.sudokuwiki.org/sudoku.htm'; + } + + /** + * 解析URL中的bd参数,获取数独字符串 + * @param {string} url - 完整URL + * @returns {string|null} 81位数独字符串 + */ + parseUrl(url) { + try { + const urlObj = new URL(url); + const bdParam = urlObj.searchParams.get('bd'); + + if (!bdParam) { + throw new Error('URL中未找到bd参数'); + } + + return this.parseBdParameter(bdParam); + } catch (error) { + console.error('URL解析失败:', error); + return null; + } + } + + /** + * 解析bd参数为81位字符串 + * @param {string} bdParam - bd参数值 + * @returns {string} 81位字符串,空白用0表示 + */ + parseBdParameter(bdParam) { + // 检查参数长度 + if (bdParam.length !== 81) { + console.warn(`bd参数长度异常: ${bdParam.length},尝试清理...`); + } + + // 清理非数字字符,并将0-9以外的字符转换为0 + let cleaned = ''; + for (let char of bdParam) { + if (char >= '0' && char <= '9') { + cleaned += char; + } else { + cleaned += '0'; // 非数字字符视为空白 + } + } + + // 如果长度不足81,用0补齐 + if (cleaned.length < 81) { + cleaned = cleaned.padEnd(81, '0'); + } + + // 如果长度超过81,截取前81位 + if (cleaned.length > 81) { + cleaned = cleaned.substring(0, 81); + } + + return cleaned; + } + + /** + * 验证数独字符串 + * @param {string} sudokuString + * @returns {boolean} + */ + validate(sudokuString) { + if (!sudokuString || typeof sudokuString !== 'string') { + return false; + } + + if (sudokuString.length !== 81) { + return false; + } + + // 检查是否只包含0-9 + return /^[0-9]{81}$/.test(sudokuString); + } + + /** + * 格式化显示数独 + * @param {string} sudokuString + * @returns {string} + */ + format(sudokuString) { + if (!this.validate(sudokuString)) { + return '无效的数独字符串'; + } + + let formatted = ''; + for (let i = 0; i < 9; i++) { + const row = sudokuString.substr(i * 9, 9); + const displayRow = row.replace(/0/g, '.'); + + // 每3个数字加空格 + const parts = []; + for (let j = 0; j < 9; j += 3) { + parts.push(displayRow.substr(j, 3)); + } + formatted += parts.join(' '); + + // 添加行分隔符 + if (i < 8) { + formatted += '\n'; + if (i === 2 || i === 5) { + formatted += '------+-------+------\n'; + } + } + } + return formatted; + } + + +} + + + +export default SudokuParser \ No newline at end of file diff --git a/src/styles/global.css b/src/styles/global.css index 817cbc42..7c28e211 100644 --- a/src/styles/global.css +++ b/src/styles/global.css @@ -79,4 +79,168 @@ body { @apply outline-none shadow-outline; } +.no-prompt-modal { + padding: 0; + min-width: 300px; +} + +.button-group { + display: flex; + justify-content: flex-end; + gap: 12px; + padding: 20px; +} + +.btn-primary, .btn-secondary { + padding: 10px 24px; + border-radius: 6px; + font-size: 14px; + font-weight: 500; + cursor: pointer; + border: none; + transition: all 0.2s ease; + min-width: 80px; +} + +.btn-primary { + background-color: #3b82f6; + color: white; +} + +.btn-primary:hover { + background-color: #2563eb; + transform: translateY(-1px); +} + +.btn-primary:active { + transform: translateY(0); +} + +.btn-secondary { + background-color: #f1f5f9; + color: #64748b; +} + +.btn-secondary:hover { + background-color: #e2e8f0; + color: #475569; +} + +.no-hints-modal { + min-width: 320px; + max-width: 400px; + padding: 24px; + text-align: center; +} + +.title { + font-size: 24px; + font-weight: 600; + margin-bottom: 16px; + color: #dc2626; /* 红色表示警告 */ +} + +.message { + font-size: 16px; + line-height: 1.5; + color: #4b5563; + margin-bottom: 24px; + padding: 0 8px; +} + +.button-group { + display: flex; + justify-content: center; + gap: 12px; +} + +.btn { + padding: 10px 32px; + border-radius: 8px; + font-size: 16px; + font-weight: 500; + cursor: pointer; + border: none; + transition: all 0.2s ease; + min-width: 120px; +} + +.btn-primary { + background-color: #dc2626; + color: white; +} + +.btn-primary:hover { + background-color: #b91c1c; + transform: translateY(-1px); + box-shadow: 0 4px 12px rgba(220, 38, 38, 0.2); +} + +.btn-primary:active { + transform: translateY(0); +} + + +.modal-overlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: rgba(0, 0, 0, 0.5); + display: flex; + align-items: center; + justify-content: center; + z-index: 1000; + padding: 20px; +} + +.modal-content { + background: white; + border-radius: 12px; + padding: 30px; + max-width: 500px; + width: 100%; + max-height: 90vh; + overflow-y: auto; + box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3); +} + +.btn { + padding: 8px 16px; + border-radius: 6px; + font-weight: 500; + cursor: pointer; + border: none; + transition: all 0.2s; +} + +.btn-small { + padding: 6px 12px; + font-size: 14px; +} + +.btn-primary { + background-color: #3b82f6; + color: white; +} + +.btn-primary:hover:not(:disabled) { + background-color: #2563eb; +} + +.btn-primary:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +.btn:not(.btn-primary) { + background-color: #f3f4f6; + color: #374151; +} + +.btn:not(.btn-primary):hover { + background-color: #e5e7eb; +} + @tailwind utilities;