A command-line character generator for Marc Miller's Classic Traveller (Book 1: Characters and Combat).
Given a fresh 18-year-old recruit, rtrav rolls a Universal Personality Profile (UPP), enlists or drafts the character into one of six careers, runs successive four-year terms (with survival checks, commissions, promotions, skill acquisition, and aging), then musters the character out with cash and equipment. The result is a fully detailed Traveller character sheet printed to stdout.
- Rolls a random UPP (2d6 per stat, displayed as a six-digit hex string)
- Assigns a random gender and a random gender-aware personal name
- Selects a career by user request, statistical best-fit, or draft
- Simulates career terms: survival, commission, promotion, reenlistment, and aging
- Awards skills from four career-specific tables (personal development, service, advanced education 1 & 2)
- Caps skills at INT + EDU per Book 1 rules
- Applies age-band stat penalties at ages 34, 50, and 66+, with an aging-crisis survival throw when a stat is reduced to 0 (Medical skill helps; failure is fatal)
- Musters out with alternating cash and benefit rolls
- Pays an annual retirement pension for 5+ terms of eligible service (Book 1)
- Grants career rank titles (Ensign → Admiral, Lt → General, etc.)
- Grants nobility titles for Social stat ≥ 11 (Knight/Dame through Duke/Duchess)
- Generates multiple characters in one run
- Lets you raise the minimum 1d6 result to skew luck
- Reproducible generation via a
--seed - Human-readable or JSON output (
--format), plus optional PDF export (one formatted sheet per page) - Structured logging (on stderr) via
RUST_LOGfor per-roll trace output
- Rust toolchain (stable, 2024 edition) — install via rustup
cargo build # debug build
cargo build --release # release build (opt-level 3, panic=abort)
cargo run -- [OPTIONS] # run character generator
cargo test # run all tests
cargo clippy # lintA set of tools for Marc Miller's Classic Traveller RPG
Usage: rtrav [OPTIONS]
Options:
--career <CAREER> Career selection [default: random]
-c, --count <COUNT> Number of characters to generate [default: 1]
-m, --min <MIN> Minimum die roll for random 1d6 [default: 1]
--species <NAME:STR,DEX,END,INT,EDU,SOC> Optional species definition
--pdf [<PATH>] Write characters to a PDF [default when given: rtrav.pdf]
--seed <SEED> Seed the RNG for reproducible generation
--format <FORMAT> Output format: text or json [default: text]
-h, --help Print help
-V, --version Print version
| Flag | Short | Default | Description |
|---|---|---|---|
--career <CAREER> |
— | random |
Career to attempt enlistment in |
--count <N> |
-c |
1 |
Number of characters to generate |
--min <1-6> |
-m |
1 |
Floor for all simulated 1d6 rolls |
--species <NAME:STR,DEX,END,INT,EDU,SOC> |
— | human (none) | Species name plus six UPP modifiers (STR,DEX,END,INT,EDU,SOC), added/subtracted from the UPP after it's rolled. The name is printed under the character's summary line. |
--pdf [<PATH>] |
— | off; rtrav.pdf when flag is bare |
Also write every generated character to a PDF, one formatted sheet per page. Give a path (--pdf sheets.pdf) or use the bare flag to write rtrav.pdf in the current directory. Omit the flag entirely for stdout-only output. |
--seed <N> |
— | OS-random | Seed the RNG with a u64 for reproducible output: the same seed (with the same other flags) regenerates the exact same character(s) on any machine. With --count, all N characters share the one seeded stream, so the whole run is reproducible. Omit for OS-random generation. |
--format <text|json> |
— | text |
stdout format. text prints human-readable character summaries; json prints a single JSON array of all generated characters (ideal for piping into other tools). Logs always go to stderr, so --format json yields clean, valid JSON on stdout. --pdf is independent and works with either. |
--help |
-h |
— | Print help and exit |
--version |
-V |
— | Print version and exit |
| Value | Notes |
|---|---|
Navy |
Enlistment favors INT ≥ 8 / EDU ≥ 9 |
Marines |
Enlistment favors STR ≥ 8 / END ≥ 8 |
Army |
Enlistment favors DEX ≥ 6 / END ≥ 5 |
Scouts |
Enlistment favors INT ≥ 6 / STR ≥ 8 |
Merchants |
Enlistment favors STR ≥ 7 / INT ≥ 6 |
other |
No enlistment check — always accepted |
random |
Picks statistically best-fit career, then attempts enlistment |
Failed enlistment falls back to the draft (1d6 → career).
# Generate three random characters
cargo run -- --count 3
# Attempt Navy enlistment
cargo run -- --career Navy
# Marines with lucky dice (minimum roll of 3)
cargo run -- --career Marines --min 3
# Watch every die roll
RUST_LOG=debug cargo run -- --career Scouts
# Non-human character: a Vargr with +1 STR, -1 DEX, +2 INT, -2 SOC
cargo run -- --species "Vargr:1,-1,0,2,0,-2"
# Export three characters to a PDF (one sheet per page)
cargo run -- --count 3 --pdf party.pdf
# Bare flag writes rtrav.pdf in the current directory
cargo run -- --pdf
# Reproducible run: the same seed regenerates the same character(s)
cargo run -- --seed 999 --career Scouts
# Emit JSON (logs go to stderr, so stdout stays valid JSON)
cargo run -- --count 5 --format json 2>/dev/null | jq .Set RUST_LOG to see per-roll detail. Logs are written to stderr, so they
never interfere with character output on stdout (notably --format json):
RUST_LOG=debug cargo run # shows each roll and check
RUST_LOG=trace cargo run # maximum verbosityflowchart TD
A([Start]) --> B[Roll UPP\n2d6 per stat]
B --> C[Assign Gender]
C --> D{--career flag?}
D -- named career --> E[Attempt Enlistment]
D -- random --> F[rank_career\npick best-fit]
F --> E
E -- success --> G[Serve Career]
E -- fail --> H[Draft\n1d6 → career]
H --> G
G --> I{Term Loop}
I --> J[Survival Roll\n2d6 vs threshold]
J -- fail --> K[Term ends early\nfailed_survival = true]
J -- pass --> L{Rank < 1?}
L -- yes --> M[Commission Roll]
L -- no --> N{Rank 1-5?}
M --> N
N -- yes --> O[Promotion Roll]
N -- no --> P[Skill Rolls]
O --> P
P --> Q[Aging Check\nif age crosses band]
Q --> Q1{Stat reduced to 0?}
Q1 -- yes --> Q2[Aging Crisis\n8+ or death, Medical DM]
Q2 -- died\nmark DECEASED --> S[Muster Out]
Q2 -- survived --> R{Reenlist?}
Q1 -- no --> R
R -- yes, or forced --> I
R -- no / retired --> S
K --> S
S --> T[Cash Rolls\nGambling +1]
S --> U[Benefit Rolls\nRank > 4: +1]
S --> R2[Retirement Pay\n5+ terms, eligible careers]
T --> V([Output Character\ntext or JSON])
U --> V
R2 --> V
V --> W{--pdf flag?}
W -- yes --> X([Write PDF\none page per character])
W -- no --> Y([Done])
graph LR
main["main.rs\nCLI parsing · throw_die\nentry loop"]
character["character.rs\nUPP · Character · Term\nGender · StatKind\nAgingEffects · Hits"]
career["career.rs\nCareer trait · TermParameters\nMusterOut trait\nBENEFITS · CASH tables"]
skill["skill.rs\nSkill enum\nthrow_for_skill\nPersonal Dev / Service /\nAdv Education tables"]
equipment["equipment.rs\nGearTypes enum\nGearUtils trait"]
pdf["pdf.rs\nwrite_characters_pdf\nformatted PDF sheets"]
main --> character
main --> career
main --> pdf
career --> character
career --> equipment
skill --> character
skill --> career
equipment --> character
pdf --> character
| Module | Responsibility |
|---|---|
main.rs |
Parses CLI args, owns the RNG, runs the outer character loop. All die rolls funnel through throw_die(rng, min) — the single place that knows how a 1d6 is actually thrown. |
character.rs |
Defines UPP (six stats), Character (full state aggregate), StatKind (typed stat delta), AgingEffects (age-band stat loss), and Hits (STR+DEX+END threshold). |
career.rs |
Career selection (Career trait on UPP), term simulation (generate_term_impl + per-career TermParameters), and mustering out (MusterOut trait). BENEFITS and CASH are [CareerType][throw-1] lookup tables — keep CareerType variant order in sync with them. |
skill.rs |
Four 6×6 skill tables (rows = career, columns = 1d6 result). throw_for_skill selects the table via a first 1d6 roll, then indexes it. Advanced Education 2 requires EDU > 7. |
equipment.rs |
GearTypes enum and GearUtils::add_equipment, which increments a count in Character.equipment. |
pdf.rs |
write_characters_pdf renders the generated characters to a formatted PDF (one page each) using printpdf with the built-in Helvetica font — no external font assets. Reuses Character::display_name() so the sheet heading matches the stdout printout. |
The term loop in generate_career pulls a fixed-size batch of die throws up front per term and passes them by index — match this pattern when extending term logic rather than reaching for the RNG mid-calculation.
classDiagram
class UPP {
+u32 strength
+u32 dexterity
+u32 endurance
+u32 intelligence
+u32 education
+u32 social
+random_upp(rng, min) UPP
+Display: 6-hex-digit string
}
class Character {
+String name
+UPP upp
+HashMap~Skill,u8~ skills
+Vec~Term~ terms
+i32 hits
+i32 cash
+i32 pension
+u32 age
+HashMap~GearTypes,u8~ equipment
+CareerType prior_service
+u32 rank
+bool retired
+bool dead
+Gender gender
+bool failed_survival
+printout()
+nobility_title() Option~String~
+describe_rank() Option~String~
+retirement_pay() i32
}
class Term {
+u32 term_num
+u32 length
+CareerType career
+u32 rank
+bool fail
+bool reenlist
+bool commissioned
+bool promoted
}
Character "1" *-- "1" UPP
Character "1" *-- "0..*" Term
Each of the six stats is rolled as 2d6 (range 2–12) and displayed as a hex digit (0–9, A–F). The string "A87556" means STR=10, DEX=8, END=7, INT=5, EDU=5, SOC=6.
| Stat | Abbr | Role |
|---|---|---|
| Strength | STR | Physical power; melee, labor |
| Dexterity | DEX | Coordination; ranged combat |
| Endurance | END | Stamina; physical checks |
| Intelligence | INT | Reasoning; sets skill cap (with EDU) |
| Education | EDU | Formal learning; sets skill cap (with INT) |
| Social | SOC | Standing; nobility at 11+ |
hits (STR + DEX + END) must remain ≥ 10 for the character to continue serving. age starts at 18 and increments by Term.length each term. name is a random gender-aware personal name. pension is the annual retirement pay in Credits (0 if not eligible; see Mustering Out). dead is set if the character dies in an aging crisis (see Aging).
length is 4 years for a completed term, 2 years if the survival roll fails. Terms are appended to Character.terms in order, giving a full service history.
Each career has a TermParameters struct controlling ten values that drive generate_term_impl:
| Field | Meaning |
|---|---|
survival_attribute |
Which UPP stat is checked for survival |
survival |
2d6 threshold to pass survival |
survival_threshold |
Stat value granting +2 to survival roll |
commission_attribute |
UPP stat checked for commission |
commission |
2d6 threshold for commission |
commission_threshold |
Stat value granting +1 to commission roll |
promotion_attribute |
UPP stat checked for promotion |
promotion |
2d6 threshold for promotion |
promotion_threshold |
Stat value granting +1 to promotion roll |
reenlist |
2d6 threshold to reenlist voluntarily |
| Administration | AirRaft | BladeCombat | Brawling |
| Bribery | Computer | Electronics | Engineering |
| Forgery | ForwardObserver | Gambling | GunCombat |
| Gunnery | JackOfAllTrades | Leader | Mechanical |
| Medical | Navigation | Pilot | ShipsBoat |
| Steward | Streetwise | Tactics | VaccSuit |
| Vehicle |
Skills are stored as HashMap<Skill, u8> — the value is the skill level (0-based, increments on repeat acquisition). Total skills are capped at INT + EDU.
Each term, one skill roll is made (Scouts get two). A first 1d6 selects the table; a second 1d6 selects the skill within it:
| 1d6 | Table selected |
|---|---|
| 1–2 | Personal Development — stat increases or combat skills |
| 3–4 | Service Skills — career-specific baseline skills |
| 5 | Advanced Education 1 — technical/tactical skills |
| 6 | Advanced Education 2 (EDU > 7 required) — high-end professional skills |
If EDU ≤ 7 and a 6 is rolled, Advanced Education 1 is used instead. See src/skill.rs for the full 6×6 tables per career.
| Gear | Notes |
|---|---|
LowPassage |
Steerage-class interstellar travel |
MidPassage |
Middle-class interstellar travel |
HighPassage |
First-class interstellar travel |
Blade |
Melee weapon |
Gun |
Ranged weapon |
TravellersAid |
Travellers' Aid Society membership |
ScoutShip |
Free access to a Scout/Courier vessel |
FreeTrader |
Shares in a Free Trader |
After the career ends, the character alternates between cash rolls and benefit rolls — one roll per term served, plus one bonus roll per five terms:
- Cash rolls: indexed into a per-career
CASH[career][throw-1]table (1d6). Having Gambling skill adds +1 to the roll. - Benefit rolls: indexed into a per-career
BENEFITS[career][throw-1]table (1d6). Rank > 4 adds +1 to the roll. Results are either a stat bonus (StatKinddelta) or equipment (GearTypes).
Characters who serve five or more terms in the Navy, Marines, Army, or Merchants draw an annual pension (Scouts and Other are not eligible), per Book 1:
| Terms served | Annual pension |
|---|---|
| 5 | Cr 4,000 |
| 6 | Cr 6,000 |
| 7 | Cr 8,000 |
| 8 | Cr 10,000 |
| 9+ | +Cr 2,000 per additional term |
The pension is stored on Character.pension and shown as a Pension line in the printout, PDF, and JSON output.
At the end of each term where the character crosses an age threshold, stat penalties are applied via AgingEffects:
| Age band | Stats affected | 2d6 throw to avoid −1 |
|---|---|---|
| 34, 38, 42, 46 | STR, DEX, END | 8, 7, 8 |
| 50, 54, 58, 62 | STR, DEX, END | 9, 8, 9 |
| 66+ (every 4 yrs) | STR, DEX, END | 9, 9, 9 (−2 each if threshold missed) |
All stat reductions saturate at 0. Aging crisis: if aging reduces a stat (STR, DEX, END, or INT) to 0, the character must make a survival throw of 8+ to avoid death — the character's Medical skill level is added as a DM. On success the stat is restored to 1; on failure the character dies (Character.dead is set and a DECEASED marker appears in the output). Separately, if hits (STR+DEX+END) drops below 10, the character is too frail to continue serving (a non-fatal stop).
| Rank | Army | Marines | Navy | Merchants |
|---|---|---|---|---|
| 1 | Lieutenant | Lieutenant | Ensign | 4th Officer |
| 2 | Captain | Captain | Lieutenant | 3rd Officer |
| 3 | Major | Force Commander | Lt Commander | 2nd Officer |
| 4 | Lt Colonel | Lt Colonel | Commander | 1st Officer |
| 5 | Colonel | Colonel | Captain | Captain |
| 6 | General | Brigadier | Admiral | Captain |
Scouts and Other careers do not have rank titles.
| SOC | Female | Male |
|---|---|---|
| 11 | Dame | Knight |
| 12 | Baroness | Baron |
| 13 | Marquesa | Marquis |
| 14 | Countess | Count |
| 15 | Duchess | Duke |
By default rtrav prints each character to stdout only. Pass --pdf to also write
a PDF, with one formatted character sheet per page:
cargo run -- --count 3 --pdf party.pdf # write three sheets to party.pdf
cargo run -- --pdf # bare flag writes rtrav.pdf in the CWDEach page contains:
- the character's name / rank line (bold heading, same text as the stdout printout), plus a
DECEASEDmarker if the character died in an aging crisis - species and gender
- the UPP hex string plus a labeled STR/DEX/END/INT/EDU/SOC breakdown
- age, term count, hits, and cash
- the annual pension, if the character earned one
- an alphabetized skill list (
Skill-level), word-wrapped to fit the page - an alphabetized equipment list (
Item xN)
The PDF is generated with printpdf using the
built-in Helvetica font, so no external font files are bundled or required. Running
without the flag leaves behavior unchanged (stdout only).
rtrav is free software distributed under the GNU General Public License v3. See COPYING.md for the full license text.
