<?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: mincho milev</title>
    <description>The latest articles on DEV Community by mincho milev (@mincho_milev_c7753077d5af).</description>
    <link>https://dev.to/mincho_milev_c7753077d5af</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%2F1938279%2Fec79864f-f7ff-444d-a8f8-432d21e838f8.png</url>
      <title>DEV Community: mincho milev</title>
      <link>https://dev.to/mincho_milev_c7753077d5af</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/mincho_milev_c7753077d5af"/>
    <language>en</language>
    <item>
      <title>Mining 14,351 Chess Puzzles with Stockfish 18 (and the Illegal-FEN Trap in chess.js)</title>
      <dc:creator>mincho milev</dc:creator>
      <pubDate>Thu, 05 Nov 2026 20:20:36 +0000</pubDate>
      <link>https://dev.to/mincho_milev_c7753077d5af/mining-14351-chess-puzzles-with-stockfish-18-and-the-illegal-fen-trap-in-chessjs-3o5i</link>
      <guid>https://dev.to/mincho_milev_c7753077d5af/mining-14351-chess-puzzles-with-stockfish-18-and-the-illegal-fen-trap-in-chessjs-3o5i</guid>
      <description>&lt;p&gt;When we started building &lt;strong&gt;&lt;a href="https://brasspawn.com/" rel="noopener noreferrer"&gt;Brass Pawn&lt;/a&gt;&lt;/strong&gt; (&lt;a href="https://github.com/minchopm/chess-trainer" rel="noopener noreferrer"&gt;source on GitHub, AGPLv3&lt;/a&gt;), we needed a large offline tactics, positional judgement, and endgame dataset that ran completely on-device—both in the browser and natively on iPhone, iPad, and Mac.&lt;/p&gt;

&lt;p&gt;Transcribing chess puzzles by hand or scraping unverified problem collections breaks down fast. Two failure modes show up immediately in real training:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Cooked solutions:&lt;/strong&gt; A position has a flashy sacrifice, so a human annotator marks it as winning—missing a quiet defensive resource at depth 18, or missing a &lt;em&gt;second&lt;/em&gt; equally winning move that punishes a student for playing an objectively strong alternative.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Horizon flips:&lt;/strong&gt; A shallow engine pass (depth 10–12) flags a move as &lt;code&gt;+2.10&lt;/code&gt;, only for depth 22 to reveal perpetual check (&lt;code&gt;0.00&lt;/code&gt;).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Instead of transcribing puzzles, we built a deterministic mining and independent verification pipeline powered by &lt;strong&gt;Stockfish 18&lt;/strong&gt;. Along the way, we hit a subtle FEN validation edge case in &lt;code&gt;chess.js&lt;/code&gt; that silently breaks UCI engines.&lt;/p&gt;




&lt;h2&gt;
  
  
  Stage 1: Mining for Uniqueness, Not Just Blunders
&lt;/h2&gt;

&lt;p&gt;A good tactics puzzle isn't simply "someone blundered in a game." The actual mathematical requirement for a fair puzzle is &lt;strong&gt;uniqueness&lt;/strong&gt;: &lt;em&gt;exactly one legal move must preserve or seize a decisive advantage, while every alternative drops significant win probability.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Our generator runs a multi-stage filter:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Human-strength self-play seeding:&lt;/strong&gt; Stockfish plays games at calibrated Elo bands (1320–2500), randomizing among shallow top candidates in the opening so positions diversify across pawn structures rather than repeating five mainlines.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Shallow two-line screen (Depth 12, &lt;code&gt;MultiPV = 2&lt;/code&gt;):&lt;/strong&gt; Every candidate position is evaluated with two principal variations. Most positions either have multiple good moves or zero good moves; both are discarded cheaply here.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deep uniqueness confirmation (Depth 20, &lt;code&gt;MultiPV = 2&lt;/code&gt;):&lt;/strong&gt; Survivors are re-searched at depth 20. We only keep a candidate if &lt;code&gt;bestMove&lt;/code&gt; beats the second-best move by &lt;strong&gt;at least 140 centipawns&lt;/strong&gt; (&lt;code&gt;&amp;gt;= 140 cp&lt;/code&gt;) and achieves a concrete evaluation swing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Branchless line extension:&lt;/strong&gt; We step forward along the principal variation, extending the puzzle ply by ply &lt;em&gt;only&lt;/em&gt; while every solver turn continues to have a single uniquely best reply. The moment two moves become interchangeable, the puzzle terminates cleanly.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That pipeline produced &lt;strong&gt;14,351 verified tactics puzzles&lt;/strong&gt; spanning 760–2899 puzzle rating, alongside 116 positional judgement exercises (where &lt;em&gt;no&lt;/em&gt; forced tactical win exists) and canonical endgame drills.&lt;/p&gt;




&lt;h2&gt;
  
  
  Stage 2: Independent Depth-22 Verification &amp;amp; Time Caps
&lt;/h2&gt;

&lt;p&gt;Never trust the same search depth that generated your dataset to grade your dataset. In &lt;a href="https://github.com/minchopm/chess-trainer/blob/main/scripts/verify-puzzles.mjs" rel="noopener noreferrer"&gt;&lt;code&gt;scripts/verify-puzzles.mjs&lt;/code&gt;&lt;/a&gt;, we spin up a fresh Stockfish instance and re-verify every puzzle two plies deeper (&lt;code&gt;depth 22&lt;/code&gt;) with a per-position wall-clock ceiling (&lt;code&gt;6000 ms&lt;/code&gt;) so fortress-like positions cannot stall batch runs for hours:&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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;lines&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;engine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;analyse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;puzzle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fen&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;depth&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;DEPTH&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;       &lt;span class="c1"&gt;// Default: 22 (two plies deeper than mining)&lt;/span&gt;
  &lt;span class="na"&gt;movetime&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;TIME_CAP&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// 6,000 ms ceiling for pathological positions&lt;/span&gt;
  &lt;span class="na"&gt;multipv&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="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;lines&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;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;problems&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;only one legal move&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="k"&gt;else&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;lines&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="nx"&gt;move&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;puzzle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;solution&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="nx"&gt;problems&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`deeper search prefers &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;lines&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="nx"&gt;move&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; over &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;puzzle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;solution&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="s2"&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;gap&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;cpFor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;lines&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="nx"&gt;score&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;solverColor&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nf"&gt;cpFor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;lines&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="nx"&gt;score&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;solverColor&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;lt;&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;problems&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`solution is not unique (gap &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="s2"&gt;cp)`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Why Endgames Need Time Budgets Instead of Fixed Depth
&lt;/h3&gt;

&lt;p&gt;For endgame drills (rook vs. pawn, opposition, Lucena, Philidor, two bishops), fixed search depth fails in both directions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;In a bare &lt;code&gt;KR vs K&lt;/code&gt; position, &lt;strong&gt;depth 28&lt;/strong&gt; finishes in milliseconds.&lt;/li&gt;
&lt;li&gt;In a minor-piece endgame with pawns on both wings, &lt;strong&gt;depth 28&lt;/strong&gt; explodes combinatorially and times out.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Switching endgame verification from fixed plies to a fixed &lt;strong&gt;&lt;code&gt;4,000 ms&lt;/code&gt; movetime budget&lt;/strong&gt; gave consistent tablebase-like stability across all drills, asserting &lt;code&gt;cp &amp;gt;= 400&lt;/code&gt; (or forced mate) on positions labelled &lt;code&gt;"win"&lt;/code&gt; and &lt;code&gt;|cp| &amp;lt;= 150&lt;/code&gt; on positions labelled &lt;code&gt;"draw"&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Illegal-FEN Trap &lt;code&gt;chess.js&lt;/code&gt; Won't Catch
&lt;/h2&gt;

&lt;p&gt;When curating hand-crafted endgame drills or mutating FEN strings in tests, there is one mistake that is shockingly easy to make: placing a piece so that &lt;strong&gt;the side &lt;em&gt;not&lt;/em&gt; to move is already in check&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;No legal sequence of chess moves can ever reach such a position—because the player who just moved would have left (or moved) their own king into check.&lt;/p&gt;

&lt;p&gt;Here is the catch:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;new Chess(fen)&lt;/code&gt; in &lt;code&gt;chess.js&lt;/code&gt; &lt;strong&gt;accepts these FENs without throwing&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;When you pass that same FEN to Stockfish over UCI (&lt;code&gt;position fen ...&lt;/code&gt; -&amp;gt; &lt;code&gt;go movetime 4000&lt;/code&gt;), Stockfish immediately responds with &lt;code&gt;bestmove (none)&lt;/code&gt;—which looks identical to an engine crash or pipe timeout if you aren't checking for it!&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The fix is a 10-line helper that flips the active turn token in the FEN, clears the en-passant target square, and asks &lt;code&gt;chess.js&lt;/code&gt; whether the swapped side is in check:&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;Chess&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;chess.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="cm"&gt;/**
 * True when the side *not* to move is in check — an illegal position
 * that no game can reach. chess.js accepts these, and Stockfish replies
 * with "bestmove (none)", which is easy to misread as an engine crash.
 */&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;opponentInCheck&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fen&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;parts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;fen&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt; &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;parts&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="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;parts&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="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;w&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;b&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;w&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;parts&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;-&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// Clear en-passant target before swapping active color&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Chess&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;parts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt; &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;isCheck&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Adding &lt;code&gt;opponentInCheck(fen)&lt;/code&gt; before invoking UCI saved hours of debugging mysterious empty engine evaluations during endgame drill authoring.&lt;/p&gt;




&lt;h2&gt;
  
  
  Grading Human Moves: Win Probability vs. Raw Centipawns
&lt;/h2&gt;

&lt;p&gt;Raw centipawn loss (&lt;code&gt;CPL&lt;/code&gt;) is a poor teacher for human players because engine centipawns are non-linear:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Position Transition&lt;/th&gt;
&lt;th&gt;Raw Centipawn Delta&lt;/th&gt;
&lt;th&gt;Practical Impact on Game Outcome&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;+9.00&lt;/code&gt; → &lt;code&gt;+7.20&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;code&gt;-180 cp&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Negligible&lt;/strong&gt; — both positions are trivially winning&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;+0.30&lt;/code&gt; → &lt;code&gt;-1.50&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;code&gt;-180 cp&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Game-losing blunder&lt;/strong&gt; — throws away equality into a lost position&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;+2.80&lt;/code&gt; → &lt;code&gt;+0.40&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;code&gt;-240 cp&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Missed win&lt;/strong&gt; — converts a decisive edge into a probable draw&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;In Brass Pawn's coached play and native iOS/macOS trainer (which pairs &lt;strong&gt;Stockfish 18&lt;/strong&gt; with &lt;strong&gt;Reckless&lt;/strong&gt; on-device), moves are graded by &lt;strong&gt;win-probability loss&lt;/strong&gt; rather than raw centipawn deltas. Trading down into a clean, slightly lower-centipawn king-and-pawn endgame isn't penalized as an "inaccuracy" when win probability stays pinned near 99%.&lt;/p&gt;




&lt;h2&gt;
  
  
  Architecture Snapshot: Cloud Chess Platforms vs. Local Engine Study
&lt;/h2&gt;

&lt;p&gt;Because the full puzzle set and neural engine run locally, there is zero network round-trip per ply and zero account wall:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Architectural Layer&lt;/th&gt;
&lt;th&gt;Typical Cloud Chess Portal&lt;/th&gt;
&lt;th&gt;Brass Pawn (Web &amp;amp; Native iOS/macOS)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Tactics Verification&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Mixed community / game-imported&lt;/td&gt;
&lt;td&gt;14,351 MultiPV-screened &amp;amp; depth-22 verified puzzles&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Positional Training&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Usually absent or video-only&lt;/td&gt;
&lt;td&gt;116 engine-screened non-tactical judgement drills&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Engine Runtime&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Browser WASM or metered cloud server&lt;/td&gt;
&lt;td&gt;Local Stockfish 18 (Web/App) + Reckless on iOS/macOS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Move Grading Metric&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Often centipawn thresholds&lt;/td&gt;
&lt;td&gt;Win-probability delta per ply&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Offline &amp;amp; Telemetry&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Account login required for most modes&lt;/td&gt;
&lt;td&gt;100% offline capable, zero ads, zero ad tracking&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Try the Code or Run Your Own Verification Pass
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Open-Source Repo (AGPLv3):&lt;/strong&gt; &lt;a href="https://github.com/minchopm/chess-trainer" rel="noopener noreferrer"&gt;github.com/minchopm/chess-trainer&lt;/a&gt; (&lt;code&gt;npm install &amp;amp;&amp;amp; npm start&lt;/code&gt;, or inspect &lt;code&gt;scripts/verify-puzzles.mjs&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Live Web &amp;amp; Native App Overview:&lt;/strong&gt; &lt;a href="https://brasspawn.com/" rel="noopener noreferrer"&gt;brasspawn.com&lt;/a&gt; (also available natively on &lt;a href="https://apps.apple.com/us/app/brass-pawn-chess-trainer/id6803566012" rel="noopener noreferrer"&gt;iPhone, iPad &amp;amp; Mac&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Technical Chess Guides &amp;amp; Trainer Benchmarks:&lt;/strong&gt; &lt;a href="https://brasspawn.com/guides" rel="noopener noreferrer"&gt;brasspawn.com/guides&lt;/a&gt; and &lt;a href="https://brasspawn.com/compare" rel="noopener noreferrer"&gt;brasspawn.com/compare&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you maintain chess tooling or puzzle generators in JavaScript or Swift, drop &lt;code&gt;opponentInCheck()&lt;/code&gt; into your FEN ingestion tests—it catches broken test fixtures before they ever reach UCI.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>swift</category>
      <category>opensource</category>
      <category>gamedev</category>
    </item>
  </channel>
</rss>
