<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Spelling Bee Solver Engineering]]></title><description><![CDATA[Spelling Bee Solver Engineering]]></description><link>https://spellingbeesolver.hashnode.dev</link><generator>RSS for Node</generator><lastBuildDate>Mon, 26 Oct 2026 21:18:08 GMT</lastBuildDate><atom:link href="https://spellingbeesolver.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Two-Pass Word Matching: Making Song-Title Feedback Consistent]]></title><description><![CDATA[A title-guessing puzzle becomes useful when a wrong answer gives the player a next step. “Wrong song” ends the conversation. Word-level feedback can…]]></description><link>https://spellingbeesolver.hashnode.dev/two-pass-word-matching-making-song-title-feedback-consistent</link><guid isPermaLink="true">https://spellingbeesolver.hashnode.dev/two-pass-word-matching-making-song-title-feedback-consistent</guid><dc:creator><![CDATA[Ethan Lee]]></dc:creator><pubDate>Wed, 14 Oct 2026 21:05:18 GMT</pubDate><content:encoded><![CDATA[<p>A title-guessing puzzle becomes useful when a wrong answer gives the player a next step. “Wrong song” ends the conversation. Word-level feedback can instead say: keep this word, move that one, and stop looking for another copy of a word already accounted for.</p>
<p>The tricky part is repeated words. A simple membership check—“does the answer contain this word?”—can give misleading clues because it treats one occurrence as an unlimited supply.</p>
<h2>Define the unit before assigning colors</h2>
<p>For Song Wordle, the unit is a whole word in a song title, not an individual letter. Each submitted guess is a complete title selected from the game's collection. The server validates that title before calculating feedback; arbitrary invented titles are not playable guesses.</p>
<p>Keep the original spelling for display and use normalized words for comparison. The current implementation splits on whitespace, ignores punctuation-only pieces, and compares words after lowercasing and removing punctuation and common accent marks. This makes formatting differences less distracting without claiming to correct misspellings.</p>
<p>The three results have separate meanings:</p>
<ul>
<li><strong>Correct:</strong> this occurrence matches the same position.</li>
<li><strong>Present:</strong> an unmatched occurrence exists elsewhere.</li>
<li><strong>Absent:</strong> no unmatched occurrence remains for this guess word.</li>
</ul>
<p>That last definition matters more than the color itself.</p>
<h2>Reserve exact matches first</h2>
<p>Consider the fictional answer <strong>“Moon River Moon.”</strong> These made-up titles illustrate the algorithm; they are not claims about playable entries.</p>
<table>
<thead>
<tr>
<th>Fictional guess</th>
<th>Feedback, from left to right</th>
</tr>
</thead>
<tbody><tr>
<td>Moon Moon Moon</td>
<td>Correct · Absent · Correct</td>
</tr>
<tr>
<td>River Moon River</td>
<td>Present · Present · Absent</td>
</tr>
</tbody></table>
<p>In the first row, the first and last words reserve both answer occurrences of “Moon.” The middle guess receives no match. Marking it present would imply an extra occurrence that the answer does not have.</p>
<p>In the second row, no position matches exactly. The first “River” and “Moon” each consume an available occurrence. The final “River” cannot reuse the occurrence already assigned to the first.</p>
<p>A clear implementation uses two passes and a frequency map. This independent pseudocode assumes both inputs are already normalized word arrays:</p>
<pre><code class="language-text">marks = one "absent" result per guess word
available = empty frequency map (missing counts are zero)

for each answer position i:
    if guess[i] equals answer[i]:
        marks[i] = "correct"
    else:
        available[answer[i]] += 1

for each guess position i:
    if marks[i] is "correct": continue
    if available[guess[i]] &gt; 0:
        marks[i] = "present"
        available[guess[i]] -= 1

return marks
</code></pre>
<p>The first pass removes exact matches from the available pool. The second spends each remaining occurrence at most once. With a hash map, processing is linear in the combined number of answer and guess words; map storage follows the number of distinct unmatched answer words.</p>
<p>The project currently uses an array of unmatched answer words rather than this map. Consuming an array entry implements the same matching rule; the map makes the counting idea easier to see.</p>
<h2>Turn feedback into the next guess</h2>
<p>Keep correct words in their positions. Move present words into other positions. Treat an absent duplicate carefully: it rules out an additional occurrence, not necessarily the word everywhere. In the second example, “River” is still required even though another “River” is marked absent.</p>
<p>Then combine those constraints with the audio clue and available title choices. The algorithm narrows a search; it does not identify a recording by itself.</p>
<p>I use these rules in <a href="https://guesssonggame.com/">Guess Song Game</a>. Disclosure: I’m Ethan Lee, part of the project. This article was prepared with AI assistance and checked against the current matching implementation.</p>
]]></content:encoded></item><item><title><![CDATA[Putting Safety Checks Around an Asynchronous Image Job]]></title><description><![CDATA[How an image job coordinates input and output moderation, atomic reservations, idempotent retries, and private result delivery.]]></description><link>https://spellingbeesolver.hashnode.dev/putting-safety-checks-around-an-asynchronous-image-job</link><guid isPermaLink="true">https://spellingbeesolver.hashnode.dev/putting-safety-checks-around-an-asynchronous-image-job</guid><category><![CDATA[JavaScript]]></category><dc:creator><![CDATA[Ethan Lee]]></dc:creator><pubDate>Tue, 13 Oct 2026 15:55:33 GMT</pubDate><content:encoded><![CDATA[<p>A photo styling job has several useful checkpoints: the input is ready, the generation provider has accepted the request, a usable image has arrived, and the result has passed its own safety check. Keeping those checkpoints distinct makes a small image tool easier to reason about.</p>
<p>I work on <a href="https://aiphotostyler.com/">AI Photo Styler</a>, a browser tool with three eighties-inspired photo styles. Its backend combines fal.ai for generation with SeeAPI for image moderation. The interesting engineering work is in the orchestration around those services: ordering the checks, handling uncertain responses, and giving each request one consistent outcome.</p>
<h2>Reserve the job before calling either service</h2>
<p>The application reserves a job before starting its input safety check. That reservation gives the work an identity and accounts for the allowance it occupies. It also keeps concurrent requests from each acting on an outdated view of the remaining quota.</p>
<p>The database makes the decision in a single conditional insert. The insert checks the relevant quota and concurrency limits, then creates the job only if those conditions hold. A unique combination of session ownership and request ID catches a duplicate racing in from another browser tab or worker instance.</p>
<p>A read-only quota check helps the interface respond quickly, but the insert is the final authority. This separation is useful anywhere an external request consumes a limited resource: an early check improves the response, while the atomic write protects the accounting.</p>
<h2>Put moderation on both sides of generation</h2>
<p>Once a new job is reserved, the backend checks the input through SeeAPI. Only a successful input check allows the fal generation request to begin. After fal returns a decoded image, a separate output check must pass before the application marks the job complete.</p>
<p>The order can be expressed with this simplified outline; the real implementation also handles leases and state transitions:</p>
<pre><code class="language-js">const reservation = await reserveJob(input);
if (reservation.reused) return reservation.job;

await checkInput(input.image, reservation.job.id);
const receipt = await submitGeneration(input);
await saveProviderReceipt(reservation.job.id, receipt);

// Later, while polling the same job:
const result = await pollGeneration(receipt);
if (result.status === 'complete') {
  await checkOutput(result.imageUrl, reservation.job.id);
  await markComplete(reservation.job.id, result.imageUrl);
}
</code></pre>
<p>That final check has its own purpose. An accepted source photograph and a generated image are different inputs to the moderation service. Treating them separately keeps the publication decision attached to the actual result the visitor will receive.</p>
<h2>Accept a precise moderation response</h2>
<p>The moderation adapter creates an inference, then polls the same task until it completes within a deadline. It checks the task identity, expected model and endpoint, provider, completion status, result type, and category structure.</p>
<p>The safety flag must be a Boolean. A missing field, <code>null</code>, or the string <code>"false"</code> is a malformed response rather than an approval. The application requires the expected completed response with <code>flagged: false</code> before proceeding.</p>
<p>This is a useful boundary for integrations: validate the response contract before interpreting its content. A JSON response alone does not establish that the requested operation finished. An HTTP error, mismatched task ID, or incomplete result stays outside the successful path.</p>
<h2>Keep known failures separate from uncertainty</h2>
<p>An input moderation failure happens before any generation submission. The application can end that job and release the visitor's reserved photo allowance.</p>
<p>A lost response during the generation POST requires a different decision. The provider may already have accepted the job. The backend records an uncertain submission and keeps its reservation while the outcome is unresolved. Replaying the original request returns the same job instead of issuing another generation POST.</p>
<p>The request ID is also bound to the image payload and selected style. Reusing it for a different payload is a conflict. This makes retries predictable: a retry continues the same work, while a different photo is a new operation.</p>
<p>Definite failures release the visitor's slot while retaining the attempt accounting used for service limits. That distinction keeps the user allowance and the application's external-service budget consistent.</p>
<h2>Let one poller update the result</h2>
<p>Polling uses a database lease, so simultaneous status requests do not all perform the same provider work. The lease belongs to a particular job and session, has a bounded lifetime, and is released with a matching token.</p>
<p>A failed status GET does not trigger another generation POST. The next status check can continue observing the existing request. This keeps a temporary read failure from turning into a second write operation.</p>
<p>The application exposes a result only after the job reaches its completed state. The browser receives an application image route associated with its session; the public job response does not expose the provider's media URL. Image responses use private, no-store caching.</p>
<h2>Test the boundaries, not just the happy path</h2>
<p>The focused local moderation, job-store and worker test suites completed 185 tests successfully. These use intercepted provider responses and local database fixtures, so they exercise application behavior without spending generation credits.</p>
<p>The checks include malformed moderation payloads, mismatched task IDs, rejected input, rejected output, uncertain submissions, duplicate requests and concurrent reservations. One integration test checks the actual request order: input moderation, generation submission, then output moderation. It also confirms that repeating the request does not repeat either check or the generation.</p>
<p>These tests verify the orchestration contract. Moderation model accuracy and live provider behavior are separate checks. For a developer building a similar workflow, the reusable idea is to give each transition a precise meaning: reserve the work, validate the input, submit once, observe the same job, check the output, and then make the result available.</p>
]]></content:encoded></item><item><title><![CDATA[Inside a browser-side SVG tracing pipeline]]></title><description><![CDATA[How local SVG tracing combines canvas pixels, transferable buffers, WebAssembly workers, progress updates and output validation.]]></description><link>https://spellingbeesolver.hashnode.dev/inside-a-browser-side-svg-tracing-pipeline</link><guid isPermaLink="true">https://spellingbeesolver.hashnode.dev/inside-a-browser-side-svg-tracing-pipeline</guid><category><![CDATA[WebAssembly]]></category><category><![CDATA[SVG]]></category><dc:creator><![CDATA[Ethan Lee]]></dc:creator><pubDate>Mon, 12 Oct 2026 05:04:48 GMT</pubDate><content:encoded><![CDATA[<p>Tracing an image into SVG involves several different jobs: decoding a file, grouping its colors, finding shapes, fitting paths, and presenting a result someone can inspect. WebAssembly handles the tracing computation in this implementation, while ordinary browser APIs handle the surrounding workflow.</p>
<p>The example here comes from JPG to SVG. Its conversion engine uses VisionCortex’s <a href="https://github.com/visioncortex/vtracer">VTracer</a>, packaged as a local WebAssembly asset and a module worker. The interesting implementation details are the boundaries between those pieces: when memory is allocated, which buffer is transferred, and what happens when a job ends.</p>
<h2>Validate dimensions before decoding</h2>
<p>Compressed file size tells only part of the memory story. A 2,000 × 2,000 image needs 16 million bytes for a single RGBA pixel buffer, before canvases, decoded images, and tracing data are considered.</p>
<p>The loader reads the file bytes and inspects the image signature and dimensions before calling the browser decoder. It accepts supported JPEG, PNG, and still WebP inputs within three separate limits: 20,000,000 file bytes, 4,000,000 pixels, and 8,192 pixels along either edge.</p>
<p>After decoding, dimensions are checked again. The image is drawn into a canvas, and <code>getImageData()</code> supplies the RGBA array. The loader also records whether any pixels are transparent or partially transparent. Those flags later help the interface explain how to inspect soft edges.</p>
<p>This stage still involves main-thread canvas work. Moving tracing into a worker does not make every part of image preparation asynchronous.</p>
<h2>Make buffer ownership explicit</h2>
<p>The usual tracing path creates a worker for each call; a separate one-pixel fast path can return a rectangle directly. Here is the worker message handoff from the existing engine function, with surrounding validation and error handlers omitted:</p>
<pre><code class="language-ts">worker = new Worker('/vendor/vtracer/worker.js', {
  type: 'module',
  name: 'local-vtracer'
});

worker.postMessage({
  type: 'load',
  width: work.width,
  height: work.height,
  pixels: work.pixels.buffer
}, [work.pixels.buffer]);

worker.postMessage({
  type: 'trace',
  job,
  options: mapSettings(effective)
});
</code></pre>
<p>The second argument to the first <code>postMessage()</code> transfers ownership of the working buffer. The sending side must treat that buffer as detached afterward.</p>
<p>The engine prepares a separate working pixel array before this handoff, so it can retain the original image for another attempt. This is not a claim that the entire pipeline avoids copying: the JavaScript-to-WASM binding also copies pixels into WebAssembly memory. Transferable buffers address one specific boundary.</p>
<h2>Let the worker report actual work</h2>
<p>The worker loads its sibling WASM binary from the same origin and constructs the tracer from the supplied dimensions and pixels. It then starts color clustering and path generation.</p>
<p>During tracing, a loop calls the engine’s <code>tick()</code> method and gathers the returned SVG markup. It aims to batch this work within roughly 25-millisecond intervals, yielding through a <code>MessageChannel</code> between batches. An individual tick can take longer, so that interval is a scheduling target, not a timing guarantee.</p>
<p>The worker sends separate messages for clustering progress, emitted shapes, completion, and errors. The interface can therefore draw paths that actually exist. Loading has an indeterminate state; clustering and tracing each report their own progress. Combining them into a single smooth percentage would require a weighting model the implementation does not have.</p>
<h2>Keep previews separate from final output</h2>
<p>For parameter previews, the engine normally reduces the longest image edge to 480 pixels. Formal conversions use the full accepted input dimensions. Very narrow images are an exception to preview resizing, because rounding their short side up to one pixel could distort their proportions.</p>
<p>The two controls map to tracing parameters. Color precision affects retained color information; a value of six does not mean “produce six colors.” Smoothing selects pixel paths at zero and spline paths above zero, with a corresponding simplification tolerance.</p>
<p>A reduced preview helps choose settings, but the full result deserves its own inspection, especially around narrow gaps, small lettering, and partially transparent edges.</p>
<h2>Finish with validation and cleanup</h2>
<p>For worker-based traces, the final SVG is parsed before becoming the accepted result. This implementation requires at least one path, permits only <code>svg</code>, <code>g</code>, and <code>path</code> elements, and rejects event-handler attributes and resource references. It also sets dimensions and the <code>viewBox</code> from the traced image. That is an output check for this known producer, not a general-purpose sanitizer for arbitrary SVG uploads.</p>
<p>For these jobs, completion, cancellation, and the default 60-second timeout all terminate the worker. When the source image is no longer needed, its object URL is revoked and its retained pixels are released. Giving each job a clear lifetime makes cancellation meaningful and avoids leaving obsolete tracing work running.</p>
<p>You can explore the resulting workflow in <a href="https://jpgtosvg.io/">JPG to SVG</a>. Image pixels stay in the browser during conversion; the engine asset itself is downloaded like other site resources. When investigating a slow job or an unexpected result, follow those pixels through decoding, canvas readback, worker transfer, WASM memory, and the final SVG. Each boundary has a different job to do.</p>
<p><em>Disclosure: I’m affiliated with JPG to SVG, the project discussed here.</em></p>
]]></content:encoded></item><item><title><![CDATA[Inside Krillion Answers: Complete Saved Pools and Exact Scores]]></title><description><![CDATA[I built Krillion Answers to make saved official game data easy to explore. It is my free, independent companion for the Krillion word-and-knowledge game…]]></description><link>https://spellingbeesolver.hashnode.dev/inside-krillion-answers-complete-saved-pools-and-exact-scores</link><guid isPermaLink="true">https://spellingbeesolver.hashnode.dev/inside-krillion-answers-complete-saved-pools-and-exact-scores</guid><category><![CDATA[JavaScript]]></category><dc:creator><![CDATA[Ethan Lee]]></dc:creator><pubDate>Sun, 11 Oct 2026 09:57:07 GMT</pubDate><content:encoded><![CDATA[<p>I built <a href="https://krillionanswers.net/">Krillion Answers</a> to make saved official game data easy to explore. It is my free, independent companion for the Krillion word-and-knowledge game. You can browse answers, choose a prompt, and check an answer's recorded rarity and score without creating an account.</p>
<p>The product brings three useful things together: readable answer cards, a checker that searches the complete saved pool for a selected prompt, and dated archives. Each question card presents three top official picks with exact scores and a copy button. The checker gives you a way to explore your own answer, while the separate full-score collection groups every saved 100-point answer by question.</p>
<p>Keeping the exact score beside the answer was an important design choice. The recorded tiers span 10, 15, 30, 60, 85 and 100 points. Showing the tier and points together makes it easy to distinguish an accepted answer from a full-score pick. When the official record includes a quip, the card displays that too, adding the source's own commentary to the card.</p>
<p>Underneath, the checker uses a deterministic lookup. Each request identifies the game date, prompt and snapshot version. The server loads that prompt's full saved answer pool and matches the input against recorded answers and official aliases. This keeps the lookup tied to the exact question being checked.</p>
<p>I also kept text normalization simple and predictable. The checker trims surrounding spaces, normalizes Unicode to NFC and ignores case. An input such as <code>Piano</code> becomes <code>piano</code> for matching. The returned result preserves the saved answer text, rarity tier and exact score. That gives the interface a direct connection to the underlying record.</p>
<p>The publishing pipeline gives those records a clear history. Before publishing, it validates the date, all seven prompt identities, and each answer's text, tier and score. It saves the original source alongside the processed data. A SHA-256 content hash identifies each snapshot, and a D1 transaction publishes the snapshot and its publication pointers together. The seven question pools belong to one complete version.</p>
<p>Dates are visible throughout the experience. Saved reveals carry their official game date, and the archive opens the days the site has collected. The checker sends the page's version with each request, keeping the question and answer pool connected. Source revisions receive their own version, while unchanged data retains its existing saved-update timestamp.</p>
<p>What I like about this implementation is how the engineering supports the product directly. Complete pools make the checker useful beyond the displayed picks. Exact scores make comparisons clear. Saved dates, source records and versions make each result traceable. All of that sits behind a straightforward flow: choose a prompt, enter an answer, and read its saved official score.</p>
]]></content:encoded></item><item><title><![CDATA[Filtering Seven-Letter Word Puzzles with JavaScript Bitsets]]></title><description><![CDATA[A seven-letter word puzzle can be described as a small set of constraints. A candidate must contain at least four letters, include a required center…]]></description><link>https://spellingbeesolver.hashnode.dev/filtering-seven-letter-word-puzzles-with-javascript-bitsets</link><guid isPermaLink="true">https://spellingbeesolver.hashnode.dev/filtering-seven-letter-word-puzzles-with-javascript-bitsets</guid><dc:creator><![CDATA[Ethan Lee]]></dc:creator><pubDate>Thu, 08 Oct 2026 09:48:55 GMT</pubDate><content:encoded><![CDATA[<p>A seven-letter word puzzle can be described as a small set of constraints. A candidate must contain at least four letters, include a required center letter, and use only the supplied letters. Repeated characters are allowed. A pangram is a candidate that contains all seven different letters. These constraints are useful to separate from any particular dictionary: the rules decide which entries fit, while the dictionary decides which entries are available to search.</p>
<p>This tutorial uses A, C, E, L, N, P, and T, with A in the center. It is a practice set, not an official daily puzzle. The implementation below follows the same mask and subset approach used in the project I maintain, with a tiny sample list replacing the stored dictionary index.</p>
<h2>Represent distinct letters, not quantities</h2>
<p>Assign each lowercase letter a bit position: A uses position zero, B position one, and so on. Combining the bits produces one integer describing the different letters present in a word. English A–Z needs 26 positions, which fit within JavaScript’s 32-bit bitwise operations.</p>
<p>The important detail is that setting an already-set bit changes nothing. PALACE contains A twice, but its mask contains the A bit once. That matches the puzzle rule: repeated letters are permitted rather than deducted from a limited inventory. Word length remains a separate value, so the mask does not accidentally turn a six-letter word into a five-letter word for scoring.</p>
<p>For a candidate to fit, its set of bits must be a subset of the puzzle’s bits and contain the center bit. A direct filter could check those two conditions for every dictionary entry. Here, we first group entries by their distinct-letter mask, then retrieve eligible groups.</p>
<h2>A small runnable example</h2>
<p>Run this JavaScript in a modern browser console or Node.js. It has no dependencies or network requests. The supplied puzzle is hardcoded and valid; input validation for an interactive form is discussed afterward.</p>
<pre><code class="language-js">const words = ["pace", "plane", "palace", "placenta", "peel", "cat", "panic"];
const letters = "acelnpt";
const center = "a";

function maskFor(text) {
  let mask = 0;
  for (const c of text) mask |= 1 &lt;&lt; (c.charCodeAt(0) - 97);
  return mask;
}

const index = new Map();
for (const word of words) {
  if (!/^[a-z]{4,}$/.test(word) || new Set(word).size &gt; 7) continue;
  const mask = maskFor(word);
  if (!index.has(mask)) index.set(mask, []);
  index.get(mask).push(word);
}

const allowed = maskFor(letters);
const centerBit = maskFor(center);
const found = [];
for (let subset = allowed; subset; subset = (subset - 1) &amp; allowed) {
  if (subset &amp; centerBit) found.push(...(index.get(subset) ?? []));
}

const results = [...new Set(found)].sort().map(word =&gt; {
  const pangram = maskFor(word) === allowed;
  return { word, pangram, points: (word.length === 4 ? 1 : word.length)
    + (pangram ? 7 : 0) };
});
console.log(JSON.stringify(results, null, 2));
</code></pre>
<p>The resulting words are PACE, PALACE, PLACENTA, and PLANE. PEEL disappears because it lacks A. CAT is removed while building the index because it is too short. PANIC contains I, so its mask cannot be reached by enumerating subsets of the supplied letters. PLACENTA is the sole pangram in this sample: its eight letters cover all seven different letters, with A reused.</p>
<p>The displayed point values are 1, 6, 15, and 5 respectively. Four-letter words receive one point; longer words receive their length; a pangram adds seven. These are rule-based calculations for illustrative candidates, not confirmation that a publisher accepts them.</p>
<h2>Keep the index separate from the interface</h2>
<p>The subset expression visits 127 nonempty subsets for a seven-letter mask. The center check removes groups without A. Reading and returning the matched words still depends on group sizes; this is not a benchmark or a promise about loading speed. In the project, the dictionary index is stored ahead of time, so a request does not rebuild it from the full word list.</p>
<p>The result should preserve actual words, not just masks. Multiple words can share a letter set, and repeats can produce different lengths. For hints, count the words by length, first letter, or first two letters. Do not use the number of matching mask groups as the word count. Likewise, a presentation filter that hides four-letter words should clearly distinguish its visible subset from the full puzzle totals.</p>
<h2>Validate inputs and label the source</h2>
<p>An interactive solver needs checks before mask conversion: exactly seven distinct English letters and an unambiguous required center. Our input convention uses one uppercase character, as in Acelnpt. This is a form convention; the puzzle rule itself is that A must appear in every candidate.</p>
<p>I maintain <a href="https://spellingbeesolver.co/">Spelling Bee Solver</a>, an independent dictionary-based tool. Its hints keep word names folded until the user reveals them. Custom searches use SCOWL American English, while daily pages separately display verified published lists when available. Neither bitsets nor a dictionary entry can establish NYT acceptance: the game uses an edited answer list. Keeping that boundary explicit makes both the code and its results easier to understand.</p>
<p><em>The sample was checked against the project code and executed locally.</em></p>
]]></content:encoded></item></channel></rss>