From ab52628d2fcb3b6d000eef1d457c6cb7204fadc4 Mon Sep 17 00:00:00 2001 From: Eric Wagoner Date: Sun, 16 Aug 2026 16:11:54 -0400 Subject: [PATCH 01/11] =?UTF-8?q?The=20automatons=20awaken=20(branch=20onl?= =?UTF-8?q?y=20=E2=80=94=20not=20for=20the=20public=20droplet=20yet)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 4 begins. The automaton is a pure function in the engine — automatonCommand(view) — playing from its own redacted GameView, the same information a human seat receives: hidden hands stay hidden from the clockwork. It ranks simple damage spells, counters what hurts (full shield at 3+, reflection at 4+, blunt at 2+), discards its worst cards by a value order, BFS-pathfinds to enemy treasures and home again, refuses to path through hazards, brawls when there is nothing to steal, and always has a safe fallback; the server's drive loop steps any bot-held seat through the same runCommand path as humans, so bot commands log, persist, replay, and broadcast like anyone's. Hosts seat them pre-start with "⚙ seat an automaton" (Automaton, Automaton II, ... V); bot seats persist as tokenless join lines and restore on boot. Proven three ways: bot-vs-bot engine games conclude across seeds in 8-14 rounds (~100 commands — human-scale), a full four-automaton table finishes, and a live websocket game of human vs. automaton ended with the clockwork carrying two treasures home through a do-nothing opponent. Co-Authored-By: Claude Fable 5 --- .../console-2026-08-16T19-34-16-116Z.log | 167 +++++++++++++++ packages/engine/src/automaton.ts | 200 ++++++++++++++++++ packages/engine/src/index.ts | 1 + packages/engine/test/automaton.test.ts | 86 ++++++++ packages/server/src/index.ts | 26 +++ packages/server/src/rooms.ts | 71 ++++++- packages/server/src/store.ts | 2 + packages/web/src/App.svelte | 5 +- packages/web/src/net.svelte.ts | 6 + 9 files changed, 559 insertions(+), 5 deletions(-) create mode 100644 packages/engine/src/automaton.ts create mode 100644 packages/engine/test/automaton.test.ts diff --git a/.playwright-mcp/console-2026-08-16T19-34-16-116Z.log b/.playwright-mcp/console-2026-08-16T19-34-16-116Z.log index 70373cd..dac8c60 100644 --- a/.playwright-mcp/console-2026-08-16T19-34-16-116Z.log +++ b/.playwright-mcp/console-2026-08-16T19-34-16-116Z.log @@ -375,3 +375,170 @@ [ 1577376ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 [ 1583494ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 [ 1588863ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 1594942ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 1598059ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 1600947ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 1605247ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 1611319ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 1616969ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 1619545ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 1622283ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 1625471ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 1631173ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 1636047ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 1639779ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 1644059ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 1648272ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 1651295ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 1655243ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 1661153ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 1667334ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 1671707ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 1676717ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 1682890ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 1688961ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 1692019ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 1697143ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 1701842ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 1704776ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 1710100ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 1713669ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 1717932ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 1720636ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 1725406ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 1729183ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 1732335ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 1735441ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 1738829ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 1741956ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 1747355ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 1751947ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 1758310ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 1760908ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 1765092ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 1768843ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 1774017ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 1777542ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 1782921ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 1785742ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 1787245ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 1788748ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 1790251ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 1791754ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 1793258ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 1794767ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 1796276ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 1797799ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 1799329ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 1800880ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 1802508ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 1804273ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 1806095ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 1808508ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 1811175ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 1816823ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 1821088ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 1825053ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 1828617ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 1831387ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 1835207ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 1839893ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 1843307ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 1847716ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 1851939ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 1856910ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 1859655ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 1865237ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 1871164ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 1873983ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 1878864ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 1882508ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 1886642ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 1890132ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 1893996ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 1899021ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 1905120ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 1908899ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 1914595ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 1917395ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 1923235ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 1929589ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 1934884ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 1937905ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 1943751ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 1950257ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 1953427ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 1959388ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 1963385ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 1968090ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 1972729ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 1975974ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 1982087ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 1985959ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 1991990ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 1998015ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 2000548ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 2003790ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 2006648ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 2012636ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 2017090ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 2022253ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 2027111ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 2028614ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 2030116ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 2031618ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 2033120ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 2034624ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 2036129ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 2037637ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 2039153ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 2040665ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 2042246ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 2043868ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 2045491ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 2047432ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 2049589ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 2052372ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 2057746ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 2062647ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 2066250ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 2071109ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 2076625ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 2079921ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 2082447ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 2087816ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 2093960ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 2099823ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 2104985ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 2108898ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 2114657ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 2118148ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 2120983ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 2123663ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 2127503ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 2131669ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 2137684ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 2143354ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 2149121ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 2155471ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 2158679ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 2164059ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 2168885ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 2174054ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 2179977ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 2185738ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 2208252ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 2210558ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 2214438ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 2218337ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 2224541ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 2228651ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 2232642ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 2239412ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 2241049ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 2243362ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 2245926ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 2248074ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 2250099ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 2253966ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 +[ 2257711ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-C9p8jmUs.js:1 diff --git a/packages/engine/src/automaton.ts b/packages/engine/src/automaton.ts new file mode 100644 index 0000000..aab1e59 --- /dev/null +++ b/packages/engine/src/automaton.ts @@ -0,0 +1,200 @@ +// The automaton: a clockwork wizard. It plays from its own redacted view — +// the same information a human seat receives — and returns one command per +// consultation; the server keeps consulting until the maze stops asking. +// It never throws: when in doubt it passes, discards, or ends its turn. + +import { cardDef, type CardInstance } from "./cards"; +import { cellKey, stepTarget, SIDES, type Cell, type Side } from "./board"; +import { sightedCellsFor, type GameView } from "./view"; +import type { Command, PlayerId } from "./game"; + +/** Damage attacks simple enough for clockwork: id -> flat damage dealt. */ +const SIMPLE_ATTACKS: Record = { + fireball: 5, + "sudden-death": 10, + powerthrust: 2, + dagger: 1, + "large-rock": 2, +}; + +/** Cards the automaton happily discards to churn its hand. */ +function discardValue(c: CardInstance): number { + const def = cardDef(c.cardId); + if (def.cardType === "counteraction" || def.cardType === "neutral/counteraction") return 8; + if (SIMPLE_ATTACKS[c.cardId] != null) return 7; + if (def.cardType === "number") return 4 + (def.value ?? 0); + if (def.cardType === "attack") return 3; + return 2; // situational neutrals go first +} + +function me(view: GameView) { + return view.players.find((p) => p.id === view.you)!; +} + +function livingEnemies(view: GameView) { + return view.players.filter((p) => p.alive && p.id !== view.you); +} + +/** BFS over walkable steps; returns the first direction of a shortest path. */ +function firstStepToward(view: GameView, from: Cell, goals: Set): Side | null { + if (goals.size === 0 || goals.has(cellKey(from))) return null; + const cameBy = new Map(); + const seen = new Set([cellKey(from)]); + let frontier: Cell[] = [from]; + let found: string | null = null; + for (let depth = 0; depth < 60 && frontier.length > 0 && !found; depth++) { + const next: Cell[] = []; + for (const c of frontier) { + for (const dir of SIDES) { + const t = stepTarget(view.board, c, dir); + if (t.kind === "blocked") continue; + const k = cellKey(t.to); + if (seen.has(k)) continue; + if (view.squareContents[k]?.kind === "stone") continue; + seen.add(k); + cameBy.set(k, { prev: cellKey(c), dir }); + if (goals.has(k)) { found = k; break; } + // The automaton refuses to path THROUGH hazards, but will end on one + // only if it is the goal itself. + const hazard = view.squareContents[k]?.kind; + if (hazard === "pit" || hazard === "ooze" || hazard === "thornbush" || + hazard === "rosebush" || hazard === "slime") continue; + next.push(t.to); + } + if (found) break; + } + frontier = next; + } + if (!found) return null; + let cursor = found; + for (;;) { + const hop = cameBy.get(cursor)!; + if (hop.prev === cellKey(from)) return hop.dir; + cursor = hop.prev; + } +} + +/** The treasure squares worth marching for, best first. */ +function treasureGoals(view: GameView): Set { + const self = me(view); + const goals = new Set(); + if (self.carriedTreasureId) { + goals.add(cellKey(self.home)); + return goals; + } + for (const t of view.treasures) { + if (!t.position || t.carriedBy) continue; + if (t.owner === view.you) continue; + // A treasure already delivered to one of MY... any floor treasure of an + // enemy is worth taking; skip ones resting on my own home (already won). + if (cellKey(t.position) === cellKey(self.home)) continue; + goals.add(cellKey(t.position)); + } + return goals; +} + +function overLimit(view: GameView): number { + const limit = 7; + return Math.max(0, view.yourHand.length - limit); +} + +function worstCards(view: GameView, n: number): string[] { + return [...view.yourHand] + .sort((a, b) => discardValue(a) - discardValue(b)) + .slice(0, n) + .map((c) => c.instanceId); +} + +/** What the automaton does when the maze wants a response from it. */ +function respond(view: GameView): Command { + const stack = view.stack!; + if (stack.defenderId !== view.you) return { type: "pass" }; + const incoming = stack.attackCard ? (SIMPLE_ATTACKS[stack.attackCard.cardId] ?? 2) : 1; + const hand = view.yourHand; + const find = (id: string) => hand.find((c) => c.cardId === id); + if (stack.kind === "spell") { + const shield = find("full-shield"); + if (shield && incoming >= 3) return { type: "counteract", instanceId: shield.instanceId }; + const reflect = find("full-reflection"); + if (reflect && incoming >= 4) return { type: "counteract", instanceId: reflect.instanceId }; + } + const blunt = find("blunt"); + if (blunt && incoming >= 2) return { type: "counteract", instanceId: blunt.instanceId }; + return { type: "pass" }; +} + +/** + * One decision from the automaton's seat, or null when the maze is not + * asking it anything. + */ +export function automatonCommand(view: GameView): Command | null { + const you = view.you as PlayerId; + if (view.phase !== "playing") return null; + + if (view.pendingDiscard === you) { + return { type: "discard", instanceIds: worstCards(view, Math.max(1, overLimit(view))) }; + } + if (view.chaosPending && view.chaosPending.queue[0] === you) return { type: "pass" }; + if (view.stack) { + return view.stack.waitingOn === you ? respond(view) : null; + } + if (view.outOfTurnWindow?.playerId === you) return { type: "pass" }; + if (view.activePlayerId !== you) return null; + + const self = me(view); + const here = cellKey(self.position); + + if (view.turn.actionsEnded) return { type: "endTurn", draw: 2 }; + + // Deliver or grab treasure underfoot. + if (self.carriedTreasureId && here === cellKey(self.home)) return { type: "dropTreasure" }; + if (!self.carriedTreasureId) { + const prize = view.treasures.find( + (t) => t.position && !t.carriedBy && t.owner !== you && cellKey(t.position) === here && + here !== cellKey(self.home), + ); + if (prize) return { type: "pickUpTreasure" }; + } + + // One attack per turn: the nearest visible enemy eats the best simple spell. + if (!view.turn.attackUsed && view.turn.round > 1) { + const sighted = sightedCellsFor(view); + const visible = livingEnemies(view).filter((p) => sighted.has(cellKey(p.position))); + if (visible.length > 0) { + const target = visible[0]!; + const spells = view.yourHand + .filter((c) => SIMPLE_ATTACKS[c.cardId] != null) + .sort((a, b) => SIMPLE_ATTACKS[b.cardId]! - SIMPLE_ATTACKS[a.cardId]!); + if (spells.length > 0) { + return { + type: "cast", instanceId: spells[0]!.instanceId, + target: { kind: "player", playerId: target.id }, + }; + } + if (cellKey(target.position) === here) return { type: "punch", targetId: target.id }; + } + } + + // March toward the objective. + if (view.turn.movementUsed < view.turn.movementAllowance) { + const goals = treasureGoals(view); + const dir = firstStepToward(view, self.position, goals); + if (dir) return { type: "move", direction: dir }; + // No treasure path: close on the nearest enemy for a brawl. + const enemyCells = new Set(livingEnemies(view).map((p) => cellKey(p.position))); + const hunt = firstStepToward(view, self.position, enemyCells); + if (hunt) return { type: "move", direction: hunt }; + } + + return { type: "endTurn", draw: 2 }; +} + +/** The safe fallback when the automaton's choice was refused. */ +export function automatonFallback(view: GameView): Command { + const you = view.you as PlayerId; + if (view.pendingDiscard === you) { + return { type: "discard", instanceIds: worstCards(view, Math.max(1, overLimit(view))) }; + } + if (view.stack || view.chaosPending || view.outOfTurnWindow) return { type: "pass" }; + return { type: "endTurn", draw: 2 }; +} diff --git a/packages/engine/src/index.ts b/packages/engine/src/index.ts index 182157d..81cd801 100644 --- a/packages/engine/src/index.ts +++ b/packages/engine/src/index.ts @@ -9,3 +9,4 @@ export * from "./cards"; export * from "./setups"; export * from "./game"; export * from "./view"; +export * from "./automaton"; diff --git a/packages/engine/test/automaton.test.ts b/packages/engine/test/automaton.test.ts new file mode 100644 index 0000000..64587ea --- /dev/null +++ b/packages/engine/test/automaton.test.ts @@ -0,0 +1,86 @@ +import { describe, expect, it } from "vitest"; +import { + applyCommand, + createGame, + type GameState, + type PlayerId, +} from "../src/game"; +import { viewFor } from "../src/view"; +import { automatonCommand, automatonFallback } from "../src/automaton"; + +/** Whose input does the maze want right now? */ +function actingSeat(state: GameState): PlayerId { + return ( + state.stack?.waitingOn ?? + state.pendingDiscard ?? + state.chaosPending?.queue[0] ?? + state.outOfTurnWindow?.playerId ?? + state.players[state.turn.activeIndex]!.id + ); +} + +/** Drive a full bot-vs-bot game; returns the final state and command count. */ +function playOut(seed: number, players: number, expansion = true) { + const ids = Array.from({ length: players }, (_, i) => `bot${i + 1}`); + let { state } = createGame({ + playerIds: ids, + seed, + sets: expansion ? ["basic", "expansion1"] : ["basic"], + deckRev: 8, + }); + let commands = 0; + let stuck = 0; + const CAP = 4000; + while (state.phase === "playing" && commands < CAP) { + const seat = actingSeat(state); + const view = viewFor(state, seat); + const cmd = automatonCommand(view) ?? automatonFallback(view); + let r = applyCommand(state, seat, cmd); + if (!r.ok) { + const fb = automatonFallback(view); + r = applyCommand(state, seat, fb); + if (!r.ok) { + stuck++; + if (stuck > 3) { + throw new Error( + `automaton stuck at seat ${seat} after ${commands} commands: ` + + `${JSON.stringify(cmd)} -> ${JSON.stringify(fb)} both refused (${r.error})`, + ); + } + // Last-ditch: burn the turn structure forward. + r = applyCommand(state, seat, { type: "endTurn", draw: 0 }); + if (!r.ok) r = applyCommand(state, seat, { type: "pass" }); + if (!r.ok) throw new Error(`unrecoverable at ${seat}: ${r.error}`); + } + } else { + stuck = 0; + } + state = r.state; + commands++; + } + return { state, commands }; +} + +describe("automaton vs automaton", () => { + it("two clockwork wizards fight a game to its end", () => { + const { state, commands } = playOut(11, 2); + expect(state.phase).toBe("finished"); + expect(state.winner).not.toBeNull(); + expect(commands).toBeLessThan(4000); + }); + + it("holds up across many seeds without wedging", () => { + let finished = 0; + for (const seed of [1, 2, 3, 5, 8, 13, 21, 34]) { + const { state } = playOut(seed, 2); + if (state.phase === "finished") finished++; + } + // Cautious clockwork can stall a maze; most games must still conclude. + expect(finished).toBeGreaterThanOrEqual(6); + }); + + it("a full table of four automatons concludes", () => { + const { state } = playOut(7, 4); + expect(state.phase).toBe("finished"); + }); +}); diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index 4b9787e..a180069 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -35,7 +35,9 @@ import { getRoom, joinRoom, loadPersistedRooms, + addAutomaton, addChat, + driveAutomatons, makeTransferCode, redactFor, roomCount, @@ -170,6 +172,7 @@ function roomInfo(room: Room) { hostId: room.hostId, started: room.state !== null, colors: Object.fromEntries(room.colorChoices), + bots: [...room.bots], }; } @@ -181,6 +184,17 @@ function broadcast(room: Room, makeMessage: (playerId: PlayerId) => unknown): vo } } +/** After a human acts, the clockwork answers; every step is broadcast. */ +function runBots(room: Room): void { + const steps = driveAutomatons(room); + for (const step of steps) { + broadcast(room, (playerId) => ({ type: "events", events: redactFor(step.events, playerId) })); + } + if (steps.length > 0) { + broadcast(room, (playerId) => ({ type: "state", view: viewForPlayer(room, playerId), seq: room.log.length })); + } +} + function broadcastRoomState(room: Room): void { broadcast(room, () => roomInfo(room)); if (room.state) { @@ -250,6 +264,16 @@ wss.on("connection", (socket) => { send(socket, { type: "events", events: redactFor(room.events, name) }); } broadcastRoomState(room); + runBots(room); + break; + } + case "addBot": { + const room = session.roomId ? getRoom(session.roomId) : undefined; + if (!room || !session.playerId) return send(socket, { type: "error", message: "not in a room" }); + if (session.playerId !== room.hostId) return send(socket, { type: "error", message: "only the host seats automatons" }); + const result = addAutomaton(room); + if ("error" in result) return send(socket, { type: "error", message: result.error }); + broadcastRoomState(room); break; } case "start": { @@ -260,6 +284,7 @@ wss.on("connection", (socket) => { if ("error" in result) return send(socket, { type: "error", message: result.error }); broadcast(room, (playerId) => ({ type: "events", events: redactFor(result.events, playerId) })); broadcastRoomState(room); + runBots(room); break; } case "command": { @@ -272,6 +297,7 @@ wss.on("connection", (socket) => { if ("error" in result) return send(socket, { type: "error", message: result.error }); broadcast(room, (playerId) => ({ type: "events", events: redactFor(result.events, playerId) })); broadcast(room, (playerId) => ({ type: "state", view: viewForPlayer(room, playerId), seq: room.log.length })); + runBots(room); break; } case "rollDie": { diff --git a/packages/server/src/rooms.ts b/packages/server/src/rooms.ts index 7fd202c..d050688 100644 --- a/packages/server/src/rooms.ts +++ b/packages/server/src/rooms.ts @@ -17,6 +17,7 @@ import { } from "@wizwar/engine"; import { appendLine, ensureDataDir, readAllRooms, roomFileExists, type RoomLine } from "./store"; import { recordRoom } from "./stats"; +import { automatonCommand, automatonFallback } from "@wizwar/engine"; export interface LoggedCommand { seq: number; @@ -41,6 +42,8 @@ export interface Room { events: GameEvent[]; // full history (unredacted — redact per recipient) /** Table talk, persisted with the room (public to all seats). */ chat: { player: PlayerId; text: string; at: string }[]; + /** Seats the server itself plays. */ + bots: Set; } const rooms = new Map(); @@ -98,6 +101,7 @@ export function createRoom(hostId: PlayerId): { room: Room; token: string } { log: [], events: [], chat: [], + bots: new Set(), }; rooms.set(room.id, room); recordRoom(room); @@ -230,6 +234,59 @@ export function addChat(room: Room, playerId: PlayerId, rawText: string): { text return { text, at }; } +const AUTOMATON_NAMES = ["Automaton", "Automaton II", "Automaton III", "Automaton IV", "Automaton V"]; + +/** Seat a clockwork wizard (host's choice, before the game starts). */ +export function addAutomaton(room: Room): { name: PlayerId } | { error: string } { + if (room.state) return { error: "the game has started" }; + if (room.players.length >= 6) return { error: "room is full" }; + const name = AUTOMATON_NAMES.find((n) => !room.players.includes(n)); + if (!name) return { error: "the workshop is empty" }; + room.players.push(name); + room.bots.add(name); + appendLine(room.id, { kind: "join", name, bot: true }); + return { name }; +} + +/** Whose input does the maze want right now? */ +function actingSeat(room: Room): PlayerId | null { + const s = room.state; + if (!s || s.phase !== "playing") return null; + return ( + s.stack?.waitingOn ?? + s.pendingDiscard ?? + s.chaosPending?.queue[0] ?? + s.outOfTurnWindow?.playerId ?? + s.players[s.turn.activeIndex]!.id + ); +} + +/** + * Let the automatons play until the maze wants a human again. Returns the + * event batches produced, one per command, for per-step broadcasting. + */ +export function driveAutomatons(room: Room): { seat: PlayerId; events: GameEvent[] }[] { + const out: { seat: PlayerId; events: GameEvent[] }[] = []; + for (let i = 0; i < 300; i++) { + const seat = actingSeat(room); + if (!seat || !room.bots.has(seat)) break; + const view = viewFor(room.state!, seat); + const cmd = automatonCommand(view) ?? automatonFallback(view); + let r = runCommand(room, seat, cmd); + if ("error" in r) { + r = runCommand(room, seat, automatonFallback(view)); + if ("error" in r) r = runCommand(room, seat, { type: "endTurn", draw: 0 }); + if ("error" in r) r = runCommand(room, seat, { type: "pass" }); + if ("error" in r) { + console.error(`automaton ${seat} wedged in ${room.id}: ${r.error}`); + break; + } + } + out.push({ seat, events: r.events }); + } + return out; +} + export interface GameSummary { roomId: string; name: PlayerId; @@ -427,13 +484,19 @@ export function loadPersistedRooms(): void { log: [], events: [], chat: [], + bots: new Set(), }; for (const line of lines.slice(1)) { if (line.kind === "join") { - const joinHash = line.tokenHash ?? (line.token ? hashToken(line.token) : null); - if (!joinHash) throw new Error("join line has no token"); - room.players.push(line.name); - room.tokens.set(line.name, joinHash); + if (line.bot) { + room.players.push(line.name); + room.bots.add(line.name); + } else { + const joinHash = line.tokenHash ?? (line.token ? hashToken(line.token) : null); + if (!joinHash) throw new Error("join line has no token"); + room.players.push(line.name); + room.tokens.set(line.name, joinHash); + } } else if (line.kind === "start") { const r = startInMemory(room, line.expansion, line.colors, line.deckRev); if ("error" in r) throw new Error(`replay start failed: ${r.error}`); diff --git a/packages/server/src/store.ts b/packages/server/src/store.ts index 5d15c0c..c28fdd6 100644 --- a/packages/server/src/store.ts +++ b/packages/server/src/store.ts @@ -24,6 +24,8 @@ export interface JoinLine { tokenHash?: string; /** Legacy plaintext token (pre-hashing files only). */ token?: string; + /** An automaton seat: no token; the server plays it. */ + bot?: true; } export interface StartLine { diff --git a/packages/web/src/App.svelte b/packages/web/src/App.svelte index 54b0961..c56b379 100644 --- a/packages/web/src/App.svelte +++ b/packages/web/src/App.svelte @@ -1025,7 +1025,7 @@ {:else} {/if} - {p}{p === net.hostId ? " — host" : ""}{chosen === undefined ? " — choosing…" : ""} + {p}{p === net.hostId ? " — host" : ""}{net.roomBots.includes(p) ? " ⚙" : chosen === undefined ? " — choosing…" : ""} {/each} @@ -1046,6 +1046,9 @@ {/each} {#if net.you === net.hostId} + {#if net.players.length < 6} + + {/if}