ctchargen
A Go CLI that generates rules-accurate Classic Traveller characters.
Ruleset baseline: Books 1–3 only — the FFE reprints of the © 1977 text.
Character generation is Book 1 pp. 4–25; Books 2 and 3 are consulted only
where Book 1 points at them. Every implemented rule carries its printed-page
cite, and every place the text is silent or ambiguous has a recorded reading.
Install
A binary, needing no Go toolchain. This is the line for an Apple-silicon Mac:
curl -Lo ctchargen https://github.com/philoserf/ctchargen/releases/download/v1.0.0-beta.1/ctchargen-v1.0.0-beta.1-darwin-arm64
chmod +x ctchargen
For another machine, substitute darwin-amd64, linux-amd64 or linux-arm64
in the filename. The tag is named rather than releases/latest, which resolves
only to a non-prerelease and so resolves to nothing here. A checksums.txt is
attached beside the binaries.
curl is what the line uses because a browser download is quarantined and
needs xattr -d com.apple.quarantine ctchargen before it will run. The
binaries are also unsigned, so a recent macOS may refuse one regardless —
if it does, the tool is not broken; install it with Go instead:
go install github.com/philoserf/ctchargen/cmd/ctchargen@v1.0.0-beta.1
Go excludes prereleases from @latest, so an alpha installs by name. That is
the intended friction.
Do not install v1.0.0-alpha.1 or v1.0.0-alpha.2. Both predate the
rebuild at 41a213a and are builds of a different implementation — they have
a replay subcommand and flags this tool does not. Their release notes are
accurate about the tag they head and describe nothing below.
From a clone:
go build ./cmd/ctchargen
Status
All six services generate, the book's own worked character (pp. 23–25)
replays against the engine, the record has a schema, and characters can be
written to files, generated in batches and read back. new without
--auto walks the procedure a question at a time, showing each throw
between the questions.
Every command lists its own flags, which is where the current set lives:
ctchargen --help # the commands
ctchargen new --help # one command's flags, with their values and defaults
Using it
Four commands. new generates one character; batch generates many from one
base seed; render reads a record back as a sheet or as the transcript; and
version writes the build.
ctchargen new --auto --seed 145 --service merchants --sheet
ctchargen new --auto --name "Alexander Jamison" -o jamison.json
ctchargen new --auto --history # the transcript, throw by throw
ctchargen new --seed 145 --sheet # asks at every choice point
ctchargen batch --count 20 --auto --seed 145 --service merchants
ctchargen batch --count 20 --auto --seed 145 --service merchants -o characters/
ctchargen render characters/00000000000000000145.json
ctchargen render --history characters/00000000000000000145.json
Batch members number from zero, so the first of that batch is the character
the first new above generated, and render shows the same sheet.
batch with no -o writes NDJSON to standard output, one record to the
line, which is the shape that pipes — and it streams, so the first record
reaches the pipe before the run finishes rather than after:
ctchargen batch --count 100 --auto --seed 145 | jq -r '[.upp, .service, .terms] | @tsv'
batch requires --auto, because it has nobody to ask.
A batch says what it did, on standard error so it cannot corrupt the JSONL:
$ ctchargen batch --count 100 --auto --seed 1000 --service scouts > roster.jsonl
100 written, 76 died
That is the rules working — the default serve strategy re-enlists until the
service throws him out, so the survival throw eventually gets most of them.
When what you want is twenty scouts for the starport rather than a hundred
outcomes, --survivors passes over the dead and goes on to the next seed:
$ ctchargen batch --count 20 --auto --seed 1000 --service scouts --survivors > roster.jsonl
20 written, 68 passed over for dying
It does not reroll anybody. Each character written is still exactly the
character his own seed makes, so new --seed <that> brings him back — the
seeds are simply no longer consecutive.
A session that stops before the end offers the way back in. A half-built
character is not a record, so it cannot be written out — but the seed and the
answers already given are enough to walk back to the question you stopped on,
and those are what a long session cannot retype:
$ ctchargen new --seed 7 --service other
...
Re-run the same command with --seed 7 --answers 1,2,1 to pick up at this question.
--answers replays those and then asks the next question as normal. It is the
same command deliberately: an answer is a number in the list a question
offered, so a re-run that drops --service or --name asks a different
sequence and spends the answers on it. A list longer than the run has
questions for is refused rather than half-applied, and --answers cannot be
given with --auto, which answers every question itself.
Without --service the tool picks one, and with --auto that pick is the
policy's rather than yours. The sheet says so on its headline, and so does the
record. Naming a service does not guarantee you get it either: a failed
enlistment throw sends the character to the draft, which may put him somewhere
else entirely — the sheet says that too.
Three flags steer --auto where the procedure offers a choice, on both
new and batch. docs/POLICY.md carries a row per choice
point saying what each one does:
| Flag |
Values |
--career |
serve (default) · retire · oneterm |
--skills |
advanced (default) · service · personal |
--muster |
cash (default) · goods · spartan |
Nothing is overwritten without --force. -o onto an existing file is
refused, and so is a batch any of whose members would replace one — the
whole batch, before a byte is written, so a refusal leaves the directory as it
was.
Death is an outcome and not an error: a character killed by a survival throw
(Book 1 p. 5) gets a complete record like anyone else, and no flag rerolls him.
The documents
| File |
What it holds |
| docs/PRD.md |
The delivered v1 contract — historical, kept for why the tree has this shape. |
| docs/ERRATA.md |
Every recorded reading, with its page cite and its stamping condition. |
| docs/POLICY.md |
The --auto decision table, one row per choice point. |
| docs/COVERAGE.md |
Every implemented rule mapped to its page cite, its implementation and its test. |
| docs/character.schema.json |
What the tool writes, with a minimal and a complete example beside it. |
| docs/PRERELEASE.md |
What each tag ships with open, and the review that preceded it. |
| CLAUDE.md |
Authority, source precedence, and the working rules for agents. |
The gate
task
go mod tidy -diff, go vet, golangci-lint, NilAway, go test -race, and a
coverage ratchet that holds each package's count of uncovered statements. CI
runs exactly this.
The toolchain is unpinned on purpose, and golangci-lint runs with
default: all, so a linter added upstream arrives switched on. Formatting is
gofumpt, run inside golangci-lint rather than beside it, so there is one
definition of formatted rather than two that can disagree.
Licence
MIT.