<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Jamie Cole</title>
    <description>The latest articles on DEV Community by Jamie Cole (@jamiecoleai).</description>
    <link>https://dev.to/jamiecoleai</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3443861%2F4e11423b-f62e-4127-86bc-d615c4728112.png</url>
      <title>DEV Community: Jamie Cole</title>
      <link>https://dev.to/jamiecoleai</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/jamiecoleai"/>
    <language>en</language>
    <item>
      <title>Building Reliable AI Generation Workflows: The API Call Is the Easy Part</title>
      <dc:creator>Jamie Cole</dc:creator>
      <pubDate>Thu, 20 Aug 2026 14:11:25 +0000</pubDate>
      <link>https://dev.to/jamiecoleai/building-reliable-ai-generation-workflows-the-api-call-is-the-easy-part-5hd5</link>
      <guid>https://dev.to/jamiecoleai/building-reliable-ai-generation-workflows-the-api-call-is-the-easy-part-5hd5</guid>
      <description>&lt;p&gt;Over the past month, I have been building an AI generation product with asynchronous video tasks.&lt;/p&gt;

&lt;p&gt;I originally expected model integration to be the difficult part.&lt;/p&gt;

&lt;p&gt;It was not.&lt;/p&gt;

&lt;p&gt;Sending a request to an AI provider and receiving a task ID is relatively straightforward. The harder engineering work begins when the provider is slow, the network fails at the wrong moment, the user refreshes the page, or money has already changed hands.&lt;/p&gt;

&lt;p&gt;This article covers the architecture lessons I learned while turning an AI API into a workflow that can recover from uncertainty.&lt;/p&gt;

&lt;h2&gt;
  
  
  The basic workflow
&lt;/h2&gt;

&lt;p&gt;At a high level, an asynchronous generation request looks simple:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Client
  → Generation API
  → Local task record
  → AI provider
  → Polling or webhook
  → Result reconciliation
  → Persistent result
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The happy path is easy:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Validate the request.&lt;/li&gt;
&lt;li&gt;Calculate the cost.&lt;/li&gt;
&lt;li&gt;Deduct credits.&lt;/li&gt;
&lt;li&gt;Submit the provider task.&lt;/li&gt;
&lt;li&gt;Poll until it succeeds.&lt;/li&gt;
&lt;li&gt;Store the result.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Production systems rarely stay on the happy path.&lt;/p&gt;

&lt;p&gt;The interesting problems appear between those steps.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Treat submission as a state machine
&lt;/h2&gt;

&lt;p&gt;A generation task is not simply &lt;code&gt;pending&lt;/code&gt;, &lt;code&gt;success&lt;/code&gt;, or &lt;code&gt;failed&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;There is another important state:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;submission_unknown
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Imagine sending a &lt;code&gt;POST&lt;/code&gt; request to a provider. The provider accepts it and starts the generation, but the response is lost because a gateway times out.&lt;/p&gt;

&lt;p&gt;From the application's perspective, two realities are possible:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the provider never accepted the request;&lt;/li&gt;
&lt;li&gt;the provider accepted it, but the application did not receive confirmation.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Automatically retrying the request may create a second paid generation.&lt;/p&gt;

&lt;p&gt;Treating it as an ordinary failure may refund the user while an expensive provider task continues running.&lt;/p&gt;

&lt;p&gt;The system therefore needs to distinguish a confirmed rejection from an uncertain submission.&lt;/p&gt;

&lt;p&gt;A conceptual state model might look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;GenerationState&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;created&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;submitting&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;pending&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;succeeded&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;failed&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;submission_unknown&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The exact names are not important. The important part is preserving uncertainty instead of forcing every outcome into success or failure.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Give every submission a stable idempotency key
&lt;/h2&gt;

&lt;p&gt;Users double-click buttons. Browsers retry requests. Mobile connections disconnect. UI state gets restored after a refresh.&lt;/p&gt;

&lt;p&gt;The generation endpoint must assume that the same logical submission can arrive more than once.&lt;/p&gt;

&lt;p&gt;Each client submission should carry a stable idempotency key:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;GenerateRequest&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;idempotencyKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On the server, that key should resolve to one local task for one user:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;existingTask&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;findTaskByIdempotencyKey&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;idempotencyKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;existingTask&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;existingTask&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Database uniqueness is still necessary because two requests may pass the initial lookup concurrently.&lt;/p&gt;

&lt;p&gt;A safe flow is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Check for an existing task.&lt;/li&gt;
&lt;li&gt;Attempt to create the task with a unique key.&lt;/li&gt;
&lt;li&gt;If the insert loses a race, load and return the winning task.&lt;/li&gt;
&lt;li&gt;Never deduct credits twice for the same logical submission.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Idempotency is not only protection against double-clicks. It is what makes recovery possible across the entire workflow.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Do not confuse an observation timeout with a task timeout
&lt;/h2&gt;

&lt;p&gt;Polling is an observation mechanism. It is not the task itself.&lt;/p&gt;

&lt;p&gt;A browser may stop polling after thirty seconds, but the provider task can still be running correctly.&lt;/p&gt;

&lt;p&gt;This distinction matters:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Polling window ended ≠ Generation task failed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the client treats every polling timeout as a terminal failure, it may submit another task when the user tries again.&lt;/p&gt;

&lt;p&gt;A better approach is to preserve the original task ID and resume observation later:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;resumeTask&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;taskId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;localTask&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;loadTask&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;taskId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;isTerminal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;localTask&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;localTask&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;observeExistingTask&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;taskId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Refreshing the page, reopening the application, or returning from another device should continue tracking the same task whenever possible.&lt;/p&gt;

&lt;p&gt;The UI can say that observation has paused without claiming that generation has failed.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Make reconciliation shared and idempotent
&lt;/h2&gt;

&lt;p&gt;A task may reach a terminal result through several paths:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;foreground polling;&lt;/li&gt;
&lt;li&gt;a provider webhook;&lt;/li&gt;
&lt;li&gt;scheduled recovery;&lt;/li&gt;
&lt;li&gt;an admin repair operation.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These paths should not contain separate versions of the completion logic.&lt;/p&gt;

&lt;p&gt;They should all call the same reconciliation function:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;reconcileTask&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;taskId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;providerStatus&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ProviderStatus&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;source&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;poll&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;webhook&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;recovery&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Load the current task.&lt;/span&gt;
  &lt;span class="c1"&gt;// Ignore an already-finalized task.&lt;/span&gt;
  &lt;span class="c1"&gt;// Persist the result or failure once.&lt;/span&gt;
  &lt;span class="c1"&gt;// Refund credits at most once.&lt;/span&gt;
  &lt;span class="c1"&gt;// Return the canonical local state.&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Without shared reconciliation, race conditions become likely.&lt;/p&gt;

&lt;p&gt;A webhook may complete the task while foreground polling is still active. Both paths may attempt to save the result or issue a refund.&lt;/p&gt;

&lt;p&gt;Terminal transitions must therefore be idempotent:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;finalizedAt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;task&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In practice, a simple application-level check may not be enough. Database constraints or transactional updates should enforce the invariant.&lt;/p&gt;

&lt;p&gt;The rule is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Many observers may discover the outcome, but only one transition should finalize it.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  5. Calculate billing on the server
&lt;/h2&gt;

&lt;p&gt;The frontend needs to display the expected credit cost, but it must not decide the final charge.&lt;/p&gt;

&lt;p&gt;Generation cost may depend on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;model;&lt;/li&gt;
&lt;li&gt;duration;&lt;/li&gt;
&lt;li&gt;resolution;&lt;/li&gt;
&lt;li&gt;output count;&lt;/li&gt;
&lt;li&gt;generation mode;&lt;/li&gt;
&lt;li&gt;optional capabilities.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The server should parse and validate the real request parameters, then calculate the authoritative cost:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;options&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;parseGenerationOptions&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;creditCost&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;calculateCreditCost&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;duration&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;duration&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;resolution&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;resolution&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;outputCount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;outputCount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The frontend should consume the same shared pricing definitions when possible, but the server must still recompute the charge.&lt;/p&gt;

&lt;p&gt;Otherwise, stale clients or modified requests can create a gap between displayed pricing and actual billing.&lt;/p&gt;

&lt;p&gt;Refunds need the same discipline.&lt;/p&gt;

&lt;p&gt;A refund should be tied to a unique task and transaction reason so that repeated recovery attempts cannot refund the same task twice.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Persist enough information to recover the user experience
&lt;/h2&gt;

&lt;p&gt;Task recovery is not only a backend problem.&lt;/p&gt;

&lt;p&gt;After a refresh, the frontend needs enough information to reconstruct what the user was doing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the original prompt;&lt;/li&gt;
&lt;li&gt;selected model;&lt;/li&gt;
&lt;li&gt;input assets;&lt;/li&gt;
&lt;li&gt;generation settings;&lt;/li&gt;
&lt;li&gt;local task ID;&lt;/li&gt;
&lt;li&gt;current status;&lt;/li&gt;
&lt;li&gt;result metadata.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Persisting only the provider task ID is not enough.&lt;/p&gt;

&lt;p&gt;The provider knows how to generate the output, but it does not necessarily know how your product should present the user's intent.&lt;/p&gt;

&lt;p&gt;A local task record should remain the application's source of truth. Provider state is external evidence used to update that record.&lt;/p&gt;

&lt;p&gt;This separation also prevents internal provider identifiers and implementation details from leaking into the user interface.&lt;/p&gt;

&lt;h2&gt;
  
  
  What changed in my thinking
&lt;/h2&gt;

&lt;p&gt;Before building this workflow, I thought the central engineering problem was model integration.&lt;/p&gt;

&lt;p&gt;Now I see three different layers:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Provider transport&lt;/strong&gt; — submitting and querying external tasks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Product workflow&lt;/strong&gt; — task identity, recovery, billing, and reconciliation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;User experience&lt;/strong&gt; — explaining progress, uncertainty, failure, and recovery clearly.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The provider API only solves the first layer.&lt;/p&gt;

&lt;p&gt;The product is built in the second and third.&lt;/p&gt;

&lt;p&gt;AI tools can make implementation faster, but they also make it easier to add features before the underlying state model is reliable.&lt;/p&gt;

&lt;p&gt;The most valuable work was not adding another model. It was defining what must remain true when several things fail at once.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final takeaway
&lt;/h2&gt;

&lt;p&gt;For asynchronous AI products, reliability comes from preserving identity and uncertainty:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;one logical submission maps to one task;&lt;/li&gt;
&lt;li&gt;an unknown outcome is not silently treated as failure;&lt;/li&gt;
&lt;li&gt;polling timeouts do not recreate work;&lt;/li&gt;
&lt;li&gt;every completion path shares one reconciliation contract;&lt;/li&gt;
&lt;li&gt;billing is authoritative on the server;&lt;/li&gt;
&lt;li&gt;refunds and terminal transitions are idempotent;&lt;/li&gt;
&lt;li&gt;the UI resumes existing work instead of submitting again.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The API call may be the visible feature.&lt;/p&gt;

&lt;p&gt;The recovery model is the real product.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Disclosure: This article is based on my own implementation experience and was reviewed against the system's actual behavior. AI was used to help organize the structure and edit the English draft.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>saas</category>
      <category>productivity</category>
    </item>
    <item>
      <title>How to Keep Characters Consistent in AI Videos: A Reference-First Workflow</title>
      <dc:creator>Jamie Cole</dc:creator>
      <pubDate>Mon, 10 Aug 2026 07:18:10 +0000</pubDate>
      <link>https://dev.to/jamiecoleai/how-to-keep-characters-consistent-in-ai-videos-a-reference-first-workflow-4il8</link>
      <guid>https://dev.to/jamiecoleai/how-to-keep-characters-consistent-in-ai-videos-a-reference-first-workflow-4il8</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1nc322560rdross62687.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1nc322560rdross62687.webp" alt="Two consistent AI video characters dancing together in a rain-soaked street" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Shot one looks right. Shot two is polished too, but the lead character now has different eyes, a wider jaw, and a jacket that changed color. Each clip works on its own. Put them together, and the story falls apart.&lt;/p&gt;

&lt;p&gt;Character consistency in AI video is not mainly a matter of writing a longer prompt. It is a workflow problem. If every shot begins with a fresh text description, the model has room to invent a fresh person. A reference-first workflow reduces that room before motion generation begins.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Short Answer
&lt;/h2&gt;

&lt;p&gt;To keep a character recognizable across AI video shots:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Define the identity details that must not change.&lt;/li&gt;
&lt;li&gt;Approve one clean master reference before generating video.&lt;/li&gt;
&lt;li&gt;Add only the extra angles or expressions a shot genuinely needs.&lt;/li&gt;
&lt;li&gt;Use image or reference inputs instead of rebuilding the character from text.&lt;/li&gt;
&lt;li&gt;Write prompts about action, camera, and environment rather than redesigning the person.&lt;/li&gt;
&lt;li&gt;Generate short, controlled shots and review identity before moving on.&lt;/li&gt;
&lt;li&gt;Save successful frames as tested anchors for later scenes.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The goal is not perfect pixel-level duplication. The goal is a character your audience recognizes from shot to shot without stopping to wonder whether the actor changed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Characters Drift Between AI Video Shots
&lt;/h2&gt;

&lt;p&gt;A text prompt describes a type of person. A reference image shows a specific person.&lt;/p&gt;

&lt;p&gt;That difference matters. If you prompt for “a woman with short dark hair in a navy jacket” five times, the model may satisfy the sentence with five different faces. Even a detailed description leaves many visual decisions open: jaw shape, eye spacing, hairline, proportions, fabric details, and how those features look from another angle.&lt;/p&gt;

&lt;p&gt;Motion makes the problem harder. During a head turn, fast gesture, close interaction, or dramatic camera move, the model must preserve identity while also inventing unseen views and maintaining temporal continuity. The more constraints competing for attention, the easier it is for facial features, clothing, or body proportions to drift.&lt;/p&gt;

&lt;p&gt;This is why “add more appearance details” often disappoints. The better first move is to give the generation a stronger visual anchor.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbkph9ulbtxh8ylugz023.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbkph9ulbtxh8ylugz023.png" alt="The same AI-generated character maintained across different video scenes" width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;A consistent character shown across changing scenes.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Build a Character Anchor Kit Before You Animate
&lt;/h2&gt;

&lt;p&gt;A useful anchor kit is small. You do not need a folder of nearly identical portraits. You need a few approved images in which the character is easy to read.&lt;/p&gt;

&lt;h3&gt;
  
  
  Start with one master reference
&lt;/h3&gt;

&lt;p&gt;Choose a clear front or three-quarter view with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Sharp facial details&lt;/li&gt;
&lt;li&gt;Even lighting&lt;/li&gt;
&lt;li&gt;An unobstructed face and hairline&lt;/li&gt;
&lt;li&gt;A simple background&lt;/li&gt;
&lt;li&gt;One visible character&lt;/li&gt;
&lt;li&gt;The exact hairstyle, outfit, and signature details you want to preserve&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Avoid using a dramatic action frame as the master reference. Motion blur, a hidden eye, a hand across the face, or colored stage lighting may look cinematic, but each one removes information the model needs later.&lt;/p&gt;

&lt;p&gt;If the character does not exist yet, create several candidates with the &lt;a href="https://imagetovideoai.tools/text-to-image" rel="noopener noreferrer"&gt;Text to Image AI Generator&lt;/a&gt;, select one design, and stop redesigning it. If you already have the right person but need a cleaner angle, background, or wardrobe treatment, use &lt;a href="https://imagetovideoai.tools/image-to-image" rel="noopener noreferrer"&gt;Image to Image AI&lt;/a&gt; to create controlled variations from the approved source.&lt;/p&gt;

&lt;h3&gt;
  
  
  Add views because the shot needs them
&lt;/h3&gt;

&lt;p&gt;A second or third reference is useful when the planned camera angle reveals information the master image does not contain. For example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Add a side view before a profile shot.&lt;/li&gt;
&lt;li&gt;Add a full-body view before a walking scene.&lt;/li&gt;
&lt;li&gt;Add a close portrait before an emotional close-up.&lt;/li&gt;
&lt;li&gt;Add a tested outfit reference when clothing continuity matters.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;More references are not automatically better. Two images with different hairstyles, apparent ages, lighting, or facial proportions create a conflict instead of a stronger identity. Every image in the kit should look like evidence of the same character on the same production.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fsw8m7oq64kw36lcqo44r.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fsw8m7oq64kw36lcqo44r.png" alt="A four-panel character reference sheet with face, front, side, and back views" width="799" height="446"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;A three-view reference layout that keeps the face, clothing, and silhouette visible on one canvas.&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Record the non-negotiables
&lt;/h3&gt;

&lt;p&gt;Write a short identity lock beside the images. Keep it limited to details the audience would notice immediately if they changed:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Face and hairstyle&lt;/li&gt;
&lt;li&gt;Signature clothing or accessory&lt;/li&gt;
&lt;li&gt;Approximate age and build&lt;/li&gt;
&lt;li&gt;Art direction, such as photorealistic or stylized 3D&lt;/li&gt;
&lt;li&gt;Details that must stay fixed, such as a scar, glasses, or an ear cuff&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is not a second giant prompt. It is a review checklist for deciding whether a generated shot belongs in the sequence.&lt;/p&gt;

&lt;h2&gt;
  
  
  Choose the Right Visual Control for Each Shot
&lt;/h2&gt;

&lt;p&gt;Reference images, a starting frame, and start/end frames solve related but different problems.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://imagetovideoai.tools/" rel="noopener noreferrer"&gt;Image to Video AI&lt;/a&gt; separates these controls into focused browser workflows. You can begin with an approved still, provide visual references, or guide a shot with start and end frames, then choose the available model and video settings, generate the clip, and review the MP4 result. The useful question is not which mode has the most inputs. It is which input gives the current shot the control it actually needs.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2ddnt1991em2lpsdhpjl.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2ddnt1991em2lpsdhpjl.png" alt="A visual anchor workflow showing one character carried into multiple scenes" width="799" height="436"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;One neutral character anchor reused across different environments and camera conditions.&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Use Reference to Video for identity across new scenes
&lt;/h3&gt;

&lt;p&gt;Choose &lt;a href="https://imagetovideoai.tools/reference-to-video" rel="noopener noreferrer"&gt;Reference to Video&lt;/a&gt; when the character should remain recognizable but the new shot can begin in a different composition, location, or pose. The reference supplies identity and visual context; the prompt describes what happens next.&lt;/p&gt;

&lt;p&gt;This is a strong fit for a recurring character moving between scenes: a presenter in a studio, then at a product launch, then walking through a city. The number of reference images available depends on the selected model, so use the model limit as a ceiling, not a target.&lt;/p&gt;


  Your browser does not support embedded video playback.


&lt;p&gt;&lt;em&gt;Example of a recurring character moving through multiple European locations.&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Use Image to Video when the opening composition is already correct
&lt;/h3&gt;

&lt;p&gt;Choose &lt;a href="https://imagetovideoai.tools/" rel="noopener noreferrer"&gt;Image to Video AI&lt;/a&gt; when you have approved the exact still that should become the first frame. This is often the simplest route to a stable single shot because the face, pose, wardrobe, lighting, and composition are already established before motion begins.&lt;/p&gt;

&lt;p&gt;It works especially well for portrait motion, product demonstrations, subtle expressions, and scenes where the first frame matters more than matching a separate reference sheet.&lt;/p&gt;


  Your browser does not support embedded video playback.


&lt;p&gt;&lt;em&gt;A restrained portrait-motion example generated from a reference image.&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Use Frames to Video when the shot needs a controlled destination
&lt;/h3&gt;

&lt;p&gt;Choose &lt;a href="https://imagetovideoai.tools/frames-to-video" rel="noopener noreferrer"&gt;Frames to Video&lt;/a&gt; when both the beginning and ending composition matter. A start frame and an end frame can guide a reveal, transformation, camera move, or planned transition.&lt;/p&gt;

&lt;p&gt;This gives you endpoint control, but the two frames must agree. If the face, outfit, or proportions differ between them, the model has to reconcile the conflict during the shot, often by morphing. Build both frames from the same approved character kit before asking the video model to connect them.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgd6f8dfvcsxrx3a4ziw2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgd6f8dfvcsxrx3a4ziw2.png" alt="A first-frame and last-frame video generation workflow" width="640" height="640"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;First and last frames define the two visual endpoints the model must connect.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  A Reference-First Workflow for Consistent AI Video Characters
&lt;/h2&gt;

&lt;p&gt;The following process is deliberately sequential. Do not generate ten finished scenes and inspect identity at the end. Validate the character at each stage, when mistakes are still cheap to replace.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Define one shot, not the whole film
&lt;/h3&gt;

&lt;p&gt;Write down the job of the next clip in one sentence:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A medium tracking shot of the character walking through a rainy street, glancing toward the camera, and stopping beneath a streetlight.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This sentence defines the subject, action, camera relationship, environment, and endpoint. If it contains three locations, several costume changes, and four camera moves, split it into separate shots.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: Match the reference to the camera angle
&lt;/h3&gt;

&lt;p&gt;For the rainy street example, a three-quarter or full-body reference is more useful than an extreme facial close-up. If the camera will orbit behind the character, add a rear or side reference rather than asking the model to invent every unseen detail.&lt;/p&gt;

&lt;p&gt;Check the reference at the size viewers will actually see. A face that looks clear when zoomed in may become ambiguous in a full vertical frame.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3: Separate identity from motion in the prompt
&lt;/h3&gt;

&lt;p&gt;Let the image establish who the character is. Use the prompt to direct what changes.&lt;/p&gt;

&lt;p&gt;A weak prompt keeps redesigning the subject:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Beautiful young woman with dark hair, cinematic face, perfect features, stylish clothes, walking in a city, dramatic movement, dynamic camera, highly detailed.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The adjectives are broad, and several of them invite a new interpretation of the person.&lt;/p&gt;

&lt;p&gt;A stronger motion prompt is easier to execute:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The referenced character walks slowly through a rain-soaked street, glances toward the camera once, and stops beneath a warm streetlight. Medium tracking shot, gentle handheld movement, natural body motion, shallow depth of field. Preserve the same face, short black hair, silver ear cuff, and navy jacket. No outfit change and no additional people.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The prompt has one action sequence, one camera idea, one environment, and a short identity lock.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 4: Keep the first motion test simple
&lt;/h3&gt;

&lt;p&gt;Test a restrained version before adding a fast turn, spinning camera, crowd, flying fabric, or physical interaction. A slow walk, small head movement, or seated gesture tells you whether the reference and prompt work together.&lt;/p&gt;

&lt;p&gt;When the simple version holds the character, add one layer of complexity at a time. That makes failures diagnosable. If identity breaks only after adding a fast orbit, the camera move is the variable to change—not the character design.&lt;/p&gt;


  Your browser does not support embedded video playback.


&lt;p&gt;&lt;em&gt;A more demanding spinning-camera example for reviewing face, outfit, and silhouette stability during motion.&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 5: Generate a short shot and inspect specific frames
&lt;/h3&gt;

&lt;p&gt;Do not judge consistency from a looping thumbnail. Pause at the beginning, middle, and end. Then inspect the moment with the largest head turn or body movement.&lt;/p&gt;

&lt;p&gt;Check:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Do the eyes, nose, jaw, and hairline still belong to the same person?&lt;/li&gt;
&lt;li&gt;Did the outfit color, neckline, sleeves, or accessories change?&lt;/li&gt;
&lt;li&gt;Did body proportions shift during movement?&lt;/li&gt;
&lt;li&gt;Did fingers, hair, or another character merge into the face?&lt;/li&gt;
&lt;li&gt;Does the final frame remain usable as an edit point?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Approve identity first. A beautiful camera move is not worth keeping if it breaks the character between shots.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 6: Save successful outputs as production assets
&lt;/h3&gt;

&lt;p&gt;When a shot works, save the prompt, model, aspect ratio, reference set, and a clean frame from the result. That frame is now tested visual evidence of the character in a new angle or environment.&lt;/p&gt;

&lt;p&gt;For the next shot, begin from the closest tested asset instead of returning to the oldest portrait every time. The reference library should grow from approved results, not from random variations.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to Fix Common Character Consistency Problems
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The face changes as soon as the character turns
&lt;/h3&gt;

&lt;p&gt;The model may not have enough information about the side of the face. Add a matching three-quarter or profile reference, reduce the speed of the turn, or change the camera so it does not demand a full unseen angle in one motion.&lt;/p&gt;

&lt;h3&gt;
  
  
  The outfit changes color or shape
&lt;/h3&gt;

&lt;p&gt;Keep the outfit visible in the reference and name only its defining details in the identity lock. Remove style words that conflict with it. “Futuristic fashion,” for example, may encourage the model to redesign a simple jacket even if the reference shows exactly what you want.&lt;/p&gt;

&lt;h3&gt;
  
  
  Two characters blend together
&lt;/h3&gt;

&lt;p&gt;Establish each character separately before generating interaction shots. Make their silhouettes, clothing colors, and signature features visually distinct. Begin with simple staging, such as standing side by side, before attempting an embrace, fight, or fast dance where limbs and faces overlap.&lt;/p&gt;

&lt;h3&gt;
  
  
  The first frame is right but the face degrades over time
&lt;/h3&gt;

&lt;p&gt;Reduce simultaneous demands. Shorten the action, simplify the background, slow the camera, or divide the sequence into two clips. A single generation that must preserve identity, perform complex choreography, reveal a new environment, and execute a dramatic camera move is carrying too many jobs.&lt;/p&gt;

&lt;h3&gt;
  
  
  Every generation is a different kind of wrong
&lt;/h3&gt;

&lt;p&gt;Return to the anchor kit. If the references disagree, prompts cannot reliably repair them. Pick one master identity, rebuild only the necessary angles from it, and run a neutral motion test before returning to the production scene.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Practical Prompt Formula
&lt;/h2&gt;

&lt;p&gt;Use this order when writing a character video prompt:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Referenced character + one action + environment + camera framing/movement + lighting/mood + short identity lock + exclusions&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The referenced character sits at a cafe table and closes a sketchbook, then looks through the window. Medium close-up with a slow push-in, soft morning light, quiet reflective mood. Preserve the same face, bob haircut, round glasses, and green cardigan. No speaking, no extra people, no wardrobe change.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The formula is a diagnostic tool, not a reason to make every prompt long. If the result drifts, remove optional details until the model can preserve the character and perform the central action. Then add detail back carefully.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final Consistency Checklist
&lt;/h2&gt;

&lt;p&gt;Before generating:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;One approved master reference&lt;/li&gt;
&lt;li&gt;Additional views only when required by the shot&lt;/li&gt;
&lt;li&gt;Matching identity, outfit, age, and visual style across references&lt;/li&gt;
&lt;li&gt;One clear action and one primary camera idea&lt;/li&gt;
&lt;li&gt;A short list of identity details that must not change&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Before accepting a clip:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Same recognizable face at the start, middle, and end&lt;/li&gt;
&lt;li&gt;Stable hair, clothing, accessories, and body proportions&lt;/li&gt;
&lt;li&gt;No unwanted character merging or new people&lt;/li&gt;
&lt;li&gt;Motion that does not hide a severe identity break&lt;/li&gt;
&lt;li&gt;A final frame that can cut cleanly into the next shot&lt;/li&gt;
&lt;li&gt;Saved prompt, settings, references, and approved output&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Consistency Comes From the Pipeline
&lt;/h2&gt;

&lt;p&gt;The recurring mistake is treating every clip as a separate prompt-writing contest. A sequence becomes more reliable when each shot inherits approved visual information from the one before it.&lt;/p&gt;

&lt;p&gt;Start with one character, one clean reference, and one simple action. Use &lt;a href="https://imagetovideoai.tools/reference-to-video" rel="noopener noreferrer"&gt;Reference to Video&lt;/a&gt; when identity must travel into a new scene, use &lt;a href="https://imagetovideoai.tools/" rel="noopener noreferrer"&gt;Image to Video AI&lt;/a&gt; when the opening still is already correct, and use &lt;a href="https://imagetovideoai.tools/frames-to-video" rel="noopener noreferrer"&gt;Frames to Video&lt;/a&gt; when the destination frame matters too.&lt;/p&gt;

&lt;p&gt;Lock the character first. Direct the motion second. Review before you scale. That order will do more for continuity than another paragraph of adjectives.&lt;/p&gt;

</description>
      <category>ai</category>
    </item>
    <item>
      <title>Validating Paths in a Browser-Based Word Puzzle Game</title>
      <dc:creator>Jamie Cole</dc:creator>
      <pubDate>Sat, 13 Jun 2026 05:42:22 +0000</pubDate>
      <link>https://dev.to/jamiecoleai/validating-paths-in-a-browser-based-word-puzzle-game-2nim</link>
      <guid>https://dev.to/jamiecoleai/validating-paths-in-a-browser-based-word-puzzle-game-2nim</guid>
      <description>&lt;p&gt;Word puzzle games often look simple: players connect letters, form words, and finish the board.&lt;/p&gt;

&lt;p&gt;The implementation is less simple.&lt;/p&gt;

&lt;p&gt;A browser-based word puzzle needs to answer a few important questions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Is the selected path legal?&lt;/li&gt;
&lt;li&gt;Are the selected tiles adjacent?&lt;/li&gt;
&lt;li&gt;Is any tile reused?&lt;/li&gt;
&lt;li&gt;Does the selected path form an accepted word?&lt;/li&gt;
&lt;li&gt;Can the final board be completed fairly?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This post focuses on the validation layer behind a small word path puzzle game.&lt;/p&gt;

&lt;h2&gt;
  
  
  Representing a tile
&lt;/h2&gt;

&lt;p&gt;A tile needs more than just a letter. It also needs a position.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;tile&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;row&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;col&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;letter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;R&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;row&lt;/code&gt; and &lt;code&gt;col&lt;/code&gt; values are important because every movement rule depends on position.&lt;/p&gt;

&lt;p&gt;Without stable coordinates, the game cannot reliably check whether two tiles are connected.&lt;/p&gt;

&lt;h2&gt;
  
  
  Checking adjacency
&lt;/h2&gt;

&lt;p&gt;The first rule is movement.&lt;/p&gt;

&lt;p&gt;If diagonal movement is allowed, a tile can move to any of the eight neighboring tiles.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;isAdjacent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;dx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;abs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;row&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;dy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;abs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;col&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;col&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;dx&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;dy&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;dx&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;dy&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This function rejects the same tile and accepts horizontal, vertical, and diagonal neighbors.&lt;/p&gt;

&lt;p&gt;If the puzzle should only allow horizontal and vertical movement, the rule can be changed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;isOrthogonallyAdjacent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;dx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;abs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;row&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;dy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;abs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;col&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;col&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;dx&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;dy&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This small difference changes the entire feel of the puzzle.&lt;/p&gt;

&lt;p&gt;Diagonal movement gives players more freedom. Orthogonal movement creates stricter paths and often makes puzzles easier to analyze.&lt;/p&gt;

&lt;h2&gt;
  
  
  Preventing reused tiles
&lt;/h2&gt;

&lt;p&gt;A common rule in word path puzzles is that a tile cannot be reused inside the same path.&lt;/p&gt;

&lt;p&gt;The simplest way to check this is to store each tile coordinate in a set.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;hasDuplicateTiles&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;seen&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Set&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;tile&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;tile&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;,&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;tile&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;col&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;seen&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nx"&gt;seen&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This prevents loops where the player returns to a tile that has already been used.&lt;/p&gt;

&lt;h2&gt;
  
  
  Validating the full path
&lt;/h2&gt;

&lt;p&gt;Once adjacency and duplicate checks exist, a full path validator is simple.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;isValidPath&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;hasDuplicateTiles&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nf"&gt;isAdjacent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This only validates movement.&lt;/p&gt;

&lt;p&gt;It does not decide whether the path forms a real word. That should be handled separately.&lt;/p&gt;

&lt;h2&gt;
  
  
  Converting a path into a word
&lt;/h2&gt;

&lt;p&gt;A path can be converted into a word by joining the tile letters.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;pathToWord&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tile&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;tile&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;letter&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then it can be checked against an accepted word list.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;isAcceptedWord&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;acceptedWords&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;word&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;pathToWord&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;acceptedWords&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;word&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For puzzle games, I usually prefer a curated answer list instead of a large dictionary.&lt;/p&gt;

&lt;p&gt;A large dictionary may create many accidental valid words. A curated list gives the designer more control over the intended solution.&lt;/p&gt;

&lt;h2&gt;
  
  
  Checking board coverage
&lt;/h2&gt;

&lt;p&gt;Some word path puzzles require the final solution to cover every required tile.&lt;/p&gt;

&lt;p&gt;That means the game needs to know how many unique tiles are covered by all solved paths.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getCoveredTiles&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;paths&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;covered&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Set&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;paths&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;tile&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;covered&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;tile&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;,&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;tile&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;col&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;covered&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then the board can be checked like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;isBoardComplete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;paths&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;requiredTileCount&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;getCoveredTiles&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;paths&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;size&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;requiredTileCount&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is useful because a puzzle can contain valid words but still be incomplete.&lt;/p&gt;

&lt;p&gt;A fair board should not leave impossible or confusing leftover tiles.&lt;/p&gt;

&lt;h2&gt;
  
  
  Avoiding overlapping solution paths
&lt;/h2&gt;

&lt;p&gt;If each tile should belong to only one final answer, paths must not overlap.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;pathsOverlap&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;paths&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;used&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Set&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;paths&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;tile&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;tile&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;,&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;tile&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;col&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;

      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;used&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;

      &lt;span class="nx"&gt;used&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This check becomes important when generating puzzles automatically.&lt;/p&gt;

&lt;p&gt;A generator can accidentally create boards where multiple solution paths fight for the same tile.&lt;/p&gt;

&lt;h2&gt;
  
  
  A practical generation approach
&lt;/h2&gt;

&lt;p&gt;For a first version, I would not start with fully random letters.&lt;/p&gt;

&lt;p&gt;Random boards are easy to create but hard to make fair.&lt;/p&gt;

&lt;p&gt;A more controlled approach is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;choose a board size&lt;/li&gt;
&lt;li&gt;choose a small curated word list&lt;/li&gt;
&lt;li&gt;place each word as a legal path&lt;/li&gt;
&lt;li&gt;reject overlapping paths&lt;/li&gt;
&lt;li&gt;check board coverage&lt;/li&gt;
&lt;li&gt;review difficulty before publishing&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This is not a perfect generator, but it is a useful starting point.&lt;/p&gt;

&lt;p&gt;For browser games, a small reliable generator is better than a large unreliable one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Daily puzzles are easier to control
&lt;/h2&gt;

&lt;p&gt;An infinite random mode sounds exciting, but quality control becomes harder.&lt;/p&gt;

&lt;p&gt;Daily puzzles are easier because the game only needs one good board per day.&lt;/p&gt;

&lt;p&gt;That allows extra checks:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;path legality&lt;/li&gt;
&lt;li&gt;word validity&lt;/li&gt;
&lt;li&gt;board coverage&lt;/li&gt;
&lt;li&gt;duplicate tile prevention&lt;/li&gt;
&lt;li&gt;difficulty review&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For a small V0, daily mode is often the better choice.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final thoughts
&lt;/h2&gt;

&lt;p&gt;The hard part of a word path puzzle is not rendering the grid.&lt;/p&gt;

&lt;p&gt;The hard part is validation.&lt;/p&gt;

&lt;p&gt;Before adding accounts, leaderboards, animations, or sharing features, the core rules should be solid:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;legal movement&lt;/li&gt;
&lt;li&gt;no reused tiles&lt;/li&gt;
&lt;li&gt;accepted words only&lt;/li&gt;
&lt;li&gt;no invalid overlap&lt;/li&gt;
&lt;li&gt;complete board coverage&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Once those rules work, the game becomes much easier to improve.&lt;/p&gt;

&lt;p&gt;I used this approach while experimenting with a small independent browser puzzle project. The main lesson was simple: build the validator first, then build the game around it.&lt;/p&gt;

</description>
      <category>gamedev</category>
      <category>webdev</category>
      <category>algorithms</category>
    </item>
    <item>
      <title>How I Built a Performant Logic Puzzle Game with Next.js and a Web Worker</title>
      <dc:creator>Jamie Cole</dc:creator>
      <pubDate>Tue, 19 Aug 2025 02:05:11 +0000</pubDate>
      <link>https://dev.to/jamiecoleai/how-i-built-a-performant-logic-puzzle-game-with-nextjs-and-a-web-worker-4381</link>
      <guid>https://dev.to/jamiecoleai/how-i-built-a-performant-logic-puzzle-game-with-nextjs-and-a-web-worker-4381</guid>
      <description>&lt;p&gt;Hey DEV community!&lt;/p&gt;

&lt;p&gt;I've always been a huge fan of logic puzzles. Recently, I got hooked on the &lt;a href="https://queensgame.io/" rel="noopener noreferrer"&gt;Queens puzzle game&lt;/a&gt; but felt the classic rules were a bit too simple. This sparked an idea: what if I could create a more modern, more challenging version?&lt;/p&gt;

&lt;p&gt;Today, I'm excited to share the result of that idea: Enhanced Queens Game, a free-to-play web game I built from scratch.&lt;/p&gt;

&lt;p&gt;🎮 You can play it here:  &lt;a href="https://queensgame.io/" rel="noopener noreferrer"&gt;queens game &lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This post is a quick breakdown of the journey, the tech stack, and one interesting technical challenge I solved.&lt;/p&gt;

&lt;p&gt;The Core Idea: Adding a "Third Dimension"&lt;br&gt;
The biggest change from the classic puzzle is the addition of colored regions. On top of having only one queen per row and column, you must also have only one per colored block. This simple twist turns a 2D puzzle into a much deeper 3D spatial reasoning challenge.&lt;/p&gt;

&lt;p&gt;![Insert a GIF of the gameplay, showing the colored regions and a queen turning red]&lt;/p&gt;

&lt;p&gt;The Tech Stack&lt;br&gt;
I chose a modern stack to build this, focusing on performance and developer experience:&lt;/p&gt;

&lt;p&gt;Framework: Next.js (for great SEO and fast performance)&lt;/p&gt;

&lt;p&gt;Language: TypeScript (a must for managing complex game logic)&lt;/p&gt;

&lt;p&gt;Styling: Tailwind CSS (for building the clean UI quickly)&lt;/p&gt;

&lt;p&gt;The Coolest Technical Challenge: Real-time Validation without Lag&lt;br&gt;
A key feature is that a queen turns red instantly if it breaks a rule. On larger 7x7 boards, checking all conflicts (row, column, region, and adjacency) on every single move could potentially lag the main UI thread.&lt;/p&gt;

&lt;p&gt;The solution? A Web Worker.&lt;/p&gt;

&lt;p&gt;I offloaded the most intensive validation logic to a separate background thread using a Web Worker (solveQueensWorker.ts in the repo). The main thread sends the current board state to the worker, the worker runs the heavy computations, and then sends the results (any conflicts) back.&lt;/p&gt;

&lt;p&gt;This ensures the UI remains buttery smooth and responsive, even while complex calculations are happening in the background. It was a perfect use case for a worker and a fun problem to solve.&lt;/p&gt;

&lt;p&gt;Feedback Welcome!&lt;br&gt;
This has been an incredibly rewarding personal project. I'd be honored if you'd take a look. I'm open to any and all feedback—whether it's on the game's difficulty, the UI/UX, or the code itself.&lt;/p&gt;

&lt;p&gt;Thanks for reading!&lt;/p&gt;

</description>
      <category>gamedev</category>
    </item>
  </channel>
</rss>
