Skip to content

Latest commit

 

History

2 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Polytopia Web

A Polytopia-like 4X strategy game in one self-contained index.html. Vanilla JS, isometric canvas renderer, no build step, no assets, no npm dependencies.

Run

  • Solo / hotseat: open index.html in a browser.
  • Online multiplayer: node server.js then open http://localhost:8765 (or your LAN/public address). Hit Play online, create a room, share the 4-letter code. PORT=3000 node server.js to change ports.

Online play — how it works

server.js is a single zero-dependency Node file that both serves the game over HTTP and speaks WebSocket on the same port (RFC-6455 framing implemented by hand, so there is no npm install). It is authoritative: it runs the same rules code extracted from index.html, and every client action is validated server-side (correct seat + legal under applyActionObj) before being broadcast to both clients, which apply it deterministically to their local copies. Room flow:

  1. Player A: Create room → gets a code like KQZW, waits.
  2. Player B: Join room with the code → server creates the game and sends both players the full state + their seat.
  3. Actions stream as tiny JSON messages; turn order is enforced server-side. Disconnects keep the room alive — rejoin with the same code and the server re-sends the full current state.

To host for friends over the internet: run it on any box with Node and a reachable port (a $4 VPS, fly.io, a Tailscale/ngrok tunnel from your laptop — anything that can expose TCP works, no other infra needed). Honest caveat for competitive play: clients hold the full state and render through their own fog, so a determined cheater could read the opponent's positions from devtools — fine among friends; server-side fog filtering is the eventual fix.

Play

  • Modes: vs Robot (heuristic or RL policy), 2-player hotseat (pass-the-device blackout screen), or online via room code.
  • Seed: same seed = same map.
  • Camera: drag to pan, wheel to zoom, shift-drag (or right-drag) to tilt the view between top-down and side-on. resets.
  • Click a unit → blue tiles move, red diamonds attack (hover for damage preview both ways).
  • Click your city → harvest/train panel, and its territory lights up on the map.
  • Capturing: move onto a village or enemy city; next turn a gold flag appears and a Capture button/action becomes available. Capturing is optional and consumes the unit's turn.
  • Win: capture the enemy capital, or higher score at turn 30. Enter ends turn, Esc deselects.
  • Save/Load (Menu) exports the entire game as a base64 string.

Mechanics notes (sourced from the Polytopia wiki)

  • Tech tree (15 nodes): hunting→(archery, forestry→mathematics); farming→shields; climbing→mining→smithery; riding→free spirit→chivalry; fishing→sailing→navigation. Forestry builds lumber huts on bare forest (3★ → 1 pop); mathematics unlocks the catapult; fishing harvests fish (2★ → 1 pop); sailing lets units enter water as rafts (no attacking or retaliating at sea); navigation makes water stop costing the whole move and grants vision 2 at sea.
  • Unit stats are the published values (warrior 2★ 10hp 2/2, archer 3★ 10hp 2/1 rng2, defender 3★ 15hp 1/3, rider 3★ 10hp 2/1 mv2, swordsman 5★ 15hp 3/3, knight 8★ 10hp 3.5/1 mv3, catapult 8★ 10hp 4/0 rng3 no skills, giant 40hp 5/4).
  • Your own units never block harvesting/building on a tile — only enemy occupation matters (a unit standing on a resource shows the harvest button in its panel).
  • Combat uses the published formula verbatim (atk × hp/maxHp vs def × hp/maxHp × defBonus, ×4.5, rounded).
  • Defense bonus: 1.5 forest (archery) / mountain (climbing) / own city, 4.0 with wall. City bonuses require the fortify skill — giants never get them. Besieging units get no bonus.
  • Besieged cities (enemy unit on them) produce no income.
  • Capture is an explicit action: the unit must start its turn on the city tile (so earliest is the turn after arriving), and may choose to do something else instead.
  • Fog is one-time: once a tile is explored it stays fully visible forever, including enemy units on it (as in the real game).
  • No retaliation if: defender dies, attacker out of range, or the attacker stands on a tile the defender has never scouted.
  • Melee units take the tile on a kill; ranged units never move.
  • Idle units heal +4/turn in own territory, +2 elsewhere. 3 kills → veteran (+5 max HP, full heal).

Architecture

  • S is one plain JSON-serializable state object; every rule is a pure function of it (reachable, calcCombat, applyMove/Attack/Harvest/Research/Train/Reward, endTurnLogic...).
  • render() only reads S + a local ui object. The logic layer never touches the DOM — it runs headless in Node, which is how it's tested and how RL training works.

RL (ported from polytopia_rl)

The game embeds a gym-style bridge mirroring the polytopia_rl repo's interfaces:

  • enumerateActions(S) / applyActionObj(S, a) — deterministic ordered action list (like listActionsJson/stepByIndex), including the explicit CAPTURE action (same as Tribes' ACTION set). When a city levels up, the only legal actions are the two reward choices.
  • encodeObs(S, p) — same layout as register_env._dict_to_array: [terrain(256), unitID(256), cityID(256), stars, score, numCities, kills, tick, activeTribeID] = 774 floats, with Tribes terrain codes (PLAIN=0, WATER=1, MOUNTAIN=3, VILLAGE=4, CITY=5, FOREST=6).
  • makePPOAgent(weights) — hand-written forward pass of the CleanRL PPO actor (Linear(774,64) → Tanh → Linear(64,64) → Tanh → Linear(64,200)), with the same Discrete(200) modulo action masking as TribesGymWrapper.step.

Training a policy against this game

The polytopia_rl repo has no saved checkpoints, and a model trained on the Java Tribes env wouldn't transfer (different board semantics + action ordering). Instead, train directly against this engine:

  1. Env servernode rl/web_env_server.js speaks a JSON-lines gym protocol (reset/step, reward = Δscore/100 + relative/1000 − 0.01, ±100 terminal — same shape as gym_env.py). The agent is player 0; the heuristic robot plays player 1.
  2. Python wrapperrl/polytopia_web_env.py is a drop-in gym.Env (same obs/action spaces as TribesGymWrapper). Point the make_env thunk in py_rl/clean_rl_nc/ppo.py at PolytopiaWebEnv and train as usual. Smoke test: python rl/polytopia_web_env.py (needs gymnasium, numpy, Node on PATH).
  3. Exporttorch.save(agent.state_dict(), "ppo.pth"), then python rl/export_weights.py ppo.pth weights.json.
  4. Play it — in the game: Menu → RL weights… → paste weights.json, then start a game with Robot brain = RL policy.

Random-weight policies work mechanically (verified) but play badly — the loader exists so trained weights drop straight in.

Roadmap: online play (.io-style)

A game is (seed, list of actions) and applyActionObj already validates every action against state, so it doubles as a server-side rule check. Remaining work: a tiny WebSocket room relay; reconnect = send the base64 save string.

Testing (all headless, no browser)

node --check on the extracted script, full heuristic-vs-heuristic games across seeds, mechanics unit checks (wall ×4, fortify, siege income, fog retaliation, mountain gating), action-enumeration fuzz (random playouts — every enumerated action must apply legally), PPO forward pass + full RL-vs-heuristic games, gym-server protocol round-trip, deterministic mapgen, save/load identity.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages