|
Trait Generator 2.1.2
Personality traits from a weighted catalogue with conflicts, synergies and Big Five axes for C++23
|
This guide covers every feature of the trait-generator library. For a quick overview, see the README. For the full API reference, run doxygen Doxyfile from the repository root and open doc/api/html/index.html.
Copy dasmig/traitgen.hpp, dasmig/core.hpp and the dasmig/core/ directory into your include path, keeping them together. Supply resources/ next to the program, or define DASMIG_TRAIT_EMBED to compile the catalogue in.
instance() loads the lite tier on first use, from the compiled-in copy or from the first of resources/, ../resources, trait-generator/resources and ../trait-generator/resources that has it.
A directory must contain traits.tsv; axes.tsv, loadings.tsv and relations.tsv are read when present. Each load replaces the data and every layer loaded before.
These are the only generation signatures: (request) and (request, seed). The request defaults to count = 3, with empty include and exclude lists. Every call supplies its own choices; they never become defaults for later calls. Data loading and catalogue layers are configured on the instance.
count includes the forced traits. Fewer traits are returned when nothing else is compatible with those already chosen (a small catalogue with many conflicts).include traits come first, in the order given, and the draw treats them as already chosen: their conflicts are skipped and their synergies apply. They must exist and must not conflict with each other.exclude traits are never drawn; unknown names are ignored, so a request still works after a layer removes a trait.| Member | Meaning |
|---|---|
traits | Each with key, label, group (empty for none) and tags. Included traits first, then in the order drawn |
axes | Every axis, in catalogue order, with key, label, base (drawn) and value (base plus the chosen traits' loadings) |
has(key) | Whether a trait is present |
value(key) | An axis's resolved value, or empty for an unknown axis |
tags() | All the traits' tags, each once, in order of appearance (for example to feed nickname-generator) |
seed(), signature() | Replay; see below |
Each step draws from its own stream split from the call seed, so adding an axis or a step later leaves the others unchanged.
"axis:<key>". An axis with sd 0 takes its mean.exp(sum of loading × z), where z is each axis's standardised base value. With a loading of 0.9 on conscientiousness, organized is about 2.5 times likelier for someone one standard deviation above the mean, and 2.5 times less likely one below. The exponent is clamped to ±30."traits"). A trait is skipped when it is already chosen, excluded, in conflict with a chosen trait, or in the same group as one. Each chosen trait multiplies the weight of its synergy partners by the synergy's factor.Four tables in resources/lite (see Data and Method for how they were curated):
| File | Key | Columns |
|---|---|---|
traits.tsv | trait | label, weight (>= 0; 0 means never drawn, only included), group (optional; traits in a group exclude each other), tags (optional, ;-separated) |
axes.tsv | axis | label, mean, sd (>= 0) |
loadings.tsv | trait, axis | loading |
relations.tsv | trait, other | relation (conflict or synergy), factor (synergy only, > 0, default 2) |
Relations apply both ways, so a pair is defined once: a second row for the same two traits, in either order, is an error.
Loadings and relations that name a trait or axis that is not loaded are ignored, so removing a trait needs no other change. unresolved() lists them (for example ‘loadings.tsv:12: unknown axis 'opennes’`), to catch misspelt names.
The shipped catalogue is a base layer. Files with the same columns can be loaded on top of it to add traits, axes, loadings and relations, change them or remove them, without editing the shipped files.
dasmig::core::tsv_error with the file, line and column, and the data stays as it was.layers(table) lists what is loaded in a table: source, mode, rows added, replaced and removed, and warnings such as unknown columns.load(...) replaces the base and drops every layer.get_personality(request, seed) gives the same result for the same seed, request and data, on every platform, without advancing the instance engine.get_personality(request) draws a fresh seed from the instance engine, initially seeded from system entropy. There are no generator-level seed() or unseed() controls.dasmig::core::rng in the caller and pass its next value as the seed: gen.get_personality(request, rng()).p.seed() is the seed a result was generated from: get_personality(request, p.seed()) replays it with the same request and data. The signature does not store the request; keep it alongside the result when replay is needed.Replace get_personality() with get_personality(dasmig::trait_request{}) and get_personality(seed) with get_personality(dasmig::trait_request{}, seed). Replace instance seed() / unseed() calls with explicit per-call seeds (or a caller-owned dasmig::core::rng for sequences).
Every result records the seed and the data it came from:
to_json() gives the result as JSON, the form the WebAssembly layer and the game consume:
Seeds are strings because JavaScript numbers cannot hold every 64-bit value. to_json(writer) writes into an existing dasmig::core::json_writer, for composing larger documents.
Define DASMIG_TRAIT_EMBED before including the header to compile the catalogue into the program (about 11 KB). load(dataset::lite) and instance() then use it and need no files. The compiled-in data has the same data_hash() as the files, so seeds replay the same either way.
dasmig/traitgen_data.hpp is generated from resources/lite by make embed; CI checks it is up to date.
Each trtg instance is independent. Concurrent calls to get_personality(request) on the same instance require external synchronisation (e.g. a std::mutex). The (request, seed) overload is const and does not touch the engine. Data loading and extension must not run concurrently with generation.
Every seeded getter has batch forms: get_personality_batch return a std::vector, and for_each_personality streams the items to a sink in index order without holding the batch.
i is exactly get_personality(request, core::item_seed(batch_seed, first + i)). A range can be generated directly (first = 1000 starts at item 1000), two batches that follow each other equal one, and the output is the same for any threads (0 means the hardware count). Without a batch_seed one is drawn like the single getter draws its own.core::batch_options has count, first, threads and unique. The request describes one item; the options describe the batch..unique = core::unique_rule::by<personality>(key) regenerates an item whose key already appeared in the batch, with a deterministic retry seed, and throws std::runtime_error naming the item when the attempts run out.core::jsonl_writer writes one to_json() per line, core::csv_writer flattens each result's JSON into dotted columns. Any callable (std::uint64_t index, personality&) works.| Exception | When |
|---|---|
std::runtime_error("No trait data loaded. Call load() first.") | Any generation call before data is loaded |
std::invalid_argument("Unknown trait: x") | An included trait is not loaded |
std::invalid_argument | An included trait is repeated, excluded or in conflict with another included trait, or more traits are included than count |
std::invalid_argument | extend(path) with a file that is not one of the four tables |
dasmig::core::tsv_error | A malformed file or layer, or a relation defined twice (message gives file, line and column); the data is unchanged |