Skip to content

About

Go CLI that generates rules-accurate Classic Traveller subsectors and sectors from Book 3's Worlds chapter (pp. 1-12, © 1977 text) — a seeded JSON record rendered as the Markdown listing a referee runs from, or a printable PDF booklet with the hex map drawn and the trade routes joined.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

0 stars

Watchers

1 watching

Forks

Repository files navigation

ctworldgen

Status: Beta

A Go CLI that generates rules-accurate Classic Traveller subsectors and sectors from Book 3 Worlds and Adventures, the Worlds chapter (pp. 1-12, © 1977 text). It writes a JSON record from a recorded seed and renders it as the Markdown listing a referee runs from, or as a printable PDF booklet with the hex map drawn and the routes joined.

Install

go install github.com/philoserf/ctworldgen/cmd/ctworldgen@latest

What is built, and what is not

The engine walks the whole of Book 3 pp. 1-12 -- the eighty-hex occurrence scan, starport types, naval and scout bases, commercial routes, and the eight characteristics of every world -- all from one seed. render turns a record into the listing, opening with a text map of the p. 3 hex grid, or into the booklet. sector lays sixteen subsectors on one 32x40 grid and throws for the routes at their seams.

Every page of pp. 1-12 is built, including p. 1's offer of an occurrence DM "on broad areas within a subsector": --occurrence-area puts a rift in one corner and a cluster in the other. It is new's alone -- an area is a rectangle of one grid's numbering, and a sector's sixteen members are each generated on their own p. 3 grid.

docs/COVERAGE.md is the live map of rule to code to test, and it carries a row for what is not built as well as for what is. Ask it whether a rule is in here; this section only says what the shape of the thing is.

What the tool should do comes from issue 1, the first alpha's report: it was played as a referee rather than as a developer, and what that found is the backlog. The second alpha shipped everything it asked for, the technological levels tables included, and it wants reading the same way -- play it as a referee and file what the table finds.

Use

ctworldgen new    [--seed N] [--name X] [--occurrence-dm N] [--occurrence-area DM@FROM-TO]... [-o file] [--force]
ctworldgen sector [--seed N] [--name X] [--occurrence-dm N] [-o file] [--force]
ctworldgen render [--format markdown|pdf] [--lanes legible|all] [-o file] [--force] record.json
ctworldgen version
$ ctworldgen new --seed 1977 --name Aramis --occurrence-dm -1

sector writes one record covering sixteen subsectors on a single 32x40 grid, 0101 through 3240, and throws for the commercial routes that cross between them -- the routes sixteen independent subsectors can never have. Every member is unchanged: member i of sector --seed N is exactly the subsector new --seed N+i writes, so the subsector you read is the subsector you get. The seams are one further reading (ERRATA.md E006), stamped on every sector record.

render reads either record, a subsector or a sector, and writes the Markdown listing by default. A sector's listing opens with an index map of the whole 32x40 grid and a table of its sixteen, and then carries the sixteen sub-sector listings themselves -- each on its own p. 3 grid, ringed by one hex of its neighbours so the lanes crossing a seam have somewhere to land, and each headed with the seed that writes it standalone (ERRATA.md E008). --format pdf writes the booklet instead: the map beside its roster on the first page, then the routes and a page of detail per world. It is the one output that draws p. 2's "line connecting the two worlds on the map", which a monospace grid has nowhere to put. A booklet is a binary, so --format pdf needs -o, and it reproduces byte for byte from the same record.

--occurrence-dm takes -1, 0 or +1 and nothing else, and defaults to 0. It applies to every hex no broad area covers.

--occurrence-area is p. 1's other half -- a DM "on broad areas within a subsector" -- and takes a DM, an @, and the two opposite corners of a rectangle of hexes: --occurrence-area -1@0101-0805. It may be given more than once, areas may not overlap, and the record carries every one of them so a run still reproduces from what it holds. The reading is ERRATA E012. A sector takes none: an area is a rectangle of one grid's numbering, and a sector's sixteen members are each generated on their own p. 3 grid.

$ ctworldgen new --seed 1977 --name Aramis \
    --occurrence-area -1@0101-0805 --occurrence-area +1@0106-0810

Without --seed, a seed is drawn from OS entropy and written into the record, so a run is reproducible after the fact; --seed 0 is therefore an explicit and distinct choice rather than a request for a random one. Existing files are never overwritten without --force, and flags precede any filename.

docs/examples/complete.json is a full record with every field populated; task regenerate rewrites it, so it never drifts from what the tool writes. docs/examples/minimal.json is the smallest record the schema admits, and is held by hand.

What a seed fixes, and what it does not

A seed and the inputs reproduce a subsector exactly, and only exactly. All the dice come from one stream in procedure order, so anything that changes how many throws are made before a given world changes that world.

Changing either occurrence flag regenerates the subsector; it does not thin the one you have. The occurrence scan throws one die per hex over all eighty before anything else, so the same seed gives the same eighty faces and the star fields nest: every hex placed at -1 is placed at 0, and every hex placed at 0 is placed at +1. The map looks like a dial. But each extra world consumes dice for its starport, its bases and its characteristics, so the hexes two runs share keep almost nothing else — different starports, different populations, a different string of digits. "The same subsector, a touch sparser" is not available: change the seed for another subsector, and hold the DM to keep this one. A broad area is the same story told over fewer hexes -- it changes which faces succeed inside its rectangle, and every world after the first difference is a different world.

Separate subsector files are a loop over new. sector is the answer when the sixteen belong on one grid, because it throws for the routes at their seams. When you want them as independent records instead, their seeds are simply consecutive:

for i in $(seq 0 15); do
  ctworldgen new --seed $((1977 + i)) --name Aramis -o "aramis-$i.json"
done

Sector seeds closer together than sixteen share subsectors. A sector's members run on seed through seed + 15, so sector --seed 100 and sector --seed 110 have six subsectors in common -- the same worlds with the same digits, sitting in different corners of the two maps. Nothing warns about it, because each sector is individually correct. Leave a gap of at least sixteen when generating a second sector to set beside the first.

Which lanes are drawn

A dense subsector throws a hundred and sixty commercial routes over forty-six worlds, and a map with all of them on it cannot be read. P. 2 offers the map-drawer a way out in the book's own voice -- a connection already present "may be ignored" -- and render takes it by default: a lane whose two worlds are already joined by shorter lanes is not drawn and not listed (ERRATA.md E007).

It removes about 46% of the lanes and changes the reachability of nothing, because a lane is only dropped when its ends are already joined. The summary line says how many were drawn, the route section says how many were not, and --lanes all draws every one.

The record is unchanged and carries every lane. This is a decision about ink, not about dice: the engine still examines every pair and consumes every die (E003), so no seed's meaning moves.

Writing in the record

The record is the referee's notebook page, so there is a place to write in it. notes on a world, and notes on the record as a whole, belong to the referee: the tool never generates either and never reads them back. Both survive re-rendering -- a world's note becomes a line in its detail block and the record's becomes a paragraph under the heading, in the listing and in the booklet alike.

{
  "name": "Tessarane",
  "notes": "The rift campaign. Players start at 0602.",
  "worlds": [{ "hex": "0602", "name": "Reagan", "notes": "dust storms" }]
}

Nothing else is admitted. Every other key the record does not define is still refused, which is what makes the generated fields worth trusting; notes is a field the record names rather than a hole in that rule.

Where the rules come from

Rules come only from the held PDFs of the FFE reprints, never from memory. Training-data Traveller is mostly the 1981 revision and later editions, and the held © 1977 page governs even where it differs -- most visibly in the string of digits, which carries no hyphen (A867A698, not A867A69-8).

Book 3 pp. 1-12 is the ruleset. Book 1 pp. 2-3 and p. 8 supply the die roll conventions and the hexadecimal notation that Book 3 uses without restating. Nothing else is in authority: not Books 2 and 4+, not the supplements, the Starter Edition, The Traveller Book, JTAS, the Consolidated Errata, or the rest of Book 3.

Two habits follow, and they are the ones worth knowing from outside:

  • Every table is transcribed from a visual read of the page, then transcribed a second time inside the table package's tests, so the two must agree. The held PDFs' embedded font maps the em-dash to the glyph 4 and the minus sign to 3, so a text extraction renders the jump routes table's empty cells as the digit 4 and the size formula 2D − 2 as 2D32. Both readings are wrong and both look like data.
  • No ambiguity is resolved in silence. Where the page does not settle something, the reading goes in docs/ERRATA.md with its page cite and the condition under which a record stamps it, and every record carries the stamps that applied to it.

The documents

docs/ERRATA.md records every reading of an ambiguous or silent page. docs/COVERAGE.md maps the rules to the implementation and the tests. docs/record.schema.json is the record's schema, with a minimal and a complete example beside it. CLAUDE.md carries the authority model in full, and the traps -- the font, the hex grid parity, the dice-stream consumption order -- that a change to this code has to respect.

THEORY.md is the account of why the code is shaped as it is: what a maintainer has to hold in mind to change it without damaging it, and where that account is uncertain. WALKTHROUGH.md is the tour of how it runs, entry point to output, and every fenced block in it is executable -- uvx showboat verify WALKTHROUGH.md re-runs them all and diffs the captured output, so the document cannot quietly stop being true.

Development

task              # the whole gate: tidy, vet, lint (formatting included), nilaway, test -race, coverage ratchet, prettier
task regenerate   # rewrite the golden fixtures and the complete example, then read the diff

CI runs exactly task. The toolchain is deliberately unpinned: the gate is meant to fail when a tool moves rather than drift behind it, so a red gate on code you did not touch is the signal working. Answer the finding; do not pin a tool or add a linter disable to silence it.

main is protected -- pull request, a green gate, linear history, no bypass -- so work lands on a branch and merges through CI.

Licence

MIT. See LICENSE. Traveller is © Far Future Enterprises; this tool implements the rules and reproduces none of the text beyond the table labels a listing needs.

One thing is cited rather than implemented. What a technological index means is glossed from Traveller5 Core Book 2 pp. 230-232 (© 2015-2019 Far Future Enterprises), because Book 3 pp. 10-11 print it as ten sparse columns of 1977 shorthand that cannot be read at a table. The borrowing is description only -- it changes no throw, no record field and no stamp -- and both books are FFE's, so this is one of their books glossing another. docs/ERRATA.md E011 states its whole extent.

About

Go CLI that generates rules-accurate Classic Traveller subsectors and sectors from Book 3's Worlds chapter (pp. 1-12, © 1977 text) — a seeded JSON record rendered as the Markdown listing a referee runs from, or a printable PDF booklet with the hex map drawn and the trade routes joined.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

0 stars

Watchers

1 watching

Forks

Releases

Sponsor this project

Used by

Contributors

Languages