ready for publish
This commit is contained in:
@@ -5,6 +5,10 @@ bower_components/
|
|||||||
# Build outputs
|
# Build outputs
|
||||||
dist/
|
dist/
|
||||||
|
|
||||||
|
# npm package tarballs
|
||||||
|
*.tgz
|
||||||
|
vimazing-vim-sudoku-*.tgz
|
||||||
|
|
||||||
# TypeScript
|
# TypeScript
|
||||||
*.tsbuildinfo
|
*.tsbuildinfo
|
||||||
|
|
||||||
|
|||||||
+7
-1
@@ -1,6 +1,12 @@
|
|||||||
src/
|
src/
|
||||||
|
example/
|
||||||
node_modules/
|
node_modules/
|
||||||
tsconfig.json
|
tsconfig.json
|
||||||
bun.lockb
|
bun.lockb
|
||||||
*.log
|
*.log
|
||||||
|
.git
|
||||||
|
.gitignore
|
||||||
|
NPM_PUBLISHING_CHECKLIST.md
|
||||||
|
vim_sudoku_implementation.md
|
||||||
|
vim_sudoku_game_mechanics.md
|
||||||
|
vimazing-vim-sudoku-*.tgz
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
# Changelog
|
||||||
|
|
||||||
|
All notable changes to @vimazing/vim-sudoku will be documented in this file.
|
||||||
|
|
||||||
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||||
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## [0.1.0] - 2025-01-15
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Initial release of @vimazing/vim-sudoku
|
||||||
|
- VIM-style modal editing with navigation and edit modes
|
||||||
|
- Full VIM motions (h/j/k/l, counted moves, ^/$/0, repeat with .)
|
||||||
|
- Smart cell locking for pre-filled clues
|
||||||
|
- Real-time validation system (optional, disabled by default)
|
||||||
|
- Automatic win detection using sum-to-45 algorithm
|
||||||
|
- Time-based scoring with keystroke penalties
|
||||||
|
- Tokyo Night themed CSS with 3x3 box separators
|
||||||
|
- Configurable size multiplier via CSS variables
|
||||||
|
- Complete TypeScript support with type declarations
|
||||||
|
- Composable hook architecture (useGame, useScore, useBoardState, etc.)
|
||||||
|
- Tree-shakeable exports
|
||||||
|
|
||||||
|
### Features
|
||||||
|
- `useGame(platformHook?, options?)` - Main game orchestration hook
|
||||||
|
- `validateMoves` option for real-time move validation (default: false)
|
||||||
|
- Automatic win detection when board is complete and valid
|
||||||
|
- Scoring formula: `max(0, 100000 - timeInSeconds × 10 - extraMoves × 50)`
|
||||||
|
- VIM modal editing: navigation mode (default) and edit mode (R/C/I to enter)
|
||||||
|
- Visual feedback for all game states (navigation glow, edit mode pulse, locked cells)
|
||||||
|
- Support for counted motions (3j, 5l, etc.)
|
||||||
|
- Row navigation shortcuts (^, $, 0)
|
||||||
|
- Repeat last motion with `.`
|
||||||
|
|
||||||
|
### Documentation
|
||||||
|
- Comprehensive README with usage examples
|
||||||
|
- VIM modal editing guide
|
||||||
|
- Cell state rules reference
|
||||||
|
- API documentation for all hooks
|
||||||
|
- Build and release instructions
|
||||||
|
|
||||||
|
[0.1.0]: https://github.com/vimazing/vim-sudoku/releases/tag/v0.1.0
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2025 André Padez
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
# 📋 npm Publishing Checklist for @vimazing/vim-sudoku
|
||||||
|
|
||||||
|
## 🔴 Critical (Must Fix Before Publishing)
|
||||||
|
|
||||||
|
- [ ] **Fix TypeScript build errors** - Package won't compile
|
||||||
|
- [ ] `Sudoku.ts`: operator type issues (lines 77, 93, 95)
|
||||||
|
- [ ] `useBoardState.ts`: remove unused `useCallback`, `useMemo` imports
|
||||||
|
- [ ] `types.ts`: remove non-existent `BoardString` export
|
||||||
|
- [ ] Remove unused variables: `idx`, `max_square`
|
||||||
|
|
||||||
|
- [ ] **Add LICENSE file** - Required for MIT license in package.json
|
||||||
|
|
||||||
|
- [ ] **Update package.json author** - Currently `you@example.com`
|
||||||
|
|
||||||
|
## 🟡 Important (Should Fix)
|
||||||
|
|
||||||
|
- [ ] **Update README.md**
|
||||||
|
- [ ] Remove "coming soon" for win detection (✅ implemented!)
|
||||||
|
- [ ] Document new `validateMoves` option in `useGame`
|
||||||
|
- [ ] Update scoring formula to include move penalty
|
||||||
|
|
||||||
|
- [ ] **Fix tsconfig.json** - Conflicting `declaration` and `emitDeclarationOnly` flags
|
||||||
|
|
||||||
|
- [ ] **Verify exports** - Check `src/index.ts` exports everything needed
|
||||||
|
|
||||||
|
- [ ] **Add CHANGELOG.md** - Document version history and changes
|
||||||
|
|
||||||
|
- [ ] **Add package.json keywords** - Improve npm discoverability (sudoku, vim, react, game, puzzle)
|
||||||
|
|
||||||
|
- [ ] **Update .npmignore** - Verify `example/` is excluded
|
||||||
|
|
||||||
|
## 🟢 Nice to Have
|
||||||
|
|
||||||
|
- [ ] **Test locally** - Run `npm pack` and inspect tarball contents
|
||||||
|
|
||||||
|
- [ ] **Add npm badges** - Version and downloads badges in README
|
||||||
|
|
||||||
|
- [ ] **Add engines field** - Specify Node.js version requirements
|
||||||
|
|
||||||
|
- [ ] **GitHub topics** - Add repository tags for discoverability
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Total: 18 tasks** (6 critical, 6 important, 6 nice-to-have)
|
||||||
|
|
||||||
|
**Status:** Ready to start working on critical issues 🚀
|
||||||
@@ -1,5 +1,11 @@
|
|||||||
# @vimazing/vim-sudoku
|
# @vimazing/vim-sudoku
|
||||||
|
|
||||||
|
[](https://www.npmjs.com/package/@vimazing/vim-sudoku)
|
||||||
|
[](https://www.npmjs.com/package/@vimazing/vim-sudoku)
|
||||||
|
[](https://github.com/vimazing/vim-sudoku/blob/main/LICENSE)
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
Lightweight, typed **React hooks** and utilities for building interactive sudoku games with **VIM-style modal editing**.
|
Lightweight, typed **React hooks** and utilities for building interactive sudoku games with **VIM-style modal editing**.
|
||||||
|
|
||||||
Part of the [VIMazing](https://github.com/andrepadez/vimazing-vimaze) project.
|
Part of the [VIMazing](https://github.com/andrepadez/vimazing-vimaze) project.
|
||||||
@@ -146,9 +152,14 @@ Jump to end: Press $
|
|||||||
|
|
||||||
## Core Hooks
|
## Core Hooks
|
||||||
|
|
||||||
### `useGame(platformHook?)`
|
### `useGame(platformHook?, options?)`
|
||||||
|
|
||||||
Main hook that orchestrates the entire sudoku game. Returns:
|
Main hook that orchestrates the entire sudoku game.
|
||||||
|
|
||||||
|
**Options:**
|
||||||
|
- `validateMoves?: boolean` (default: `false`) - Enable real-time move validation with conflict detection. When `false`, allows harder gameplay without immediate feedback.
|
||||||
|
|
||||||
|
**Returns:**
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
{
|
{
|
||||||
@@ -201,11 +212,17 @@ import { useBoardState } from "@vimazing/vim-sudoku/useGame/useBoardState";
|
|||||||
|
|
||||||
### Scoring
|
### Scoring
|
||||||
|
|
||||||
Time-based scoring formula:
|
Scoring formula with time and move penalties:
|
||||||
```
|
```
|
||||||
score = max(0, 100000 - timeInSeconds × 10)
|
score = max(0, 100000 - timeInSeconds × 10 - extraMoves × 50)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Where:
|
||||||
|
- `baseScore = 100000`
|
||||||
|
- `timePenalty = timeInSeconds × 10`
|
||||||
|
- `extraMoves = actualKeystrokes - minMovesRequired`
|
||||||
|
- `movePenalty = extraMoves × 50`
|
||||||
|
|
||||||
### Game Over Conditions
|
### Game Over Conditions
|
||||||
|
|
||||||
- ⏱️ **Time limit**: 60 seconds
|
- ⏱️ **Time limit**: 60 seconds
|
||||||
@@ -219,7 +236,13 @@ const KEYSTROKE_LIMIT = 100;
|
|||||||
|
|
||||||
### Win Detection
|
### Win Detection
|
||||||
|
|
||||||
Currently manual (solving via UI or `solveBoard()`). Automatic win detection coming soon!
|
Automatic win detection is fully implemented using an efficient sum-to-45 algorithm:
|
||||||
|
- Validates when board is complete (no empty cells)
|
||||||
|
- Checks all rows sum to 45 (1+2+...+9)
|
||||||
|
- Checks all columns sum to 45
|
||||||
|
- Checks all 3x3 boxes sum to 45
|
||||||
|
|
||||||
|
Manual win trigger available via `solveBoard()` for testing.
|
||||||
|
|
||||||
### Board Format
|
### Board Format
|
||||||
|
|
||||||
|
|||||||
@@ -25,21 +25,24 @@ export const GameOverlay = ({ gameStatus, scoreManager }: GameOverlayProps) => {
|
|||||||
export const GameWaitingContent = () => {
|
export const GameWaitingContent = () => {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="text-[#c8d3f5] text-[2em] font-mono text-center select-none"
|
className="text-[#c8d3f5] font-mono text-center select-none"
|
||||||
style={{ textShadow: "2px 2px 4px rgba(27, 29, 43, 0.9)" }}
|
style={{
|
||||||
|
textShadow: "2px 2px 4px rgba(27, 29, 43, 0.9)",
|
||||||
|
fontSize: "calc(2rem * var(--sudoku-size-multiplier))"
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<div className="flex flex-col gap-10">
|
<div className="flex flex-col" style={{ gap: "calc(2.5rem * var(--sudoku-size-multiplier))" }}>
|
||||||
<p className="text-[3rem]">Press <strong className="text-[#82aaff]">New Puzzle</strong> to start</p>
|
<p style={{ fontSize: "calc(3rem * var(--sudoku-size-multiplier))" }}>Press <strong className="text-[#82aaff]">New Puzzle</strong> to start</p>
|
||||||
<div className="text-[1.2em]">
|
<div style={{ fontSize: "calc(1.2rem * var(--sudoku-size-multiplier))" }}>
|
||||||
<p className="mb-4"><strong className="text-[#ffc777]">Navigation Mode</strong> (default):</p>
|
<p style={{ marginBottom: "calc(1rem * var(--sudoku-size-multiplier))" }}><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><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 style={{ marginTop: "calc(0.5rem * var(--sudoku-size-multiplier))" }}><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 style={{ marginTop: "calc(1.5rem * var(--sudoku-size-multiplier))", marginBottom: "calc(1rem * var(--sudoku-size-multiplier))" }}><strong className="text-[#ffc777]">Edit Mode</strong>:</p>
|
||||||
<p>Type <strong className="text-[#82aaff]">1-9</strong> to fill cell</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 style={{ marginTop: "calc(0.5rem * var(--sudoku-size-multiplier))" }}><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]">
|
<p className="text-[#828bb8]" style={{ marginTop: "calc(1.5rem * var(--sudoku-size-multiplier))", fontSize: "calc(0.8rem * var(--sudoku-size-multiplier))" }}>
|
||||||
Try vim motions: <strong className="text-[#82aaff]">$</strong>, <strong className="text-[#82aaff]">^</strong>, <strong className="text-[#82aaff]">0</strong>, counted moves
|
Try vim motions: <strong className="text-[#82aaff]">$</strong>, <strong className="text-[#82aaff]">^</strong>, <strong className="text-[#82aaff]">0</strong>, counted moves
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -53,13 +56,17 @@ export const GameWonContent = ({ scoreManager }: { scoreManager: UseScoreType })
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="text-[#c8d3f5] text-[2em] grid gap-4 font-mono uppercase text-center select-none"
|
className="text-[#c8d3f5] grid font-mono uppercase text-center select-none"
|
||||||
style={{ textShadow: "2px 2px 4px rgba(27, 29, 43, 0.9)" }}
|
style={{
|
||||||
|
textShadow: "2px 2px 4px rgba(27, 29, 43, 0.9)",
|
||||||
|
fontSize: "calc(2rem * var(--sudoku-size-multiplier))",
|
||||||
|
gap: "calc(1rem * var(--sudoku-size-multiplier))"
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<p className="text-[#c3e88d]">🎉 Puzzle Solved!</p>
|
<p className="text-[#c3e88d]">🎉 Puzzle Solved!</p>
|
||||||
<p>Final Score:</p>
|
<p>Final Score:</p>
|
||||||
<p className="text-[#82aaff] text-[2.5em]">{finalScore.toLocaleString()}</p>
|
<p className="text-[#82aaff]" style={{ fontSize: "calc(2.5rem * var(--sudoku-size-multiplier))" }}>{finalScore.toLocaleString()}</p>
|
||||||
<p className="text-[#828bb8] text-[0.65em] normal-case">
|
<p className="text-[#828bb8] normal-case" style={{ fontSize: "calc(0.65rem * var(--sudoku-size-multiplier))" }}>
|
||||||
<span className="text-[#c099ff]">max</span>
|
<span className="text-[#c099ff]">max</span>
|
||||||
<span className="text-[#c8d3f5]">(</span>
|
<span className="text-[#c8d3f5]">(</span>
|
||||||
<span className="text-[#ff966c]">0</span>
|
<span className="text-[#ff966c]">0</span>
|
||||||
@@ -82,8 +89,8 @@ export const GameOverContent = () => {
|
|||||||
className="text-[#ff757f] font-mono uppercase text-center select-none"
|
className="text-[#ff757f] font-mono uppercase text-center select-none"
|
||||||
style={{ textShadow: "2px 2px 4px rgba(27, 29, 43, 0.9)" }}
|
style={{ textShadow: "2px 2px 4px rgba(27, 29, 43, 0.9)" }}
|
||||||
>
|
>
|
||||||
<p className="text-[5em]">Game Over</p>
|
<p style={{ fontSize: "calc(5rem * var(--sudoku-size-multiplier))" }}>Game Over</p>
|
||||||
<p className="text-[2em] text-[#828bb8]">Press <strong className="text-[#ffc777] normal-case">i</strong> to play again</p>
|
<p className="text-[#828bb8]" style={{ fontSize: "calc(2rem * var(--sudoku-size-multiplier))" }}>Press <strong className="text-[#ffc777] normal-case">i</strong> to play again</p>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -20,11 +20,14 @@ export const MobileGameOverlay = ({ gameStatus, scoreManager }: MobileGameOverla
|
|||||||
export const GameWaitingContent = () => {
|
export const GameWaitingContent = () => {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="text-[#c8d3f5] text-[1em] font-mono text-center select-none"
|
className="text-[#c8d3f5] font-mono text-center select-none"
|
||||||
style={{ textShadow: "2px 2px 4px rgba(27, 29, 43, 0.9)" }}
|
style={{
|
||||||
|
textShadow: "2px 2px 4px rgba(27, 29, 43, 0.9)",
|
||||||
|
fontSize: "calc(1rem * var(--sudoku-size-multiplier))"
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<div className="flex flex-col gap-3">
|
<div className="flex flex-col" style={{ gap: "calc(0.75rem * var(--sudoku-size-multiplier))" }}>
|
||||||
<p className="text-[1.5rem]">Tap New Puzzle to start</p>
|
<p style={{ fontSize: "calc(1.5rem * var(--sudoku-size-multiplier))" }}>Tap New Puzzle to start</p>
|
||||||
<p>VIM motions available <br /> via on-screen keyboard</p>
|
<p>VIM motions available <br /> via on-screen keyboard</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -36,13 +39,17 @@ export const GameWonContent = ({ scoreManager }: { scoreManager: UseScoreType })
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="text-[#c8d3f5] text-[1em] grid gap-4 font-mono uppercase text-center select-none"
|
className="text-[#c8d3f5] grid font-mono uppercase text-center select-none"
|
||||||
style={{ textShadow: "2px 2px 4px rgba(27, 29, 43, 0.9)" }}
|
style={{
|
||||||
|
textShadow: "2px 2px 4px rgba(27, 29, 43, 0.9)",
|
||||||
|
fontSize: "calc(1rem * var(--sudoku-size-multiplier))",
|
||||||
|
gap: "calc(1rem * var(--sudoku-size-multiplier))"
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<p className="text-[#c3e88d]">🎉 Solved!</p>
|
<p className="text-[#c3e88d]">🎉 Solved!</p>
|
||||||
<p>Final Score:</p>
|
<p>Final Score:</p>
|
||||||
<p className="text-[#82aaff] text-[2.5em]">{finalScore.toLocaleString()}</p>
|
<p className="text-[#82aaff]" style={{ fontSize: "calc(2.5rem * var(--sudoku-size-multiplier))" }}>{finalScore.toLocaleString()}</p>
|
||||||
<p className="text-[#828bb8] text-[0.65em] normal-case">
|
<p className="text-[#828bb8] normal-case" style={{ fontSize: "calc(0.65rem * var(--sudoku-size-multiplier))" }}>
|
||||||
<span className="text-[#c099ff]">max</span>
|
<span className="text-[#c099ff]">max</span>
|
||||||
<span className="text-[#c8d3f5]">(</span>
|
<span className="text-[#c8d3f5]">(</span>
|
||||||
<span className="text-[#ff966c]">0</span>
|
<span className="text-[#ff966c]">0</span>
|
||||||
@@ -62,8 +69,11 @@ export const GameWonContent = ({ scoreManager }: { scoreManager: UseScoreType })
|
|||||||
export const GameOverContent = () => {
|
export const GameOverContent = () => {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="text-[#ff757f] text-[5em] font-mono uppercase text-center select-none"
|
className="text-[#ff757f] font-mono uppercase text-center select-none"
|
||||||
style={{ textShadow: "2px 2px 4px rgba(27, 29, 43, 0.9)" }}
|
style={{
|
||||||
|
textShadow: "2px 2px 4px rgba(27, 29, 43, 0.9)",
|
||||||
|
fontSize: "calc(5rem * var(--sudoku-size-multiplier))"
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
Game Over
|
Game Over
|
||||||
</div>
|
</div>
|
||||||
@@ -1 +1,5 @@
|
|||||||
@import "tailwindcss";
|
@import "tailwindcss";
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--sudoku-size-multiplier: 3;
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import type { Config } from 'tailwindcss'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
content: [
|
||||||
|
'./src/**/*.{js,ts,jsx,tsx}',
|
||||||
|
'../src/**/*.{js,ts,jsx,tsx}',
|
||||||
|
],
|
||||||
|
} satisfies Config
|
||||||
+15
-1
@@ -2,7 +2,7 @@
|
|||||||
"name": "@vimazing/vim-sudoku",
|
"name": "@vimazing/vim-sudoku",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"description": "VIMazing VIM Sudoku engine — a lightweight, typed React hook set for sudoku games.",
|
"description": "VIMazing VIM Sudoku engine — a lightweight, typed React hook set for sudoku games.",
|
||||||
"author": "André Padez <you@example.com>",
|
"author": "André Padez <andre.padez@gmail.com>",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/vimazing/vim-sudoku.git"
|
"url": "https://github.com/vimazing/vim-sudoku.git"
|
||||||
@@ -12,7 +12,21 @@
|
|||||||
},
|
},
|
||||||
"homepage": "https://github.com/vimazing/vim-sudoku#readme",
|
"homepage": "https://github.com/vimazing/vim-sudoku#readme",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"keywords": [
|
||||||
|
"sudoku",
|
||||||
|
"vim",
|
||||||
|
"react",
|
||||||
|
"hooks",
|
||||||
|
"game",
|
||||||
|
"puzzle",
|
||||||
|
"modal-editing",
|
||||||
|
"typescript",
|
||||||
|
"vimazing"
|
||||||
|
],
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18.0.0"
|
||||||
|
},
|
||||||
"main": "./dist/index.js",
|
"main": "./dist/index.js",
|
||||||
"types": "./dist/index.d.ts",
|
"types": "./dist/index.d.ts",
|
||||||
"exports": {
|
"exports": {
|
||||||
|
|||||||
+9
-8
@@ -51,11 +51,14 @@ function initialize() {
|
|||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
sudoku.generate = function (difficulty: Difficulty, unique?: boolean): string {
|
sudoku.generate = function (difficulty: Difficulty, unique?: boolean): string {
|
||||||
|
let numericDifficulty: number;
|
||||||
if (typeof difficulty === "string" || typeof difficulty === "undefined") {
|
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;
|
unique = unique ?? true;
|
||||||
|
|
||||||
const blank_board = sudoku.BLANK_BOARD;
|
const blank_board = sudoku.BLANK_BOARD;
|
||||||
@@ -74,7 +77,7 @@ sudoku.generate = function (difficulty: Difficulty, unique?: boolean): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
single_candidates.length >= difficulty &&
|
single_candidates.length >= numericDifficulty &&
|
||||||
sudoku._strip_dups(single_candidates).length >= 8
|
sudoku._strip_dups(single_candidates).length >= 8
|
||||||
) {
|
) {
|
||||||
let board = "";
|
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);
|
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];
|
const target = givens_idxs[i];
|
||||||
board =
|
board =
|
||||||
board.substring(0, target) +
|
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;
|
reverse = reverse || false;
|
||||||
|
|
||||||
let max_nr_candidates = 0;
|
let max_nr_candidates = 0;
|
||||||
let max_square = "";
|
|
||||||
for (const s of SQUARES) {
|
for (const s of SQUARES) {
|
||||||
const n = candidates[s].length;
|
const n = candidates[s].length;
|
||||||
if (n > max_nr_candidates) {
|
if (n > max_nr_candidates) {
|
||||||
max_nr_candidates = n;
|
max_nr_candidates = n;
|
||||||
max_square = s;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (max_nr_candidates === 1) return candidates;
|
if (max_nr_candidates === 1) return candidates;
|
||||||
|
|||||||
+5
-19
@@ -1,28 +1,14 @@
|
|||||||
/* =========================================================
|
/* =========================================================
|
||||||
🔧 Controls (tweak these)
|
🔧 Controls (tweak these)
|
||||||
========================================================= */
|
========================================================= */
|
||||||
/* :root { */
|
|
||||||
/* --cell-size: 32px; */
|
|
||||||
/* --cell-font-size: 16px; */
|
|
||||||
/* --grid-gap: 2px; */
|
|
||||||
/* --box-separator: 3px; */
|
|
||||||
/* } */
|
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--cell-size: 48px;
|
--sudoku-size-multiplier: 3;
|
||||||
--cell-font-size: 32px;
|
--cell-size: calc(32px * var(--sudoku-size-multiplier));
|
||||||
--grid-gap: 4px;
|
--cell-font-size: calc(16px * var(--sudoku-size-multiplier));
|
||||||
--box-separator: 6px;
|
--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
|
Container & Grid Base
|
||||||
========================================================= */
|
========================================================= */
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ export type UseScoreType = ReturnType<typeof useScore>;
|
|||||||
export type {
|
export type {
|
||||||
Difficulty,
|
Difficulty,
|
||||||
DifficultyName,
|
DifficultyName,
|
||||||
BoardString,
|
|
||||||
} from "./lib/Sudoku";
|
} from "./lib/Sudoku";
|
||||||
|
|
||||||
export type GamePhase = "idle" | "playing";
|
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 { sudoku } from "../lib/Sudoku";
|
||||||
import type { UseCursorType } from "./useCursor";
|
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 { cursor, setCursor } = cursorManager;
|
||||||
|
const { validateMoves = false } = options || {};
|
||||||
const [board, setBoard] = useState<string>(sudoku.generate("medium"));
|
const [board, setBoard] = useState<string>(sudoku.generate("medium"));
|
||||||
const [initialBoard, setInitialBoard] = useState<string>(board);
|
const [initialBoard, setInitialBoard] = useState<string>(board);
|
||||||
const [solution, setSolution] = useState<string | null>(null);
|
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 => {
|
const isValidPlacement = (testBoard: string, row: number, col: number, digit: string): boolean => {
|
||||||
if (digit === ".") return true;
|
if (digit === ".") return true;
|
||||||
|
|
||||||
const idx = row * 9 + col;
|
|
||||||
|
|
||||||
for (let c = 0; c < 9; c++) {
|
for (let c = 0; c < 9; c++) {
|
||||||
const checkIdx = row * 9 + c;
|
const checkIdx = row * 9 + c;
|
||||||
if (c !== col && testBoard[checkIdx] === digit) {
|
if (c !== col && testBoard[checkIdx] === digit) {
|
||||||
@@ -131,7 +134,7 @@ export function useBoardState(cursorManager: UseCursorType) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const setDigit = (d: string) => {
|
const setDigit = (d: string) => {
|
||||||
const i = cursor.r * 9 + cursor.c;
|
const cellIndex = cursor.r * 9 + cursor.c;
|
||||||
|
|
||||||
if (!canEditCell(cursor.r, cursor.c)) {
|
if (!canEditCell(cursor.r, cursor.c)) {
|
||||||
window.dispatchEvent(new CustomEvent("sudoku-locked-cell"));
|
window.dispatchEvent(new CustomEvent("sudoku-locked-cell"));
|
||||||
@@ -139,10 +142,10 @@ export function useBoardState(cursorManager: UseCursorType) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const arr = board.split("");
|
const arr = board.split("");
|
||||||
arr[i] = d;
|
arr[cellIndex] = d;
|
||||||
const newBoard = arr.join("");
|
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);
|
const conflicts = getConflicts(newBoard, cursor.r, cursor.c);
|
||||||
window.dispatchEvent(
|
window.dispatchEvent(
|
||||||
new CustomEvent("sudoku-invalid-move", {
|
new CustomEvent("sudoku-invalid-move", {
|
||||||
@@ -153,11 +156,13 @@ export function useBoardState(cursorManager: UseCursorType) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setBoard(newBoard);
|
setBoard(newBoard);
|
||||||
|
if (validateMoves) {
|
||||||
window.dispatchEvent(
|
window.dispatchEvent(
|
||||||
new CustomEvent("sudoku-valid-move", {
|
new CustomEvent("sudoku-valid-move", {
|
||||||
detail: { row: cursor.r, col: cursor.c, digit: d },
|
detail: { row: cursor.r, col: cursor.c, digit: d },
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -6,14 +6,19 @@ import { useBoardState } from "./useBoardState";
|
|||||||
import { useKeyBindings } from "./useKeyBindings";
|
import { useKeyBindings } from "./useKeyBindings";
|
||||||
import { useScore } from "../useScore";
|
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 rendererManager = useRenderer();
|
||||||
const { containerRef, renderEmptyBoard, renderBoard, highlightCursor } = rendererManager;
|
const { containerRef, renderEmptyBoard, renderBoard, highlightCursor } = rendererManager;
|
||||||
const cursorManager = useCursor(rendererManager);
|
const cursorManager = useCursor(rendererManager);
|
||||||
const { cursor, moveCursor } = cursorManager;
|
const { cursor, moveCursor } = cursorManager;
|
||||||
const gameManager = useGameStatus(rendererManager, cursorManager);
|
const gameManager = useGameStatus(rendererManager, cursorManager);
|
||||||
const { gameStatus, setGameStatus, startGame, stopGame } = gameManager;
|
const { gameStatus, setGameStatus, startGame, stopGame } = gameManager;
|
||||||
const boardManager = useBoardState(cursorManager);
|
const boardManager = useBoardState(cursorManager, { validateMoves });
|
||||||
const {
|
const {
|
||||||
board,
|
board,
|
||||||
initialBoard,
|
initialBoard,
|
||||||
|
|||||||
@@ -10,6 +10,8 @@
|
|||||||
"isolatedModules": true,
|
"isolatedModules": true,
|
||||||
"moduleDetection": "force",
|
"moduleDetection": "force",
|
||||||
"noEmit": false,
|
"noEmit": false,
|
||||||
|
"declaration": true,
|
||||||
|
"emitDeclarationOnly": false,
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"noUnusedLocals": true,
|
"noUnusedLocals": true,
|
||||||
"noUnusedParameters": true,
|
"noUnusedParameters": true,
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 1021 KiB |
Reference in New Issue
Block a user