auto: docs sync 2026-08-20T06:00:04+00:00
Files changed: engine/lint-report.md vault/.obsidian/workspace.json vault/projects/meshwars.md
This commit is contained in:
parent
a96d7275f9
commit
43f047950d
3 changed files with 101 additions and 5 deletions
95
vault/projects/meshwars.md
Normal file
95
vault/projects/meshwars.md
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
---
|
||||
title: MeshWars — territory game over mesh radio
|
||||
type: project
|
||||
tags:
|
||||
- mesh
|
||||
aliases:
|
||||
- meshwars.com
|
||||
related:
|
||||
- [[meshcore-transport]]
|
||||
status: live
|
||||
updated: 2026-08-20
|
||||
---
|
||||
|
||||
# MeshWars
|
||||
|
||||
A territory control game played over mesh radio. Seven teams claim roughly 300 metre squares of ground by reaching the mesh from them. Public open source at `zvx-echo6/meshwars`, live at meshwars.com.
|
||||
|
||||
## Where it runs
|
||||
|
||||
CT 119 (`mesh-territory`) on the utility host. A single Docker container built from the repo, FastAPI plus SQLite, no external services. The database is one file at `/data/game.db` inside the container. TLS terminates on the CT 101 Caddy, which proxies to port 8090.
|
||||
|
||||
Credentials — admin token, invite code, the owner's player key — live in `credentials`, never in this vault.
|
||||
|
||||
## Two boards, one model
|
||||
|
||||
MeshWars runs two separate games side by side, one per radio protocol. Since 2026-08-19 both run on **the same model**: seven teams, the same flat grid, the same scoring tables, the same player registration. The only thing separating them is a `protocol` column on the season table holding `mc` or `mt`. Every other scoring table hangs off a season id, so the split flows down from that one place.
|
||||
|
||||
Each protocol runs its own season, on its own clock, with its own winner.
|
||||
|
||||
**MeshCore** gets position by push. Players run MeshMapper, a wardriving app, and configure it to forward a copy of its wardrive to MeshWars alongside whatever it already does. There is no pull API on the MeshCore side. A radio binds itself to a player the first time a ping arrives carrying its contact key — the eight hex characters at the front of the radio's public key — which is why MeshMapper's "Include Contact Key" toggle is a required setup step.
|
||||
|
||||
**Meshtastic** gets position by pull. A background loop polls a public meshview instance every 45 seconds for position packets, then asks which nodes actually heard each one. Only nodes registered to a player are scored; everything else is fetched and discarded. Meshtastic radios cannot self-bind, because the packets we pull carry no key back to us, so they are added explicitly by the player.
|
||||
|
||||
## Scoring
|
||||
|
||||
A paint earns points, and points decide who holds a square. Points decay a quarter point a day, a freshly captured square cannot be flipped for fifteen minutes regardless of score, and after that it falls to any team that out-scores the current holder — the holder alone, never the highest scorer across all seven teams. The same player cannot repaint the same square inside five minutes.
|
||||
|
||||
The two protocols differ only in where the points come from, and it is inverted between them:
|
||||
|
||||
- **MeshCore** scores on how many repeaters the player *heard*.
|
||||
- **Meshtastic** scores on how many MQTT feeders *heard the player*, which is the only direction meshview can observe.
|
||||
|
||||
Both run a tenth of a point per station, capped at one point per ping, as separate config knobs so the boards can be tuned independently. A ping that reaches nobody scores nothing and claims nothing.
|
||||
|
||||
Meshtastic runs with **no hop limit**, so a packet relayed across the mesh counts every gateway that eventually logs it. In practice most of those are nowhere near the player — a typical node sees one gateway in direct range and half a dozen more via relays. Tightening `MAX_HOPS` is the lever if scoring should reflect actual local reach rather than how far a packet travelled.
|
||||
|
||||
Bonuses and cooldowns are keyed on the **player**, never the radio. Ten radios in one square earn what one earns.
|
||||
|
||||
## Node identity — the rule that has bitten twice
|
||||
|
||||
`player_node.node_ref` is stored **bare lowercase 8-hex** (`7a357d79`), never with a leading `!`, for both protocols. `app/node_ref.py` is the single normalizer. `_node_hex()` in `app/api.py` still returns the `!`-prefixed form and is correct for repeater ids and for display — the two are not interchangeable, and conflating them silently unbinds radios with no error anywhere.
|
||||
|
||||
Display is deliberately separate from storage: a Meshtastic radio renders as `!a1b2c3d4` because that is the ecosystem convention, a MeshCore one renders bare.
|
||||
|
||||
A migration strips a legacy `!` prefix on startup, collision-guarded so it cannot fail on a database where both forms exist.
|
||||
|
||||
## Keys
|
||||
|
||||
A player registers once and their API key is shown exactly once. Only a hash is stored, so **recovery is impossible** — do not try to build it. Three paths exist instead:
|
||||
|
||||
- A MeshCore player's key is readable from their own MeshMapper settings, which is the first thing to suggest.
|
||||
- An operator can issue an **extra** key, leaving the old one working, for someone who simply mislaid theirs.
|
||||
- An operator can **revoke and replace** every key a player holds, for a key that has actually leaked. This breaks their MeshMapper until they reconfigure it, so it is the wrong tool for a lost key.
|
||||
|
||||
Operators can also add and remove a player's radios directly, which means fixing someone never requires their key at all.
|
||||
|
||||
## Net check-ins
|
||||
|
||||
Live since 2026-08-20. A second way to earn, alongside holding ground. Weekly net, Wednesday evenings, 17:00-23:59 America/Boise. Checking in is worth **25 points**, a config value, credited **once per player per net** no matter how many messages they post. Points combine with squares into the team total, so the season is won on the two together.
|
||||
|
||||
Themed Wardrivers and Netrunners, but those are two **activities, not two kinds of player** — one account, one team, and someone doing both appears in both rankings. There is deliberately no class picker anywhere.
|
||||
|
||||
The mechanic paints no square and needs no position, which is what makes it work for people who operate but never wardrive. Points are season-scoped and reset with territory.
|
||||
|
||||
**Two feeds, read on a timer.** MeshCore reads the `#weekly-net` channel messages from `live.mwmesh.com`; Meshtastic reads meshview text packets (`portnum=1`) filtered for `#freq51`, case-insensitively. Neither feed carries a net session id and none is needed — the net *is* the Wednesday, so the award key is the player plus that local date.
|
||||
|
||||
**Identity differs sharply between the protocols, and this is the subtle part.**
|
||||
|
||||
Meshtastic is easy: the packet carries `from_node_id`, which is already what every registered radio is stored as. Anyone registered earns automatically.
|
||||
|
||||
MeshCore channel messages carry **no sender public key at all** — they are encrypted to a shared channel key, not signed per sender, so there is nothing to expose and no point asking the maintainer for it. The only sender field is a display name. The bridge that solves it: `live.mwmesh.com/api/nodes?limit=N` lists every node with both `name` and `public_key`, so a player's bound contact (the first 8 hex of their key, which MeshMapper supplies) prefix-matches a directory entry and yields the name their check-ins appear under. Resolution runs key first, name second — an impostor who renames their node resolves to nobody, because their key is bound to nobody. Ambiguity is refused and logged, never guessed.
|
||||
|
||||
A contact reaches `player_node` three ways, indistinguishable downstream: MeshMapper auto-binds it on the first wardriving ping, the player picks their node off a searchable list on the join page, or they type the 8 hex in. A self-declared name survives only for players whose key is absent from the directory entirely.
|
||||
|
||||
**`CHECKIN_NET_START_DATE` gates everything** and defaults to empty meaning *block all*, never *unbounded*. Without it, a first poll would retroactively award every past net still sitting in the feeds — the MeshCore endpoint returns its newest 100 messages, spanning weeks. Production is set to `2026-08-19`, which credited exactly four awards on launch: Malice on both boards, plus Londy-D and WLZ.
|
||||
|
||||
Known and not yet addressed: the poller hits mwmesh every 30 seconds around the clock, roughly 2,900 requests a day, when only about seven hours a week can produce an award. Tight polling is right during a net, since that feed holds only 100 messages, but it should ease off outside the window.
|
||||
|
||||
## Operational notes
|
||||
|
||||
Use `meshview.freq51.net` — the instance production actually polls. There is another meshview instance on the same domain fed by a single gateway; measuring anything against it gives wildly misleading feeder counts.
|
||||
|
||||
Three seasons of the original two-team geohash game are retained in the legacy tables and never read or written. Their row counts are not evidence of anything. To check whether the retired path really stopped, compare the newest row's timestamp against the cutover, not row counts against an older snapshot.
|
||||
|
||||
There is no automated test suite. Verification is done by running against a read-only copy of the live database and by screenshotting pages and looking at them — every real defect found during the 2026-08 migration was caught by looking at an image, not by reading code.
|
||||
Loading…
Add table
Add a link
Reference in a new issue