<?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: Ecom Solutions</title>
    <description>The latest articles on DEV Community by Ecom Solutions (@ecom_solutions_04a07df036).</description>
    <link>https://dev.to/ecom_solutions_04a07df036</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%2F4101933%2Fa90d95b5-6d95-46e8-86c0-13febf90f948.png</url>
      <title>DEV Community: Ecom Solutions</title>
      <link>https://dev.to/ecom_solutions_04a07df036</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ecom_solutions_04a07df036"/>
    <language>en</language>
    <item>
      <title>How I faked a real-time opponent on shared hosting with zero background processes</title>
      <dc:creator>Ecom Solutions</dc:creator>
      <pubDate>Mon, 31 Aug 2026 01:01:26 +0000</pubDate>
      <link>https://dev.to/ecom_solutions_04a07df036/how-i-faked-a-real-time-opponent-on-shared-hosting-with-zero-background-processes-9n4</link>
      <guid>https://dev.to/ecom_solutions_04a07df036/how-i-faked-a-real-time-opponent-on-shared-hosting-with-zero-background-processes-9n4</guid>
      <description>&lt;p&gt;Shared hosting has one rule that quietly kills most multiplayer designs: &lt;strong&gt;you cannot run a persistent process.&lt;/strong&gt; No WebSocket server, no daemon, no long-lived worker. Your PHP is born when a request arrives and dies when the response is sent.&lt;/p&gt;

&lt;p&gt;I ran into this building a head-to-head word game — two players racing to guess the same five-letter word. The multiplayer part was fine. The problem was what happens when nobody else is online.&lt;/p&gt;

&lt;p&gt;An empty matchmaking queue is the fastest way to kill a new game. So I needed a bot. And a bot, conventionally, is a process: something that wakes up every few seconds, decides on a move, and writes it somewhere. That is exactly the thing I could not have.&lt;/p&gt;

&lt;p&gt;Here is what I did instead.&lt;/p&gt;

&lt;h2&gt;
  
  
  The constraint reframed
&lt;/h2&gt;

&lt;p&gt;The instinct is to reach for a cron job that ticks bot games forward. That works, but it is coarse (most shared hosts cap you at one minute), it needs state, and it burns resources on games nobody is watching.&lt;/p&gt;

&lt;p&gt;The better question is: &lt;strong&gt;does the bot need to exist between requests?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It does not. Nobody observes the bot except the opponent, and the opponent only observes it when they poll for status. If I can compute what the bot &lt;em&gt;would have done&lt;/em&gt; by now, at the moment I'm asked, the bot never needs to run at all.&lt;/p&gt;

&lt;p&gt;That turns the bot from a process into a pure function:&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="n"&gt;bot_state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;f&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;battle_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;elapsed_seconds&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same inputs, same output, every time. No storage, no scheduler, no race conditions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Making it deterministic
&lt;/h2&gt;

&lt;p&gt;The whole thing hinges on randomness that isn't random. Every decision the bot makes is seeded from the battle's own ID:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nv"&gt;$seed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;crc32&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$battle&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'id'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s1"&gt;'_bot_timing_v2'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Separate seeds for separate concerns — one for guess timing, one for which words it picks, one for chat messages:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nv"&gt;$wordSeed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;crc32&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$battle&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'id'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s1"&gt;'_bot_words_v2'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nv"&gt;$chatSeed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;crc32&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$battle&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'id'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s1"&gt;'_bot_chat_v2'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Because &lt;code&gt;crc32&lt;/code&gt; of a fixed string is stable, battle #4471 produces the same bot every single time it is evaluated. Two polls a second apart return a consistent world. A page refresh doesn't reroll the opponent. Two devices watching the same battle agree.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;_v2&lt;/code&gt; suffix is doing real work, by the way. It is a cheap version namespace: when I want to retune bot behaviour, I bump it and every &lt;em&gt;new&lt;/em&gt; battle gets the new personality while in-flight ones stay consistent. Changing the seed string is the migration.&lt;/p&gt;

&lt;h2&gt;
  
  
  Deciding the outcome up front
&lt;/h2&gt;

&lt;p&gt;The second trick is that the bot's result is chosen when the battle is created, not discovered as it plays:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// bot_total_guesses: 0 = fail (6 wrong guesses), 3-6 = solve on that guess&lt;/span&gt;
&lt;span class="nv"&gt;$botTotalGuesses&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)(&lt;/span&gt;&lt;span class="nv"&gt;$battle&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'bot_total_guesses'&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="nv"&gt;$botWillFail&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$botTotalGuesses&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This feels like cheating until you think about what it buys you.&lt;/p&gt;

&lt;p&gt;It makes difficulty &lt;strong&gt;tunable as a distribution&lt;/strong&gt; rather than emergent from a solver. I am not writing an AI that plays well and then trying to handicap it. I am picking a target outcome — "this bot solves on guess 4" — and then working backwards to a plausible-looking path. Balancing becomes a matter of adjusting probabilities, not rewriting logic.&lt;/p&gt;

&lt;p&gt;It also means the bot cannot embarrass itself. A real solver has bad days; it stumbles into a lookalike trap and burns four guesses on &lt;code&gt;PARER&lt;/code&gt; / &lt;code&gt;PACER&lt;/code&gt; / &lt;code&gt;PAPER&lt;/code&gt;. That is realistic and it is a terrible new-player experience.&lt;/p&gt;

&lt;p&gt;The reveal is still honest — the bot's guesses appear over time, at human-plausible intervals, and you genuinely can beat it by being faster. What is predetermined is the &lt;em&gt;ceiling&lt;/em&gt;, not the race.&lt;/p&gt;

&lt;h2&gt;
  
  
  Polling, and why it's fine here
&lt;/h2&gt;

&lt;p&gt;Every few seconds the client hits one endpoint:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;GET /api/battle/status.php?id=42
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;which returns both players' progress and any chat. On each call the server recomputes the bot from scratch:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nv"&gt;$battle&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'is_bot'&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;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nv"&gt;$battle&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'status'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="s1"&gt;'active'&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nv"&gt;$battle&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'started_at'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// derive bot guesses from elapsed time + seed&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Polling gets a bad reputation it only partly deserves. It is genuinely wasteful for chat apps, live cursors, anything with high-frequency updates. But this game has a handful of state changes over roughly two minutes. The update rate is &lt;em&gt;low&lt;/em&gt;. WebSockets would buy me latency I cannot perceive and cost me a server I cannot run.&lt;/p&gt;

&lt;p&gt;Match the transport to the update frequency, not to what sounds modern.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this pattern is actually good for
&lt;/h2&gt;

&lt;p&gt;The general shape is: &lt;strong&gt;replace a background process with a deterministic function of time and a stable seed.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It works when the state is derivable from a start timestamp plus fixed parameters, when nobody observes it except on request, and when you can accept computing it repeatedly instead of once. Simulated opponents, staged rollouts, fake-but-consistent activity feeds, scheduled reveals all fit.&lt;/p&gt;

&lt;p&gt;It breaks the moment the simulated thing has to &lt;em&gt;react&lt;/em&gt; to something unpredictable, or when recomputation gets expensive, or when the entity must act while nobody is watching.&lt;/p&gt;

&lt;p&gt;For a word duel it fits exactly. The bot has no free will to model — it is racing a clock, and clocks are the one thing you can always compute.&lt;/p&gt;

&lt;h2&gt;
  
  
  The honest trade
&lt;/h2&gt;

&lt;p&gt;I would not defend this as the right way to build a multiplayer game. It is the right way to build one &lt;em&gt;under this constraint&lt;/em&gt;, and it bought a working head-to-head mode on hosting that costs a few dollars a month.&lt;/p&gt;

&lt;p&gt;The stack is PHP 8.3, MySQL, and no other moving parts. The game is &lt;a href="https://wordlyplay.com" rel="noopener noreferrer"&gt;WordlyPlay&lt;/a&gt; if you want to see the polling in action — open a battle and watch the network tab.&lt;/p&gt;

&lt;p&gt;The wider lesson I keep relearning: constraints that look like they block a feature usually just block the &lt;em&gt;obvious implementation&lt;/em&gt; of it. There is often a stateless version of the stateful thing you wanted, and it is frequently simpler than what you would have built with no constraints at all.&lt;/p&gt;

</description>
      <category>php</category>
      <category>webdev</category>
      <category>gamedev</category>
      <category>architecture</category>
    </item>
  </channel>
</rss>
