<?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: Lucian (LKB)</title>
    <description>The latest articles on DEV Community by Lucian (LKB) (@lucian_lkb_1f009d).</description>
    <link>https://dev.to/lucian_lkb_1f009d</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%2F4026097%2Feb9247a0-cd26-41b9-9613-ddcb120a9d66.jpg</url>
      <title>DEV Community: Lucian (LKB)</title>
      <link>https://dev.to/lucian_lkb_1f009d</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/lucian_lkb_1f009d"/>
    <language>en</language>
    <item>
      <title>Domain coloring: plotting complex functions f(z) in the browser</title>
      <dc:creator>Lucian (LKB)</dc:creator>
      <pubDate>Sat, 19 Sep 2026 08:44:31 +0000</pubDate>
      <link>https://dev.to/lucian_lkb_1f009d/domain-coloring-plotting-complex-functions-fz-in-the-browser-56ne</link>
      <guid>https://dev.to/lucian_lkb_1f009d/domain-coloring-plotting-complex-functions-fz-in-the-browser-56ne</guid>
      <description>&lt;p&gt;An ordinary graph maps a number to a number, so a curve fits on a flat screen. A &lt;strong&gt;complex&lt;/strong&gt; function maps a plane to a plane — two inputs, two outputs, four dimensions — and there's no curve to draw.&lt;/p&gt;

&lt;p&gt;So we do what mathematicians do on paper: we paint the plane. Here's how &lt;strong&gt;domain coloring&lt;/strong&gt; works, the small evaluator behind it, and how it stays interactive in a browser tab without WebGL. Everything below is live in the free &lt;a href="https://lkforge.com/tools/math/graphing-calculator/" rel="noopener noreferrer"&gt;LK Forge graphing calculator&lt;/a&gt; — press &lt;strong&gt;f(z)&lt;/strong&gt; and follow along.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two numbers, two channels of colour
&lt;/h2&gt;

&lt;p&gt;Every output &lt;code&gt;f(z)&lt;/code&gt; is a complex number, and a complex number has exactly two visible parts: an &lt;strong&gt;angle&lt;/strong&gt; (its argument) and a &lt;strong&gt;size&lt;/strong&gt; (its modulus). Domain coloring spends one visual channel on each:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Hue&lt;/strong&gt; = the argument of &lt;code&gt;f(z)&lt;/code&gt; — red one way, cyan the opposite way, a full colour wheel for a full turn.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Brightness&lt;/strong&gt; = the modulus — dark where &lt;code&gt;f(z)&lt;/code&gt; is near zero, bright where it grows large.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That one rule makes the features that matter jump out. A &lt;strong&gt;zero&lt;/strong&gt; is a dark spot with the whole colour wheel wrapped around it. A &lt;strong&gt;pole&lt;/strong&gt; (where the function runs to infinity) is a bright spot, also ringed by every hue. Faint contour rings mark each &lt;em&gt;doubling&lt;/em&gt; of the modulus, so you read growth like a topographic map reads elevation.&lt;/p&gt;

&lt;h2&gt;
  
  
  The evaluator: the same parser, a different number
&lt;/h2&gt;

&lt;p&gt;The 2D graph and the complex plane share one expression parser — a shunting-yard compiler with &lt;strong&gt;no &lt;code&gt;eval()&lt;/code&gt; anywhere&lt;/strong&gt;. The only thing that changes is what sits on the evaluation stack:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Real graph → the stack holds ordinary numbers.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;f(z)&lt;/code&gt; → the stack holds &lt;code&gt;{re, im}&lt;/code&gt; pairs, and each operator does complex arithmetic.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The imaginary unit &lt;code&gt;i&lt;/code&gt; is just the constant &lt;code&gt;{re: 0, im: 1}&lt;/code&gt;, and every function gets its principal-branch definition, so the identities you'd check by hand come out exactly right:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;i²      = -1
√(-1)   = i
ln(-1)  = πi
e^(iπ)  = -1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each of those is what the live evaluator returns, to six decimals.&lt;/p&gt;

&lt;p&gt;Speed matters, because the picture is built one pixel at a time. Timed in the browser, the complex evaluator runs about &lt;strong&gt;3.4 million evaluations per second&lt;/strong&gt; (200,000 evaluations of a rational function in 58 ms) on a single thread — enough to colour a full board and still respond to a drag.&lt;/p&gt;

&lt;h2&gt;
  
  
  Interactive speed without WebGL
&lt;/h2&gt;

&lt;p&gt;There's no 3D here and no shader — the whole plane is drawn on a plain 2D canvas. The trick is resolution:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The plane is sampled into a &lt;strong&gt;half-resolution&lt;/strong&gt; offscreen canvas (one sample per 2×2 block, ~77,000 for a full board) and scaled up with smoothing — close enough that the eye can't tell.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;While dragging or zooming&lt;/strong&gt; it drops to quarter resolution for a fluid feel, then repaints once at higher resolution 140 ms after you stop.&lt;/li&gt;
&lt;li&gt;No animation loop runs when the image is still, so an idle plane costs nothing.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Because it's all arithmetic through a safe parser, a shared link can only ever draw a picture — &lt;code&gt;?cx=&lt;/code&gt; in the URL loads any &lt;code&gt;f(z)&lt;/code&gt; straight onto the plane.&lt;/p&gt;

&lt;h2&gt;
  
  
  A field guide: reading functions by their fingerprint
&lt;/h2&gt;

&lt;p&gt;Once you know the rule, each function has a signature you can read at a glance. Count how many times the colour wheel wraps a spot and you have the &lt;strong&gt;order&lt;/strong&gt; of that zero or pole.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;z²&lt;/code&gt; — a double zero.&lt;/strong&gt; One dark point at the origin, but the hue wheel wraps it &lt;em&gt;twice&lt;/em&gt;: squaring doubles every angle. &lt;a href="https://lkforge.com/tools/math/graphing-calculator/?cx=z%5E2" rel="noopener noreferrer"&gt;Open it →&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;1/z&lt;/code&gt; — a single pole.&lt;/strong&gt; A bright point instead of a dark one, hue running the opposite way. Poles are zeros turned inside out. &lt;a href="https://lkforge.com/tools/math/graphing-calculator/?cx=%5Cfrac%7B1%7D%7Bz%7D" rel="noopener noreferrer"&gt;Open it →&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;(z² − 1)/(z² + 1)&lt;/code&gt; — zeros meet poles.&lt;/strong&gt; Two dark zeros at ±1, two bright poles at ±i. &lt;a href="https://lkforge.com/tools/math/graphing-calculator/?cx=%5Cfrac%7Bz%5E2-1%7D%7Bz%5E2%2B1%7D" rel="noopener noreferrer"&gt;Open it →&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;z³ − 1&lt;/code&gt; — three roots of unity.&lt;/strong&gt; Three dark zeros evenly spaced on the unit circle. &lt;a href="https://lkforge.com/tools/math/graphing-calculator/?cx=z%5E3-1" rel="noopener noreferrer"&gt;Open it →&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;sin(z)&lt;/code&gt; — a row of zeros.&lt;/strong&gt; Dark spots at every multiple of π, brightness climbing fast off the real axis. &lt;a href="https://lkforge.com/tools/math/graphing-calculator/?cx=%5Csin%5Cleft(z%5Cright)" rel="noopener noreferrer"&gt;Open it →&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;eᶻ&lt;/code&gt; — no zeros, no poles.&lt;/strong&gt; Horizontal bands of hue: the argument depends only on the imaginary part, brightness only on the real part. &lt;a href="https://lkforge.com/tools/math/graphing-calculator/?cx=e%5E%7Bz%7D" rel="noopener noreferrer"&gt;Open it →&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;ln(z)&lt;/code&gt; — a branch cut.&lt;/strong&gt; One zero at z = 1 and a sharp seam along the negative real axis where the colour jumps. Domain coloring makes branch cuts &lt;em&gt;visible&lt;/em&gt;. &lt;a href="https://lkforge.com/tools/math/graphing-calculator/?cx=%5Cln%5Cleft(z%5Cright)" rel="noopener noreferrer"&gt;Open it →&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Try it on your own function
&lt;/h2&gt;

&lt;p&gt;Domain coloring turns "what does this function do near its zeros?" from a paragraph of algebra into a picture you read in a second. Open the &lt;a href="https://lkforge.com/tools/math/graphing-calculator/" rel="noopener noreferrer"&gt;graphing calculator&lt;/a&gt;, press &lt;strong&gt;f(z)&lt;/strong&gt;, and type something of your own — &lt;code&gt;conj(z)&lt;/code&gt;, &lt;code&gt;z + 1/z&lt;/code&gt;, &lt;code&gt;(z − i)/(z + i)&lt;/code&gt; — then hover to read &lt;code&gt;z&lt;/code&gt; and &lt;code&gt;f(z)&lt;/code&gt;. A &lt;strong&gt;Colour intensity&lt;/strong&gt; slider runs from pastel to vivid.&lt;/p&gt;

&lt;p&gt;Full write-up with the maths: &lt;strong&gt;&lt;a href="https://lkforge.com/blog/plotting-complex-functions-domain-coloring/" rel="noopener noreferrer"&gt;Domain Coloring: How We Plot Complex Functions in the Browser&lt;/a&gt;&lt;/strong&gt;. Free, no sign-up, everything runs client-side.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>webdev</category>
      <category>math</category>
      <category>datavis</category>
    </item>
    <item>
      <title>Which Video Format Is Actually Smallest? H.264 vs VP9 vs AV1, Measured</title>
      <dc:creator>Lucian (LKB)</dc:creator>
      <pubDate>Thu, 17 Sep 2026 04:22:49 +0000</pubDate>
      <link>https://dev.to/lucian_lkb_1f009d/which-video-format-is-actually-smallest-h264-vs-vp9-vs-av1-measured-1b12</link>
      <guid>https://dev.to/lucian_lkb_1f009d/which-video-format-is-actually-smallest-h264-vs-vp9-vs-av1-measured-1b12</guid>
      <description>&lt;p&gt;"Convert it to MP4 to save space" is advice you hear constantly, and it's mostly wrong. MP4 is a &lt;strong&gt;container&lt;/strong&gt; — a box that holds video, audio and metadata — not a compression method. The thing that actually decides the file size is the &lt;strong&gt;codec&lt;/strong&gt; inside it: H.264, VP9 or AV1. Rename a &lt;code&gt;.webm&lt;/code&gt; to &lt;code&gt;.mp4&lt;/code&gt; and you've changed nothing about how big it is. So we measured how much the codec really matters.&lt;/p&gt;

&lt;h2&gt;
  
  
  How we measured it
&lt;/h2&gt;

&lt;p&gt;We took two standard 720p test clips — Big Buck Bunny and Jellyfish — and encoded each across a full quality sweep in &lt;strong&gt;H.264&lt;/strong&gt;, &lt;strong&gt;VP9&lt;/strong&gt; and &lt;strong&gt;AV1&lt;/strong&gt;. Rather than trust the encoder's quality dial, we scored every output with &lt;strong&gt;VMAF&lt;/strong&gt;, Netflix's perceptual quality metric, so we could compare file sizes &lt;em&gt;at equal perceived quality&lt;/em&gt; instead of at equal settings. Every number below is the mean of the two clips.&lt;/p&gt;

&lt;h2&gt;
  
  
  The result: the codec is everything
&lt;/h2&gt;

&lt;p&gt;At a target quality of &lt;strong&gt;VMAF 90&lt;/strong&gt; (visually indistinguishable from the source for most viewers):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;VP9 is about 40% smaller than H.264.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;AV1 is about 60% smaller than H.264.&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Made concrete, the same 10-second 720p clip lands at roughly:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Codec&lt;/th&gt;
&lt;th&gt;Size (10s, 720p, VMAF 90)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;H.264&lt;/td&gt;
&lt;td&gt;3.3 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;VP9&lt;/td&gt;
&lt;td&gt;2.0 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AV1&lt;/td&gt;
&lt;td&gt;1.3 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Same picture, same perceived quality — and AV1 is less than half the size of H.264. Meanwhile the &lt;strong&gt;container extension does nothing on its own&lt;/strong&gt;: a &lt;code&gt;.mp4&lt;/code&gt;, &lt;code&gt;.webm&lt;/code&gt; and &lt;code&gt;.mkv&lt;/code&gt; holding the same H.264 stream are the same size. If you want a smaller file, you change the codec, not the extension.&lt;/p&gt;

&lt;h2&gt;
  
  
  The catch: encode time
&lt;/h2&gt;

&lt;p&gt;Newer codecs buy their smaller files with CPU. &lt;strong&gt;AV1 and VP9 cost far more to encode&lt;/strong&gt; than H.264 — often dramatically so. That reframes the choice as a trade:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Encode once, stream many times&lt;/strong&gt; (a video you publish): spend the CPU on AV1/VP9; the bandwidth savings compound over every view.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One-off or fast turnaround&lt;/strong&gt; (a clip you send once): H.264 is fine — the encode is quick and universally supported.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Reproduce it
&lt;/h2&gt;

&lt;p&gt;The whole study is an FFmpeg quality sweep plus VMAF scoring, driven by a short script — no proprietary tooling. Point it at any clip and you get the same shape of result.&lt;/p&gt;

&lt;p&gt;If you just need to convert something, the &lt;a href="https://lkforge.com/tools/video/" rel="noopener noreferrer"&gt;video tools&lt;/a&gt; — MP4 ⇄ MOV, WebM/MKV/AVI → MP4, GIF ⇄ video, and a trimmer — run entirely in your browser with &lt;code&gt;ffmpeg.wasm&lt;/code&gt;, so nothing is uploaded.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://lkforge.com/blog/video-format-size-h264-vp9-av1/" rel="noopener noreferrer"&gt;LK Forge&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>video</category>
      <category>webdev</category>
      <category>ffmpeg</category>
      <category>performance</category>
    </item>
    <item>
      <title>Real game AI, not a chatbot: why these opponents don't use an LLM</title>
      <dc:creator>Lucian (LKB)</dc:creator>
      <pubDate>Wed, 16 Sep 2026 23:02:28 +0000</pubDate>
      <link>https://dev.to/lucian_lkb_1f009d/real-game-ai-not-a-chatbot-why-these-opponents-dont-use-an-llm-5h79</link>
      <guid>https://dev.to/lucian_lkb_1f009d/real-game-ai-not-a-chatbot-why-these-opponents-dont-use-an-llm-5h79</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Syndicated from the original on &lt;strong&gt;&lt;a href="https://lkforge.com/blog/game-ai-not-llms/" rel="noopener noreferrer"&gt;lkforge.com&lt;/a&gt;&lt;/strong&gt;. The engines are playable in your browser at &lt;a href="https://lkforge.com/games/" rel="noopener noreferrer"&gt;lkforge.com/games&lt;/a&gt;; the harness that produced these numbers is public and seeded.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Every "AI" in a product now seems to mean a large language model. The AI that plays against you on my site &lt;strong&gt;doesn't&lt;/strong&gt; — it's classical game-tree search: minimax, expectimax, breadth-first search. That's a deliberate engineering choice, and it's the difference between an opponent that's &lt;strong&gt;provably correct and instant&lt;/strong&gt; and one that's &lt;strong&gt;plausible and slow&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The core point
&lt;/h2&gt;

&lt;p&gt;My &lt;a href="https://lkforge.com/games/tictactoe/" rel="noopener noreferrer"&gt;tic-tac-toe&lt;/a&gt; engine returns a provably-optimal move in about &lt;strong&gt;0.3 ms&lt;/strong&gt;, on your device, with &lt;strong&gt;zero network calls&lt;/strong&gt; — and it has lost &lt;strong&gt;0 of 1,200&lt;/strong&gt; test games. Those are properties a language model, by construction, cannot offer: &lt;strong&gt;determinism, a correctness proof, and sub-frame latency without a server.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  "Why not just use an LLM?"
&lt;/h2&gt;

&lt;p&gt;Fair question in 2026 — you could prompt a model with the board and ask for a move. The reason I don't: a language model is trained to predict the next token of text, &lt;strong&gt;not to search a game tree&lt;/strong&gt;. It can explain tic-tac-toe strategy fluently and still play a losing move, because fluent text and optimal play are different objectives. Winning a solved game is a &lt;em&gt;search&lt;/em&gt; problem, and we already have exact, fast algorithms for it.&lt;/p&gt;

&lt;p&gt;The three engines — minimax + alpha-beta for tic-tac-toe, expectimax for &lt;a href="https://lkforge.com/games/2048/" rel="noopener noreferrer"&gt;2048&lt;/a&gt;, and BFS for &lt;a href="https://lkforge.com/games/lines/" rel="noopener noreferrer"&gt;Color Lines&lt;/a&gt; — are textbook, deterministic, and return a move in a couple of milliseconds or less in a browser tab.&lt;/p&gt;

&lt;h2&gt;
  
  
  Search vs. a language model, point by point
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Game-tree search (mine)&lt;/th&gt;
&lt;th&gt;A language model&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Decides a move by&lt;/td&gt;
&lt;td&gt;searching the tree of legal positions&lt;/td&gt;
&lt;td&gt;predicting likely next tokens&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Correctness&lt;/td&gt;
&lt;td&gt;provable at full depth&lt;/td&gt;
&lt;td&gt;none — fluent ≠ optimal&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Same board →&lt;/td&gt;
&lt;td&gt;same move (deterministic)&lt;/td&gt;
&lt;td&gt;varies with sampling/phrasing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Latency&lt;/td&gt;
&lt;td&gt;a few milliseconds or less, on-device&lt;/td&gt;
&lt;td&gt;a network round-trip&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Needs a server&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Every row is an &lt;em&gt;architectural&lt;/em&gt; difference — how each system decides — not a quoted benchmark. The only measured numbers here are mine.&lt;/p&gt;

&lt;h2&gt;
  
  
  The payoff: a strength number you can actually pin down
&lt;/h2&gt;

&lt;p&gt;Because the engines are deterministic, I can put an &lt;strong&gt;exact&lt;/strong&gt; figure on how strong they are — run the shipped code headlessly, hundreds of times, and count. That's far harder for a model whose output shifts with sampling and phrasing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2048 solver, 250 self-play games:&lt;/strong&gt; 92.8% of games reach the 2048 tile, 64% reach 4096, and &lt;strong&gt;10.8% of the 250 reached 8192&lt;/strong&gt; — a corner-snake expectimax search with probability-threshold pruning at ~2 ms/move. A number, with error bars you could compute, precisely because the same board always drives the same search.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tic-tac-toe&lt;/strong&gt; is the cleaner case: full-depth minimax is provably optimal, so "unbeatable" is a theorem, not a vibe. Across &lt;strong&gt;1,200 self-play games&lt;/strong&gt; (1,000 vs random, 200 vs a perfect copy) it lost none. Alpha-beta keeps full depth cheap: &lt;strong&gt;36,528 nodes instead of 549,945&lt;/strong&gt; at the opening move — a &lt;strong&gt;93% cut&lt;/strong&gt; — in about 0.3 ms.&lt;/p&gt;

&lt;h2&gt;
  
  
  The right tool, not the trendy one
&lt;/h2&gt;

&lt;p&gt;None of this is anti-LLM. Language models are extraordinary at &lt;em&gt;language&lt;/em&gt; — and a couple of the tools on my site that are genuinely language tasks could use one. But a board game with fixed rules and a finite tree is exactly the problem classical search was invented for.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Full write-up with charts: *&lt;/em&gt;&lt;a href="https://lkforge.com/blog/game-ai-not-llms/" rel="noopener noreferrer"&gt;lkforge.com/blog/game-ai-not-llms&lt;/a&gt;*&lt;em&gt;. Related: &lt;a href="https://lkforge.com/blog/game-ai-three-algorithms/" rel="noopener noreferrer"&gt;Six Games, Three Classic Algorithms&lt;/a&gt; · &lt;a href="https://lkforge.com/blog/minimax-alpha-beta-explained/" rel="noopener noreferrer"&gt;Minimax &amp;amp; Alpha-Beta, Visualized&lt;/a&gt; · and the companion experiment, &lt;a href="https://lkforge.com/blog/chatgpt-vs-grok-game-ai-benchmark/" rel="noopener noreferrer"&gt;We Asked ChatGPT and Grok to Benchmark Our Game AI&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>algorithms</category>
      <category>gamedev</category>
      <category>webdev</category>
    </item>
    <item>
      <title>I put my own "no-upload" tools on a network monitor. Here's what actually left the browser.</title>
      <dc:creator>Lucian (LKB)</dc:creator>
      <pubDate>Wed, 16 Sep 2026 04:29:05 +0000</pubDate>
      <link>https://dev.to/lucian_lkb_1f009d/i-put-my-own-no-upload-tools-on-a-network-monitor-heres-what-actually-left-the-browser-7ia</link>
      <guid>https://dev.to/lucian_lkb_1f009d/i-put-my-own-no-upload-tools-on-a-network-monitor-heres-what-actually-left-the-browser-7ia</guid>
      <description>&lt;p&gt;"Runs in your browser, nothing uploaded" is the easiest claim in web tooling to make -- and one of the easiest to fake. A tool can pretty-print your JSON locally &lt;em&gt;and&lt;/em&gt; quietly POST it to a server for "analytics." The only way to know is to watch the network.&lt;/p&gt;

&lt;p&gt;So I did that to my own tools. I opened 15 of them across nine categories with the browser's network panel recording every request, and wrote down exactly where each one went. This is the method and the result -- and, more usefully, a snippet you can paste to check &lt;em&gt;any&lt;/em&gt; tool yourself.&lt;/p&gt;

&lt;h2&gt;
  
  
  The method (repeat it on anything)
&lt;/h2&gt;

&lt;p&gt;Every browser keeps a complete list of the hosts a page contacted. The Network tab shows it visually; the Performance API returns it as data. Load the tool, use it, then run this in the console:&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;hosts&lt;/span&gt; &lt;span class="o"&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;e&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;performance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getEntriesByType&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;resource&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;h&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;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;host&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;h&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;endsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;lkforge.com&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="nx"&gt;hosts&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;h&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;hosts&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;h&lt;/span&gt;&lt;span class="p"&gt;]&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="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="p"&gt;}&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;table&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;hosts&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Swap &lt;code&gt;lkforge.com&lt;/code&gt; for whatever origin you're testing. It prints every host the page touched that &lt;em&gt;isn't&lt;/em&gt; the site itself. On a genuinely local tool you'll see almost nothing; on one that phones home, you'll see where.&lt;/p&gt;

&lt;p&gt;The stronger test needs no code at all: &lt;strong&gt;load the tool, kill your network, keep using it.&lt;/strong&gt; Anything that still works was never talking to a server.&lt;/p&gt;

&lt;h2&gt;
  
  
  The result: three tiers, not two
&lt;/h2&gt;

&lt;p&gt;"Local vs. uploads" turned out to be too coarse. The tools fell into three clear tiers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tier 1 -- fully local (9 of 15).&lt;/strong&gt; The formatters, a CSV converter, a Base64 encoder, a PDF splitter, an EXIF remover, a loan calculator, a chess engine: every request went to the site's own origin -- HTML, CSS, JS, and the fonts. None of them sent the text, file, or image you give them. Once the code has loaded, they do their work with the network off.&lt;/p&gt;

&lt;p&gt;One detail that surprised me: &lt;strong&gt;the fonts are served first-party, not fetched from Google Fonts.&lt;/strong&gt; No &lt;code&gt;fonts.gstatic.com&lt;/code&gt; request at all. (It's Cloudflare rewriting the Google Fonts stylesheet into same-origin &lt;code&gt;@font-face&lt;/code&gt; at the edge -- the browser never contacts Google.)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tier 2 -- same-origin proxy (3 of 15).&lt;/strong&gt; A few tools genuinely need outside data -- your public IP, where an asteroid will pass, which volcanoes are erupting. The interesting part is &lt;em&gt;how&lt;/em&gt; they get it: the browser only ever talked to the site's own &lt;code&gt;/api/*&lt;/code&gt; endpoints. An edge worker fetches the third party server-side and relays the result. From NASA's or the IP-lookup service's point of view, the &lt;em&gt;server&lt;/em&gt; asked -- your browser and your IP stayed put.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tier 3 -- direct public API (3 of 15).&lt;/strong&gt; Three tools did contact an outside host directly: an earthquake feed (USGS), the ISS position (wheretheiss), and a currency table (er-api). In each case the request &lt;em&gt;asks for&lt;/em&gt; public data and carries nothing personal. The currency tool, for instance, downloads the day's rate table once and then converts locally.&lt;/p&gt;

&lt;h2&gt;
  
  
  The honest exceptions (this is the important part)
&lt;/h2&gt;

&lt;p&gt;"Nothing ever leaves your browser" is &lt;strong&gt;not&lt;/strong&gt; true of every feature on any real site, and pretending otherwise is how you lose trust the first time someone opens DevTools. A handful of tools &lt;em&gt;have&lt;/em&gt; to send something to do their job -- you cannot look up a domain's DNS, or identify a plant from a photo, without sending the domain or the photo. On my side that list is: weather (your location), DNS lookup (the domain), a plant identifier (your photo), voice dictation (your audio, via the browser's own Web Speech API), and the contact form (your message).&lt;/p&gt;

&lt;p&gt;The point isn't that a privacy-first site has zero exceptions. It's that &lt;strong&gt;the exceptions are enumerated, not hidden.&lt;/strong&gt; A claim survives DevTools only if the edge cases are named up front.&lt;/p&gt;

&lt;h2&gt;
  
  
  The one third-party host I couldn't hand-wave
&lt;/h2&gt;

&lt;p&gt;On the "fully local" tools, exactly one non-origin host showed up: &lt;code&gt;static.cloudflareinsights.com&lt;/code&gt; -- Cloudflare's Web Analytics. It's cookieless and receives the page URL, screen size, and load timing, not the contents of any tool. I'm counting it here rather than pretending it isn't there, because that's the whole exercise: measure, then report what you actually saw.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaways if you build or use browser tools
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Don't trust the label -- trust the Network tab.&lt;/strong&gt; The snippet above turns "they say it's private" into "I watched it."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;If you build tools, enumerate your exceptions.&lt;/strong&gt; One honest "here's what leaves and why" page is worth more than a blanket "nothing is uploaded" that a single request disproves.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Same-origin proxying is an underused privacy pattern.&lt;/strong&gt; Relaying third-party data through your own edge keeps the user's IP and query off the third party entirely.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Full write-up with the per-tool table and the exceptions list is on the original post. And genuinely -- run the snippet on the next "free online tool" you paste something sensitive into. It takes ten seconds and it's occasionally alarming.&lt;/p&gt;

</description>
      <category>privacy</category>
      <category>webdev</category>
      <category>javascript</category>
      <category>security</category>
    </item>
    <item>
      <title>How Long Would It Take to Crack Your Password?</title>
      <dc:creator>Lucian (LKB)</dc:creator>
      <pubDate>Tue, 15 Sep 2026 04:22:29 +0000</pubDate>
      <link>https://dev.to/lucian_lkb_1f009d/how-long-would-it-take-to-crack-your-password-5ak3</link>
      <guid>https://dev.to/lucian_lkb_1f009d/how-long-would-it-take-to-crack-your-password-5ak3</guid>
      <description>&lt;p&gt;The strength of a randomly generated password is one number — its &lt;strong&gt;entropy&lt;/strong&gt;, in bits&lt;br&gt;
— and it comes from a single line of arithmetic. I computed it across every length and&lt;br&gt;
character-set choice using the exact character sets our &lt;a href="https://lkforge.com/tools/generators/password/" rel="noopener noreferrer"&gt;password&lt;br&gt;
generator&lt;/a&gt; ships, then turned each into&lt;br&gt;
a crack time. One thing comes out very clearly: &lt;strong&gt;eight characters is no longer enough&lt;/strong&gt;,&lt;br&gt;
and length matters far more than sprinkling in symbols.&lt;/p&gt;

&lt;h2&gt;
  
  
  Strength is one number: entropy
&lt;/h2&gt;

&lt;p&gt;When a password is generated randomly, its unpredictability is exactly &lt;strong&gt;length ×&lt;br&gt;
log2(pool size)&lt;/strong&gt;, where the pool is how many characters could sit at each position. With&lt;br&gt;
all four types on — lowercase, uppercase, digits and symbols — the generator draws from&lt;br&gt;
an &lt;strong&gt;88-character&lt;/strong&gt; pool. Each extra &lt;em&gt;bit&lt;/em&gt; of entropy doubles the number of guesses an&lt;br&gt;
attacker must make, so this one number decides everything.&lt;/p&gt;

&lt;h2&gt;
  
  
  Crack time, by length
&lt;/h2&gt;

&lt;p&gt;Entropy becomes a time once you assume a guessing rate. Below, every password uses all&lt;br&gt;
four character types. &lt;strong&gt;Offline&lt;/strong&gt; assumes a GPU rig managing ~1 trillion guesses a second&lt;br&gt;
against a fast or unsalted hash — the worst realistic case if a site's password database&lt;br&gt;
leaks. Times are for the average attack, which searches half the space.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Length&lt;/th&gt;
&lt;th&gt;Entropy&lt;/th&gt;
&lt;th&gt;Rating&lt;/th&gt;
&lt;th&gt;Offline (10¹²/s)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;38.8 bits&lt;/td&gt;
&lt;td&gt;Weak&lt;/td&gt;
&lt;td&gt;instant&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;51.7 bits&lt;/td&gt;
&lt;td&gt;Fair&lt;/td&gt;
&lt;td&gt;~30 minutes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;64.6 bits&lt;/td&gt;
&lt;td&gt;Strong&lt;/td&gt;
&lt;td&gt;~5 months&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;12&lt;/td&gt;
&lt;td&gt;77.5 bits&lt;/td&gt;
&lt;td&gt;Strong&lt;/td&gt;
&lt;td&gt;~3,400 years&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;14&lt;/td&gt;
&lt;td&gt;90.4 bits&lt;/td&gt;
&lt;td&gt;Very strong&lt;/td&gt;
&lt;td&gt;~2.6×10⁷ years&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;16&lt;/td&gt;
&lt;td&gt;103.4 bits&lt;/td&gt;
&lt;td&gt;Very strong&lt;/td&gt;
&lt;td&gt;~2.0×10¹¹ years&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;20&lt;/td&gt;
&lt;td&gt;129.2 bits&lt;/td&gt;
&lt;td&gt;Very strong&lt;/td&gt;
&lt;td&gt;~1.2×10¹⁹ years&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The cliff is between 8 and 12. An 8-character password is a coffee break; a 12-character&lt;br&gt;
one is thousands of years; by 16 the numbers stop meaning anything human. That is the&lt;br&gt;
whole case for a password manager: you never type these, so there is no reason not to&lt;br&gt;
make them 20 characters of noise.&lt;/p&gt;

&lt;h2&gt;
  
  
  Length beats complexity
&lt;/h2&gt;

&lt;p&gt;The instinct to reach for &lt;code&gt;!&lt;/code&gt; and &lt;code&gt;$&lt;/code&gt; is right, but weaker than the instinct to add&lt;br&gt;
characters. Turning on symbols multiplies the pool &lt;strong&gt;once&lt;/strong&gt;. Adding a character multiplies&lt;br&gt;
the difficulty again at &lt;strong&gt;every&lt;/strong&gt; position. The proof is blunt: an &lt;strong&gt;11-character&lt;br&gt;
lowercase-only&lt;/strong&gt; password already has more entropy than an &lt;strong&gt;8-character password using&lt;br&gt;
all four types&lt;/strong&gt; (51.7 bits). A 16-character lowercase-only password holds 75.2 bits —&lt;br&gt;
well past "strong" — from an alphabet of just 26 letters.&lt;/p&gt;

&lt;p&gt;So if you have to choose, choose length, then add the other types on top because there's&lt;br&gt;
no reason not to. Using all four types, you cross 40 bits at 7 characters, 60 at 10, and&lt;br&gt;
80 — comfortably "very strong" — at 13.&lt;/p&gt;

&lt;h2&gt;
  
  
  The one caveat
&lt;/h2&gt;

&lt;p&gt;All of this assumes the password is &lt;strong&gt;randomly generated&lt;/strong&gt;. A human-chosen "P@ssw0rd!"&lt;br&gt;
has nowhere near the entropy its length and character variety suggest, because attackers&lt;br&gt;
guess predictable substitutions first. The formula is only honest for passwords a machine&lt;br&gt;
picked at random — which is exactly what a generator is for.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reproduce it
&lt;/h2&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;entropy&lt;/span&gt; &lt;span class="o"&gt;=&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;pool&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;length&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;log2&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pool&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;     &lt;span class="c1"&gt;// pool = 88, all four types&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;crackSeconds&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;bits&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;rate&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;bits&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="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;rate&lt;/span&gt;    &lt;span class="c1"&gt;// average attack&lt;/span&gt;
&lt;span class="c1"&gt;// entropy(8, 88) = 51.7 bits  -&amp;gt;  crackSeconds(51.7, 1e12) ≈ 30 minutes&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Full write-up, chart and the live &lt;a href="https://lkforge.com/tools/generators/password/" rel="noopener noreferrer"&gt;password&lt;br&gt;
generator&lt;/a&gt; (it shows the entropy as you&lt;br&gt;
type):&lt;br&gt;
&lt;a href="https://lkforge.com/blog/how-long-to-crack-a-password/" rel="noopener noreferrer"&gt;How Long Would It Take to Crack Your Password?&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://lkforge.com/blog/how-long-to-crack-a-password/" rel="noopener noreferrer"&gt;LK Forge&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>security</category>
      <category>javascript</category>
      <category>webdev</category>
      <category>datascience</category>
    </item>
    <item>
      <title>Do the Top Domains Actually Protect Their Email? SPF, DMARC and MTA-STS, Measured</title>
      <dc:creator>Lucian (LKB)</dc:creator>
      <pubDate>Mon, 14 Sep 2026 10:53:32 +0000</pubDate>
      <link>https://dev.to/lucian_lkb_1f009d/do-the-top-domains-actually-protect-their-email-spf-dmarc-and-mta-sts-measured-3n68</link>
      <guid>https://dev.to/lucian_lkb_1f009d/do-the-top-domains-actually-protect-their-email-spf-dmarc-and-mta-sts-measured-3n68</guid>
      <description>&lt;p&gt;SPF, DMARC and MTA-STS are the DNS records that stop someone spoofing mail from your domain.&lt;br&gt;
Everyone says to set them — so I queried live DNS for &lt;strong&gt;152 of the most-used domains&lt;/strong&gt; to see&lt;br&gt;
who actually has. The short version: the basics are solved at the top of the web, most domains&lt;br&gt;
&lt;em&gt;enforce&lt;/em&gt; rather than just monitor, and one newer standard is still almost entirely missing.&lt;/p&gt;
&lt;h2&gt;
  
  
  The funnel
&lt;/h2&gt;

&lt;p&gt;Of the 152 domains, &lt;strong&gt;146 actually receive mail&lt;/strong&gt; (they publish an MX record). Among those:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Signal&lt;/th&gt;
&lt;th&gt;Adoption&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;SPF&lt;/td&gt;
&lt;td&gt;100%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DMARC published&lt;/td&gt;
&lt;td&gt;99.3%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DMARC enforced (quarantine/reject)&lt;/td&gt;
&lt;td&gt;92% (of all 152)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MTA-STS&lt;/td&gt;
&lt;td&gt;9.2%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The first three are effectively universal. The last one falls off a cliff.&lt;/p&gt;
&lt;h2&gt;
  
  
  Publishing DMARC isn't the story — enforcing it is
&lt;/h2&gt;

&lt;p&gt;A DMARC record can be decorative. The &lt;code&gt;p=&lt;/code&gt; policy decides what a receiver does with mail that&lt;br&gt;
fails authentication:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;p=none&lt;/strong&gt; — monitor only; deliver the failing (possibly spoofed) mail anyway. Almost no
protection.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;p=quarantine&lt;/strong&gt; — treat it as suspicious (spam folder).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;p=reject&lt;/strong&gt; — drop it outright. The strongest anti-spoofing stance.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So the real question isn't "do they publish DMARC" but "do they enforce it." At the top, they do:&lt;br&gt;
&lt;strong&gt;114 of 152 are on p=reject&lt;/strong&gt; — the strictest setting — &lt;strong&gt;26 on p=quarantine&lt;/strong&gt;, and only &lt;strong&gt;10 sit&lt;br&gt;
at monitor-only p=none&lt;/strong&gt;. That's &lt;strong&gt;75% on the strictest policy&lt;/strong&gt;, and 93% of everyone who publishes&lt;br&gt;
DMARC is actually enforcing it. The common half-measure — publish a record, leave it on p=none&lt;br&gt;
forever — is rare among the biggest domains.&lt;/p&gt;
&lt;h2&gt;
  
  
  The open frontier: MTA-STS at 9.2%
&lt;/h2&gt;

&lt;p&gt;SPF and DMARC authenticate &lt;em&gt;who sent&lt;/em&gt; a message. They do nothing about &lt;em&gt;how it travels&lt;/em&gt; between&lt;br&gt;
mail servers. &lt;strong&gt;MTA-STS&lt;/strong&gt; closes that: it publishes a policy saying "always deliver to me over TLS,&lt;br&gt;
and don't fall back to plaintext," defeating a downgrade attacker sitting on the wire. It's the&lt;br&gt;
natural next step after DMARC — and among these top domains, only &lt;strong&gt;9.2%&lt;/strong&gt; have it.&lt;/p&gt;

&lt;p&gt;The reason is friction. SPF and DMARC are each a single TXT record. MTA-STS needs a &lt;strong&gt;hosted policy&lt;br&gt;
file&lt;/strong&gt; on an &lt;code&gt;mta-sts.&lt;/code&gt; subdomain &lt;strong&gt;plus&lt;/strong&gt; a DNS record — more moving parts, so most haven't bothered.&lt;br&gt;
That's the entire gap. If you've already done DMARC and want the next genuine win, MTA-STS is it,&lt;br&gt;
and you'd be joining the 9%, not following the crowd.&lt;/p&gt;
&lt;h2&gt;
  
  
  About the sample
&lt;/h2&gt;

&lt;p&gt;These are 152 of the most-used global domains across tech, retail, media, finance, government and&lt;br&gt;
education — which means they &lt;strong&gt;skew sophisticated&lt;/strong&gt;. Read the numbers as "what the best-run domains&lt;br&gt;
do," not the general web, where SPF and especially DMARC adoption is far lower. The value is the&lt;br&gt;
&lt;em&gt;shape&lt;/em&gt;: at the very top, authentication is solved and enforced, while transport security is still&lt;br&gt;
the exception.&lt;/p&gt;
&lt;h2&gt;
  
  
  Reproduce it
&lt;/h2&gt;

&lt;p&gt;Plain DNS lookups against a fixed, documented domain list — no auth, no proprietary data:&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;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;promises&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;dns&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;node:dns&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;spf&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;dns&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;resolveTxt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;domain&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;flat&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;some&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;t&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;t&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="nf"&gt;startsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;v=spf1&lt;/span&gt;&lt;span class="dl"&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;dmarc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;dns&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;resolveTxt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;_dmarc.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;  &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;domain&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;flat&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;t&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;t&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="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;v=dmarc1&lt;/span&gt;&lt;span class="dl"&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;sts&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;dns&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;resolveTxt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;_mta-sts.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;domain&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;flat&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;some&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;t&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;t&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="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;v=stsv1&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;DNS changes over time, so a re-run may move a point or two — that's expected. To check your own&lt;br&gt;
domain, the &lt;a href="https://lkforge.com/tools/networks/dns/" rel="noopener noreferrer"&gt;DNS lookup&lt;/a&gt; pulls its raw MX and TXT records,&lt;br&gt;
and the &lt;a href="https://lkforge.com/tools/networks/smtp-test/" rel="noopener noreferrer"&gt;SMTP test&lt;/a&gt; checks whether its mail host&lt;br&gt;
answers — both from the browser.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://lkforge.com/blog/do-top-domains-protect-their-email/" rel="noopener noreferrer"&gt;LK Forge&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>security</category>
      <category>devops</category>
      <category>dns</category>
    </item>
    <item>
      <title>Measuring Chaos in the Browser: a 0.057 Difference, Gone in 7 Seconds</title>
      <dc:creator>Lucian (LKB)</dc:creator>
      <pubDate>Sun, 13 Sep 2026 17:47:45 +0000</pubDate>
      <link>https://dev.to/lucian_lkb_1f009d/measuring-chaos-in-the-browser-a-0057-difference-gone-in-7-seconds-3bpd</link>
      <guid>https://dev.to/lucian_lkb_1f009d/measuring-chaos-in-the-browser-a-0057-difference-gone-in-7-seconds-3bpd</guid>
      <description>&lt;p&gt;"Chaotic" gets thrown around loosely, so I made it a number. The &lt;a href="https://lkforge.com/tools/physics/" rel="noopener noreferrer"&gt;LK Forge physics labs&lt;/a&gt; run real integrators in the browser, so I took the exact engines behind two of them — the &lt;a href="https://lkforge.com/tools/physics/double-pendulum/" rel="noopener noreferrer"&gt;double pendulum&lt;/a&gt; and the &lt;a href="https://lkforge.com/tools/physics/bifurcation-diagram/" rel="noopener noreferrer"&gt;bifurcation diagram&lt;/a&gt; — and measured how fast order becomes unpredictability. Every number below is computed by a dependency-free script that copies the simulators' own code, so it's fully reproducible.&lt;/p&gt;

&lt;h2&gt;
  
  
  A thousandth of a radian, gone in seven seconds
&lt;/h2&gt;

&lt;p&gt;Release two identical double pendulums from almost the same spot and watch how long they stay in step. Both start from the simulator's default (173.12° and 178.85° from hanging); one is nudged by just &lt;strong&gt;0.001 radian&lt;/strong&gt; — &lt;strong&gt;0.057°&lt;/strong&gt;, about a seventeenth of a single degree — and both are integrated forward with the same RK4 solver the live lab uses.&lt;/p&gt;

&lt;p&gt;They stay visually locked for about &lt;strong&gt;5.6 seconds&lt;/strong&gt;, then fully decorrelate by &lt;strong&gt;7.2 seconds&lt;/strong&gt;. The largest Lyapunov exponent is ≈ &lt;strong&gt;1.095 per second&lt;/strong&gt; — a Lyapunov time of &lt;strong&gt;0.91 s&lt;/strong&gt;, meaning the gap between them multiplies by &lt;em&gt;e&lt;/em&gt; (~2.7×) roughly every second.&lt;/p&gt;

&lt;p&gt;Plotted on a log scale, the gap climbs an almost straight line: the error doesn't grow steadily, it grows &lt;em&gt;exponentially&lt;/em&gt;. Make the initial nudge ten times bigger and you don't lose ten times the time — full divergence just arrives sooner (2.8 s for a 0.05 rad start vs 7.2 s for 0.001 rad). No matter how precisely you measure the start, the unknown part doubles and redoubles until it swamps everything. That's why the third swing of a double pendulum is, for all practical purposes, unpredictable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where chaos begins: r ≈ 3.5699
&lt;/h2&gt;

&lt;p&gt;The double pendulum is chaos in continuous motion. The bifurcation diagram shows the other classic route in, from one of the simplest equations that can misbehave — the logistic map &lt;code&gt;x → r·x·(1−x)&lt;/code&gt;. Pick a growth rate &lt;code&gt;r&lt;/code&gt;, iterate, and see what value the sequence settles onto. For low &lt;code&gt;r&lt;/code&gt; it settles on one number. Turn &lt;code&gt;r&lt;/code&gt; up and that value splits in two, then four, then eight — a &lt;strong&gt;period-doubling cascade&lt;/strong&gt; — with the splits crowding together until, at a precise point, the sequence never repeats.&lt;/p&gt;

&lt;p&gt;From the map's own iterations:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;period 2 at &lt;strong&gt;r ≈ 3.00&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;period 4 at &lt;strong&gt;r ≈ 3.449&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;period 8 at &lt;strong&gt;r ≈ 3.544&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;period 16 at &lt;strong&gt;r ≈ 3.564&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;chaos at &lt;strong&gt;r ≈ 3.5699&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;About &lt;strong&gt;39%&lt;/strong&gt; of the range r ∈ [3, 4] is chaotic; the rest still settles to a finite cycle. And the spacing between successive doublings shrinks by a fixed ratio — from these numbers, &lt;strong&gt;4.75&lt;/strong&gt; then &lt;strong&gt;4.65&lt;/strong&gt;, converging on the &lt;strong&gt;Feigenbaum constant, 4.669&lt;/strong&gt;. That's the strange, beautiful part: the same 4.669 governs the double pendulum too, and dripping taps, and heart-rhythm models. The route into chaos is &lt;em&gt;universal&lt;/em&gt; — it doesn't care what the underlying equation is.&lt;/p&gt;

&lt;h2&gt;
  
  
  Chaos isn't randomness
&lt;/h2&gt;

&lt;p&gt;Both systems here are fully deterministic — the same starting numbers produce the same trajectory every single run, which is exactly why this is reproducible from one short script. Chaos is not randomness; it's sensitive dependence on initial conditions. The rules are exact, but any uncertainty in the starting point grows exponentially, so long-term prediction becomes impossible in practice even though the system is, in principle, perfectly determined.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reproduce it
&lt;/h2&gt;

&lt;p&gt;The whole study is two tiny engines:&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="c1"&gt;// Double pendulum — one RK4 step of the equations of motion (g = 9.8, DT = 1/240)&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;deriv&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nx"&gt;th1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;w1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;th2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;w2&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;d&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;th1&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;th2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;cd&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;cos&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nx"&gt;sd&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;sin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;d&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;den&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3&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;cos&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;th1&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;*&lt;/span&gt;&lt;span class="nx"&gt;th2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                &lt;span class="c1"&gt;// equal masses &amp;amp; lengths&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;a1&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;g&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="mi"&gt;3&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;sin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;th1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;g&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;sin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;th1&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;*&lt;/span&gt;&lt;span class="nx"&gt;th2&lt;/span&gt;&lt;span class="p"&gt;)&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;*&lt;/span&gt;&lt;span class="nx"&gt;sd&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;w2&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;w2&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;w1&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;w1&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;cd&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;den&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;a2&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;sd&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;w1&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;w1&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;+&lt;/span&gt; &lt;span class="nx"&gt;g&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;*&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;cos&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;th1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;w2&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;w2&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;cd&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;den&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;w1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;a1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;w2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;a2&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Logistic map — the whole of it&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;next&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;r&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;r&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;x&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run two pendulums 0.001 rad apart and time the separation → 7.2 s to full divergence. Iterate the logistic map while raising &lt;code&gt;r&lt;/code&gt; and record where the cycle length doubles → chaos at r ≈ 3.5699.&lt;/p&gt;

&lt;h2&gt;
  
  
  See it move
&lt;/h2&gt;

&lt;p&gt;Open the &lt;a href="https://lkforge.com/tools/physics/double-pendulum/" rel="noopener noreferrer"&gt;double pendulum&lt;/a&gt;, turn on the ghost twin, and watch the two arms trace each other before splitting for good. Then drag the growth-rate slider on the &lt;a href="https://lkforge.com/tools/physics/bifurcation-diagram/" rel="noopener noreferrer"&gt;bifurcation diagram&lt;/a&gt; across r ≈ 3.5699 and see the single line shatter into a band. Both run entirely in your browser — nothing is uploaded.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://lkforge.com/blog/measuring-chaos-in-the-browser/" rel="noopener noreferrer"&gt;LK Forge&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>physics</category>
      <category>datascience</category>
      <category>javascript</category>
      <category>science</category>
    </item>
    <item>
      <title>Does thinking twice as long make a Go AI twice as good? I measured it</title>
      <dc:creator>Lucian (LKB)</dc:creator>
      <pubDate>Sat, 12 Sep 2026 22:35:24 +0000</pubDate>
      <link>https://dev.to/lucian_lkb_1f009d/does-thinking-twice-as-long-make-a-go-ai-twice-as-good-i-measured-it-3mj8</link>
      <guid>https://dev.to/lucian_lkb_1f009d/does-thinking-twice-as-long-make-a-go-ai-twice-as-good-i-measured-it-3mj8</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Syndicated from the original on &lt;strong&gt;&lt;a href="https://lkforge.com/blog/go-mcts-scaling/" rel="noopener noreferrer"&gt;lkforge.com&lt;/a&gt;&lt;/strong&gt;. The live engine is at &lt;a href="https://lkforge.com/games/go/" rel="noopener noreferrer"&gt;lkforge.com/games/go&lt;/a&gt;; the full harness is &lt;a href="https://gist.github.com/lucian-devops/40c3ce8a3b35907ab4f6b6b3661cfcd6" rel="noopener noreferrer"&gt;in this gist&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;My browser Go engine uses &lt;strong&gt;Monte Carlo Tree Search&lt;/strong&gt;: to pick a move it plays out thousands of fast random games and keeps the move that wins most. The obvious knob is &lt;em&gt;how many&lt;/em&gt; playouts it runs per move — so the obvious question is what you actually buy by turning it up. I measured it, on a 9×9 board, with a headless self-play harness that pits the engine against weaker copies of itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  The counterintuitive result
&lt;/h2&gt;

&lt;p&gt;I expected diminishing returns. I got the opposite.&lt;/p&gt;

&lt;p&gt;On 9×9 Go, each doubling of MCTS playouts adds about &lt;strong&gt;+233 Elo&lt;/strong&gt; — and the gains &lt;strong&gt;grow&lt;/strong&gt; as you climb, from &lt;strong&gt;+171&lt;/strong&gt; at the first doubling to &lt;strong&gt;+357&lt;/strong&gt; at the last. Out to 1,600 playouts a move, more search just keeps paying off, with no flattening in sight.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;doubling of playouts&lt;/th&gt;
&lt;th&gt;deeper engine wins&lt;/th&gt;
&lt;th&gt;Elo gained&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;50 → 100&lt;/td&gt;
&lt;td&gt;73%&lt;/td&gt;
&lt;td&gt;+171&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;100 → 200&lt;/td&gt;
&lt;td&gt;72%&lt;/td&gt;
&lt;td&gt;+162&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;200 → 400&lt;/td&gt;
&lt;td&gt;74%&lt;/td&gt;
&lt;td&gt;+180&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;400 → 800&lt;/td&gt;
&lt;td&gt;85%&lt;/td&gt;
&lt;td&gt;+297&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;800 → 1600&lt;/td&gt;
&lt;td&gt;89%&lt;/td&gt;
&lt;td&gt;+357&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Cumulative: &lt;strong&gt;+1,167 Elo&lt;/strong&gt; from 50 to 1,600 playouts.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why playouts, not milliseconds
&lt;/h2&gt;

&lt;p&gt;The live game gives its AI a time budget — about a second per move on 9×9 — because that keeps the page responsive. But a time budget is the wrong unit for a benchmark: "0.9 seconds of thinking" buys a fast laptop three times as many playouts as a phone, so a strength number tied to milliseconds says more about the reader's hardware than the engine.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Playouts per move&lt;/strong&gt; is the hardware-independent knob. Fix the playout count and the same seed produces the same games on any machine — which is the whole point of publishing a number someone else can check.&lt;/p&gt;

&lt;h2&gt;
  
  
  The method
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;9×9, komi 7.5, area (Chinese) scoring, using the exact &lt;code&gt;mcts()&lt;/code&gt; function the browser ships.&lt;/li&gt;
&lt;li&gt;For each doubling step, 100 games between two playout budgets, &lt;strong&gt;alternating which side is Black&lt;/strong&gt; so first-move advantage and komi cancel out.&lt;/li&gt;
&lt;li&gt;Seeded RNG (mulberry32), so every game — and every Elo figure — reproduces exactly.&lt;/li&gt;
&lt;li&gt;Elo per pairing = &lt;code&gt;400 · log10(p / (1 − p))&lt;/code&gt; from the colour-balanced win rate &lt;code&gt;p&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why the gains grow instead of shrink
&lt;/h2&gt;

&lt;p&gt;A plausible read: at 50–200 playouts the search is still too noisy to convert its reading into the right move, so the extra rollouts are half-wasted. From 400 playouts up it reliably reads out the captures and life-and-death that actually decide a 9×9 game, so each doubling unlocks &lt;em&gt;more&lt;/em&gt;, not less. Saturation must arrive eventually — but not by 1,600 playouts a move.&lt;/p&gt;

&lt;p&gt;(This is an honest-amateur engine, strongest on 9×9. The Elo figures are internal — the engine against itself at different budgets, not against human ranks or a program like KataGo. A 9×9 result won't transfer unchanged to 19×19.)&lt;/p&gt;

&lt;h2&gt;
  
  
  Bonus: is 9×9 fair?
&lt;/h2&gt;

&lt;p&gt;Black moves first, so White gets 7.5 points of &lt;em&gt;komi&lt;/em&gt; as compensation. Holding both sides at 800 playouts, Black won &lt;strong&gt;41%&lt;/strong&gt; of 100 games — so at this strength on 9×9, 7.5 komi slightly &lt;em&gt;over&lt;/em&gt;-compensates, leaving a small edge to White. Close to fair, not a landslide.&lt;/p&gt;

&lt;p&gt;Full harness, engine snapshot, and reproduce steps: &lt;strong&gt;&lt;a href="https://gist.github.com/lucian-devops/40c3ce8a3b35907ab4f6b6b3661cfcd6" rel="noopener noreferrer"&gt;gist&lt;/a&gt;&lt;/strong&gt; · original writeup with charts: &lt;strong&gt;&lt;a href="https://lkforge.com/blog/go-mcts-scaling/" rel="noopener noreferrer"&gt;lkforge.com/blog/go-mcts-scaling&lt;/a&gt;&lt;/strong&gt; · play the engine: &lt;strong&gt;&lt;a href="https://lkforge.com/games/go/" rel="noopener noreferrer"&gt;lkforge.com/games/go&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;

</description>
      <category>gamedev</category>
      <category>algorithms</category>
      <category>javascript</category>
      <category>ai</category>
    </item>
    <item>
      <title>I asked ChatGPT and Grok to benchmark my game AI. Then I ran the code.</title>
      <dc:creator>Lucian (LKB)</dc:creator>
      <pubDate>Sat, 12 Sep 2026 05:49:14 +0000</pubDate>
      <link>https://dev.to/lucian_lkb_1f009d/i-asked-chatgpt-and-grok-to-benchmark-my-game-ai-then-i-ran-the-code-cbm</link>
      <guid>https://dev.to/lucian_lkb_1f009d/i-asked-chatgpt-and-grok-to-benchmark-my-game-ai-then-i-ran-the-code-cbm</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Syndicated from the original on &lt;strong&gt;&lt;a href="https://lkforge.com/blog/chatgpt-vs-grok-game-ai-benchmark/" rel="noopener noreferrer"&gt;lkforge.com&lt;/a&gt;&lt;/strong&gt;. The two games under test are playable at &lt;a href="https://lkforge.com/games/tictactoe/" rel="noopener noreferrer"&gt;tic-tac-toe&lt;/a&gt; and &lt;a href="https://lkforge.com/games/2048/" rel="noopener noreferrer"&gt;2048&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The games on my site don't think with a language model. Tic-Tac-Toe runs &lt;strong&gt;minimax with alpha-beta pruning&lt;/strong&gt;; 2048 runs an &lt;strong&gt;expectimax&lt;/strong&gt; search over the random tile spawns — classic, deterministic algorithms, not a chatbot. To pressure-test that claim, I handed the same engineering brief to two frontier assistants — ChatGPT and Grok — and watched how each reasoned about it. Then I did the one thing neither of them actually did: &lt;strong&gt;I ran the code.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The brief given to both
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;Build a comparative benchmarking tool that evaluates classical game algorithms like Minimax and Expectimax against LLM-based game agents — comparing move-time (ms), memory footprint, and win-rate consistency across 100 rounds of Tic-Tac-Toe and 2048, to demonstrate the deterministic advantage of algorithm engines over stochastic models.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Read the wording carefully: the prompt asks for a &lt;em&gt;conclusion&lt;/em&gt; — "&lt;strong&gt;to demonstrate&lt;/strong&gt; the deterministic advantage." That framing is the whole experiment. A careful builder measures first and lets the numbers speak. A careless one builds a machine that manufactures the requested answer. I got one of each.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two builds from one brief
&lt;/h2&gt;

&lt;p&gt;Both replies correctly named the algorithms. Where they split is &lt;strong&gt;method and honesty&lt;/strong&gt; — specifically, how each handled the part of the brief it &lt;em&gt;couldn't&lt;/em&gt; actually deliver: a real, measured LLM opponent.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;ChatGPT — measured.&lt;/strong&gt; Built the honest, incomplete version: a runnable browser tool (5 files) that computes figures live, with &lt;strong&gt;no numbers bundled&lt;/strong&gt;. Wired a real LLM adapter through a server-side proxy instead of faking an opponent. Disclaimed what a browser can't measure (provider-side model RAM). Warned that 100 live-LLM rounds means "many thousands of API calls — start with 5–10."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Grok — assumed.&lt;/strong&gt; Built the impressive, pre-decided version: a self-contained Python script that runs out of the box. But the "LLM opponent" is a &lt;strong&gt;simulation, not an LLM&lt;/strong&gt; — random moves 12% of the time plus Gaussian noise:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;LLMAgent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Simulates an LLM: temperature sampling + occasional illegal proposals.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It bundled "illustrative" numbers, printed &lt;strong&gt;&lt;code&gt;DETERMINISTIC ADVANTAGE DEMONSTRATED&lt;/code&gt;&lt;/strong&gt;, and — because each round is self-play — it never actually pits classical against LLM at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  So I ran Grok's code
&lt;/h2&gt;

&lt;p&gt;Its engine code is genuinely fine, so I executed it as written. Every figure below is &lt;strong&gt;measured on one laptop&lt;/strong&gt;, not illustrative. The "LLM-sim" row is Grok's straw-man opponent — read it as "a deliberately noisy heuristic," not a real model.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tic-Tac-Toe — 100 rounds each · self-play · minimax at full depth&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Agent&lt;/th&gt;
&lt;th&gt;W / D / L&lt;/th&gt;
&lt;th&gt;Avg move&lt;/th&gt;
&lt;th&gt;Move SD&lt;/th&gt;
&lt;th&gt;Peak mem&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Minimax (classical)&lt;/td&gt;
&lt;td&gt;0 / 100 / 0&lt;/td&gt;
&lt;td&gt;3.450 ms&lt;/td&gt;
&lt;td&gt;0.037 ms&lt;/td&gt;
&lt;td&gt;2.3 KB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LLM-sim (stochastic)&lt;/td&gt;
&lt;td&gt;69 / 2 / 29&lt;/td&gt;
&lt;td&gt;0.012 ms&lt;/td&gt;
&lt;td&gt;0.002 ms&lt;/td&gt;
&lt;td&gt;0.8 KB&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;2048 — 8 rounds each · single-agent · expectimax depth 3–5&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Agent&lt;/th&gt;
&lt;th&gt;Reached 2048&lt;/th&gt;
&lt;th&gt;Median tile&lt;/th&gt;
&lt;th&gt;Avg score&lt;/th&gt;
&lt;th&gt;Avg move&lt;/th&gt;
&lt;th&gt;Peak mem&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Expectimax (classical)&lt;/td&gt;
&lt;td&gt;6 / 8&lt;/td&gt;
&lt;td&gt;2048&lt;/td&gt;
&lt;td&gt;27,976&lt;/td&gt;
&lt;td&gt;110.6 ms&lt;/td&gt;
&lt;td&gt;66.8 KB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LLM-sim (stochastic)&lt;/td&gt;
&lt;td&gt;0 / 8&lt;/td&gt;
&lt;td&gt;128&lt;/td&gt;
&lt;td&gt;1,287&lt;/td&gt;
&lt;td&gt;0.19 ms&lt;/td&gt;
&lt;td&gt;8.9 KB&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;On 2048 that's a &lt;strong&gt;~22× gap&lt;/strong&gt; in average score: the lookahead search reaches the 2048 tile in 6 of 8 games; the one-move-ahead guesser never does.&lt;/p&gt;

&lt;h2&gt;
  
  
  The "illustrative" numbers were never actually run
&lt;/h2&gt;

&lt;p&gt;My 8-round 2048 sample took &lt;strong&gt;21.5 minutes&lt;/strong&gt; — about 161 seconds per round for expectimax. Extrapolate to the brief's 100 rounds and you're looking at roughly &lt;strong&gt;16,126 seconds ≈ 4.5 hours&lt;/strong&gt; of compute. That's why I sampled 8. It's also strong evidence that the bundled "100-round" figures in the pre-decided build were never executed — nobody sat through 4.5 hours to print a conclusion they'd already hard-coded.&lt;/p&gt;

&lt;h2&gt;
  
  
  The takeaway
&lt;/h2&gt;

&lt;p&gt;The interesting result isn't "classical beats a noisy heuristic" — that was never in doubt. It's that a &lt;strong&gt;leading prompt&lt;/strong&gt; split two capable assistants cleanly into &lt;em&gt;measure-then-report&lt;/em&gt; and &lt;em&gt;report-then-decorate&lt;/em&gt;, and only running the code tells you which one you got.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Full methodology, both AI transcripts, and the exact commands are on the original: *&lt;/em&gt;&lt;a href="https://lkforge.com/blog/chatgpt-vs-grok-game-ai-benchmark/" rel="noopener noreferrer"&gt;lkforge.com/blog/chatgpt-vs-grok-game-ai-benchmark&lt;/a&gt;*&lt;em&gt;. Related: &lt;a href="https://lkforge.com/blog/game-ai-not-llms/" rel="noopener noreferrer"&gt;Real Game AI, Not a Chatbot&lt;/a&gt; · &lt;a href="https://lkforge.com/blog/benchmarking-game-ai/" rel="noopener noreferrer"&gt;Benchmarking Game AI&lt;/a&gt; · &lt;a href="https://lkforge.com/blog/game-ai-three-algorithms/" rel="noopener noreferrer"&gt;Six Games, Three Classic Algorithms&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>algorithms</category>
      <category>gamedev</category>
    </item>
    <item>
      <title>The North Star isn't the brightest star. It's not even in the top 40.</title>
      <dc:creator>Lucian (LKB)</dc:creator>
      <pubDate>Fri, 11 Sep 2026 06:28:54 +0000</pubDate>
      <link>https://dev.to/lucian_lkb_1f009d/the-north-star-isnt-the-brightest-star-its-not-even-in-the-top-40-430j</link>
      <guid>https://dev.to/lucian_lkb_1f009d/the-north-star-isnt-the-brightest-star-its-not-even-in-the-top-40-430j</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Syndicated from the original on &lt;strong&gt;&lt;a href="https://lkforge.com/blog/is-the-north-star-the-brightest/" rel="noopener noreferrer"&gt;lkforge.com&lt;/a&gt;&lt;/strong&gt;. The ranking below is printed by one short, dependency-free script.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The North Star is the most famous star in the sky, and almost everyone assumes fame means brightness. It doesn't. Rank the stars by how bright they actually appear and Polaris isn't close to the top - it isn't even in the top forty.&lt;/p&gt;

&lt;h2&gt;
  
  
  The correction
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Polaris ranks about 48th&lt;/strong&gt; in apparent brightness.&lt;/li&gt;
&lt;li&gt;The real brightest star, &lt;strong&gt;Sirius (magnitude -1.46), outshines Polaris (+1.98) by roughly 24 to 1.&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Polaris shines at only &lt;strong&gt;4.2% of Sirius&lt;/strong&gt;, beaten by two dozen household-name stars before you even reach the fainter ones.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The actual brightest stars (as a share of Sirius)
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;#&lt;/th&gt;
&lt;th&gt;Star&lt;/th&gt;
&lt;th&gt;Brightness vs Sirius&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Sirius&lt;/td&gt;
&lt;td&gt;100%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Canopus&lt;/td&gt;
&lt;td&gt;52%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Alpha Centauri&lt;/td&gt;
&lt;td&gt;33%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Arcturus&lt;/td&gt;
&lt;td&gt;27%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;Vega&lt;/td&gt;
&lt;td&gt;25%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;Capella&lt;/td&gt;
&lt;td&gt;24%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;Rigel&lt;/td&gt;
&lt;td&gt;23%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;Procyon&lt;/td&gt;
&lt;td&gt;19%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;Betelgeuse&lt;/td&gt;
&lt;td&gt;18%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;...&lt;/td&gt;
&lt;td&gt;...(35+ more)&lt;/td&gt;
&lt;td&gt;...&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;~48&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Polaris&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;4.2%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Why the numbers run backwards
&lt;/h2&gt;

&lt;p&gt;The scale is the confusing part. &lt;strong&gt;Apparent magnitude&lt;/strong&gt; measures brightness with &lt;em&gt;smaller = brighter&lt;/em&gt; - a leftover from the ancient Greeks, who called the brightest stars "first magnitude" and the faintest "sixth." It's also logarithmic: every 5 steps of magnitude is a factor of exactly 100 in brightness.&lt;/p&gt;

&lt;p&gt;So Sirius at -1.46 is genuinely brilliant, Polaris at +1.98 is middling, and a magnitude-6 star is at the edge of naked-eye visibility. To turn magnitudes into a linear "how many times brighter," you use:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;brightness ratio = 10 ^ (-0.4 * delta-magnitude)&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;For Sirius vs Polaris that's a gap of ~3.44 magnitudes, or about 24x.&lt;/p&gt;

&lt;h2&gt;
  
  
  So why is Polaris famous?
&lt;/h2&gt;

&lt;p&gt;Position, not brightness. &lt;strong&gt;Polaris sits within about 0.7 degrees of the north celestial pole&lt;/strong&gt; - the point Earth's axis points at - so as the planet turns, every other star wheels in a circle while Polaris barely moves. It marks true north and holds still, which for two thousand years made it the single most useful star for navigation in the Northern Hemisphere.&lt;/p&gt;

&lt;p&gt;It won't hold the job forever: Earth's axis slowly wobbles (precession), so the pole star changes over millennia - the bright star Vega sat near the pole around 12,000 BC and will again near 13,700 AD. Polaris is simply the star on duty now.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reproduce it
&lt;/h2&gt;

&lt;p&gt;Every figure comes from &lt;a href="https://gist.github.com/lucian-devops/713652004ebe524af16cfbcb1d5c0831" rel="noopener noreferrer"&gt;one short script&lt;/a&gt;: standard published apparent magnitudes (Hipparcos / Yale Bright Star Catalogue), ranked, then converted to linear brightness. Several bright stars are slightly variable, so ranks near a tie can shift a place; Polaris's ~48th is the widely cited figure.&lt;/p&gt;

&lt;p&gt;Full write-up with the ranked chart is on the original: &lt;strong&gt;&lt;a href="https://lkforge.com/blog/is-the-north-star-the-brightest/" rel="noopener noreferrer"&gt;Is the North Star the Brightest?&lt;/a&gt;&lt;/strong&gt; You can find Polaris and everything wheeling around it with the &lt;a href="https://lkforge.com/tools/space/sky/" rel="noopener noreferrer"&gt;Sky Explorer&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>data</category>
      <category>science</category>
      <category>astronomy</category>
      <category>javascript</category>
    </item>
    <item>
      <title>The UN voted to drop the Mercator map. Here's exactly how wrong it was, measured.</title>
      <dc:creator>Lucian (LKB)</dc:creator>
      <pubDate>Thu, 10 Sep 2026 21:24:50 +0000</pubDate>
      <link>https://dev.to/lucian_lkb_1f009d/the-un-voted-to-drop-the-mercator-map-heres-exactly-how-wrong-it-was-measured-4d81</link>
      <guid>https://dev.to/lucian_lkb_1f009d/the-un-voted-to-drop-the-mercator-map-heres-exactly-how-wrong-it-was-measured-4d81</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Syndicated from the original on &lt;strong&gt;&lt;a href="https://lkforge.com/blog/true-size-of-countries/" rel="noopener noreferrer"&gt;lkforge.com&lt;/a&gt;&lt;/strong&gt;. The country data is the same set behind my &lt;a href="https://lkforge.com/tools/atlas/" rel="noopener noreferrer"&gt;World Atlas&lt;/a&gt;, and the script that prints every number below is public and dependency-free.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;On September 4, 2026 the UN General Assembly backed a push to retire the &lt;strong&gt;Mercator&lt;/strong&gt; projection - the world map most of us grew up with - in favor of equal-area maps. The complaint is old but correct: Mercator badly misrepresents size. I wanted the actual numbers, so I took the real land areas behind my World Atlas and measured it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The headline: Greenland is not the size of Africa
&lt;/h2&gt;

&lt;p&gt;On a Mercator map they look about equal. They are not close.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Africa is 14.0x larger than Greenland&lt;/strong&gt; (30.3M km2 across 50 countries vs 2.17M km2).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Africa is 1.8x larger than Russia&lt;/strong&gt; (17.1M km2), the biggest country on Earth.&lt;/li&gt;
&lt;li&gt;Greenland is only the &lt;strong&gt;12th-largest landmass&lt;/strong&gt; on the list. &lt;strong&gt;Algeria alone (2.38M km2) is bigger than Greenland.&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The real top of the ranking (millions of km2)
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;#&lt;/th&gt;
&lt;th&gt;Country&lt;/th&gt;
&lt;th&gt;Area&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Russia&lt;/td&gt;
&lt;td&gt;17.1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Canada&lt;/td&gt;
&lt;td&gt;10.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;China&lt;/td&gt;
&lt;td&gt;9.7&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;United States&lt;/td&gt;
&lt;td&gt;9.4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;Brazil&lt;/td&gt;
&lt;td&gt;8.5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;...&lt;/td&gt;
&lt;td&gt;...&lt;/td&gt;
&lt;td&gt;...&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;Algeria&lt;/td&gt;
&lt;td&gt;2.4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;11&lt;/td&gt;
&lt;td&gt;DR Congo&lt;/td&gt;
&lt;td&gt;2.3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;12&lt;/td&gt;
&lt;td&gt;Greenland&lt;/td&gt;
&lt;td&gt;2.2&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Why the map lies, in one formula
&lt;/h2&gt;

&lt;p&gt;Mercator keeps compass directions straight, and the price is size. It stretches the map by a factor that grows toward the poles, on &lt;strong&gt;both&lt;/strong&gt; axes - so &lt;strong&gt;area&lt;/strong&gt; is scaled by that factor squared:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Mercator area inflation = sec^2(latitude)&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Evaluate it at each country's centroid latitude and the whole illusion falls out:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Country&lt;/th&gt;
&lt;th&gt;Centroid lat&lt;/th&gt;
&lt;th&gt;Area shown on Mercator&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Greenland&lt;/td&gt;
&lt;td&gt;72 N&lt;/td&gt;
&lt;td&gt;x10.5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Russia&lt;/td&gt;
&lt;td&gt;60 N&lt;/td&gt;
&lt;td&gt;x4.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Canada&lt;/td&gt;
&lt;td&gt;60 N&lt;/td&gt;
&lt;td&gt;x4.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;United States&lt;/td&gt;
&lt;td&gt;38 N&lt;/td&gt;
&lt;td&gt;x1.6&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DR Congo&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;x1.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Brazil&lt;/td&gt;
&lt;td&gt;10 S&lt;/td&gt;
&lt;td&gt;x1.0&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Greenland sits near 72 N, where Mercator inflates area more than tenfold - which is exactly how a 2-million-km2 island ends up looking like a 30-million-km2 continent. Countries on the equator are drawn true to size.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reproduce it
&lt;/h2&gt;

&lt;p&gt;Every figure here is printed by &lt;a href="https://gist.github.com/lucian-devops/1b0fe6703fa3e43cc502a8124f56a954" rel="noopener noreferrer"&gt;one short script&lt;/a&gt; that fetches the same country dataset (area + centroid latitude) my World Atlas ships and does the arithmetic - no dependencies. The centroid-latitude distortion is a per-country approximation, since a country spans latitudes, but it captures the effect.&lt;/p&gt;

&lt;p&gt;The full write-up, with the ranked chart and the distortion table, is on the original: &lt;strong&gt;&lt;a href="https://lkforge.com/blog/true-size-of-countries/" rel="noopener noreferrer"&gt;The True Size of Countries&lt;/a&gt;&lt;/strong&gt;. You can watch the distortion appear and vanish by switching between the flat map and the globe in the &lt;a href="https://lkforge.com/tools/atlas/" rel="noopener noreferrer"&gt;atlas&lt;/a&gt; itself.&lt;/p&gt;

</description>
      <category>data</category>
      <category>geography</category>
      <category>maps</category>
      <category>javascript</category>
    </item>
    <item>
      <title>How Much Quality Do You Lose Compressing an Image? We Measured It on 24 Photos</title>
      <dc:creator>Lucian (LKB)</dc:creator>
      <pubDate>Thu, 10 Sep 2026 07:01:24 +0000</pubDate>
      <link>https://dev.to/lucian_lkb_1f009d/how-much-quality-do-you-lose-compressing-an-image-we-measured-it-on-24-photos-18mo</link>
      <guid>https://dev.to/lucian_lkb_1f009d/how-much-quality-do-you-lose-compressing-an-image-we-measured-it-on-24-photos-18mo</guid>
      <description>&lt;p&gt;"Compress without losing quality" is a promise lossy formats can't fully keep — every step of compression trades some fidelity for bytes. The useful question isn't &lt;em&gt;whether&lt;/em&gt; you lose quality but &lt;em&gt;how much, for how many bytes saved&lt;/em&gt;. So we measured the trade directly.&lt;/p&gt;

&lt;h2&gt;
  
  
  The method
&lt;/h2&gt;

&lt;p&gt;Across the &lt;strong&gt;24-image Kodak reference suite&lt;/strong&gt; — the standard set for this kind of test — we saved every photo as &lt;strong&gt;JPEG&lt;/strong&gt; and &lt;strong&gt;WebP&lt;/strong&gt; at qualities 20 through 95, and scored each output on &lt;strong&gt;SSIM&lt;/strong&gt; (structural similarity to the original, 0 to 1) rather than eyeballing it. Every figure below is the mean over all 24 images, so it's not one lucky photo.&lt;/p&gt;

&lt;h2&gt;
  
  
  Diminishing returns are brutal
&lt;/h2&gt;

&lt;p&gt;The size-vs-quality curve bends hard: the first kilobytes buy almost all of the quality, and after that it flattens into a long tail where you pay a lot for very little.&lt;/p&gt;

&lt;p&gt;The clearest example: pushing JPEG from &lt;strong&gt;quality 85 to 95 nearly doubles the file&lt;/strong&gt; — from about 14% to 26% of the original — for an SSIM gain of just &lt;strong&gt;0.025&lt;/strong&gt; (0.95 → 0.98). That is double the bytes for a difference most people can't see on most images. The habit of exporting at 95 or 100 "to be safe" is, for photographs, mostly wasted space.&lt;/p&gt;

&lt;h2&gt;
  
  
  Format beats quality-cranking
&lt;/h2&gt;

&lt;p&gt;Changing the codec does more than turning the quality dial:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;WebP is about 30% smaller than JPEG at the same SSIM&lt;/strong&gt; — 29.6% smaller at SSIM 0.95, 31.9% at 0.97. Same perceived fidelity, a third less weight.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Staying fully lossless costs roughly 7× a quality-85 JPEG.&lt;/strong&gt; "Keep it lossless to be safe" usually means paying seven times over for bytes no one will perceive.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The practical takeaway
&lt;/h2&gt;

&lt;p&gt;For photographs, the &lt;strong&gt;sweet spot is quality 80–85&lt;/strong&gt;, ideally in WebP. But the honest answer is that the right setting is image-dependent — a flat sky compresses beautifully, fine texture does not — so &lt;strong&gt;a live preview beats any single recommended number&lt;/strong&gt;. Pick the point on the curve where the preview stops changing, not a fixed "95".&lt;/p&gt;

&lt;h2&gt;
  
  
  Reproduce it
&lt;/h2&gt;

&lt;p&gt;The study is a straightforward encode-and-score sweep over the Kodak set, driven by a short script — point it at your own images and you'll get the same shape of curve. And if you just need to compress something, the &lt;a href="https://lkforge.com/tools/image/compressor/" rel="noopener noreferrer"&gt;image compressor&lt;/a&gt; and the rest of the &lt;a href="https://lkforge.com/tools/image/" rel="noopener noreferrer"&gt;image tools&lt;/a&gt; run entirely in your browser with a live preview — nothing uploaded.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://lkforge.com/blog/image-compression-quality-vs-file-size/" rel="noopener noreferrer"&gt;LK Forge&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>performance</category>
      <category>images</category>
      <category>javascript</category>
    </item>
  </channel>
</rss>
