A browser-based island empire game in the spirit of the classic early-2000s German browser strategy games — slow real-time timers, table-driven UI, alliances-and-raids gameplay. Original name, assets, text and numbers.
Bots play alongside humans using the exact same rules and actions.
Play it live: https://nostr.social/tideholm/ — sign in with a pod (create one).
| Your island | The world map |
|---|---|
![]() |
![]() |
Available in English, German and Czech — pick a language on the login
screen or in-game; the whole UI, error messages and even battle reports are
rendered in each player’s own language (public/i18n.js holds the
dictionary; adding a language is one new block there plus a help page).
node server.js
Then open http://localhost:3000 and register with any name + password.
No dependencies. State is saved to data/world.json every 30 seconds and on
shutdown. Delete that file to start a fresh world.
World & server
| Var | Default | Meaning |
|---|---|---|
PORT |
3000 | HTTP port |
GAME_SPEED |
5 | Multiplies production, divides build times. 1 ≈ classic pace (hours), 5 = minutes. Lower it (2–3) for a slower, more deliberate world. |
BOTS |
20 | Bots spawned when a new world is created |
FREE_ISLES |
30 | Uncharted (colonizable) islands on a new world |
LAND_RESPAWN |
0 (off) | Uncharted islands added when the map runs out of them. Fires on world load, i.e. a server restart — so with it on, a deploy can reshape a live map and move the dominance threshold (all islands count toward WIN_SHARE). Off by default; see #36. |
WORLD_THEME |
generated | Map backdrop: generated (seeded fictional chart) or aegean (real coastlines; build other regions with tools/build-region.js) |
WIN_SHARE |
0.6 | Island share a player/alliance must hold for a dominance victory |
WONDER_WIN_LEVEL |
5 | Great Beacon level that wins the world. Raising it stretches the endgame: each level costs 1.55× more and takes 1.5× longer, and every completed level is announced world-wide. |
NIGHT_BONUS |
— | Night window HH-HH (e.g. 22-6) where defenders get a bonus |
ADMIN_TOKEN |
— | Enables the admin panel at /admin.html (stats, announce, reset) |
Season lifecycle (#8, #2)
| Var | Default | Meaning |
|---|---|---|
WORLD_START |
now | Launch time (epoch ms or ISO date). Before it, the world is frozen and shows a signup countdown so everyone starts together. |
SEASON_AUTO |
off | Auto-recycle a finished world into the next season |
SEASON_END_GRACE_HOURS |
48 | Bragging window a won world lingers before auto-reset |
SEASON_SIGNUP_HOURS |
24 | Pregame countdown for the next auto-started season |
Balance / difficulty (#7, #13) — all default to prior behavior
| Var | Default | Meaning |
|---|---|---|
PROTECT_GRACE_HOURS |
72 ÷ speed | Newcomer shield in real hours (independent of speed). Set e.g. 48 for two real days. |
PROTECTED_POINTS |
40 | Points below which a player can’t be attacked |
BULLY_RATIO |
3 | Attack point-band: no punching up/down past this ratio |
MORALE_FLOOR |
0.3 | Min attack-power multiplier when outweighing the defender |
BOT_MORALE_FLOOR |
= MORALE_FLOOR |
Same, but vs bot defenders. Set 1 so the leader can fight bots at full power while human newcomers stay protected. |
BOT_GARRISON_RATIO |
∞ | Cap a bot’s standing defence to islandPoints × ratio (raiders exempt). Set e.g. 12 to stop bots turtling into unbeatable fortresses. |
TRADE_SLOTS_PER_HARBOR |
1 | Merchant slots granted per Harbor level (#30). A shipment occupies one slot for the round trip, so distance costs throughput, not just patience — supplying a distant island is genuinely expensive. Applies to trade and market shipments; the Tidepool is exempt. New worlds only: existing seasons load unlimited (tradeSlots: null) so a restart cannot tighten logistics under plans already in motion. 0 grants no slots at all. |
COLONY_COST_GROWTH |
1 (flat) | Colony Ship price multiplier per step along the expansion curve — cost × growth^(position-1), stepped per ship. Position is islands owned plus every colony ship already paid for (in a garrison, in a training queue, or sailing to settle), so ordering singly costs exactly the same as ordering a batch — counting islands alone let a player split orders for a 25% discount. 1.3 is the tuned brake on going wide: the 10th step costs ~29k all-in instead of 2.7k, the 15th ~106k. Steeper bites much harder than it looks (1.6 puts the 15th at 1.9M). Clamped to [1, 3] — anything outside falls back to flat, because an unbounded value made the ship unbuildable at any wealth while reporting “not enough resources” (#61). The train form quotes the real batch total, which is dearer than the single-ship price times the count (#62). Only meaningful from a fresh world. |
Example — a fair, self-running “casual” world:
GAME_SPEED=3 \
WORLD_START=2026-07-20T18:00:00Z \
PROTECT_GRACE_HOURS=48 BOT_GARRISON_RATIO=12 BOT_MORALE_FLOOR=1 \
SEASON_AUTO=1 SEASON_SIGNUP_HOURS=24 SEASON_END_GRACE_HOURS=48 \
ADMIN_TOKEN=... node server.js
supportCostsPop): on for new seasons, off for seasons that
began before the rule existed, so a restart cannot change the terms
mid-season.MORALE_FLOOR, 30% by default). Optional night defense bonus via
NIGHT_BONUS=22-6.WIN_SHARE (default 60%) of all
islands wins the world — or raises the Great Beacon to
WONDER_WIN_LEVEL. Either way it’s announced to everyone, with a banner
in the UI. Start the next season from the admin panel’s reset (which
archives the old world and can open a signup countdown), or let
SEASON_AUTO=1 recycle a won world on its own.BOT_PERSONAS, global pace via BOT_TEMPO.Attack power A vs defense power D (home garrison defends). If A > D the
attacker wins and loses fraction (D/A)^1.5 of the army; the defender’s
garrison is wiped. Otherwise the attacker is wiped and the defender loses
(A/D)^1.5. Loot is capped by the survivors’ carry capacity, drawn
proportionally from the target’s stocks.
Defense counts the garrison plus all stationed support, then the Wall:
D = (defenders + 15×wall) × (1 + 0.08×wall). A sacked island loses one
wall level.
If a Flagship survives a winning attack it breaks 25-40 loyalty; at 0 the island changes owner: the Flagship is consumed, survivors stay as the garrison, and the island’s buildings and resources transfer. Loyalty regenerates at 2/h (×speed). An attack that arrives at an island you already own (e.g. captured by an earlier wave) reinforces it.
See deploy/DEPLOY.md — a systemd + Caddy recipe for a VPS, or Fly.io.
The server enforces a single instance via a lockfile, keeps rolling world
backups in data/backups/, rate-limits registration/login per IP and
actions per player, and logs actions and errors (not polling) to stdout.
Set TRUST_PROXY=1 behind a reverse proxy so rate limiting sees real
client IPs and session cookies are marked Secure.
Run the engine test suite with node tests.js. Balance-test a simulated
world with node playtest.js [days] [bots].
Set ADMIN_TOKEN to enable the admin panel at /admin.html: world stats,
announcements to all players, and season reset (archives the old world to
data/backups/, keeps the hall of fame). Without the env var the admin
API is disabled entirely.
Tideholm doubles as “plugin zero” for JSS’s plugin system
(JavaScriptSolidServer#206):
mounted at /tideholm inside a pod server, where your WebID is your
player — no password, auto-provisioned on first request. See
jss-plugin/ (adapter + manifest) and run the end-to-end proof with
npm install && node jss-plugin/demo.js (JSS >= 0.0.214 comes from the
devDependency; the game itself stays zero-dependency).
tests.js / tests.app.js — engine invariants and app-layer (HTTP,
prefix mount, host identity) tests: node tests.js && node tests.app.jsgame.js — pure game engine (formulas, lazy time resolution, actions, persistence)bots.js — bot spawn + decision tick, built on the same engine actionsapp.js — application core: world lifecycle, sessions, JSON API, static
files. createApp({ basePath, identify, ... }) is transport-agnostic so a
host process (e.g. a JSS plugin) can mount the game under a prefix with its
own identity provider (WebID → player)server.js — thin standalone launcher: socket, signals, port hopping,
rate limiting, lockfile, backupspublic/ — vanilla JS single-page client (+ help.html)tests.js — engine math and invariant testsAMM.md — the Tidepool: how the constant-product pool works, how its
parameters were derived, and what to lift if you want one elsewhereNEWS.md — announcements sent to players in-game, with the exact text and
what each one shippeddeploy/ — systemd unit, Caddyfile, Dockerfile, fly.toml, deploy guide