Skip to content
Edit on GitHub

Author player-facing after-action science narratives and reading-level bounds

verifiedAgainst 3629ae9 · verifiedOn 2026-09-11.

In Voxamine, when a vessel reaction completes (e.g. roasting copper ore with charcoal, calcining quicklime, or pyrolyzing wood), the game does not show an arcane numerical error code, nor does it grant the player omniscient insight into unobserved hidden variables (MASTERPLAN.md §34.2).

Instead, the presentation layer derives an honest, readable after-action account answering the six core questions of §34.2:

  1. What changed? (AfterActionSection.WhatChanged)
  2. What evidence do we have? (AfterActionSection.WhatEvidence)
  3. What caused it? (AfterActionSection.WhatCaused)
  4. Why did it end? (AfterActionSection.WhyItEnded)
  5. Where did it go? (AfterActionSection.WhereItWent)
  6. How trustworthy is this? (AfterActionSection.HowTrustworthy)

Crucially, this narrative is strictly bounded by observational evidence:

  • If no thermometer was fitted to the vessel, the narrative never fabricates a temperature.
  • Causal language is bounded: the text says “caused by” only when a reaction was positively identified; otherwise it says “consistent with” or “could not tell”.
  • Every sentence preserves its causal provenance (AfterActionLine.SourceEventId).
  • Every sentence is held to an automated Flesch–Kincaid grade-level ceiling ($\le 10$) in ScienceReadingLevel.cs.
  • Every technical term is defined in plain language in ScienceGlossary.cs.
// Example: Narrating a completed vessel run with limited sensors:
List<AfterActionLine> lines = ScienceAfterActionNarrator.Narrate(account, access);
foreach (AfterActionLine line in lines)
{
// Evaluates: "[WhatCaused] The change was consistent with a reduction reaction."
// Preserves: line.Section, line.Evidence, line.SourceEventId
Assert.That(ScienceReadingLevel.FleschKincaidGrade(line.Text), Is.LessThanOrEqualTo(10d));
}

The narrative system sits between the raw deterministic chemistry solver and player presentation:

  • Chemistry Solver (ScienceVesselRun & ScienceEventLog) records committed simulation events (ScienceEvent) during vessel ticks.
  • After-Action Builder (ScienceAfterActionBuilder) converts event logs into factual claims (ScienceAfterActionClaim), filtering by the apparatus’s installed sensor channels (ScienceObservationAccess).
  • Narrator (ScienceAfterActionNarrator) renders claims into ordered, evidence-bounded sentences (AfterActionLine) across the six §34.2 sections.
  • Readability & Glossary (ScienceReadingLevel & ScienceGlossary) enforces sentence simplicity (grade $\le 10$) and registers first-use definitions.
  • HUD & Journal (VesselRunViewRuntime & JournalNudgeMath) presents latest-run cards in world space and weights journal notifications (Routine, Notable, Major).
flowchart TD
  subgraph SOLVER["Chemistry Solver"]
    RUN["ScienceVesselRun\n(Ticks & Phase Transitions)"]
    EVENTS["ScienceEventLog\n(Committed ScienceEvents)"]
  end

  subgraph CLAIMS["Factual Extraction"]
    ACCESS["ScienceObservationAccess\n(Contents, Temp, Pressure, Reaction Identity)"]
    BUILDER["ScienceAfterActionBuilder\n(Filters events by access)"]
    CLAIMS_BUF["ScienceAfterActionClaim Buffer"]
  end

  subgraph PROSE["Narrative Generation"]
    NARRATOR["ScienceAfterActionNarrator\n(6-section monotonic prose)"]
    READING["ScienceReadingLevel\n(Flesch-Kincaid Grade ≤ 10)"]
    GLOSSARY["ScienceGlossary\n(Plain-language technical definitions)"]
  end

  subgraph VIEW["Player Presentation"]
    CARD["VesselRunViewRuntime\n(In-world targeted vessel card)"]
    NUDGE["JournalNudgeMath\n(Routine / Notable / Major weight)"]
  end

  RUN --> EVENTS
  EVENTS --> BUILDER
  ACCESS --> BUILDER
  BUILDER --> CLAIMS_BUF
  CLAIMS_BUF --> NARRATOR
  NARRATOR --> READING
  NARRATOR --> GLOSSARY
  NARRATOR --> CARD
  NARRATOR --> NUDGE

Read Docs/MASTERPLAN.md §34.2 (“The journal’s player-facing text”) and Docs/HANDOFF.md §34.2, then inspect:

Keep the fundamental principles in mind:

Rule Meaning Violation Example
Simplify sentence, not chemistry Use clear, short sentence structures; never dumb down or hide actual chemical reactions. Bad: “The rock magically changed.”
Good: “The malachite reacted with carbon to produce copper.”
Evidence bounding A sentence can only claim what active sensors actually measured. Claiming “heated to 1150 K” when no thermometer was present.
Monotonic section ordering Lines always render in strict 1–6 section order. Displaying “How trustworthy” before “What changed”.
Reading level guardrail Prose must stay at or below Flesch–Kincaid grade 10. Convoluted academic phrasing with excessive nested clauses.

In ScienceAfterActionNarrator.cs, every generated sentence belongs to exactly one section:

public enum AfterActionSection : byte
{
WhatChanged = 1,
WhatEvidence = 2,
WhatCaused = 3,
WhyItEnded = 4,
WhereItWent = 5,
HowTrustworthy = 6,
}

Every line produced is encapsulated in an immutable struct carrying:

  • Section: which of the six questions it answers.
  • TemplateId: stable string identifier allowing future localization without rewriting logic.
  • Text: plain-English readable sentence.
  • Evidence: whether it represents a direct measurement, a visual observation, or an inference.
  • SourceEventId: committed simulation event ID backing this claim.

In ScienceAfterActionNarrator.cs, causal sentences adjust strictly based on ScienceObservationChannels:

// When the apparatus carries reaction-identification instrumentation:
if ((access.Channels & ScienceObservationChannels.ReactionIdentity) != 0)
{
destination.Add(new AfterActionLine(
AfterActionSection.WhatCaused,
"caused-by-identified-reaction",
$"The change was caused by {reactionName}.",
ScienceEvidenceKind.Measured,
sourceEventId));
}
// When only contents or bulk properties were observed:
else
{
destination.Add(new AfterActionLine(
AfterActionSection.WhatCaused,
"consistent-with-reaction",
$"The change was consistent with {reactionName}.",
ScienceEvidenceKind.Inferred,
sourceEventId));
}

If neither temperature nor pressure sensors exist on the vessel, no sensor values are displayed under WhatEvidence:

destination.Add(new AfterActionLine(
AfterActionSection.WhatEvidence,
"temperature-unobserved",
"Temperature was not measured.",
ScienceEvidenceKind.DirectObservation,
sourceEventId));

In ScienceReadingLevel.cs, measure sentence complexity:

$$\text{Grade} = 0.39 \left(\frac{\text{words}}{\text{sentences}}\right) + 11.8 \left(\frac{\text{syllables}}{\text{word}}\right) - 15.59$$

public static double FleschKincaidGrade(string text)
{
if (string.IsNullOrWhiteSpace(text)) return 0d;
int sentences = CountSentences(text);
int words = 0;
int syllables = 0;
foreach (string word in EnumerateWords(text))
{
words++;
syllables += CountSyllables(word);
}
if (words == 0) return 0d;
double wordsPerSentence = words / (double)Math.Max(1, sentences);
double syllablesPerWord = syllables / (double)words;
return (0.39d * wordsPerSentence) + (11.8d * syllablesPerWord) - 15.59d;
}

The heuristic syllable counter splits vowel clusters and accounts for silent trailing es. Any generated sentence scoring above TargetGradeCeiling = 10d fails the test suite.

In ScienceGlossary.cs, define every technical term in plain language for its first-use tooltip:

Definitions["stoichiometry"] =
"The fixed whole-number ratio in which substances react. " +
"Two parts hydrogen always join with one part oxygen to make water, never some other mix.";
Definitions["calcination"] =
"Heating a mineral hard enough to drive a gas out of it, leaving a different solid behind. " +
"Heating limestone this way makes quicklime and gives off carbon dioxide.";

Technical words are never hidden or deleted to lower a readability score; instead, they are taught clearly on first encounter.

Completed runs are classified by RunSignificance to drive player journal notifications:

  • Routine: Brief tick with negligible change (minor notification).
  • Notable: Significant mass change, gas vent, or phase transition (standard notification).
  • Major: Confirmed newly identified chemical reaction or catastrophic vessel failure (prominent notification).

Run the dedicated after-action test suite:

Terminal window
/opt/unity/Editor/Unity -batchmode -nographics \
-projectPath /home/soulwax/workspace/engines/unity/minecraft/Minecraft-HD \
-runTests -testPlatform EditMode \
-testFilter VoxelSandbox.Tests.ScienceAfterActionNarratorTests

Verify that all key assertions pass:

  • AllGeneratedProseMeetsTheReadingLevelCeiling: runs Flesch–Kincaid evaluation on every single sentence generated across all channel combinations, verifying score $\le 10.0$.
  • SectionOrderIsStrictlyMonotonicAndEverySentenceHasASection: ensures section numbers strictly ascend ($1 \to 6$) with no gaps or inversions.
  • SentenceCausalLanguageIsBoundedByAvailableChannels: confirms “caused by” never appears without ReactionIdentity channel.
  • UnobservedChannelsNeverLeakNumbersIntoProse: proves absent gauges never leak numeric values.
  • EveryNarratedSentencePreservesItsSourceEventId: checks causal traceability for every line.

To add a new narrative template (e.g. Electrochemical Cell Depletion or Supercritical Fluid Extraction):

  1. Add Template ID: Define a new constant string in ScienceAfterActionNarrator.
  2. Assign Section: Place the line in the appropriate AfterActionSection.
  3. Register Glossary Terms: If using new terminology (“overpotential”, “supercritical”), add plain definitions in ScienceGlossary.cs.
  4. Audit Reading Level: Check the generated prose with ScienceReadingLevel.FleschKincaidGrade to ensure it scores $\le 10$.
  5. Add Test Assertions: Add test cases to ScienceAfterActionNarratorTests.cs.
Symptom Cause Fix
Text says “caused by calcination” when no analyzer was attached. Failed to check ScienceObservationChannels.ReactionIdentity before selecting template. Restrict “caused by” to measured reaction identity; use “consistent with” for inferred reactions.
Narrative mentions exact degrees Kelvin when no thermometer was fitted. Read solver temperature directly instead of gating on ScienceObservationChannels.Temperature. Gate temperature readouts on installed sensor channels.
Test suite fails with reading level exceeded (> 10). Authored sentence was too long or used excessive polysyllabic non-glossary words. Break long sentences into two concise sentences.
Lines display out of order in the HUD. Appended lines to list without sorting by AfterActionSection. Always emit lines grouped by AfterActionSection in monotonic ascending order ($1 \to 6$).
Cannot trace which solver tick caused a vessel failure line. Passed 0UL instead of sourceEventId to AfterActionLine constructor. Always propagate event.EventId into AfterActionLine.SourceEventId.

User-contributed notes

Corrections, clarifications, and practical tips for this page. Anonymous is fine — a name is optional. Basic Markdown works: **bold**, *italic*, `code`, and links.

Notes policy

Notes are lightly filtered for spam and may be edited or removed. Keep them about this page — no support requests, no personal data, nothing you would not publish. Links are limited and marked nofollow.

  1. Loading notes…