<?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: 许鑫</title>
    <description>The latest articles on DEV Community by 许鑫 (@_8b9f989e3bd077f5726863).</description>
    <link>https://dev.to/_8b9f989e3bd077f5726863</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%2F4236206%2F306b0907-85cb-493b-b646-df07d14e72d5.jpg</url>
      <title>DEV Community: 许鑫</title>
      <link>https://dev.to/_8b9f989e3bd077f5726863</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/_8b9f989e3bd077f5726863"/>
    <language>en</language>
    <item>
      <title>How I Built a Browser Gomoku Game with AI and Multiplayer Support</title>
      <dc:creator>许鑫</dc:creator>
      <pubDate>Tue, 10 Nov 2026 10:17:03 +0000</pubDate>
      <link>https://dev.to/_8b9f989e3bd077f5726863/how-i-built-a-browser-gomoku-game-with-ai-and-multiplayer-support-12lh</link>
      <guid>https://dev.to/_8b9f989e3bd077f5726863/how-i-built-a-browser-gomoku-game-with-ai-and-multiplayer-support-12lh</guid>
      <description>&lt;p&gt;Gomoku (also known as "five in a row") looks simple on the surface: place stones on a grid, and the first player to line up five in a row wins.&lt;/p&gt;

&lt;p&gt;But once you try to build it properly, you run into three different problems:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The &lt;strong&gt;massive&lt;/strong&gt; game tree, which makes naive search too slow.&lt;/li&gt;
&lt;li&gt;An AI that feels fair instead of either braindead or unbeatable.&lt;/li&gt;
&lt;li&gt;Multiplayer over a lossy connection, without a database and without forcing users to sign up.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This post walks through how I built a browser Gomoku game with three AI difficulties and real online multiplayer. You can try the final result here:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://ecjtu.cloud/tool/gomoku?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=gomoku" rel="noopener noreferrer"&gt;Play Gomoku in the browser&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The board and rules
&lt;/h2&gt;

&lt;p&gt;The board is a classic 15×15 grid:&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;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="no"&gt;BOARD_SIZE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;15&lt;/span&gt;

&lt;span class="c1"&gt;// 0 = empty, 1 = black, 2 = white&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nc"&gt;CellValue&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nc"&gt;Board&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;CellValue&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Players take turns placing stones. Black moves first. A player wins by forming an unbroken line of five (or more) stones horizontally, vertically, or diagonally. If the board fills up with no winner, it is a draw.&lt;/p&gt;

&lt;p&gt;Checking for a win after every move only requires scanning four directions from the last placed stone, which is cheap. The hard part is the AI.&lt;/p&gt;

&lt;h2&gt;
  
  
  Building the Gomoku AI
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Why not a full game tree search?
&lt;/h3&gt;

&lt;p&gt;A full search over a 15×15 board is computationally hopeless without heavy pruning. Even with alpha-beta pruning, a branching factor of 200+ quickly becomes unmanageable in the browser.&lt;/p&gt;

&lt;p&gt;Instead, I used a &lt;strong&gt;pattern-based evaluation&lt;/strong&gt; approach. It plays fast, scales to thousands of candidate positions, and — importantly — feels human.&lt;/p&gt;

&lt;h3&gt;
  
  
  Scoring patterns
&lt;/h3&gt;

&lt;p&gt;For every candidate move, I look in four directions and count how many stones are connected and how many ends are open. That gives a score per direction:&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="no"&gt;SCORE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="no"&gt;FIVE&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1_000_000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="no"&gt;OPEN_FOUR&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;100_000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="no"&gt;FOUR&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;10_000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;      &lt;span class="c1"&gt;// "closed four" (one end blocked)&lt;/span&gt;
  &lt;span class="no"&gt;OPEN_THREE&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;8_000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="no"&gt;THREE&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;800&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="no"&gt;OPEN_TWO&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="no"&gt;TWO&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="no"&gt;ONE&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;directionScore&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;openEnds&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="n"&gt;count&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;SCORE&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="no"&gt;FIVE&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;openEnds&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="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;        &lt;span class="c1"&gt;// blocked on both sides&lt;/span&gt;
  &lt;span class="k"&gt;switch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;openEnds&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="no"&gt;SCORE&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="no"&gt;OPEN_FOUR&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="no"&gt;SCORE&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="no"&gt;FOUR&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;openEnds&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="no"&gt;SCORE&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="no"&gt;OPEN_THREE&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="no"&gt;SCORE&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="no"&gt;THREE&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;openEnds&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="no"&gt;SCORE&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="no"&gt;OPEN_TWO&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="no"&gt;SCORE&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="no"&gt;TWO&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;SCORE&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="no"&gt;ONE&lt;/span&gt;
    &lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A move is then scored by summing its four directions, with extra bonuses for compound patterns like &lt;strong&gt;double open threes&lt;/strong&gt; or &lt;strong&gt;four-three combinations&lt;/strong&gt;, which are near-winning threats.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cutting the search space
&lt;/h3&gt;

&lt;p&gt;Instead of evaluating all 225 empty cells, the AI only considers empty cells &lt;strong&gt;within two squares of an existing stone&lt;/strong&gt;. On an empty board it simply plays the center.&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;// Only empty cells near existing stones are candidates.&lt;/span&gt;
&lt;span class="c1"&gt;// This dramatically reduces the work per move.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;candidates&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getCandidates&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;board&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Early in the game this collapses the search from ~225 cells to a few dozen, and it scales gracefully as the board fills up.&lt;/p&gt;

&lt;h2&gt;
  
  
  Making the AI feel fair: three difficulties
&lt;/h2&gt;

&lt;p&gt;A single AI is either too hard or too boring. So I split it into three personalities that share the same evaluation function but behave very differently.&lt;/p&gt;

&lt;h3&gt;
  
  
  The priority flow
&lt;/h3&gt;

&lt;p&gt;Before scoring anything, every difficulty follows two non-negotiable rules:&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;// 1. If I can win right now, win.&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="n"&gt;p&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="n"&gt;candidates&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="nf"&gt;createsFive&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;board&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;col&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;player&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// 2. If my opponent can win next move, block it.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;losingMoves&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;candidates&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;createsFive&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;board&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;col&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;opponent&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;// ... handle blocking&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Easy
&lt;/h3&gt;

&lt;p&gt;Easy looks at offense plus a little defense, but deliberately makes mistakes:&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="n"&gt;missChance&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.25&lt;/span&gt; &lt;span class="c1"&gt;// sometimes fails to block&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and occasionally picks a random move from a broader pool, so beginners get chances to win.&lt;/p&gt;

&lt;h3&gt;
  
  
  Medium
&lt;/h3&gt;

&lt;p&gt;Medium weights offense vs. defense and picks randomly among the top few candidates, which adds variety without being unfair:&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="n"&gt;score&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;evaluatePosition&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;board&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;col&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;player&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nf"&gt;evaluatePosition&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;board&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;col&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;opponent&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;1.1&lt;/span&gt;

&lt;span class="c1"&gt;// pick randomly among the top 3&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Hard
&lt;/h3&gt;

&lt;p&gt;Hard adds a &lt;strong&gt;one-ply lookahead&lt;/strong&gt;: for each of its top candidates, it simulates placing its stone, then asks "what is the opponent's best single-move threat now?" and penalizes moves that leave the opponent a strong reply:&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="n"&gt;board&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;cand&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;cand&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;col&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;player&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="n"&gt;oppBest&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&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="n"&gt;q&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="n"&gt;candidates&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="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;evaluatePosition&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;board&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;q&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;q&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;col&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;opponent&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="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;oppBest&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;oppBest&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="n"&gt;board&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;cand&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;cand&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;col&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="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cand&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ai&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;1.15&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;cand&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;opp&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;1.0&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;oppBest&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.9&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It only does this for the top ~16 candidates to keep the turn fast, but it is enough to avoid blunders and punish open threes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Online multiplayer without a database
&lt;/h2&gt;

&lt;p&gt;The multiplayer mode was the part I most wanted to keep frictionless: no accounts, no lobbies to sign into, just share a room code.&lt;/p&gt;

&lt;p&gt;It runs on a WebSocket server (h3's &lt;code&gt;defineWebSocketHandler&lt;/code&gt;) with an in-memory room store.&lt;/p&gt;

&lt;h3&gt;
  
  
  Room model
&lt;/h3&gt;

&lt;p&gt;Each room holds the board, whose turn it is, the status, and up to two players:&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;interface&lt;/span&gt; &lt;span class="nc"&gt;Room&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;roomCode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;
  &lt;span class="n"&gt;players&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;RoomPlayer&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="n"&gt;board&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;number&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="n"&gt;currentPlayer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Player&lt;/span&gt;
  &lt;span class="n"&gt;gameStatus&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'waiting'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="s1"&gt;'playing'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="s1"&gt;'gameover'&lt;/span&gt;
  &lt;span class="n"&gt;winner&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;GameResult&lt;/span&gt;
  &lt;span class="n"&gt;rematchRequests&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Set&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="n"&gt;createdAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;number&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A joiner gets a 6-character room code from a charset that avoids ambiguous characters (no &lt;code&gt;0/O&lt;/code&gt;, &lt;code&gt;1/I&lt;/code&gt;):&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="n"&gt;chars&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'ABCDEFGHJKLMNPQRSTUVWXYZ23456789'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The golden rule: the server is the source of truth
&lt;/h3&gt;

&lt;p&gt;The client never decides the outcome. When a player sends &lt;code&gt;make_move&lt;/code&gt;, the server:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Validates it's actually their turn.&lt;/li&gt;
&lt;li&gt;Validates the cell is empty.&lt;/li&gt;
&lt;li&gt;Places the stone.&lt;/li&gt;
&lt;li&gt;Checks for a win or draw.&lt;/li&gt;
&lt;li&gt;Switches the current player.&lt;/li&gt;
&lt;li&gt;Broadcasts the new board to both players.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This prevents cheating and keeps both boards perfectly in sync.&lt;/p&gt;

&lt;h2&gt;
  
  
  Surviving disconnects: token-based reconnect
&lt;/h2&gt;

&lt;p&gt;The scariest part of a socket game is the flaky connection. Mobile browsers background tabs, laptops sleep, wifi drops. I did not want a single hiccup to lose the match.&lt;/p&gt;

&lt;p&gt;The solution: &lt;strong&gt;identity via token, not via socket&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;When a player creates or joins a room, the server issues them a random &lt;code&gt;playerToken&lt;/code&gt;. The client stores &lt;code&gt;{ roomCode, playerToken }&lt;/code&gt; in &lt;code&gt;sessionStorage&lt;/code&gt;. Every player also tracks a &lt;code&gt;lastSeen&lt;/code&gt; timestamp.&lt;/p&gt;

&lt;p&gt;When the socket closes, the server does &lt;strong&gt;not&lt;/strong&gt; delete the room — it just marks the player as offline:&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="n"&gt;player&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;peer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;
&lt;span class="n"&gt;player&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;online&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
&lt;span class="n"&gt;player&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;lastSeen&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;// keep the room alive so the player can come back&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On reconnect, the client sends &lt;code&gt;reconnect&lt;/code&gt; with its token, and the server rebinds the new socket to the old player, then sends the full room state back:&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="n"&gt;peer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'room_rejoined'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;roomCode&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;myColor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;player&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;color&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;board&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;room&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;board&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;currentPlayer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;room&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;currentPlayer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;gameStatus&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;room&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;gameStatus&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;opponentName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;opponentOnline&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The opponent gets an &lt;code&gt;opponent_reconnected&lt;/code&gt; (or &lt;code&gt;opponent_left&lt;/code&gt;) notification, so they always know what's happening.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cleanup
&lt;/h3&gt;

&lt;p&gt;Without a database, memory has to be managed carefully. A periodic job removes rooms that are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Older than 24 hours, or&lt;/li&gt;
&lt;li&gt;Fully offline for more than 5 minutes.
&lt;/li&gt;
&lt;/ul&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="no"&gt;OFFLINE_TIMEOUT_MS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="no"&gt;ROOM_MAX_MS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;24&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So abandoned games don't leak forever, but a short disconnect never kills a live match.&lt;/p&gt;

&lt;h2&gt;
  
  
  Other details worth calling out
&lt;/h2&gt;

&lt;p&gt;A few smaller decisions that made the experience feel polished:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Rematch flow.&lt;/strong&gt; Both players must request a rematch; once both agree, the board resets automatically.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Draw detection.&lt;/strong&gt; Board-full checking runs alongside win detection so stalemates resolve cleanly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mode clarity.&lt;/strong&gt; The hub labels each game as single-player, AI, local, or online, so players know what they're getting before they start.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Local, AI, and online all in one component tree&lt;/strong&gt;, sharing the same board rendering and move logic.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;If you want to test the AI or play a friend, the game is live here:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://ecjtu.cloud/tool/gomoku?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=gomoku" rel="noopener noreferrer"&gt;Play Gomoku — play vs AI, locally, or online&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It is part of a small browser &lt;a href="https://ecjtu.cloud/games?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=gomoku" rel="noopener noreferrer"&gt;games hub&lt;/a&gt; that also includes Chinese chess, mahjong, and a few quick arcade and puzzle games.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd do next
&lt;/h2&gt;

&lt;p&gt;Some ideas I'm considering:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A trained model or deeper search for a "brutal" difficulty tier.&lt;/li&gt;
&lt;li&gt;A spectator mode for online rooms.&lt;/li&gt;
&lt;li&gt;Persistent Elo ratings once players opt into lightweight accounts.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you've built a browser-based board game, I'd love to hear how you handled the AI evaluation and the reconnection logic — those were the two hardest parts for me.&lt;/p&gt;

</description>
      <category>showdev</category>
      <category>webdev</category>
      <category>javascript</category>
      <category>gamedev</category>
    </item>
    <item>
      <title>I Built a Small Browser Game Hub for Quick Breaks</title>
      <dc:creator>许鑫</dc:creator>
      <pubDate>Tue, 10 Nov 2026 10:08:34 +0000</pubDate>
      <link>https://dev.to/_8b9f989e3bd077f5726863/i-built-a-small-browser-game-hub-for-quick-breaks-453n</link>
      <guid>https://dev.to/_8b9f989e3bd077f5726863/i-built-a-small-browser-game-hub-for-quick-breaks-453n</guid>
      <description>&lt;h1&gt;
  
  
  I Built a Small Browser Game Hub for Quick Breaks
&lt;/h1&gt;

&lt;p&gt;Sometimes I want to play a quick game between tasks, but I do not want to install anything, create an account, or spend ten minutes searching through a crowded game website.&lt;/p&gt;

&lt;p&gt;That was the idea behind &lt;a href="https://ecjtu.cloud/games?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=games_hub" rel="noopener noreferrer"&gt;Games Hub&lt;/a&gt;: a lightweight collection of browser games that can be opened and played directly from the web.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is included?
&lt;/h2&gt;

&lt;p&gt;The hub currently includes a mix of classic games, strategy games, puzzles, and small single-player experiences.&lt;/p&gt;

&lt;h3&gt;
  
  
  Classic and strategy games
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Gomoku&lt;/li&gt;
&lt;li&gt;Chinese chess&lt;/li&gt;
&lt;li&gt;Mahjong&lt;/li&gt;
&lt;li&gt;Jump chess&lt;/li&gt;
&lt;li&gt;Flight chess
&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4vpr7pg7s0n8agvgsgmg.png" alt=" " width="800" height="354"&gt;
&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fl1c5fzid9tjdthfwxrc7.png" alt=" " width="800" height="353"&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Several of these games include local play, AI opponents, or online multiplayer modes.&lt;/p&gt;

&lt;h3&gt;
  
  
  Quick single-player games
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Minesweeper&lt;/li&gt;
&lt;li&gt;Snake&lt;/li&gt;
&lt;li&gt;Flappy Bird&lt;/li&gt;
&lt;li&gt;Sliding puzzle&lt;/li&gt;
&lt;li&gt;Career simulator&lt;/li&gt;
&lt;li&gt;Virtual plant&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;challenge yourself by scanning the qrcode：&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4yxtpu93lq3fc8i8m9gk.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4yxtpu93lq3fc8i8m9gk.png" alt=" " width="413" height="645"&gt;&lt;/a&gt;&lt;br&gt;
The goal is not to compete with large gaming platforms. It is simply to make a few enjoyable games easy to find and easy to start.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why build another game page?
&lt;/h2&gt;

&lt;p&gt;There are already countless game websites online. I did not want to build a platform filled with ads, downloads, or complicated navigation.&lt;/p&gt;

&lt;p&gt;Instead, I wanted a simple starting point:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Open the page.&lt;/li&gt;
&lt;li&gt;Choose a game.&lt;/li&gt;
&lt;li&gt;Start playing.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The games are organized in one place, with short descriptions and mode labels so that it is clear whether a game supports online play, AI, local multiplayer, or single-player gameplay.&lt;/p&gt;

&lt;h2&gt;
  
  
  Designed for short sessions
&lt;/h2&gt;

&lt;p&gt;The hub is especially useful for short breaks:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A quick game of Minesweeper&lt;/li&gt;
&lt;li&gt;A few rounds of Gomoku against an AI&lt;/li&gt;
&lt;li&gt;A short puzzle session&lt;/li&gt;
&lt;li&gt;A local game with a friend&lt;/li&gt;
&lt;li&gt;A casual strategy match&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Not every game needs to become a long session. Sometimes five minutes is enough.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I learned while building it
&lt;/h2&gt;

&lt;p&gt;Building a game hub is not only about implementing individual game rules. The surrounding experience matters just as much.&lt;/p&gt;

&lt;p&gt;Some of the details that required attention included:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Keeping the game list easy to scan&lt;/li&gt;
&lt;li&gt;Making game modes visible before opening a game&lt;/li&gt;
&lt;li&gt;Providing responsive layouts for mobile screens&lt;/li&gt;
&lt;li&gt;Maintaining consistent navigation between the hub and individual games&lt;/li&gt;
&lt;li&gt;Supporting multiple languages&lt;/li&gt;
&lt;li&gt;Avoiding unnecessary steps before gameplay&lt;/li&gt;
&lt;li&gt;Making the visual style feel cohesive across very different games&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A hub page is effectively a small product catalogue. If users cannot quickly understand what each game is and how it can be played, the individual games become harder to discover.&lt;/p&gt;

&lt;h2&gt;
  
  
  More than just games
&lt;/h2&gt;

&lt;p&gt;The same website also includes a collection of practical browser tools, such as calculators, converters, generators, and productivity utilities.&lt;/p&gt;

&lt;p&gt;The Games Hub is the more playful part of the project, but the overall goal is similar: provide small, focused tools that can be used immediately in a browser.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;You can browse the games here:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://ecjtu.cloud/games?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=games_hub" rel="noopener noreferrer"&gt;Open the Games Hub&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you try one of the games, I would be interested to know which type you prefer:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Classic board games&lt;/li&gt;
&lt;li&gt;AI opponents&lt;/li&gt;
&lt;li&gt;Multiplayer games&lt;/li&gt;
&lt;li&gt;Puzzle games&lt;/li&gt;
&lt;li&gt;Short arcade games&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I am continuing to improve the hub and add small quality-of-life improvements around navigation, localization, and gameplay.&lt;/p&gt;

</description>
      <category>showdev</category>
      <category>webdev</category>
      <category>javascript</category>
      <category>gaming</category>
    </item>
  </channel>
</rss>
