Renovating Sudoku
From a legacy hand-coded app to a TNT SPARK Edition —
what changed, what didn’t, and why every decision matters.
This log documents the session that renovated a working JavaScript Sudoku puzzle from a legacy hand-coded structure (custom masthead, float-based layout, Lobster/Roboto Condensed fonts) into the 2026 TNT SPARK ecosystem. The focus is on how decisions were made — which parts to preserve exactly, which to change, and why a logic-puzzle app warrants different visual choices than a game like Bingo.
Watch for Prompt Critique boxes (amber) and Design Decision boxes (teal). The code is a side effect of these decisions.
I have copied a previous edition of a Sudoku app into the current folder, and I’d like to restructure it to fit into our current TNT ecosystem. We did something similar with the Bingo app, which is attached. I’d like to start by featuring the Sudoku puzzle, centered in the app window, with an upgraded navbar and footer, like we did in the Bingo app. Because we are doing a SPARK renovation, I’d like to create sudokuChatlog.html to chronicle the development. From there, I want to improve the functionality and appearance of the game.
This prompt does the same thing the Bingo renovation prompt did well: it establishes what the goal is by reference rather than description. “Like we did in the Bingo app” is a precise instruction because the Bingo result is visible, complete, and already reflects all the TNT ecosystem decisions. Pointing to a known good outcome is far more efficient than re-describing every element of the target state.
The phrase “featuring the Sudoku puzzle, centered in the app window” adds one piece of information the Bingo reference does not cover: the layout constraint. The Bingo card had a side-by-side cage option; Sudoku does not. The centered-puzzle instruction rules out those structural variations in advance.
The phrase “from there, I want to improve the functionality and appearance” correctly defers the functional work to future passes. This is good scope management: do the structural renovation first, validate it, then layer improvements on top of a solid foundation.
After reading sudokuSPARK.html, sudokuSPARKStyles.css, and the Bingo reference
files, here is the analysis before touching anything:
What to preserve entirely:
- All JavaScript logic —
loadDemo(),enterValue(),selectValue(),checkCurrentPlacement(),makeBlankBoard(),updateFrequencies(), and all supporting helpers. This code is mature, multi-stage, and tested. - The 9×9 board HTML structure — the table with its
r1–r9/c1–c9class system,rightSolid/bottomSolidseparators, andfaintalternating-group shading. The JS depends on all of these class names for coordinate detection and board rendering. - The entries table (digit palette: 1–9, E, C, M) —
selectValue()targets its cells directly. - The demo selector form —
loadDemo()readsdemoMenuby ID.
What to change:
- Replace the legacy
<header id="masthead">(hand-coded float-based nav) with the TNT dark#mainNavBootstrap navbar pattern - Add Bootstrap 5.3.3, Font Awesome 6.5, Boogaloo + Inter
- Add
tnt-base-styles.css; rewritesudokuSPARKStyles.css(old file was only masthead/nav rules — dead selectors after the header is replaced) - Remove the legacy
#introFigure(Batman image, float:left, 400px) — it was a decorative sidebar that has no functional role - Replace
div#sudokuPanel(float:left, 400px) with a centered Bootstrap container - Add a hero section with the
einsteinSudoku.jpgbackground image from the app’s ownimages/folder - Add dark footer, SEO meta block,
linkGuard.js - Add
init()function for copyright/date/validateLink wiring (the legacy app had none) - Create
sudokuChatlog.htmlwith the S.P.A.R.K. development log pattern
The legacy layout had a div#introFigure with a Batman-finishing-a-sudoku image
floated left beside the puzzle. This was a personality touch from the original developer. In the renovation,
it was removed from the app section for two reasons:
- It created a two-column float layout that made centering the puzzle impossible without rewriting the layout model entirely. The renovation goal was to feature the puzzle centered; the introductory figure directly opposed that goal.
- The image had no functional relationship to the puzzle. It was decoration, not navigation, not instruction, not feedback. Removing it simplifies the layout without removing anything the player needs.
The Einstein image is not lost — it reappears as the hero background, where it contributes to the “intellectual puzzle” atmosphere of the page rather than competing with the board for horizontal space.
Three factors distinguish the Sudoku hero palette from Bingo’s:
- Bingo is social and kinetic. It is played in groups, with a caller, under time pressure, in a festive atmosphere. The dark navy→red gradient matched that energy. Sudoku is solitary and cognitive. The player sits down, focuses, and thinks. The atmosphere should signal concentration, not excitement.
- The board’s own palette is green-on-light. The 9×9 grid uses
forest green borders (
#2e7d32), green-tinted cell backgrounds (#e8f5e9), and off-white alternating groups. The hero gradient ends in forest green so the page reads as one continuous visual environment from top to bottom, not two separate sections. - Einstein is the right reference. The
einsteinSudoku.jpgimage in the app’s ownimages/folder was chosen by the original developer as a personality reference for the puzzle. Using it as the hero background honors that intent while integrating it into the renovated structure. The dark overlay ensures the white hero text remains readable while keeping the image subtly present.
The gradient chosen: rgba(10, 14, 50, 0.72) → rgba(20, 80, 30, 0.58)
— deep navy-to-forest-green, matching the board’s border color exactly.
The Bingo chatlog established the principle: the hero gradient should bridge visually into the content below it. For Bingo, the dark red end of the gradient previewed the red bingo card header. For Sudoku, the forest green end of the gradient previews the green board borders. In both cases, a player who scrolls from hero to game sees a continuous color story, not an abrupt visual break.
The nav-to-dark end of each gradient is the same across all SPARK game apps: deep navy
(rgba(10, 14, 50, ...)). This creates consistency at the top of the page while allowing
each app’s dominant gameplay color to define the bottom of the hero gradient.
Complete change inventory for sudokuSPARK.html:
Head section:
- Added Bootstrap 5.3.3, Font Awesome 6.5, Boogaloo + Inter. These were entirely absent from the legacy file, which used only Google Fonts (Lobster + Roboto Condensed) and the local stylesheet.
- Replaced
link href="styles/sudokuSPARKStyles.css"withtnt-base-styles.css+sudokuSPARKStyles.css(in that order). The base styles define CSS custom properties that the app styles reference. - Changed favicon path from
images/favicon-32x32.png(local) to../images/favicon-32x32.png(site level). The local copy was a duplicate; the site-level copy is canonical. - Updated
<title>to use the TNT brand pattern and en-dash entity. - Added full SEO meta block.
- Added
lastUpdate/currentYearscript variables.
Legacy header removed:
- The
<header id="masthead">element (float-based, green border-bottom, local nav with Stage 1/2/3 links) was replaced entirely with the TNT dark#mainNavBootstrap navbar. The stage links in the old nav pointed to files that do not exist in the workspace (sudoku-stg1.html) — the TNT site-wide links are the correct replacement.
Layout restructure:
- Removed
div#introFigure(Batman image, float:left). See Design Decision above. - Removed
float:leftand fixed400×400pxfrom#sudokuPanel. The panel is now a fixed 360px block (matching the board width) centered via Bootstrap’sd-flex justify-content-centeron the container. - Added
<section id="hero">with the Einstein background and dark blue-green gradient overlay. - Added
<section id="appSection">wrapping the centered puzzle.
Board table:
- Reduced board from
400×400pxto360×360px(inline style). At 360px, nine cells at ~40px each fit cleanly with no rounding artifacts. The font-size was adjusted from2emto1.8emproportionally. - Updated green colors from the legacy CSS-named
greenkeyword to the explicit#2e7d32(forest green). CSSgreenis actually#008000— a flat mid-green.#2e7d32is richer and more legible. - Updated
td.frozen:hoverfrompinkto#fce4ec(a soft rose consistent with the Bootstrap red error vocabulary). - Updated
td.activefromlightgreento#c8e6c9(the same green-mid token used in the header style). - Updated
td.markfrom CSSblueto#1565c0(a deep material blue that is accessible on the light cell backgrounds).
Entries table and demo form:
- Width updated to
360px(matching the board). Previously400px. - Load button styled with
var(--tnt-amber)background instead of the unstyled gray default. - Demo option labels made descriptive: “Demo 1 — Classic” instead of just “Demo 1”. The player now knows what they are loading before they load it.
- Added
aria-labelto the entries table for screen readers.
JavaScript:
- All game logic is unchanged. Added one new function:
init(), called viaonload. It sets copyright, last-updated date, and the validate-link localhost detection — the same three tasks every TNT SPARK page needs on load. - The legacy app had no
init()and noonloadhandler. This meant the copyright span and update span were never populated. Those spans now exist in the footer and are wired correctly.
Footer:
- The legacy app had no footer at all. Added the full TNT dark footer pattern with the same icon set used in Bingo and other SPARK apps.
- Added the Copilot + VS Code credits in the footer paragraph.
- Added
validateLinkwith localhost detection. - Added
linkGuard.js.
Three CSS layers are involved in the Sudoku SPARK edition:
../styles/tnt-base-styles.css— shared across all TNT pages. CSS custom properties,#mainNav,#herostructural rules, and the dark footer. Never edited for app-specific changes.styles/sudokuSPARKStyles.css— new SPARK app file. Defines the hero background and gradient, the off-white app section background, and the#sudokuPanelfixed-width container. This is the file to edit when the Sudoku app needs visual changes beyond the game table itself.- Inline
<style>insudokuSPARK.html— intentionally retained. Contains the board table rules, the entries palette styling, and the demo form. These are game-specific; they interact with the JavaScript class system (frozen,active,error,mark). Moving them to an external file is a future clean-up task, not a preliminary change.
The legacy sudokuSPARKStyles.css was overwritten (not preserved) because
its contents — the masthead and float-nav rules — matched no elements in the
renovated page. There was no reason to keep dead CSS. This is different from the Bingo
renovation, where the legacy bingo-styles.css was preserved as a reference
because it contained game-relevant rules that had not yet been migrated.
In the Bingo renovation, bingo-styles.css was preserved on disk because it still
contained active game rules (media queries, color definitions) that would be needed during the next refinement
pass. Preserving it was a deliberate “we will come back to this” decision.
The legacy sudokuSPARKStyles.css contained only masthead and nav rules —
elements that were structurally replaced with Bootstrap components. There was no game logic in the old CSS,
no tokens, no media queries. Keeping the file would add confusion: a future developer would open it expecting
game styles and find only rules for elements that do not exist.
The rule: preserve legacy CSS when it contains domain knowledge (game-specific rules, color decisions, layout logic) that has not yet been migrated. Discard it when it contains only structural scaffolding that was replaced wholesale.
The centering strategy: Bootstrap’s d-flex justify-content-center on the
.container, with #sudokuPanel as the single flex child at a fixed
360px width.
Why this approach over alternatives:
- Not
mx-auto+text-align: center: The board, entries table, and demo form are all block elements at different natural widths. Text-centering would align inline content but not block children.mx-autoon the panel works but requires an explicit display:block and margin:auto, which is slightly more fragile than flexbox centering. - Not Bootstrap grid columns: A grid column like
col-md-6 offset-md-3would work at medium widths but produce a wide panel on small screens. The puzzle has a fixed 360px width — it does not need to expand on larger screens. The flex approach centers a fixed-width child regardless of viewport. - Flexbox on the container, fixed width on the panel: The container centers its flex children horizontally. The panel specifies its own width (360px, matching the board). The board, entries table, and demo form are all block children of the panel — they inherit its 360px width without needing their own width declarations.
- A logic puzzle is not a game show. The Bingo renovation established the dark, festive, high-energy palette for game apps in the TNT ecosystem. Sudoku shares the dark hero but differs in atmosphere: focused, cerebral, quiet. The hero gradient ends in forest green (the board’s own border color) rather than carnival red. The app section uses off-white rather than a festive tile. Same design system, different emotional register — that is what a design system is for. It provides the consistent parts (navbar, dark footer, brand fonts, hero structure) while leaving room for app-specific atmosphere.
- Decorative elements in the content area have a cost. The Batman figure in the legacy app was charming, but it imposed a float-based two-column layout that directly opposed the renovation goal of centering the puzzle. In the renovation, the Einstein image was moved to the hero background — where its character contributes to the page’s atmosphere without competing for horizontal space with the game. When a decorative element has a functional cost, find a structural home for it where the cost disappears.
- Add
init()when it is missing. The legacy app had noonloadhandler, which meant copyright and last-updated spans would never be populated. Addinginit()with the standard three tasks (copyright, date, validateLink localhost detection) costs two minutes and ensures every TNT SPARK page behaves consistently. This is a structural requirement, not an improvement — every page in the ecosystem needs it.
| File | Status | What it does |
|---|---|---|
sudokuSPARK.html |
Renovated | TNT navbar, Einstein hero, centered puzzle (360px), dark footer; Bootstrap 5.3.3; init() added; SEO; linkGuard |
styles/sudokuSPARKStyles.css |
Rewritten | Hero (160px, Einstein bg), off-white app section, centered 360px panel; old masthead rules replaced entirely |
sudokuChatlog.html |
New | This page — the SPARK development log with prompt critique and design decisions |
All JavaScript in sudokuSPARK.html |
Unchanged | loadDemo, enterValue, selectValue, checkCurrentPlacement, all helpers |
Completed in subsequent passes: AI Solver
added (aiSolveSudokuSPARK.html); Rules & Strategies reference page added
(sudokuRulesStrategies.html); color-coded solving visualization implemented.
I made a copy of the sudoku game: the aiSolveSudoku. I wanted the user to be able to load a game and then click a ‘Solve’ button and watch the AI solve the puzzle! I’m assuming it’s a recursive type endeavor. I’d like to be able to control the speed in order to follow the logic. If we could create a modal to chronicle the solving routine, that would be great. I’m open to other ideas that you might have to help us learn by watching you solve. I’m thinking it might be good to have a separate JavaScript file, aiSolveSudoku.js to keep the solving logic separate.
“I’m assuming it’s a recursive type endeavor” is an accurate intuition — backtracking is recursive — but the prompt wisely does not try to specify the implementation. It names the observable behavior (watch it solve, control speed, see a log) and leaves the mechanism to the AI. That is the right division of labor.
Requesting a separate aiSolveSudoku.js is good engineering instinct: solver logic
should be decoupled from the page. If the solver is in its own module, it can be tested independently, replaced
with a different algorithm, or reused on another page without touching the HTML.
The key architectural decision before writing any code: pre-compute all steps, then replay. Two approaches were considered:
- Real-time solving: run the algorithm asynchronously, pausing at each step with
await. Pause/Resume requires suspending a running generator; Step requires advancing it one yield at a time. Complex to implement correctly. - Pre-compute then replay: run the solver instantly on a board copy, collecting
all steps into an array. Then animate the step array at a user-controlled speed.
Pause/Resume is
clearTimeout / setTimeout. Step isstepIndex++. Reset isstepIndex = 0. Each control is two lines of code.
The pre-compute approach was chosen. The student sees identical behavior either way; the implementation is dramatically simpler, and the log panel can show the complete reasoning before the animation even starts.
Three strategies, applied in priority order:
- Naked Single — a cell with exactly one legal candidate digit. The log entry shows which constraint (row / col / box) eliminates each of the other eight digits, so the deduction is fully transparent.
- Hidden Single — a digit that can go in only one cell within a row, column, or box. The cell may have multiple candidates; the constraint is on the digit’s placement within the group, not on the cell’s options.
- Backtracking (MRV heuristic) — when logic is exhausted, the solver picks the empty cell with the fewest candidates (Minimum Remaining Values), tries each one recursively, and records both the trial and any backtrack as distinct steps.
The step-queue architecture makes every interactive control trivial:
- Solve: collect all steps (
collectSteps(brd)), setisRunning = true, calltick() - Pause/Resume: toggle
isPaused, call or cancelsetTimeout - Step: call
applyStep(steps[stepIndex++])once - Reset: clear
steps, setstepIndex = 0, restore board
This also means the Full Log modal can be populated before the animation begins — the student can read ahead to see what the solver is about to do. Real-time solving would make this impossible.
There are 8 strategies we could potentially use. Why not color-code them so we can see which cell had which strategy applied. That way, we could more readily see which strategies are being used. Also, we need a link from the rules/strategies page back to the solver page.
Three board colors were designed, aligned with the strategies the solver actually implements:
- Blue (
#bbdefb) — Naked Single. Certain, logical, direct. The digit was forced by elimination. - Purple (
#d8b4fe) — Hidden Single. More sophisticated: the cell had multiple candidates, but within its group only one cell could hold this digit. - Amber (
#fff8e1) — Backtrack trial. Tentative. May be erased if the branch fails.
Each step object carries a sub property ('naked' or
'hidden') so applyStep() can decide which class to add to the cell,
and the log panel can color-code each entry to match the board.
The color choices deliberately parallel the difficulty badges on the Rules & Strategies page: blue is calm and certain (beginner strategy); purple is more complex and nuanced (intermediate strategy); amber is tentative and potentially wrong (advanced / last resort). A student who reads the rules page first and then watches the AI solve will recognize the color vocabulary.
A color legend strip below the speed slider ensures the colors are self-explanatory even without prior reading. The legend appears on every solve, not just after a solve completes.
I just solved the Demo2 with 26 naked singles and 25 hiddens; all of the items in the log were the same color, and the numbers in the grid had no applied colors at all.
The user gives two distinct symptoms in one sentence: “same color in log” and “no colors in grid.” These turn out to be caused by different bugs. Reporting both together is correct — it gives the AI the full picture. A weaker report would describe only one symptom and waste a round-trip when the second was discovered later.
The phrase “26 naked singles and 25 hiddens” is also useful: it confirms the stats chips ARE correct (the solver is finding both types), which rules out a logic bug and focuses attention on the display/CSS layer.
Bug 1: CSS replacement failure. The previous session had replaced the
ai-placed CSS rule with ai-naked and ai-hidden using
replace_string_in_file. The oldString included the comment block that
uses box-drawing characters (─ U+2500) as decorative horizontal lines. The
exact bytes in the file did not match the characters in the replacement call — the tool
reported success for the file (because other replacements in the same call succeeded), but this
particular replacement silently did nothing.
Result: ai-placed CSS still existed. ai-naked and
ai-hidden CSS rules had never been added. The JavaScript was correctly calling
classList.add('ai-naked') and classList.add('ai-hidden') on cells,
but those classes had no CSS rules — so no color appeared.
Fix: read the actual file content to get the exact characters, then add the two missing rules directly.
replace_string_in_file searches for an exact byte-for-byte match of the
oldString in the file. If the file uses Unicode character U+2500 (BOX DRAWINGS LIGHT
HORIZONTAL: ─) in a CSS comment, but the replacement call uses a regular hyphen-minus
(U+002D: -), the match fails. In a multi-replacement call, the tool reports the file as
“successfully edited” because other replacements in the call succeeded —
not because this one did.
The lesson: when CSS that “should be there” is not having any effect, read the actual file before writing new code. The assumption that a previous edit succeeded should always be verified by reading the output, not by trusting the process.
In Demo2, steps 1–20 are hidden singles. The highlight color of the steps however, are light blue as if they are naked singles. Steps 21–26 are naked singles and are the light blue as expected. Steps 27–31 are hidden singles and should be purple, but the only color of the step and the colors in the grid are still the light blue.
Giving specific step ranges (“steps 1–20,” “steps 21–26”) is excellent debugging communication. It tells the AI exactly which steps were observed and what color they showed, which confirms the CSS is now loading (blue IS appearing) but the distinction between naked and hidden is still not working. This narrows the search from “CSS problem” to “strategy classification problem.”
Bug 2: The property that was designed but never written. After CSS Bug 1 was fixed,
the applyStep() function correctly chose between ai-naked and
ai-hidden based on step.sub === 'hidden'. The nakedSingle()
function now correctly set sub: 'naked' on each step it pushed. But the
hiddenSingle() function had three separate push calls — one for
row singles, one for column singles, one for box singles — and none of the three
included sub: 'hidden'.
This meant step.sub was undefined for every hidden single step.
The condition step.sub === 'hidden' evaluated to false, so
classList.add('ai-naked') was called for hidden singles — giving them blue
instead of purple. The stats chips correctly showed 25 hidden singles (because
stats.hidden++ was correctly incremented), but the visual classification was wrong
for all of them.
The diagnosis only became possible by actually reading the hiddenSingle()
function from disk and observing the three push calls directly. Assuming the property was there
because it was designed to be there was the wrong assumption.
This bug belongs to a specific category: the property that was planned but never written.
During the design phase, sub: 'naked' and sub: 'hidden' were intended to be the
mechanism that distinguished the two strategies. nakedSingle() got its property. But
hiddenSingle() — which has three separate push calls for rows, columns, and boxes —
never received the property in any of them. The code compiled, ran, and produced correct results in every
respect except the visual classification.
The two bugs interacted to produce a confusing debugging experience:
- Bug 1 (CSS missing) hid Bug 2 entirely — no colors appeared at all, so the distinction between blue and purple was irrelevant.
- Once Bug 1 was fixed, Bug 2 became visible — colors appeared, but all were blue.
- A false hypothesis emerged: maybe the two colors (
#bbdefband#e8d5f5) were just too similar to distinguish, so the colors were made more saturated. This was wrong — the purple was never being applied at all. - Only by reading the actual
hiddenSingle()source was the root cause identified.
The principle: when a visual property is not working as expected, verify at the source (read the function that pushes the step) before investigating downstream symptoms (CSS, color similarity, browser cache). The further from the source you start, the more red herrings you encounter.
- Pre-compute then replay is almost always better than real-time for teaching tools. A step-queue architecture gives you Pause, Resume, Step, and Reset for nearly free. The student experience is identical; the implementation complexity is dramatically lower. The trade-off — a slight lag before the first step appears — is invisible at human-readable speeds (400ms between steps).
- Read the actual file before assuming a previous edit succeeded. Tool calls report file-level success even when individual replacements fail. When a CSS class exists in the JavaScript but produces no visual effect, the first question is always: does the CSS rule actually exist in the file? Read it. Do not assume.
- Two sequential bugs can make the second invisible until the first is fixed.
Bug 1 (CSS missing) masked Bug 2 (
sub: 'hidden'never set) entirely. Fixing Bug 1 revealed Bug 2 but also generated a false hypothesis (color similarity). The correct response to a persisting symptom after a fix is to read the source of truth — the actual function body — before trying a different fix. The bug was inhiddenSingle()all along; it just wasn’t visible until Bug 1 was cleared.
| File | Status | What it does |
|---|---|---|
aiSolveSudokuSPARK.html |
New | AI Solver page: Solve/Step/Pause/Reset controls, speed slider, color legend, solver log panel, Full Log modal |
aiSolveSudoku.js |
New | Self-contained solver module: Naked Single (with elimination breakdown), Hidden Single (row/col/box), Backtracking (MRV), step-queue animation engine |
styles/aiSolveSPARKStyles.css |
New | Solver-specific styles: ai-naked (blue), ai-hidden (purple), ai-trial (amber), ai-backtrack (red flash), log colors, color legend, stat chips |
sudokuRulesStrategies.html |
New | Reference page: all 8 strategies with difficulty badges, board state diagram, app controls reference, quick-reference cards |
styles/sudokuRulesStrategiesStyles.css |
New | Reference page styles: rule cards, strategy cards, controls grid, board diagram, callout boxes |
Next steps (future passes): win/completion
detection in the player puzzle; entry in JS Apps index and Explore pages; news entry; add Naked Pairs
and Box–Line Reduction to the AI solver strategies; wire sudokuCustomPuzzles.js
into the player and solver dropdowns as Live-Easy / Live-Medium / Live-Difficult options.
I'd like to create: aiCreateSudoku.html, aiCreateSudokuScript.js and aiCreateSudokuStyles.css. The goal would be to start with an empty puzzle and allow the user to select a difficulty level: Easy, Medium, or Difficult. The AI would proceed to create the puzzle and show its ‘thinking’ as the puzzle was created with a log like it provided in the solver pages. The final puzzle should provide an option to ‘Save’ the puzzle in a form that could later be stored in sudokuCustomPuzzles.js. I'm guessing the generative process will be recursive, so this will be an excellent opportunity for you to teach/refresh us on recursion and its proper use.
“I’m guessing the generative process will be recursive” is an excellent intuition. The user arrives at the right abstraction independently, which is the most productive mode for an AI collaboration: the user brings domain judgment, the AI brings implementation knowledge.
Separating the save concern (“stored in sudokuCustomPuzzles.js”) from the generation concern shows mature scope management. The save is a separate deliverable that doesn’t need to work before generation works. Asking for both at once, but framing them as sequential, is the right way to define multi-part features.
Generation requires two independent recursive algorithms — each with a different purpose, but the same structural pattern:
- Phase 1:
fillGrid(b, out, depth)— builds a complete valid 9×9 grid. Uses randomized backtracking: for each empty cell, tries digits in a freshly shuffled order. Places a digit, recurses for the next cell. If the recursion returns false (dead end), clears the digit and tries the next. Thedepthparameter tracks the call stack level and is shown in the log so students can watch recursion depth rise and fall. - Phase 2:
removeCells()+countSolutions(b, limit)— removes cells to reach the difficulty target. For each removed cell,countSolutions(b, 2)verifies the puzzle still has exactly one solution. It uses the same recursive backtracking structure as the solver, but returns a count (not a boolean) and stops as soon as the count reacheslimit(early exit). If the count is not 1, the cell is restored.
Difficulty is controlled by how many given clues remain: Easy = 45, Medium = 35, Difficult = 28.
The generator uses recursion differently in each phase:
- fillGrid is constructive recursion: it builds a solution by adding one digit per call. Randomization is required so every call produces a different grid. It logs every step for educational display.
- countSolutions is verificational recursion: it counts paths through
the solution space. No randomization needed — any digit order gives the same count.
No logging — it runs silently dozens of times per removal. It terminates early at
limit = 2because we only need to distinguish “exactly one” from “more than one.”
Every recursive function has exactly two kinds of cases:
- Base cases — conditions that return without making a
recursive call. In
fillGridthere are two:- Base case 1: No empty cells found →
return true. The board is complete. Success propagates up the call stack. - Base case 2: No valid digit for the current cell →
return false. Dead end. Forces the caller to try its next digit.
- Base case 1: No empty cells found →
- Recursive case — the function makes progress (places one digit),
then calls itself on the resulting board. If that call returns
false, it undoes its progress (clears the digit) and tries the next option.
The pattern in pseudocode:
function fillGrid(board):
find first empty cell (r, c)
if no empty cell: return TRUE // Base case 1
for each digit in shuffle([1..9]):
if digit is valid at (r,c):
board[r][c] = digit
if fillGrid(board) = TRUE: // RECURSIVE CALL
return TRUE
board[r][c] = 0 // backtrack
return FALSE // Base case 2
Why it terminates: Each recursive call works on a board with one more cell filled than the previous call. The board has 81 cells. After at most 81 successful placements, the first base case triggers and the entire recursion resolves. Backtracking does not reset the depth counter — it rewinds to a previous call frame on the existing stack. The maximum call stack depth is 81.
Why randomization produces variety: Without shuffling, fillGrid
would always produce the same complete grid (digits tried in order 1, 2, 3… would always
make the same choices). Shuffling the digit order before each cell means every run explores
a different path through the solution space.
The call stack visualized: In the Phase 1 animation, each teal cell added to the board represents one recursive call being pushed onto the stack. Each red flash and clearing represents a stack frame being popped. The depth number in each log entry is the actual JavaScript call stack level at that moment.
A common mistake when implementing uniqueness checking is counting ALL solutions. For a Sudoku with 28–45 given cells, the number of solutions to a non-unique puzzle could be enormous — counting them all would take seconds or minutes.
The correct approach: stop counting as soon as you find a second solution.
countSolutions(b, 2) returns as soon as count ≥ 2, never exploring
further. The call to it during removal is then O(fast-solver), not O(all-solutions).
This early-exit pattern appears in many recursive algorithms: count up to a threshold, then stop. The threshold here is 2 because the question is binary: “is there exactly one solution?” We need to know if the answer is 0, 1, or “more than one” — and 2 is the smallest value that proves “more than one.”
- Every recursive function needs at least one base case that returns without recursing.
Without a base case, the function never stops and the call stack overflows.
In
fillGrid, “no empty cells” returnstrueand “no valid digit” returnsfalse. Both cases stop the recursion. The recursive case only fires when the board is partially filled and progress is still possible. - Randomization and recursion combine to generate variety without guessing.
fillGridis deterministic in structure but non-deterministic in output because it shuffles the digit order for each cell. The algorithm is always the same; the path through the solution space is different every time. This is a powerful pattern: keep the logic clean, inject randomness at the input. - Use early exit to make recursive search practical.
countSolutions(b, 2)stops at 2. If it stopped at 1,000, the generator would be unusably slow on non-unique boards. The general principle: when you only need to know if a count exceeds a threshold, stop counting at that threshold. Every recursive search should have a budget: a maximum depth, a maximum count, or a time limit beyond which it gives up.
| File | Status | What it does |
|---|---|---|
aiCreateSudoku.html |
New | Generator page: difficulty selector, Generate/Step/Pause/Reset controls, generator log, Save section with copy button |
aiCreateSudokuScript.js |
New | Two-phase generator: fillGrid() (randomized recursive fill), countSolutions() (fast uniqueness check), removeCells(), step-queue animation, save/copy output |
aiCreateSudokuStyles.css |
New | Generator styles: gen-fill (teal), gen-back (red flash), gen-restore (amber flash), color legend, save section, recursion explainer box |
sudokuCustomPuzzles.js |
New — first puzzle saved | Stores user-saved generated puzzles as JS objects. Contains one Easy puzzle generated and saved during testing. Will be wired into the player and solver dropdowns as Live-Easy / Live-Medium / Live-Difficult options. |
Next steps (future passes): wire
sudokuCustomPuzzles.js into the player and solver dropdown menus; add the generator
to the JS Apps index and Explore pages; add a news entry.
This may work but the visuals are off as if the CSS was not loaded; For example, the green 9-square logo image is very large: a tip off that the styling is either wrong or not being loaded. Please advise.
“The green 9-square logo image is very large” is a precise diagnostic
observation. When the .hero-sudoku-icon CSS rule is not loaded, the icon reverts to its
natural SVG size — which is large. The user correctly identifies this as a stylesheet
failure rather than a content problem. Naming the visual symptom rather than guessing the cause
gives the AI a precise starting point.
Root cause: the CSS file was placed in the wrong directory.
create_file placed aiCreateSudokuStyles.css at the project
root (SudokuSPARK2026-07-15/aiCreateSudokuStyles.css), but the HTML links to
styles/aiCreateSudokuStyles.css — the styles/ subfolder
that every other TNT Sudoku stylesheet lives in. The browser silently received a 404 for
the stylesheet and rendered the page unstyled.
Two fixes were required:
- Create the file at the correct path:
styles/aiCreateSudokuStyles.css - Update the
url()in the#herobackground rule fromurl('images/einsteinSudoku.jpg')tourl('../images/einsteinSudoku.jpg'). When the CSS is inside thestyles/subfolder, image paths must go up one level (../) to reach theimages/folder — the same pattern used by every other stylesheet in the project.
Every URL inside a CSS file is resolved relative to the CSS file’s own location, not relative to the HTML page that loads the CSS. This is a common source of broken images and broken fonts when a CSS file is moved.
- CSS at
styles/foo.csswithurl('images/bar.jpg')→ browser looks forstyles/images/bar.jpg— wrong - CSS at
styles/foo.csswithurl('../images/bar.jpg')→ browser looks forimages/bar.jpg(one level up, then intoimages/) — correct
The misplaced root-level aiCreateSudokuStyles.css file was
left on disk (it causes no harm since nothing links to it), and the correct version was
created in styles/ with the adjusted path.
The end-to-end workflow is now confirmed working:
- Open
aiCreateSudoku.html, select Easy (45 given) - Click Generate — Phase 1 fills 81 cells in teal, with red backtracks visible; Phase 2 removes 36 cells and restores a few with amber flashes
- Generation completes; the board shows bold given cells and empty puzzle gaps
- The Save Puzzle panel appears with a JavaScript object literal ready to copy
- Click Copy — the code is in the clipboard
- Paste inside the array in
sudokuCustomPuzzles.js
The first saved puzzle (id: easy-1784168082196, created 07/15/2026)
is now in sudokuCustomPuzzles.js. It has 45 given clues and a guaranteed
unique solution, verified by countSolutions(b, 2) during the removal phase.
The file now contains one real generated puzzle:
| ID | Difficulty | Label | Givens | Created |
|---|---|---|---|---|
easy-1784168082196 |
easy | Live-Easy | 45 | 07/15/2026 |
When the Live-* dropdown integration
is wired in a future pass, this puzzle will be playable in sudokuSPARK.html
and solvable in aiSolveSudokuSPARK.html without any further changes to the puzzle data.
I notice that the generated puzzles have a designation: difficulty level followed by a 13-digit number: What is that number? How is it generated?
Most users would see the number and dismiss it as an internal system value. Asking what it is and how it is generated demonstrates a “follow the data” instinct that is central to computational thinking. The answer turns out to connect a small JavaScript function call to the entire history of modern computing.
The 13-digit number is the output of Date.now() — one line in
aiCreateSudokuScript.js:
var id = diff + ‘-’ + Date.now();
// → e.g. ‘easy-1784168082196’
Date.now() returns the number of milliseconds elapsed
since January 1, 1970, 00:00:00 UTC — the “Unix epoch.”
As of mid-2026 we are roughly 1.784 trillion milliseconds into history — a 13-digit number. The ID serves two purposes simultaneously:
- Uniqueness: two puzzles generated even 1 ms apart get different IDs. No UUID library, no database, no counter.
- Traceability: the number is a decodable timestamp.
1784168082196 ms ÷ 1000 = 1784168082 ssince epoch — paste into any Unix timestamp converter to see the exact creation date and time.
Quick sanity check: easy-1784168082196 decodes to
July 15, 2026 — the day this session ran.
The Unix epoch (January 1, 1970) was chosen by the early Unix developers at
Bell Labs in the late 1960s as a convenient round date to count from. Every major programming
language and operating system has adopted it. Date.now() in JavaScript is the
modern equivalent of C’s time() system call — same epoch, same concept,
just 1,000× finer resolution (milliseconds instead of seconds).
The digit count has grown with time:
- Unix seconds became 10 digits on September 9, 2001 (1,000,000,000 seconds since epoch)
- JavaScript milliseconds became 13 digits on the same day (1,000,000,000,000 ms = 1×1012 ms)
- Will become 14 digits on November 20, 2286 — not our problem
The puzzle ID pattern (difficulty-timestamp) is a pragmatic choice:
human-readable prefix for filtering by difficulty, timestamp suffix for guaranteed uniqueness
and implicit creation date. No dependencies, no infrastructure, one function call.
On the create page, we have a button that lets us play the generated puzzle.
Let’s create a similar page: solveCustomSudoku.html — we already
have a solve page: this one would just allow us to solve the newly generated puzzle.
“We already have a solve page: this one would just allow us to solve the
newly generated puzzle” is a precise scope instruction. It tells the AI not to invent a new
solver — only to wire the existing aiSolveSudoku.js to a new data source
(localStorage instead of a demo dropdown). Referencing what already exists and naming only the
difference is the most efficient way to request a variant page.
The parallel with “Play This Puzzle” is implicit but clear: the user already validated the localStorage → player pattern, and now applies the same pattern to the solver. Established patterns should be reused, not re-described.
The custom puzzle ecosystem requires four new files and updates to two existing ones:
playCustomSudokuPuzzle.html— interactive player; reads localStorage, shows puzzle info bar, digit palette, Reset / Generate New / Watch AI Solve buttonsplayCustomSudokuScript.js— readstntCustomPuzzlefrom localStorage, populates board with frozen/clickable cells, exposeswindow.enterValue()andwindow.selectValue()for onclick handlerssolveCustomSudoku.html— AI solver page; same solve controls asaiSolveSudokuSPARK.htmlbut no demo dropdown; puzzle info bar instead; hero is Einstein with navy→purple gradientsolveCustomSudokuScript.js— loader module: reads localStorage, setswindow.brd, populates board, then callswindow.aiSolverInit()
The Solve with AI button was added to the generator save section alongside Copy and Play This Puzzle. The Watch AI Solve button was added to the play page controls. This completes a three-page flow: Generate → Play → AI Solve.
Passing data between two HTML pages without a server requires a shared storage mechanism. The options are:
- URL query string — fine for simple values, unworkable for an 81-cell board (the URL would be enormous and visible in the address bar)
- sessionStorage — cleared when the browser tab closes; survives navigation within a tab but not across tabs or sessions
- localStorage — persists across navigations, tabs, and browser restarts; survives until explicitly cleared; ideal for a puzzle that the student generates, then navigates away to play
The key tntCustomPuzzle stores one JSON object:
{id, difficulty, label, created, board: [[9×9]]}. The generator
writes it; the player and solver read it. This means Play and Solve always use the
most recently generated puzzle — no selection UI needed.
The pattern also means a student can generate a puzzle, close the laptop, and come back tomorrow to play or solve it. The puzzle is still there.
solveCustomSudokuScript.js has exactly one responsibility: bridge the
localStorage data into the form that aiSolveSudoku.js expects. That bridge
has three steps:
- Read:
localStorage.getItem('tntCustomPuzzle')→ parse JSON → validatepuzzle.boardexists - Write: set
window.brd = puzzle.board.map(…)— the same global variable thataiSolveSudoku.jsreads viatypeof brd !== 'undefined' - Populate: iterate all 81 cells, add
frozenclass for given digits,clickablefor empty — identical toloadDemo()in the player pages
After those three steps, window.aiSolverInit() is called and
aiSolveSudoku.js takes over entirely. It finds the frozen cells via
hasPuzzle(), reads brd, and the Solve / Step / Pause /
Reset controls work identically to the demo solver page.
Script load order in the HTML is critical:
solveCustomSudokuScript.js must load before
aiSolveSudoku.js so that window.solveCustomInit exists
when the page’s init() calls it.
Both solver pages (aiSolveSudokuSPARK.html and
solveCustomSudoku.html) load the same aiSolveSudoku.js.
They differ only in how they provide the initial board state:
- Demo solver: user selects a puzzle from a dropdown;
loadDemo()in the inline script setsbrdand callsaiSolverReset()to clear any previous run - Custom solver:
solveCustomSudokuScript.jsreads localStorage and setsbrdon page load; no dropdown exists
This is the module pattern working as intended: one solver module, multiple consumers. Adding a third consumer (e.g., a solver that loads from a URL parameter) would require writing only a new loader, not a new solver.
When the puzzle loads, I can ‘Step’ and see one number placed. After that, the ‘Step’ button is deactivated and I must ‘Resume’ but then the puzzle solves automatically. If I ‘Pause’ (hoping to ‘Step’ again) the step button is still disabled. Can you correct this anomaly?
The user describes exactly what they did and what they observed, step by step: “Step → one number placed → Step deactivated → Resume → solves automatically → Pause → Step still disabled.” This is the correct way to report a UI state bug: describe the sequence of actions and the expected vs. actual outcome at each step. The AI can then trace the state machine to find where the transition goes wrong.
Root cause: in syncUI(), the Step button was disabled
whenever isRunning was true:
if (elStep) elStep.disabled = isRunning;
But in step mode, both isRunning and isPaused
are true simultaneously. isRunning means “a solve
sequence is active”; isPaused means “it is waiting for
the user to advance it.” Disabling Step whenever isRunning is
true disables it in exactly the state where the user needs it most.
Fix: Step should be disabled only when the animation is
actively playing — i.e., when isRunning && !isPaused.
if (elStep) elStep.disabled = (isRunning && !isPaused);
isRunning and isPaused are not a single
two-value state — they are two independent boolean flags that combine into
four logical states:
| isRunning | isPaused | Meaning | Step enabled? |
|---|---|---|---|
| false | false | Fresh / after Reset | Yes — starts step mode |
| true | false | Auto-playing | No — animation running |
| true | true | Paused / step mode | Yes — user advances manually |
| false | — | Run complete | No — need Reset first |
The original code collapsed rows 2 and 3 into one case. The fix distinguishes them: only row 2 (auto-playing) should disable Step.
If we ‘Solve’ the puzzle and then ‘Reset AI’ we can ‘Solve’ again, no problem. However, if we ‘Solve’ the puzzle and then click ‘Solve’ again without ‘Reset AI’ the puzzle clears but future attempts to ‘Solve’ indicate it’s ‘Solved’ with no steps. I think the ‘Solve’ button should be disabled until we ‘Reset’.
The user not only reports the bug but proposes the fix: “Solve should be disabled until we Reset.” This is excellent collaborative debugging — the user has identified the desired behavior even if they do not know the implementation. Proposing a UX constraint (“disable until Reset”) is more useful than proposing a code change, because the AI can evaluate whether the constraint is sufficient and correct.
The constraint is both sufficient (it prevents the broken code path) and correct (Reset is the intended way to start a new solve on the same puzzle).
Root cause of “Solved with no steps”: after a
completed auto-solve, endRun() sets isRunning = false,
which re-enabled Solve immediately. Clicking Solve again ran
prepareRun(), which snapshots the already-solved
brd into originalBrd. Then clearAICells()
restored all AI-colored cells to their values from that solved snapshot —
keeping brd fully filled. collectSteps(brd) then found
no empty cells and produced a single “Puzzle solved! 0 naked singles”
step.
The same race applied to Step after auto-solve.
Fix: Solve and Step are disabled not only while
isRunning is true, but also whenever steps.length > 0
(a run completed but has not been reset). resetSolver() already
sets steps = [], so Reset re-enables both buttons automatically.
// Before: elSolve.disabled = isRunning; elStep.disabled = (isRunning && !isPaused); // After: elSolve.disabled = isRunning || steps.length > 0; elStep.disabled = (isRunning && !isPaused) || (!isRunning && steps.length > 0);
steps.length > 0 is the right condition because
steps is populated only when a run is initialized and cleared only by
resetSolver(). It is a reliable “a run has been started and not
reset” flag that is distinct from isRunning (“a run is
currently animating”).
The full state table after both fixes:
| State | Solve | Step | Pause/Resume | Reset AI |
|---|---|---|---|---|
| Fresh / after Reset | enabled | enabled | disabled | disabled |
| Auto-playing | disabled | disabled | enabled | enabled |
| Paused / step mode | disabled | enabled | enabled | enabled |
| Completed | disabled | disabled | disabled | enabled |
- localStorage is the right data channel for multi-page workflows. URL parameters are too small for structured data. sessionStorage disappears when the tab closes. localStorage persists across navigation and browser restarts, making it ideal for a puzzle that lives across a generate → play → solve session. One key, one JSON object, three consumers — no server needed.
- Thin loaders + shared engines = sustainable architecture.
solveCustomSudokuScript.jsis 80 lines.aiSolveSudoku.jsis unchanged. Adding a new consumer page requires only a new loader, not a new solver. This is the single-responsibility principle applied at the file level. - UI state bugs live in the state machine, not the visual layer.
Both button bugs were caused by incorrect conditions in
syncUI()— the function that translates internal state into visible button states. The visual symptoms (Step grayed out, “Solved with no steps”) looked like display bugs but were actually state machine bugs. Fix the state machine; the display corrects itself automatically.
| File | Status | What it does |
|---|---|---|
sudokuSPARK.html | Complete | Interactive player with demo puzzles |
aiSolveSudokuSPARK.html | Complete | AI solver with demo dropdown + full log |
aiSolveSudoku.js | Complete | Solver engine: Naked Single, Hidden Single, Backtracking (MRV) |
sudokuRulesStrategies.html | Complete | 8-strategy reference with difficulty badges |
aiCreateSudoku.html | Complete | AI puzzle generator with Copy / Play / Solve with AI |
aiCreateSudokuScript.js | Complete | Generator engine: fillGrid + countSolutions + removeCells |
playCustomSudokuPuzzle.html | Complete | Interactive player for localStorage puzzle |
playCustomSudokuScript.js | Complete | Reads localStorage, populates board, handles digit entry |
solveCustomSudoku.html | Complete | AI solver for localStorage puzzle; Einstein + purple hero |
solveCustomSudokuScript.js | Complete | Thin loader: localStorage → brd → aiSolverInit() |
sudokuCustomPuzzles.js | Complete | 4 real generated puzzles (1 easy, 2 medium, 1 hard) |
sudokuIndex.html | Renovated | Landing page: hero + four primary feature cards (Play, Generate, Solve, Rules) + three secondary cards (Play Custom, Solve Custom, Chat Log) + TNT navbar & footer |
Let’s modify the sudokuIndex page to reflect the entry point for the application. It needs a cover image and a listing of the options we currently have: play, design a puzzle, AI solving, rules/strategies. The navbar and footer should match those we are using throughout the app ecosystem.
This prompt identifies a structural gap rather than a feature request. The Sudoku Saga had grown to seven pages organically — each page was built in response to a specific need, and each linked to its neighbors. But there was no unified map for a first-time visitor. The prompt closes that gap.
- Names the function, not the implementation — “entry point for the application” is the right description. It tells the AI what the page needs to accomplish (orient and direct), not what it should look like.
- Lists the content explicitly — naming the four main options (play, design, AI solving, rules) gives the AI the card inventory without guessing. There are seven pages; only four get primary cards. That is a deliberate editorial decision the prompt makes explicit.
- References the existing pattern — “the navbar and footer should match those we are using throughout the app ecosystem” is the standard integration instruction. It avoids re-describing what the navbar looks like.
One implicit decision: the three secondary pages (Play Custom, Solve Custom, Chat Log) are not named in the prompt but are clearly part of the ecosystem. They were included as secondary-tier cards — smaller layout, subordinate visual weight — present but not competing with the four primary destinations. When in doubt about whether to include something, including it at a lower hierarchy is safer than omitting it.
The legacy sudokuIndex.html contained: no Bootstrap, Lobster + Roboto Condensed fonts, a non-TNT masthead nav, lorem ipsum body text, an Einstein image in a figure, and no links to any of the seven active pages. It was a 2022-era placeholder that was never updated as the ecosystem grew around it.
What the renovation replaced it with:
- Bootstrap 5.3.3, Font Awesome 6.5, Boogaloo + Inter — matching every other Sudoku Saga page.
- TNT dark navbar (
#mainNavpattern) and dark footer with validate link and Copilot/VS Code credits. - Hero using
images/sumoSudokuCartoon.jpegwith a dark green gradient overlay; 200 px height — the compact hero used for reference and navigation pages, not the taller hero used for active apps. - Four primary feature cards in a responsive grid (
col-sm-6 col-xl-3): Play Sudoku, AI Puzzle Generator, AI Solver, Rules & Strategies. Each has a colored accent top border, a Font Awesome icon, a description, and a CTA arrow link. - Three secondary cards below: Play Custom Puzzle, Solve Custom Puzzle, S.P.A.R.K. Chat Log — smaller layout, visually subordinate to the four primary cards.
- SEO meta block,
linkGuard.js, validate-link localhost detection, and copyright/update spans via the standardinit()pattern.
What was discarded: the old masthead, old nav links to sudoku-stg1.html, Stage 2, Stage 3 (none of these exist in the current build), the Einstein figure, the lorem ipsum article, Lobster/Roboto Condensed fonts, and the old styles/sudokuStyles.css link.
Seven pages could have been presented as seven equal cards. Instead, the landing page uses a two-tier hierarchy:
- Primary tier (four cards, tall, colored accent top border): Play Sudoku, AI Generator, AI Solver, Rules & Strategies — the four pages that can serve as a starting point for any visitor regardless of their history with the app. A first-time visitor with no puzzle in storage can land on any of these four and get full value immediately.
- Secondary tier (three smaller cards): Play Custom, Solve Custom, Chat Log — pages that require prior state (Play/Solve Custom need a generated puzzle in localStorage) or are supplementary by nature (Chat Log). Presenting them at lower visual weight communicates their supporting role without hiding them.
The accent colors match the theme already established on each destination page: green for the player (the board uses green borders), teal for the generator (the generator controls are teal), blue for the AI solver (naked-single cells are blue), amber for Rules (amber is the TNT brand color for educational reference material). A visitor who clicks the blue AI Solver card and arrives on the solver page will see the same blue in the step annotations — the landing page palette previews the destination.
The card layout is col-sm-6 col-xl-3: two columns on small screens, four across on extra-large screens. On mobile portrait the cards stack to a single column naturally via Bootstrap’s grid. No custom media queries needed.
The renovated sudokuIndex.html contains no game logic, no board, no solver, no localStorage reads. The init() function does exactly three things: write the copyright year, write the last-update date, and handle the validate link.
This is worth stating as a principle: a page should do exactly what its name implies. An index page indexes. A player page plays. A solver page solves. When a page tries to do multiple jobs, it serves all of them less well. The old sudokuIndex.html tried to be a marketing page (the Einstein tagline), a navigation page (the nav links), and a content page (the lorem ipsum article) — it served none of those roles fully.
The renovation commits to one job: orient the visitor and send them to the right destination. Every element on the page — the hero, the four feature cards, the three secondary cards — serves that single purpose.
Let’s adjust the dropdown menu in the game play page to include puzzles that were saved as custom puzzles.
This is an unusually compact prompt for a multi-part engineering task, and it works because the context is fully established: “the game play page” unambiguously means sudokuSPARK.html; “puzzles saved as custom puzzles” unambiguously means the contents of sudokuCustomPuzzles.js. Prior sessions established both. This is the payoff of building incrementally with a documented chatlog: later prompts can be short because the work they reference is already named and understood.
The prompt does not specify how to wire the puzzles (static HTML options vs. dynamic population, flat list vs. optgroup, how to handle duplicate labels). Leaving implementation details to the AI is appropriate here because there is a clearly better approach — dynamic population — that can be justified rather than requiring the user to specify it.
Three changes were required in sudokuSPARK.html:
1. Script tag — load order matters:
<script src="sudokuCustomPuzzles.js"></script> was added immediately before the inline <script> block. This order is non-negotiable: the inline script’s init() reads sudokuCustomPuzzles by name on page load. If the inline block parses before sudokuCustomPuzzles.js loads, the variable is undefined at the exact moment it is needed.
2. Dynamic dropdown population in init():
After the existing validateLink block, a loop over sudokuCustomPuzzles builds an <optgroup label="Live Puzzles"> and appends one <option> per puzzle. Each option’s value is the puzzle’s id (the timestamp); its visible text is the label plus a running counter — so two puzzles both labeled Live-Medium become Live-Medium 1 and Live-Medium 2 automatically. The entire block is guarded by typeof sudokuCustomPuzzles !== 'undefined' so the page functions correctly even if the data file is absent or empty.
3. Custom puzzle lookup in loadDemo():
The existing else brd = b1 fallback was replaced with a search loop: if the selected dropdown value matches a puzzle id in sudokuCustomPuzzles, that puzzle’s board is loaded with a deep copy (.map(row => row.slice())). The deep copy is required: brd is mutated during play and by the AI solver. Without copying, entering a digit would permanently alter the stored puzzle data in sudokuCustomPuzzles, making it unreloadable mid-session. If no match is found — which should not happen in normal use — b1 remains the fallback.
Two approaches existed for adding the custom puzzles to the dropdown:
- Static HTML options — add
<option>elements directly in the HTML for each puzzle. Simple, but requires editing the HTML every time a puzzle is added tosudokuCustomPuzzles.js. Two file changes for one new puzzle — easy to forget one. - Dynamic population in
init()— read the array on page load and build the options in JavaScript. The HTML never changes; only the data file changes when puzzles are added.
Dynamic population is the right choice because sudokuCustomPuzzles.js is explicitly designed to grow — its own header comment says “paste generated puzzle objects here.” Every future puzzle addition should require exactly one file change. Making data-driven UI updates automatic is the correct use of JavaScript.
The <optgroup label="Live Puzzles"> provides visual separation between the fixed built-in demos and the user-generated puzzles. A student glancing at the dropdown can immediately see which category they are selecting from. Without the optgroup, the two types would merge into an undifferentiated list that grows with every generated puzzle.
Browser scripts execute in document order. An inline <script> block runs when the browser reaches it — not after all scripts on the page have loaded. If the inline block references a variable declared in a separate file, that file must appear earlier in the HTML.
The loading sequence in sudokuSPARK.html after this change:
- Bootstrap JS bundle (CDN)
sudokuCustomPuzzles.js— declaresvar sudokuCustomPuzzles = […]- Inline
<script>block — definesinit()andloadDemo(), which both referencesudokuCustomPuzzles ../scripts/linkGuard.js
The typeof sudokuCustomPuzzles !== 'undefined' guard adds a second layer of safety. It handles the edge case where the data file fails to load (network error, file renamed, path typo). Without the guard, a missing file would throw a ReferenceError on page load that silently breaks the entire game. With the guard, the game loads and plays normally — the Live Puzzles optgroup simply does not appear.
This defensive pattern — load the data, guard against its absence — applies whenever a page has optional external data. Make the page fully functional without the data; treat the external file as an enhancement, not a hard dependency.
We adjusted the sample game menu in the sudokuSpark page. That menu should be the same in the aiSolveSudokuSpark page too, for consistency, right?
The user frames the request as a consistency question (“for consistency, right?”) rather than a feature request. This is the right instinct: when two pages in the same ecosystem share a UI control for the same purpose, they should behave identically. A student who generates a puzzle, loads it in the player, and then opens the solver should find the same puzzle available in the solver dropdown — without generating it again.
The rhetorical question form (“right?”) is also appropriate: the user already knows the answer and is confirming it. Asking for confirmation before making a symmetric change is good practice. It catches cases where the two pages are superficially similar but the change does not actually belong in both.
The same three changes applied to sudokuSPARK.html were applied to aiSolveSudokuSPARK.html:
<script src="sudokuCustomPuzzles.js"></script>added before the inline script block- Dynamic dropdown population loop added to
init(), inserted before the existingif (window.aiSolverInit) window.aiSolverInit();call - Custom puzzle search loop replacing
else brd = b1inloadDemo()
The only structural difference from the player: the solver’s init() ends with a call to window.aiSolverInit() that the player does not have. The dropdown population was inserted before that call so the DOM element exists when aiSolverInit() wires its event listeners. The result is identical behavior in both dropdowns: the “Live Puzzles” optgroup appears beneath the built-in demos, and selecting a custom puzzle loads it via the same deep-copy lookup.
The player and solver both have a demoMenu dropdown, a Load button, and a loadDemo() function that reads the same brd variable. They serve the same user need: loading a puzzle onto the board. When a new capability is added to that control, it belongs in both pages unless there is a specific reason to exclude it from one.
The maintenance benefit compounds over time: the dynamic population code reads sudokuCustomPuzzles.js at runtime. Every future puzzle added to that file will appear in both dropdowns automatically, on the next page load, with no HTML changes to either page. If only the player had been updated, every future puzzle addition would also require a separate update to the solver HTML — an easy thing to forget.
The general rule: if two UI controls serve the same purpose, a change to one is almost always a change to both. The exception is when the pages have fundamentally different user contexts that justify different behavior. The player and solver do not — both need the same puzzle data available to load.
For all of the web pages that feature a puzzle, we need a media query that prevents the puzzle from showing when the browser width is less than 390px. As we did for the bingo app earlier, we need a notification for the user to either rotate to landscape view or use a larger device should those smaller environments be encountered.
This prompt names the pattern (“as we did for the bingo app”), defines the scope (“all pages that feature a puzzle”), and specifies the exact breakpoint (390 px). All three components are present, so the AI can execute the change without clarifying questions.
The scope definition — “pages that feature a puzzle” — excludes sudokuIndex.html, sudokuRulesStrategies.html, and sudokuChatlog.html (navigation, reference, and documentation pages respectively). The five qualifying pages are sudokuSPARK.html, aiSolveSudokuSPARK.html, aiCreateSudoku.html, playCustomSudokuPuzzle.html, and solveCustomSudoku.html — all five have a 360 px game board in their app section.
Two changes per page, applied to all five puzzle pages:
CSS (added to each page’s inline <style> block):
.sudoku-mobile-warning— hidden by default; centered, red, bold, generous padding.@media (max-width: 389px)— shows the warning and hides#appSectionwith!important. ForplayCustomSudokuPuzzle.htmlandsolveCustomSudoku.html,#puzzleInfoBaris also hidden, because it sits between the hero and the app section on those two pages and would otherwise remain visible on narrow screens.
HTML (inserted between hero and app section in each page):
- A
<p class="sudoku-mobile-warning">with a Font Awesome rotate icon and the message: “Sudoku requires a larger screen or landscape orientation. Please rotate your device or switch to a tablet or desktop.”
The class name sudoku-mobile-warning is consistent across all five pages. The warning element is placed between the hero section and the first content section, matching the Bingo pattern exactly.
The Bingo hide-and-warn breakpoint was 579 px because the bingo card’s 5×5 table, with a 60 px minimum cell width, requires approximately 400 px for the card alone, plus the optional cage panel and container padding.
The Sudoku board is defined by width: 360px — a fixed pixel value, not a percentage. Bootstrap’s default container padding adds 12 px on each side, putting the practical minimum viewport at 360 + 24 = 384 px. At 390 px there is a 3 px margin on each side — tight but functional. Below 390 px (the width of a typical compact phone, e.g., iPhone SE at 375 px), the board overflows or compresses in ways that break gameplay and legibility.
The general principle: the mobile breakpoint should be derived from the content’s minimum usable size. For Bingo, that minimum was driven by touch-target requirements. For Sudoku, it is driven by the fixed board pixel width. Choosing breakpoints based on actual content constraints produces a more defensible result than picking a round number.
Both breakpoints follow the same one-pixel-below convention: the stated threshold (“less than 390px” / “less than 580px”) maps to a CSS max-width: 389px / max-width: 579px query, so a browser exactly at the threshold still gets the full game.
When a player is playing a demo game or a custom puzzle, interruptions can occur. We need a way to managing games that are currently ‘in play’ and I could see a need to clear out (delete) a former game. When/if that happens, we need to alert the ‘Play Custom Puzzle’ feature that a custom puzzle is unavailable (a modal perhaps?) and then proceed to where a custom puzzle can be created. What is the best management technique in this case?
The prompt identifies two distinct problems and correctly frames them as related. They are related: both are about the lifecycle of a custom puzzle session — what happens when a session is in progress (autosave) and what happens when a session cannot start (missing puzzle modal). Grouping them is appropriate because they share the same data layer (localStorage) and the same user workflow (Generate → Play → possibly resume or restart).
“A modal perhaps?” is a prompt pattern worth noting: offering a tentative suggestion rather than prescribing a solution. It signals “I have a heuristic for what good looks like here” without locking the implementation. This is the right level of specificity — enough to guide the solution without over-constraining it.
The two problems required changes to playCustomSudokuScript.js and playCustomSudokuPuzzle.html:
Problem 1 — Game progress is lost on navigation.
Solution: localStorage autosave. After every digit placement or erasure, autosave() writes the current board state and puzzle ID to localStorage('tntCustomPuzzleProgress'). On the next load, restoreProgress() checks whether a matching save exists (puzzle IDs must match) and restores the player’s digit entries into all non-given cells. The Reset Puzzle button calls clearProgress() before restoring given cells, so a deliberate reset also clears the autosave — future resumptions start fresh.
Problem 2 — “No puzzle” is a static div, not a blocking message.
showNoData() was updated to trigger a Bootstrap modal (#noPuzzleModal) with data-bs-backdrop="static" and data-bs-keyboard="false". The modal is non-dismissible: the user cannot close it by clicking the backdrop or pressing Escape. Two action buttons: Generate a Puzzle (navigates to the generator) and Sudoku Home (navigates to the index). The existing #noDataMessage div is retained as a fallback for environments where Bootstrap’s modal JS is unavailable.
What was not implemented for this session: autosave for the built-in demo games in sudokuSPARK.html. Demo game autosave would require storing the selected demo value alongside the board state, and is straightforward to add using the same pattern — it is a flagged next step.
Three storage mechanisms were available:
- URL parameters — unworkable. An 81-cell board would produce an enormous URL and expose board state visibly in the address bar. Bookmarking would create a fragile and unintended restore mechanism.
- sessionStorage — too ephemeral. Cleared when the tab closes. A player interrupted mid-game who reopens their browser would lose progress — wrong durability for a puzzle that might take 30–60 minutes.
- localStorage — the correct choice. Persists across navigation, tab closures, and browser restarts. The same durability level as the puzzle definition key (
tntCustomPuzzle), which ensures consistent behavior.
The puzzle ID validation — progress.puzzleId !== currentPuzzleId — guards a subtle data-integrity case: if the user generates a new puzzle (overwriting tntCustomPuzzle) while old progress exists, the IDs will not match and the stale progress is silently discarded. Without this check, the wrong progress would be applied to the wrong puzzle.
The original #noDataMessage was a div that hid the app section and showed an inline message. A non-dismissible modal is better for three reasons:
- Intent clarity: A modal communicates “the page cannot proceed without your action” more clearly than an inline message. The player understands immediately that this is a blocking state, not a warning they can scroll past.
- Non-dismissibility:
backdrop="static"andkeyboard="false"mean the modal cannot be closed without choosing a destination. There is no valid action on the play page without a puzzle. Removing the ability to dismiss removes the ability to make a choice that has no meaning. - Actionable exits: The modal provides two meaningful destinations: the generator (to create a puzzle) and the index (to go somewhere else entirely). The old div provided only a single “Go to Generator” link embedded in a warning message — a weaker UX that required reading before acting.
The #noDataMessage div is retained as a CSS-hidden fallback. In normal operation it will never appear; if Bootstrap’s modal JS fails to load, showNoData() falls back to showing the div so the page is never completely unusable.