- Python 67.8%
- JavaScript 27.3%
- CSS 3.4%
- HTML 1.2%
- Shell 0.3%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
| pentarch | ||
| scripts | ||
| web | ||
| .gitignore | ||
| README.md | ||
Pentarch
A turn-based 5X strategy game in the Civilization V tradition, served over HTTP. Procedural worlds, a 99-technology tree running from the Stone Age to the Singularity, stacking armies, governments and cultures, barbarian raiders, and opponents with distinct temperaments. Play it alone against bots, or invite people and take turns at your own pace.
Sessions are long-lived: the server owns the game state, so you can log out mid-turn and pick the same world up days later from any browser.
- Server: Python 3.13, standard library only. No pip, no virtualenv.
- Client: one HTML page, three scripts, a
<canvas>map. No build step. - Storage: a single SQLite file.
Running it
python3 -m pentarch.server.main --port 8412 --data ./data
Then open http://localhost:8412/. That is the whole dependency list.
To install it as a service and publish it on the phosphorus mesh:
./scripts/install.sh
The game
Settings
A world is historical, science fiction or fantasy, chosen when you create it. This is a theme layer: the tree, the costs and the balance are identical in all three, and every name you read changes — eras, technologies, units, buildings, governments, peoples and city names. One balanced tree with three skins gives three games that feel distinct without three sets of balance bugs.
| Historical | Science fiction | Fantasy |
|---|---|---|
| Stone Age | Scavenger Age | Age of Stone |
| Archery → Archer | Kinetic Slugthrowers → Slug Trooper | Bowlore → Bowman |
| Library | Data Vault | Scriptorium |
| Monarchy | Founder Dynasty | Crowned Throne |
Quotations
Discovering a technology or finishing a wonder brings up a quotation. Every line is from a work in the public domain — Sun Tzu, Thucydides, Bacon, Milton, Shelley, Verne, Wells, the Havamal — quoted briefly and with its author and source. They are bundled rather than fetched, so the game has no runtime network dependency and no risk of a third-party service serving something unattributed or still in copyright.
Quotations are tagged by theme (war, knowledge, wealth, sea, sky, faith, chaos…) and each discovery is tagged too, so Adam Smith turns up for Banking and Tennyson for Navigation. The choice is seeded by the game id, so reloading a game never rewrites its own history.
World generation
Every world comes from one integer seed, so a game id reproduces its map exactly. The pipeline runs: tileable fractal noise → continent seeds grown into angle-warped blobs → sea level chosen by percentile → mountains and hills along the ridges → temperature from latitude, moisture from prevailing westerlies with a rain shadow → terrain → rivers walked downhill to the sea → features → resources on per-type quotas → start positions spread as widely as the map allows.
The map wraps east-west and is bounded north-south. A standard map is 72×44.
Technology
99 technologies across eleven eras: Stone, Ancient, Bronze, Classical, Medieval, Renaissance, Industrial, Modern, Atomic, Information, Future.
Note on ordering: the brief listed "medieval → classical/renaissance". The tree here uses the historical order — Classical precedes Medieval — and adds Industrial, Atomic and Information as the ramp from Renaissance to Future, so the requested Modern-era items land where they belong: Radio and Flight in Modern, nuclear weapons in Atomic, Satellites in Information.
Units and stacking
Units of the same type on one tile merge into a single stack. A stack's hit
points are 100 per body and its combat strength scales as
bodies ** 0.68, so a bigger stack is stronger but with diminishing returns.
Damage is dealt per opposing body, which keeps a 6-v-6 battle resolving at
the same pace as a 1-v-1 while still letting a large stack annihilate a small
one. Stack caps are per unit type (6 for most, 4 for civilians, 2 for nukes).
62 units, from Warriors and Work Boats to Stealth Bombers, Nuclear Submarines and Siege Walkers.
Governments and cultures
Fourteen governments, from Tribal Council to the Synthetic Directorate, each unlocked by a technology and costing a few turns of anarchy to adopt. Twelve cultures, picked once at the start, each with a permanent modifier set, a free technology chain, and a bias that shapes how the AI plays it.
Both feed the same modifier table, which moves production, food, gold, science, culture, border growth, happiness, unit upkeep and combat strength. Happiness is empire-wide and gates growth, production and combat.
Barbarians
Camps are scattered over unclaimed land at world creation, well clear of anybody's starting position. Each fields raiders on a timer, up to a world-wide cap that scales with the number of living players, and stops entirely once the world reaches the Information era. Raiders march on the nearest city within reach and sack what they take rather than holding it.
Barbarians are not a civilisation: they cannot be met, negotiated with, allied or declared upon, they never found cities, and they cannot win. Everyone is permanently hostile to them. Standing a military unit on a camp destroys it and pays a bounty, which is how the AI keeps its own borders clear.
Opponents
Six personalities — warlord, settler, scholar, merchant, turtle, opportunist — each weighting research, production, expansion and war differently. The culture a bot is dealt bends those weights again, so a Martial Warlord and a Scholastic Warlord do not play the same game. Six difficulty levels adjust a stipend rather than the rules.
Known limitation: bots do not mount naval invasions, so wars happen between continental neighbours. On an archipelago map they will mostly build.
Victory
Domination (hold every original capital), science (complete the colony ship), culture (a dominant late-game lead, not merely a large number), Transcendence (research the last technology), or highest score at the turn limit.
Playing
Click to select, click again to cycle the stacks on a tile. Click a highlighted tile to move, a red-ringed one to attack. Drag with the left or middle button to pan, scroll to zoom. The camera glides rather than jumping, whether you use the arrow keys, the minimap or a jump to a unit.
Planning a move. Hover any tile with a unit selected and the route is drawn as a dashed line with the turn it arrives at each hex — the turn number is amber wherever a new turn begins. The tooltip says how far off the destination is. Movement is then animated along the tiles actually walked, so a stack strides its route instead of teleporting.
Picking a fight. Hover a target and the game shows the odds before you commit: a percentage coloured green through orange to red, the damage you would deal and take as a range rather than a single number, and a plain warning when the exchange would destroy your stack or take the city. Ranged attacks, aircraft and missiles draw a curved trajectory to the target, and nuclear weapons arc highest of all.
Reading a city at a glance. Every city on the map carries its name and, below it, three numbers: population, defence strength and production per turn, each with a coloured marker.
City production is part of each turn. The end-turn button takes you to any city that needs a production choice before allowing you to finish. Click an item to select it; shift-click to queue up to eight items. Production is applied once when the world turn advances, and unused completion overflow carries to the next item. Empty queues do not earn additional stored production. Purchasing completes the current item immediately. Population or resource shortages pause an item and explain the reason without discarding its progress. An ongoing Wealth order converts 25% of production to gold; Writing unlocks the equivalent Research order. Ongoing orders belong at the end of a queue. The city panel also exposes specialists and the avoid-growth setting.
Scouting. Every new civilisation begins with a Scout, and Scouts can be trained from the initial technology. Select a land or sea unit and choose Wander (W) to explore automatically on this and future turns. It favours unexplored ground and stops near visible enemies or when it cannot move. Use wake / cancel orders to recall it. Existing games can train scouts without restarting.
Visual modes. The toolbar selector switches between the original CLI style, Pixel art (textured terrain, trees, mountains, cities and unit sprites), and Chess (alternating board tones and chess pieces with player colour bases). The setting is saved in your browser. All three use the same hex map, fog of war, movement and combat rules; the setting is independent of the historical, science-fiction and fantasy content themes.
| Key | Does |
|---|---|
space / enter |
end turn |
n |
next unit awaiting orders |
t g d p |
technology, government, diplomacy, the world |
| the diplomacy button carries a count when leaders are waiting on you | |
f z b u |
fortify, sleep, build/found, use great person |
w |
wander automatically; cancel from the unit action bar |
c |
jump to a city |
esc |
dismiss a quotation / close a panel / deselect |
arrows, + - |
pan and zoom |
Accounts
Registration is local to the server. Passwords are PBKDF2-HMAC-SHA256 with a per-user salt and 210,000 rounds. There is no password reset.
Sessions are bearer tokens held in localStorage, not cookies: the app is
mounted under a path prefix that it cannot see, so a cookie Path would
either be wrong or scoped to the entire domain.
Profile pictures are PNG, JPEG, GIF or WebP up to 512 KB, validated by magic bytes and stored in the database. Players without one get a deterministic generated emblem.
Elo starts at 1200. A finished rated game with two or more human players is scored as a round-robin of pairwise matches, which is the standard way to rate a free-for-all.
Diplomacy
Every leader you have met is listed in the diplomacy panel, and every name there opens an audience with them. An audience shows who they are, how they regard you, what you have already agreed, and a table with two sides.
You can put on the table: gold, gold per turn, a luxury or strategic resource for a term, a technology, peace, open borders, an alliance, a demand that they break off their agreements with a third leader, or a request that they join a war (or an offer to join theirs). Either side of the table may be empty, which is how the same screen serves a gift, a demand and a trade.
A bot answers on the spot, in its own voice — a Warlord refusing sounds nothing like a Merchant refusing. A person gets the proposal as a request to answer in their own time.
Leaders also come to you. A bot will open a dialogue when it has a reason: it wants a trade, it is losing a war and wants out, your army is sitting in its territory, you have planted a city on its doorstep, it shares an enemy with you and wants an alliance, or it is simply much stronger than you and would like some tribute. What it says depends on its personality and on what you have actually been doing.
Agreements run for a term and then lapse. Declaring war voids every agreement between the two of you at once.
Playing with other people
A world is single player or multiplayer, chosen when you create it. A multiplayer world is advertised in the lobby and anybody may take over one of its bots; you can also invite players by name, either when you create the world or at any point afterwards. An invitation seats the invited player in a bot's place, keeping their cities, units and research exactly as the bot left them.
The lobby shows, for each world: the turn and when it was last played, whether it is waiting on you, who else it is waiting on, who is currently leading, and a marker per player — a circle for a person, a square for a bot, dimmed once eliminated.
Turn model
Turns are simultaneous. Everyone acts during the same turn number, and the turn advances once every living human has ended theirs; the bots then take their turn and the clock steps. That is what lets people log out mid-turn.
Clients long-poll GET api/games/<id>/state?since=<version>&wait=25, which
blocks until the version moves or the timeout expires. No WebSocket, because
a turn-based game does not need one and polling survives any proxy.
Serving behind a path prefix
phosphorus path mode publishes at <domain>/<host>/pentarch/ and strips
/<host>/pentarch before proxying, so the server always sees /. Three
things make that work:
- Every URL the client builds is relative to the document.
- A script at the top of
index.htmlredirects to a trailing slash if the document URL lacks one, so relative URLs resolve correctly. - Auth is a bearer token, not a cookie.
index.html carries ?v=<stamp> on each asset, where the stamp comes from
the newest file under web/. A deploy therefore invalidates the browser
cache without anyone bumping a version by hand.
Layout
pentarch/engine/ the game itself, no I/O
content.py terrain, resources, techs, units, buildings, governments, cultures
hexgrid.py odd-r offset hex geometry, A*, flood fill
mapgen.py world generation
game.py state construction, players, serialisation
themes.py the three settings' vocabularies, and the quotations
rules.py yields, cities, combat, the turn pipeline
ai.py the offline opponents
diplo.py proposals, agreements, and what leaders say
view.py state as one player is allowed to see it
pentarch/server/ HTTP, accounts, persistence
db.py SQLite schema and access
auth.py passwords, sessions, avatars, Elo
manager.py live games, locking, long-poll waiters
api.py routes and the action dispatch
main.py the HTTP server and CLI
web/ the client
scripts/ systemd unit and installer
data/ SQLite database (created on first run)
Endpoints
All relative to wherever the app is mounted.
| Endpoint | Purpose |
|---|---|
GET api/legend?genre= |
all static content for a setting: techs, units, buildings, governments |
POST api/register, api/login, api/logout |
accounts |
GET api/me, POST api/me/password, api/me/prefs |
the signed-in player |
POST/DELETE api/me/avatar, GET api/avatar/<id> |
profile pictures |
GET api/leaderboard, api/profile/<id> |
rating and history |
GET/POST api/games |
list, create |
POST api/games/<id>/join, DELETE api/games/<id> |
take over a bot, delete |
POST api/games/<id>/invite, GET api/games/<id>/invites |
invite by name, list invitations |
POST api/invites/<id> |
accept or decline |
GET api/players?q= |
find someone to invite |
GET api/games/<id>/odds |
preview an attack without rolling for it |
GET api/games/<id>/state |
the fogged view; since+wait to long-poll |
GET api/games/<id>/city/<cid> |
one city, with what it can build |
POST api/games/<id>/action |
every game action, diplomacy included |
POST api/games/<id>/endturn |
flag done; advances when everyone is |
Tests
python3 scripts/selftest.py # add -v to list every check
python3 scripts/test_mechanics.py # focused production, scouting and action regressions
It starts its own server on a spare port against a throwaway database and exercises the engine directly and the HTTP API end to end: world generation across several seeds, the combat maths, fog of war, full simulated games, all three settings, the quotation pool, barbarians, diplomacy, authorisation, malformed input, long polling, invitations and persistence.