One Hundred-thousand Travellers
A follow-up to One Million Worlds. One million characters took too long to generate so I went down an order of magnitude. The median planet population has 100,000 people, after all.
Thank you to MTB and Ms. Quixotic for suggesting questions to ask the dataset.
The Generator
Generating Classic Traveller characters is a lot trickier than planets, and not just because there are more fiddly components. Whereas world generation is fully procedural, character creation involves moments of decision -- players can choose which service to attempt enlistment, can choose whether or not to re-enlist (except when a 12 is rolled), and can choose which of the Acquired Skills Tables to roll on.
I decided on the following:
- Enlistment service is fully randomized. Players will usually try to match a character to a service they're likely to succeed at (at least, I do), but I think it's fine to randomize it. Note that enlistment failure and the draft are simulated.
- Skills are completely random -- characters with EDU < 8 roll a d18 to pick a random entry from the first three tables, those with EDU of 8+ roll a d24. Real players might strategize (to get a particularly valuable skill like Pilot, or to use the personal development table to shore up low attributes), but I don't think it's that big of a difference.
- For the choice of whether or not to enlist, I wrote the character generation function with a toggle -- one setting assumes that players will always attempt to re-enlist until forced out (by failing a reenlistment roll) or dying. The other setting simulates player decision making by adding an extra roll with the same target as the survival roll -- if the roll fails and a character can drop out, they will. Thus, characters in risky services are more likely to quit while they are still alive. Because this is a bit arbitrary, I base most of my analysis on the "till failure or death" version.
So whereas the planets dataset is impartial and can be used to make inferences about any set of planets rolled with the CT ruleset, the following characters are generated with simulated player decision-making. We can draw conclusions about how the character creation rules behave, but can't perfectly generate to the set of all CT characters.
In terms of scope, I was mainly interested in how the numbers affect survival and capacity to achieve different ranks. However, I became interested in understanding how rare each skill is, so I ended up tallying those as well.
Also, I did not use Citizens of The Imperium -- the data-entry required for all the tables is pretty tedious and I didn't want to bother with it for this post, but I do hope to do a follow-up to see how they stack up.
Stats
As noted above, the following results are based on a "mindless" approach to character creation where players always seek to re-enlist. Thus, characters in the set have either failed a re-enlistment roll or a survival roll.
General
Of the 100,000 simulated characters, 48% washed out of their service before dying.
Roughly 1/4th of all characters were drafted into a random service. The rest made it into their first choice. The varying number of characters in each service is a result of enlistment difficulty -- characters that "want" to be in the Army or Other services usually make it, while those who try for Navy or Marines often get drafted into a random other service.
| Service | Count | Survival | STR | DEX | END | INT | EDU | SOC | AGE | Mean_skills |
|---|---|---|---|---|---|---|---|---|---|---|
| Army | 20419 | 0.59 | 6.97 | 7.10 | 7.13 | 6.91 | 7.12 | 6.99 | 24.29 | 3.98 |
| Marines | 11787 | 0.54 | 7.20 | 6.92 | 6.91 | 7.11 | 6.98 | 6.99 | 26.56 | 3.70 |
| Merchants | 17300 | 0.42 | 6.01 | 5.80 | 5.40 | 6.88 | 6.97 | 7.01 | 39.59 | 8.14 |
| Navy | 13431 | 0.76 | 6.55 | 6.86 | 6.71 | 7.29 | 7.44 | 7.15 | 29.26 | 4.59 |
| Other | 20185 | 0.52 | 6.35 | 6.60 | 6.44 | 6.93 | 6.96 | 6.80 | 31.35 | 4.18 |
| Scouts | 16878 | 0.05 | 6.86 | 6.83 | 6.71 | 7.11 | 7.08 | 7.00 | 27.13 | 2.93 |
Values in the Attribute columns are mean values for all characters.
Life Expectancy
Character age for living characters ranged from 22 to 82, with a median of 26 and a mean of 30.
Age at death ranged from 18 to 94, with a median of 24 and a mean of 30.

Interestingly, the Navy had the highest proportion of surviving characters. I think this is because survival odds are good and rely on INT, which also improves odds of getting into the Navy in the first place, and on top of that re-enlistment is 6+. So characters who make it into the Navy are well-suited to survive and tend to wash out before death.
Scouts, of course, have the lowest -- if you are playing a Scout and decide to push your luck every reenlistment chance, you only have a 6% chance of ending up with a viable character.
On the other hand, Merchants had by far the highest average age, thanks to forgiving survival and re-enlistment rolls. Army has the lowest, as the 7+ reenlistment roll makes one unlikely to last more than a few terms (though shockingly, there's one Army guy who washed out alive at age 66!).

The above plot depicts the percentage of characters that are still in play a given age. For example, around 60% of Merchants are still in their career at age 30. We can also see death due to aging kick in starting at around age 64 -- notice that the merchant survival rate falls off of its smooth curve and dips toward zero. (Anagathic drugs were not included in the simulation).

The effect of attributes on survival can be seen here. (Army: EDU, Marines: END, Merchants: INT, Navy: INT, Other: INT, Scouts: END). The size of the gap (I think) has to do with the math around how likely one is to qualify for the +2 DM and the math around the 2d6 survival roll. The gap is small for Scouts because the the roll is still hard even with good END, whereas high INT makes a big difference in the Navy.
The other funny thing to note is that the effects of aging are visible among the Merchants, hence the fact that the distributions for STR, DEX, and END are skewed lower. Truly the chopped uncs of the Traveller universe.
I don't know why mean STR for the Other service is so low, except to hazard a guess that because STR affects 3 other services, low STR characters are likelier to bounce out of other enlistments whereas they can easily make it into Other.
Advancement

This plot depicts the number of characters that reach each rank. A rank of 0 denotes characters that were drafted and never earned a Commission.
Because the Army has a low promotion target, characters that earn a Commission to rank 1 (Lieutenant) usually jump right on to rank 2 (Captain). The low Commission target for Merchants has lots of characters ending up at rank 1 (4th Officer) but the 10+ needed for a promotion balances out the longer careers.1
The Navy is by far the worst option for gaining ranks, with an overwhelming number of characters never gaining a Commission at all. The Marines are also quite bad, especially given their lower survival rate -- making it to ranks 5 or 6 (Colonel or Brigadier) in the Marines is vanishingly unlikely.
Skills

Characters that don't die get an average of 5.1 rolls on the skills table.
Merchants, by virtue of their longer careers, enter play with the most skills on average (8.2 rolls). There's at least one Merchant in the dataset who racked up 25 skill rolls before dying!
What about specific skills? (Here, I should reiterate that my simulated characters "choose" skills randomly. Real players would likely strategize table rolls to go after desired skills or attribute increases.)
The following table gives the mean value of each skill across all characters.
| Skill | Mean_Army | Mean_Marines | Mean_Merchants | Mean_Navy | Mean_Other | Mean_Scouts |
|---|---|---|---|---|---|---|
| ATV | 0.32 | 0.29 | 0.00 | 0.00 | 0.00 | 0.00 |
| Admin | 0.15 | 0.15 | 0.33 | 0.19 | 0.00 | 0.00 |
| Air/Raft | 0.16 | 0.00 | 0.00 | 0.00 | 0.00 | 0.22 |
| Blade Cbt | 0.31 | 0.59 | 0.33 | 0.18 | 0.34 | 0.00 |
| Brawling | 0.16 | 0.15 | 0.00 | 0.00 | 0.50 | 0.00 |
| Bribery | 0.00 | 0.00 | 0.34 | 0.00 | 0.17 | 0.00 |
| Computer | 0.16 | 0.14 | 0.33 | 0.18 | 0.17 | 0.10 |
| Electronic | 0.16 | 0.14 | 0.68 | 0.19 | 0.17 | 0.21 |
| Electronics | 0.00 | 0.00 | 0.00 | 0.00 | 0.17 | 0.00 |
| Engineering | 0.00 | 0.00 | 0.34 | 0.38 | 0.00 | 0.11 |
| Forgery | 0.00 | 0.00 | 0.00 | 0.00 | 0.50 | 0.00 |
| Fwd Obsv | 0.15 | 0.00 | 0.00 | 0.19 | 0.00 | 0.00 |
| Gambling | 0.15 | 0.15 | 0.00 | 0.00 | 0.33 | 0.00 |
| Gun Cbt | 0.47 | 0.44 | 0.33 | 0.19 | 0.17 | 0.11 |
| Gunnery | 0.00 | 0.00 | 0.34 | 0.38 | 0.00 | 0.11 |
| Jack-o-T | 0.00 | 0.00 | 0.34 | 0.19 | 0.00 | 0.32 |
| Jack-o-t | 0.00 | 0.00 | 0.00 | 0.00 | 0.17 | 0.00 |
| Leader | 0.16 | 0.15 | 0.00 | 0.00 | 0.00 | 0.00 |
| Mechanical | 0.16 | 0.15 | 0.34 | 0.19 | 0.17 | 0.21 |
| Medical | 0.15 | 0.14 | 0.67 | 0.18 | 0.17 | 0.22 |
| Navigation | 0.00 | 0.00 | 0.67 | 0.19 | 0.00 | 0.21 |
| Pilot | 0.00 | 0.00 | 0.34 | 0.18 | 0.00 | 0.11 |
| Ship's Boat | 0.00 | 0.00 | 0.00 | 0.19 | 0.00 | 0.00 |
| Steward | 0.00 | 0.00 | 0.34 | 0.00 | 0.00 | 0.00 |
| Streetwise | 0.00 | 0.00 | 0.34 | 0.00 | 0.34 | 0.00 |
| Tactics | 0.47 | 0.43 | 0.00 | 0.00 | 0.00 | 0.00 |
| Vacc Suit | 0.00 | 0.14 | 0.33 | 0.38 | 0.00 | 0.11 |
Observations:
- Marines are slightly better for Blade Combat, Army for Gun Combat
- Merchants come out strong with a wide range of useful skills (Electronics, Pilot, Medical) and more skills in general. They are the most likely to end up with the Pilot skill, so if you need someone to steer your free trader, roll a Merchant. Edit: I was reminded that Scouts get Pilot by default -- my generator did not take account of free skills, something I will fix for future analyses.
- Taking career duration into account, Scouts are the most likely to end up with Jack-of-all-Trades.
- Those in the Other service are very likely to end up with Brawling and Forgery.
Misc
8,200 characters (8.2%) have a SOC of at least 11, entitling them to the use of a hereditary title with their name.
Conclusions
On the whole, careers were shorter than I expected, but consistent with my experiences rolling them by hand.
Merchants come off pretty well -- you can get lots of skills, a good chance of useful skills, and even a shot at a Free Trader. Merchants are definitely the best class if you like to play Traveller character creation as a minigame, since you have a good chance of simulating a long career.
Marines seem a bit under-powered. The skill breakdown and average number of terms are similar to that of the Army, but it's a lot harder to enlist and gain ranks. I guess you could say that this is offset by Marines getting a shot at Traveller's Aid Society and slightly larger cash payouts, but in general Army seems better.
I will probably run some more analysis (I want to test other simulated stopping strategies), but this is getting a bit long and I am running out of steam, so I will save it for a future post.
Code
library(tidyverse)
library(knitr)
r2d6 <- function(dm = 0, max = NA, min = NA){
d1 = sample(1:6, 1)
d2 = sample(1:6, 1)
res <- d1+d2+dm
if (!is.na(max)){res <- min(res, max)}
if (!is.na(min)){res <- max(res, min)}
return(res)
}
prior_service.df <- data.frame(
Service = c("Navy", "Marines", "Army", "Scouts", "Merchants", "Other", "Castalian", "Church"),
Enlistment = c(8, 9, 5, 7, 7, 3, 10, 6),
DM_1 = c("INT 8", "INT 8", "DEX 6", "INT 6", "STR 7", NA, "INT 9", "INT 6"),
DM_2 = c("EDU 9", "STR 8", "END 5", "STR 8", "INT 6", NA, "EDU 9", "SOC 8"),
Survival = c(5, 6, 7, 7, 5, 5, 3, 6),
SDM_2 = c("INT 7", "END 8", "EDU 6", "END 9", "INT 7", "INT 9", "INT 99", "SOC 8"),
Commission = c(10, 9, 5, NA, 4, NA, 8, 10),
CDM_1 = c("SOC 9", "EDU 7", "END 7", NA, "INT 6", NA, "INT 10", "SOC 10"),
Promotion = c(8, 9, 6, NA, 10, NA, 8, 8),
PDM_1 = c("EDU 8", "SOC 8", "EDU 7", NA, "INT 9", NA, "EDU 10", "EDU 10"),
Reenlist = c(6, 6, 7, 3, 4, 5, 5, 3)
)
acq_skills.df <- data.frame(
Roll = 1:24,
Navy = c("STR", "DEX", "END", "SOC", "INT", "EDU", "Ship's Boat", "Vacc Suit", "Fwd Obsv", "Blade Cbt", "Gun Cbt", "Gunnery", "Vacc Suit", "Mechanical", "Electronic", "Engineering", "Gunnery", "Jack-o-T", "Medical", "Navigation", "Engineering", "Computer", "Pilot", "Admin"),
Marines = c("STR", "DEX", "END", "Gambling", "Brawling", "Blade Cbt", "ATV", "Vacc Suit", "Blade Cbt", "Blade Cbt", "Gun Cbt", "Gun Cbt", "ATV", "Mechanical", "Electronic", "Tactics", "Blade Cbt", "Gun Cbt", "Medical", "Tactics", "Tactics", "Computer", "Leader", "Admin"),
Army = c("STR", "DEX", "END", "Gambling", "Brawling", "EDU", "ATV", "Air/Raft", "Fwd Obsv", "Blade Cbt", "Gun Cbt", "Gun Cbt", "ATV", "Mechanical", "Electronic", "Tactics", "Blade Cbt", "Gun Cbt", "Medical", "Tactics", "Tactics", "Computer", "Leader", "Admin"),
Scouts = c("STR", "DEX", "END", "Gun Cbt", "INT", "EDU", "Air/Raft", "Vacc Suit", "Navigation", "Mechanical", "Electronic", "Jack-o-T", "Air/Raft", "Mechanical", "Electronic", "Jack-o-T", "Gunnery", "Medical", "Medical", "Navigation", "Engineering", "Computer", "Pilot", "Jack-o-T"),
Merchants = c("STR", "DEX", "END", "STR", "Blade Cbt", "Bribery", "Steward", "Vacc Suit", "STR", "Gun Cbt", "Electronic", "Jack-o-T", "Streetwise", "Mechanical", "Electronic", "Navigation", "Gunnery", "Medical", "Medical", "Navigation", "Engineering", "Computer", "Pilot", "Admin"),
Other = c("STR", "DEX", "END", "Blade Cbt", "Brawling", "-SOC", "Forgery", "Gambling", "Brawling", "Blade Cbt", "Gun Cbt", "Bribery", "Streetwise", "Mechanical", "Electronic", "Gambling", "Brawling", "Forgery", "Medical", "Forgery", "Electronics", "Computer", "Streetwise", "Jack-o-t"),
Castalian = c("INT", "INT", "EDU", "EDU", "SOC", "Forgery", "Lute", "Harpsichord", "Calligraphy", "Bookbinding", "Gardening", "Teaching", "Computer", "Mathematics", "Music Theory", "Philosophy", "EDU", "Pedagogy", "Mathematics", "Music Theory", "Astronomy", "History", "Philosophy", "Philology"),
Church = c("EDU", "INT", "SOC", "STR", "Gambling", "Bribery", "Theology", "Medical", "Streetwise", "EDU", "Jack-o-T", "Bribery", "Theology", "Admin", "Leader", "SOC", "Oration", "EDU", "Admin", "Leader", "Theology", "Casuistry", "Medical", "Computer")
)
parse_dm <- function(charsheet, service, col){
potential_dm <- str_split_1(col, "_")[2] |> as.numeric()
dm_str <- prior_service.df[prior_service.df$Service == service, col]
if (is.na(dm_str)){return(0)}
attr <- str_split_1(dm_str, " ")[1]
target <- str_split_1(dm_str, " ")[2] |> as.numeric()
char_attr <- charsheet[1, attr]
if (char_attr >= target){
return(potential_dm)
} else {
return(0)
}
}
generate_character <- function(x, simulated_decisions = F, enlistment_services = NULL){
set.seed(x)
# initialize character sheet
charsheet = data.frame(
AGE = 18,
Alive = T,
Terms = 0,
STR = r2d6(),
DEX = r2d6(),
END = r2d6(),
INT = r2d6(),
EDU = r2d6(),
SOC = r2d6(),
Service = NA,
Draft = F,
Rank = NA,
Skill_rolls = 1 # freebie extra for first term
)
# print(paste("Starting UPP:", paste0(charsheet$STR, charsheet$DEX, charsheet$END, charsheet$INT, charsheet$EDU, charsheet$SOC)))
# attempt to enlist in a service, take draft roll on failure
if (!is.null(enlistment_services)){
chosen_service <- sample(enlistment_services, 1)
} else {
chosen_service <- sample(prior_service.df$Service, 1)
}
# print(paste("Attempting to join", chosen_service))
enlistment_dm1 <- parse_dm(charsheet, chosen_service, "DM_1")
enlistment_dm2 <- parse_dm(charsheet, chosen_service, "DM_2")
enlistment_roll <- r2d6(dm = enlistment_dm1 + enlistment_dm2)
enlistment_target <- prior_service.df |> filter(Service == chosen_service) |> select(Enlistment) |> pull()
if (enlistment_roll >= enlistment_target) {
charsheet$Service <- chosen_service
charsheet$Rank <- 0
# print(paste("Enlisted successfully with", charsheet$Service))
} else { # draft
charsheet$Service <- sample(c("Navy", "Marines", "Army", "Scouts", "Merchants", "Other"), 1)
charsheet$Draft <- T
# print(paste("Drafted into", charsheet$Service))
}
mustered_out <- F
prior_skills <- 0
while (!mustered_out & charsheet$Alive){
# increment terms
charsheet$Terms <- charsheet$Terms + 1
if (is.na(charsheet$Rank) & charsheet$Terms > 1){charsheet$Rank <- 0} # if second term after being drafted, become eligible for commission.
# check for survival and increment number of terms
survival_target <- prior_service.df |> filter(Service == charsheet$Service) |> select(Survival) |> pull()
survival_dm <- parse_dm(charsheet, charsheet$Service, "SDM_2")
survival_roll <- r2d6(survival_dm)
if (survival_roll < survival_target){
# add random age, change status, and end loop
charsheet$AGE <- charsheet$AGE + sample(0:3, 1)
charsheet$Alive <- F
# print(paste("Died at", charsheet$AGE, "during term", charsheet$Terms))
break
}
charsheet$Skill_rolls <- charsheet$Skill_rolls + 1
# check for commission
commission_target <- prior_service.df |> filter(Service == charsheet$Service) |> select(Commission) |> pull()
if (!is.na(commission_target) & !is.na(charsheet$Rank) & (charsheet$Rank == 0)){ # can only be commissioned if not drafted this term.
commission_dm <- parse_dm(charsheet, charsheet$Service, "CDM_1")
commission_roll <- r2d6(commission_dm)
if (commission_roll >= commission_target){
charsheet$Rank <- 1
charsheet$Skill_rolls <- charsheet$Skill_rolls + 1
# print("Commission earned.")
}
}
# check for promotion if commission is earned
promotion_target <- prior_service.df |> filter(Service == charsheet$Service) |> select(Promotion) |> pull()
if (!is.na(promotion_target) & (charsheet$Rank %in% 1:5)){
promotion_dm <- parse_dm(charsheet, charsheet$Service, "PDM_1")
promotion_roll <- r2d6(promotion_dm)
if (promotion_roll >= promotion_target) {
charsheet$Rank <- charsheet$Rank + 1
charsheet$Skill_rolls <- charsheet$Skill_rolls + 1
# print(paste("Promoted to rank", charsheet$Rank))
}
}
# apply skills and attribute increases
new_rolls <- charsheet$Skill_rolls - prior_skills
# print(paste("Rolling", new_rolls, "new skill(s)."))
skill_die <- ifelse(charsheet$EDU >= 8, 1:24, 1:18)
new_skills <- sample(acq_skills.df[, charsheet$Service], new_rolls, replace = T)
# print(paste("New skills:", paste(new_skills, collapse = ", ")))
for (skill in new_skills){
if (skill %in% colnames(charsheet)){
charsheet[1, skill] <- charsheet[1, skill] + 1
} else if (skill == "-SOC"){
charsheet[1, "SOC"] <- charsheet[1, "SOC"] - 1
} else { # otherwise add skill at level 1
charsheet[1, skill] <- 1
}
}
prior_skills <- charsheet$Skill_rolls
# update age and resolve age
charsheet$AGE <- charsheet$AGE + 4
if (charsheet$AGE %in% 34:49){
if (r2d6() < 8){charsheet$STR <- charsheet$STR -1}
if (r2d6() < 7){charsheet$DEX <- charsheet$DEX -1}
if (r2d6() < 8){charsheet$END <- charsheet$END -1}
} else if (charsheet$AGE %in% 50:65){
if (r2d6() < 9){charsheet$STR <- charsheet$STR -1}
if (r2d6() < 8){charsheet$DEX <- charsheet$DEX -1}
if (r2d6() < 9){charsheet$END <- charsheet$END -1}
} else if (charsheet$AGE >= 66){
if (r2d6() < 9){charsheet$STR <- charsheet$STR -2}
if (r2d6() < 9){charsheet$DEX <- charsheet$DEX -2}
if (r2d6() < 9){charsheet$END <- charsheet$END -2}
if (r2d6() < 9){charsheet$INT <- charsheet$INT -1}
}
# and check for death from aging
if (charsheet$STR <= 0){
if(r2d6() >= 8){
charsheet$STR <- 1
} else {
# print("Died of age-related illness.")
charsheet$Alive <- F
break
}
}
if (charsheet$DEX <= 0){
if(r2d6() >= 8){
charsheet$DEX <- 1
} else {
# print("Died of age-related illness.")
charsheet$Alive <- F
break
}
}
if (charsheet$END <= 0){
if(r2d6() >= 8){
charsheet$END <- 1
} else {
# print("Died of age-related illness.")
charsheet$Alive <- F
break
}
}
if (charsheet$INT <= 0){
if(r2d6() >= 8){
charsheet$INT <- 1
} else {
# print("Died of age-related illness.")
charsheet$Alive <- F
break
}
}
# check for re-enlistment
re_enlist_roll <- r2d6()
reenlist_target <- prior_service.df |> filter(Service == charsheet$Service) |> select(Reenlist) |> pull()
reenlist_desire_target <- survival_target + (charsheet$AGE-34)/16 # every four terms, the desire to stay in drops
reenlist_desire_roll <- ifelse(simulated_decisions, r2d6(), 12)
if ((re_enlist_roll != 12) & ((re_enlist_roll < reenlist_target) | (reenlist_desire_roll < reenlist_desire_target))){
mustered_out <- T
}
}
return(charsheet)
}
generate_character(1)
character.df <- lapply(1:100000, generate_character, enlistment_services = c("Navy", "Marines", "Army", "Scouts", "Merchants", "Other")) |> bind_rows()
Mea culpa: when writing the code, I didn't realize that the Merchant Service only goes up to rank 5. The rank 6 bar should thus be included in the rank 5 total.↩