<?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: MediAi</title>
    <description>The latest articles on DEV Community by MediAi (@mediaaiblog).</description>
    <link>https://dev.to/mediaaiblog</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%2F3879403%2F0cb108a0-5cda-4a77-a066-e859760c6f4e.png</url>
      <title>DEV Community: MediAi</title>
      <link>https://dev.to/mediaaiblog</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/mediaaiblog"/>
    <language>en</language>
    <item>
      <title>Four bugs I only found after shipping a PixiJS card game</title>
      <dc:creator>MediAi</dc:creator>
      <pubDate>Wed, 05 Aug 2026 17:37:02 +0000</pubDate>
      <link>https://dev.to/mediaaiblog/four-bugs-i-only-found-after-shipping-a-pixijs-card-game-361o</link>
      <guid>https://dev.to/mediaaiblog/four-bugs-i-only-found-after-shipping-a-pixijs-card-game-361o</guid>
      <description>&lt;p&gt;I built a blackjack table that runs entirely in the browser — six decks, dealer stands on soft 17, split up to three hands, no framework, no build step, no server. One JS file, one CSS file, PixiJS for rendering.&lt;/p&gt;

&lt;p&gt;Code: &lt;a href="https://github.com/sorviboshky-gif/blackjack-free" rel="noopener noreferrer"&gt;github.com/sorviboshky-gif/blackjack-free&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It worked on my machine. Then it didn't, four separate times, and each failure taught me something I hadn't known.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. &lt;code&gt;hidden&lt;/code&gt; is a suggestion, not a command
&lt;/h2&gt;

&lt;p&gt;The table has a loading screen and a game canvas. Only one should be visible:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"bj-loader"&lt;/span&gt; &lt;span class="na"&gt;data-loader&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;…&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"bj-stage"&lt;/span&gt; &lt;span class="na"&gt;data-stage&lt;/span&gt; &lt;span class="na"&gt;hidden&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;…&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;loader&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hidden&lt;/span&gt; &lt;span class="o"&gt;=&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;stage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hidden&lt;/span&gt; &lt;span class="o"&gt;=&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;Both appeared at once — loader stacked on top of the game, page twice as tall as it should be. No error, nothing in the console.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;hidden&lt;/code&gt; attribute works through the user-agent stylesheet, which sets &lt;code&gt;display: none&lt;/code&gt;. Any author-level &lt;code&gt;display&lt;/code&gt; beats it:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nc"&gt;.bj-loader&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;flex&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;  &lt;span class="c"&gt;/* silently wins */&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The fix is one line, and every project with a component library should have it:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="nt"&gt;hidden&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;none&lt;/span&gt; &lt;span class="cp"&gt;!important&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;I had this bug in four places — loader, action buttons, bet bar, insurance prompt — all invisible until the exact moment they weren't.&lt;/p&gt;
&lt;h2&gt;
  
  
  2. Your game deadlocks in a background tab
&lt;/h2&gt;

&lt;p&gt;This one cost the most time.&lt;/p&gt;

&lt;p&gt;The deal sequence is a chain of awaited animations:&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="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;dealTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;playerCards&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;card1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;dealTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;dealerCards&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;card2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;dealTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;playerCards&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;card3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;dealTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;dealerCards&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;card4&lt;/span&gt;&lt;span class="p"&gt;,&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;Each &lt;code&gt;dealTo&lt;/code&gt; returns a promise resolved by the animation loop, which runs on Pixi's ticker — which runs on &lt;code&gt;requestAnimationFrame&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;requestAnimationFrame&lt;/code&gt; does not fire in a background tab.&lt;/p&gt;

&lt;p&gt;Switch tabs mid-deal and the tween never advances, the promise never resolves, &lt;code&gt;await&lt;/code&gt; never returns. Come back and the game is frozen forever: cards mid-flight, buttons disabled, &lt;code&gt;busy&lt;/code&gt; flag stuck true. Only a reload fixes it. No error, no warning — the code is just sitting there, correctly waiting for a frame that will never come.&lt;/p&gt;

&lt;p&gt;I found it because my test environment didn't paint frames at all, so &lt;em&gt;every&lt;/em&gt; hand hung. That accident saved me from shipping it.&lt;/p&gt;

&lt;p&gt;The fix is a watchdog that advances stalled animations when frames stop arriving:&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;now&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;performance&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;now&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;lastTick&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="nf"&gt;setInterval&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;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="nx"&gt;tweens&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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;lastTick&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="k"&gt;return&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;gap&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;lastTick&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;gap&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;260&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nf"&gt;stepTweens&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;gap&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;   &lt;span class="c1"&gt;// frames stopped — catch up manually&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="mi"&gt;120&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;code&gt;setInterval&lt;/code&gt; is throttled in background tabs, but it is not stopped. The state machine finishes, promises resolve, and the player returns to a settled hand instead of a corpse.&lt;/p&gt;

&lt;p&gt;Anything that awaits an rAF-driven animation has this bug. Most of the time nobody notices, because most of the time the animation isn't load-bearing.&lt;/p&gt;
&lt;h2&gt;
  
  
  3. &lt;code&gt;backgroundAlpha: 0&lt;/code&gt; did not give me a transparent canvas
&lt;/h2&gt;

&lt;p&gt;I wanted the page background to show through around the table:&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="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;init&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;backgroundAlpha&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;antialias&lt;/span&gt;&lt;span class="p"&gt;:&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;I got a solid black rectangle. &lt;code&gt;getComputedStyle(canvas).backgroundColor&lt;/code&gt; reported &lt;code&gt;rgba(0, 0, 0, 0)&lt;/code&gt; — the CSS was transparent, the pixels were not.&lt;/p&gt;

&lt;p&gt;The WebGL context is created without an alpha buffer, so "transparent" resolves to black. Chasing real transparency means context flags and compositing surprises on mobile.&lt;/p&gt;

&lt;p&gt;I stopped chasing it:&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="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;init&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;backgroundColor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mh"&gt;0x0c102b&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;backgroundAlpha&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Paint the canvas the same colour as the section behind it. Visually identical, one less thing to break, and it composites faster.&lt;/p&gt;
&lt;h2&gt;
  
  
  4. A 21 on split aces is not a blackjack
&lt;/h2&gt;

&lt;p&gt;Not a rendering bug — a rules bug, and the kind reviewers catch instantly.&lt;/p&gt;

&lt;p&gt;Split a pair of aces, catch a ten, and you have 21. Naive code pays it 3:2. Every real casino pays it 1:1, because a blackjack is by definition the &lt;em&gt;first two cards dealt to a hand&lt;/em&gt;, and a split hand isn't that.&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;isBlackjack&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;hand&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
  &lt;span class="nx"&gt;hand&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cards&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;2&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nf"&gt;score&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;hand&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cards&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;21&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="nx"&gt;hand&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fromSplit&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;That &lt;code&gt;!hand.fromSplit&lt;/code&gt; is the whole rule. Miss it and the house edge shifts in the player's favour by a visible margin — which in a demo is a bug, and in production is a hole.&lt;/p&gt;

&lt;p&gt;Same family of edge cases: split aces get exactly one card each and no more, and a natural blackjack beats a 21 assembled from three cards. Both are one line, both are wrong by default.&lt;/p&gt;
&lt;h2&gt;
  
  
  Bonus: &lt;code&gt;Math.random() * n&lt;/code&gt; is biased
&lt;/h2&gt;

&lt;p&gt;A shoe is 312 cards. Fisher–Yates needs an integer in &lt;code&gt;[0, i]&lt;/code&gt;, and the obvious version has modulo bias:&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="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;floor&lt;/span&gt;&lt;span class="p"&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;random&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;// biased, and unseedable by design&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;For a demo nobody would notice. It still felt wrong to ship a card game with a shuffle I couldn't defend, so it uses rejection sampling on the crypto RNG:&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;rnd&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;n&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;a&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;Uint32Array&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;limit&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;floor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;4294967296&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   &lt;span class="c1"&gt;// discard the ragged tail&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getRandomValues&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;v&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&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;while &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;v&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="nx"&gt;limit&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;v&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="nx"&gt;n&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;Costs nothing at 312 cards and removes an entire category of "is this rigged" questions.&lt;/p&gt;
&lt;h2&gt;
  
  
  What the whole thing looks like
&lt;/h2&gt;

&lt;p&gt;The renderer is Pixi, everything else is plain JavaScript. Cards leave the shoe along a quadratic Bézier, rotate in flight, land at a small random tilt and settle with a short overshoot — the flip lifts the card while collapsing it on &lt;code&gt;scale.x&lt;/code&gt;. &lt;code&gt;prefers-reduced-motion&lt;/code&gt; collapses all of it to 60 ms rather than disabling the game.&lt;/p&gt;

&lt;p&gt;Every user-facing string comes from one &lt;code&gt;data-i18n&lt;/code&gt; attribute, so translation is swapping JSON — &lt;a href="https://duel-guide.com/games/duel-blackjack-free/" rel="noopener noreferrer"&gt;the production build&lt;/a&gt; runs the same file in nine languages including Arabic in RTL.&lt;br&gt;
&lt;/p&gt;
&lt;div class="crayons-card c-embed text-styles text-styles--secondary"&gt;
    &lt;div class="c-embed__content"&gt;
        &lt;div class="c-embed__cover"&gt;
          &lt;a href="https://duel-guide.com/" class="c-link align-middle" rel="noopener noreferrer"&gt;
            &lt;img alt="" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fduel-guide.com%2Fog-image.jpg" height="420" class="m-0" width="800"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="c-embed__body"&gt;
        &lt;h2 class="fs-xl lh-tight"&gt;
          &lt;a href="https://duel-guide.com/" rel="noopener noreferrer" class="c-link"&gt;
            Duel Casino Review 2026 | Bonus, Login, Crypto Payouts
          &lt;/a&gt;
        &lt;/h2&gt;
          &lt;p class="truncate-at-3"&gt;
            Official Duel Casino guide: welcome bonus with promo code DEP100, login, registration, APK and crypto deposits with fast withdrawals.
          &lt;/p&gt;
        &lt;div class="color-secondary fs-s flex items-center"&gt;
            &lt;img alt="favicon" class="c-embed__favicon m-0 mr-2 radius-0" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fduel-guide.com%2Ffavicon.ico" width="48" height="48"&gt;
          duel-guide.com
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;



&lt;p&gt;Repository, MIT licensed, no dependencies beyond Pixi: &lt;a href="https://github.com/sorviboshky-gif/blackjack-free" rel="noopener noreferrer"&gt;github.com/sorviboshky-gif/blackjack-free&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The chips are practice chips. Nothing is won, nothing is deposited, nothing leaves the browser.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>gamedev</category>
      <category>webdev</category>
      <category>showdev</category>
    </item>
    <item>
      <title>I built a Russian-language AI portal in 6 days using Claude Opus — here's what it cost and what I learned</title>
      <dc:creator>MediAi</dc:creator>
      <pubDate>Tue, 14 Apr 2026 22:45:34 +0000</pubDate>
      <link>https://dev.to/mediaaiblog/i-built-a-russian-language-ai-portal-in-6-days-using-claude-opus-heres-what-it-cost-and-what-i-53n</link>
      <guid>https://dev.to/mediaaiblog/i-built-a-russian-language-ai-portal-in-6-days-using-claude-opus-heres-what-it-cost-and-what-i-53n</guid>
      <description>&lt;p&gt;The idea was simple: there's no decent Russian-language resource &lt;br&gt;
about AI models. There are translated press releases, Telegram &lt;br&gt;
channels with news, occasional articles on Habr. But a catalog &lt;br&gt;
where you can check current prices, compare models, and read an &lt;br&gt;
honest review in Russian? Doesn't exist. So I built one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Stack&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Next.js, PostgreSQL, Tailwind. Self-hosted VPS. Nothing exotic — &lt;br&gt;
a standard setup that lets you iterate fast. Custom admin panel &lt;br&gt;
for content management because no CMS handled the model catalog &lt;br&gt;
structure well enough.&lt;/p&gt;

&lt;p&gt;*&lt;em&gt;How I actually built it&lt;br&gt;
*&lt;/em&gt;&lt;br&gt;
Almost everything was written with Claude Opus 4.6. Not "asked &lt;br&gt;
it to build a website" — more like: every component, every &lt;br&gt;
complex logic piece, every bug I couldn't catch after three hours &lt;br&gt;
— solved through conversation with the model.&lt;/p&gt;

&lt;p&gt;This is a different way of working. You're the architect, &lt;br&gt;
Opus is the executor. Fast, but it requires you to actually &lt;br&gt;
understand what you're doing. Otherwise you get code that &lt;br&gt;
works but for unknown reasons.&lt;/p&gt;

&lt;p&gt;6 days from idea to first publish. Not counting the weekends &lt;br&gt;
when I just couldn't stop.&lt;/p&gt;

&lt;p&gt;*&lt;em&gt;What it cost&lt;br&gt;
*&lt;/em&gt;&lt;br&gt;
Claude Opus API during development: ~$40-50. That's a lot of &lt;br&gt;
context, a lot of iterations, a lot of "explain what's wrong here."&lt;/p&gt;

&lt;p&gt;VPS: $15/month. Domain. Total launch cost: roughly $80-90.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What's on the site now&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;200+ models with current prices and specs&lt;/li&gt;
&lt;li&gt;Articles and practical guides&lt;/li&gt;
&lt;li&gt;Free tools: prompt generator and terminal error translator&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The hardest part — it's not the code&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Pricing. Providers change token prices quietly and suddenly. &lt;br&gt;
Sometimes three times in a month. Manual tracking doesn't scale, &lt;br&gt;
automating it via each provider's API is a mess because everyone &lt;br&gt;
does it differently.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;SEO reality check&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Target traffic is organic search. People search "Claude Opus &lt;br&gt;
review", "GPT vs Claude comparison", "Gemini API pricing" — &lt;br&gt;
I want them to find us. Currently at 200-300 unique visitors &lt;br&gt;
per day, growing slowly.&lt;/p&gt;

&lt;p&gt;SEO is not a sprint. It's a marathon you start running when &lt;br&gt;
you're already tired from the sprint.&lt;/p&gt;

&lt;p&gt;Content is written manually, no pure AI generation — only as &lt;br&gt;
a rough draft that gets completely rewritten. Otherwise it's &lt;br&gt;
just noise.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Honest expectations&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I thought it would be faster. Both traffic and content. &lt;br&gt;
Six months to get to real numbers is realistic but &lt;br&gt;
psychologically brutal when you put a week of your life &lt;br&gt;
into something and watch 50 visitors a day for the first &lt;br&gt;
two weeks.&lt;/p&gt;

&lt;p&gt;If you've built something similar — curious how you solved &lt;br&gt;
the data freshness problem and where you got first traffic from.&lt;/p&gt;

&lt;p&gt;Site: &lt;a href="https://shtruzel.ru" rel="noopener noreferrer"&gt;https://shtruzel.ru&lt;/a&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>programming</category>
      <category>ai</category>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
