Deduction¶
A subject x property truth matrix with per-save reveal state and guess commitment — the data layer of the Kit's deepest-moat flagship (doc 06's market verdict), shipped here as a deliberately small seed. Doc 07 calls out the risk explicitly: this module's API shape gets pressure-tested by the Pinakes Gate-2 catalog verb once it exists, so the surface stays small on purpose — exactly what a "commit a deduction, get a confirm moment" interaction needs underneath, and nothing else.
What it is¶
- Headless core —
KnowledgeBook: a pure C# class (zeroUnityEngine/engine references) that owns truth-matrix, reveal, and guess-commitment semantics. Fully unit-testable without Unity; portable if the Kit is ever spun out standalone. - Content assets —
DeductionPropertyDefinition+DeductionMatrix: SO-authored properties (a closed set of allowed values each) and subjects (one true value per property), validated at edit time. - Service layer —
IKnowledgeService/KnowledgeService: thin glue that builds aKnowledgeBookfrom aDeductionMatrix, adds a save domain (DeductionSaveData), and forwards book changes as pooledDeductionPropertyRevealedEvent/DeductionGuessCommittedEvents on the engine'sIEventManager.
Quickstart¶
- Create one or more
DeductionPropertyDefinitionassets (Create → LoLGameplayKit → Deduction → Deduction Property Definition) — each is one deducible property (e.g. "author", "section") with a closed list of allowed value ids. - Create a
DeductionMatrixasset (Create → LoLGameplayKit → Deduction → Deduction Matrix), reference your property assets inProperties, and add one subject entry per subject (e.g. one per book) inSubjects, assigning exactly one allowed value per property as that subject's truth. Place the asset in aResourcesfolder under the exact nameDeductionMatrix. - Create a
GameplayKitConfigasset if you don't already have one, leaveenableDeductionon, and place it in aResourcesfolder under the exact nameGameplayKitConfig. - That's it — registration is automatic. The module ships
DeductionServiceRegistrar, anIServiceRegistrarthe engine's initializer discovers by assembly scan and constructs parameterlessly after all engine services are up. The registrar loads the config and matrix fromResources, checks the toggle, builds aKnowledgeService(which builds aKnowledgeBookfrom the matrix — malformed matrix content throws here, fail-loud, the same posture asCalendarServiceconstructing aCalendarModelfrom its config), callsInitialize()itself, and registersIKnowledgeService. - Resolve and use it from any game system:
var knowledge = ServiceLocator.Instance.Get<IKnowledgeService>();
// Reveal a clue the player just inspected:
knowledge.Reveal("book1", "author");
// Read it back — only returns a value once revealed, never throws:
string author = knowledge.GetTruth("book1", "author"); // "asha", or null if not yet revealed
// The catalog commit-click: submit a full set of guesses, get a per-property report back.
var guesses = new Dictionary<string, string> { ["author"] = "asha", ["section"] = "correspondence" };
GuessReport report = knowledge.CommitGuess("book1", guesses);
if (report.AllCorrect) { /* confirm moment */ }
- React to changes made anywhere in the game via the pooled events (see
Samples/Scripts/DeductionSample.csfor the full listener pattern):
public class MySystem : MonoBehaviour, IEventListener<DeductionGuessCommittedEvent>
{
void OnEnable() => this.EventStartListening<DeductionGuessCommittedEvent>();
void OnDisable() => this.EventStopListening<DeductionGuessCommittedEvent>();
public void OnGameEvent(DeductionGuessCommittedEvent evt)
{
if (evt.AllCorrect) { /* ... */ }
}
}
API overview¶
KnowledgeBook (headless core)¶
| Member | Notes |
|---|---|
Reveal(subjectId, propertyId) |
Idempotent; returns whether this call newly revealed the pair. Throws ArgumentException for an unknown subject/property id. |
IsRevealed(subjectId, propertyId) |
Never throws — an unknown id just reads as "not revealed". |
RevealedCount |
Total revealed (subject, property) pairs across every subject. |
GetTruth(subjectId, propertyId) |
Returns the true value id only if revealed; null otherwise (unrevealed, or unknown ids). Never throws. |
CommitGuess(subjectId, guesses) |
The catalog commit-click. Returns a GuessReport. Throws ArgumentException for an unknown subject id. |
TryGetCommittedGuess(subjectId, out guesses) |
Retrieves the subject's latest committed guess, if any. Never throws. |
CaptureSnapshot() / RestoreSnapshot(snapshot) |
The save-integration seam. |
event Action<string,string,string> PropertyRevealed |
subjectId, propertyId, valueId. Fires only on a real new reveal. |
event Action<string, GuessReport> GuessCommitted |
subjectId, report. Fires on every commit, even an empty one. |
IKnowledgeService mirrors this surface 1:1 (plus the inherited ILoLEngineService lifecycle),
so game code almost never touches KnowledgeBook directly — same relationship IWorldFlagService
has to WorldFlagStore.
Reveal / guess semantics table¶
| Call | Unknown subject | Unknown property | Already revealed | Result |
|---|---|---|---|---|
Reveal |
throws ArgumentException |
throws ArgumentException |
returns false, no event |
property-schema violations are fail-loud; re-revealing is a harmless idempotent no-op |
IsRevealed |
returns false |
returns false |
— | never throws — reads always degrade to "not revealed" |
GetTruth |
returns null |
returns null |
n/a (gated on reveal, not existence) | never throws — an unrevealed OR unknown pair both read as "you don't know this" |
CommitGuess |
throws ArgumentException |
guess entry silently ignored (not part of the report) | n/a | committing is a state-changing call against the subject schema, so an unknown subject is fail-loud; an unknown property key inside the guess dictionary is just noise the report doesn't care about |
A guessed value that isn't even one of a property's allowed values is not a separate error
state — it simply cannot equal the truth, so it reports GuessPropertyStatus.Incorrect like any
other wrong guess. CommitGuess never validates guessed values against a property's allowed-value
list; only KnowledgeBook's constructor validates that truth values are legal.
Key design decisions¶
GetTruthis gated on reveal, never throws. The alternative (throw for an unrevealed pair) would force every catalog-UI read through a try/catch or anIsRevealedprecondition check. Instead, "not revealed" and "doesn't exist" both collapse intonull— a catalog UI can query freely. UseIsRevealedif a caller ever needs to distinguish those cases (no current consumer does).Reveal/CommitGuessthrow on an unknown subject, fail-loud. UnlikeWorldFlagStore(where any string is a legal, freely-created key), Deduction subjects/properties come from authored content with a fixed schema — an unknown id there is a data or programmer bug, not a normal runtime state, so it surfaces immediately. This mirrorsCalendarModel.ScheduleAt's invalid-phase-index throw, notWorldFlagStore's never-throws posture.CommitGuessignores unknown property keys instead of throwing. A commit dictionary built from a UI form may legitimately contain keys the book doesn't track (e.g. a shared form struct reused across subject types); failing the whole commit over one stray key would be hostile to callers. The report only ever iterates the book's own property list.- Latest guess wins —
CommitGuessreplaces, never merges. Each call replaces the subject's previously committed guess wholesale. A catalog UI showing "my current answer" always reflects exactly what was last submitted, not an accumulation of partial edits across multiple commits. revealOnCorrectCommitdefaults tofalse. Reveal and guess are semantically distinct actions — inspecting/deducing a clue vs. committing an answer. Auto-revealing on a correct guess would let a player brute-force truths by mass-guessing without ever earning the reveal through actual clue-gathering, which is exactly the kind of hint-economy shortcut this seed's scope deliberately excludes (see "Explicitly deferred" below). The switch exists as a single boolean because Gate 2's real catalog interaction may want the opposite default — that's the pressure test doc 07 flags for this module.Clear-adjacent bulk operations fire no per-item events.RestoreSnapshotis a clear-then-load, not a sequence of transitions a listener should react to — same reasoning asWorldFlagStore.RestoreSnapshot/CalendarModel.RestoreSnapshot.- A schema-mismatched save entry is dropped, not thrown. If a
DeductionMatrixis edited after a save was written (a subject or property removed),RestoreSnapshotsilently drops the now-dangling revealed/guessed entries rather than crashing the load — the same graceful-degradation posture asCalendarModel.RestoreSnapshot's phase-index clamping.
Save behavior¶
DeductionSaveData : GameplayKitSaveData is the module's save domain (DataId =
"gameplaykit.deduction", SchemaVersion = 1). KnowledgeService:
- Gets/registers the domain via
ISaveSystem.GetData<DeductionSaveData>()inInitialize(). - Subscribes to the domain's inherited
BeforeSaveEvent/AfterLoadEvent— capturing aKnowledgeBook.CaptureSnapshot()into the domain before serialization, and callingKnowledgeBook.RestoreSnapshot()from the domain right after deserialization. - Also restores immediately in
Initialize(), in case the domain already held data.
Both DeductionSaveData.Revealed and DeductionSaveData.CommittedGuesses carry
[JsonProperty(ObjectCreationHandling = ObjectCreationHandling.Replace)] — the engine's real load
path is JsonConvert.PopulateObject into the live registered instance, and Newtonsoft's default
for a non-null List<T> is to append rather than replace, which would resurrect stale reveals/
guesses across save slots. Pinned by a regression test in DeductionTests. The nested
CommittedGuessRecord.Guesses list carries the same attribute for defense in depth, per the
Kit's blanket rule for every List<T> reachable from a save domain, even though it is not
strictly load-bearing there (each CommittedGuessRecord is always freshly constructed during a
load, never populated in place).
If ISaveSystem is not available, the service still works — reveal/guess state just doesn't
persist. A warning is logged once at Initialize().
Events contract¶
DeductionPropertyRevealedEvent/DeductionGuessCommittedEvent : GameEvent are dispatched via
IEventManager.TriggerEvent — pooled, per the Kit's Events contract. KnowledgeService also
exposes equivalent plain C# events (PropertyRevealed, GuessCommitted) for non-GameEvent
consumers (tests, editor tooling) — same shape as WorldFlagService.Changed.
If IEventManager is not available, the service still works — changes just don't raise the
pooled events (the C# events still fire either way). A warning is logged once at Initialize().
Pinakes catalog mapping¶
This module is sized to sit directly under the Pinakes catalog commit-click (Vertical Slice Plan, Phase 2 — "commit + confirm moment"):
| Deduction concept | Pinakes concept |
|---|---|
| Subject | A book |
| Property | A catalog field — author, subject, Section |
| Allowed value | One legal choice for that field (an author name, a Section id, ...) |
Reveal |
The player inspects a clue that pins down one field |
CommitGuess |
The catalog entry commit-click |
GuessReport.AllCorrect |
The confirm moment — every field matches |
DeductionSubjectEntry.ItemId |
Optional link to the book's ItemDefinition id, if Items is also enabled |
Explicitly deferred (T2, gated on a real second consumer)¶
Per doc 07's module table and the "framework-builder's disease" risk it names explicitly:
- Hint economies (costed reveals, clue budgets).
- Confidence tiers (partial certainty short of a full reveal).
- Cross-subject inference (deducing one subject's property from another's).
- Any UI — this is a data-layer seed; a catalog view is entirely the consumer's responsibility.
All of the above wait for Gate 2's real catalog interaction to demand them identically across more than one consumer, per the Kit's rule of two.
Seam ledger¶
Per the Kit's portability discipline (doc 07, "Portability discipline"), every module's docs page
lists exactly which engine surface it consumes. KnowledgeBook (the headless core) consumes
none — zero UnityEngine/LoLEngine.Core references. KnowledgeService (the service layer)
consumes:
| Engine surface | Namespace | Used for |
|---|---|---|
ILoLEngineService |
LoLEngine.Core.ServiceManagement.Interfaces |
Service lifecycle (Initialize/Shutdown). |
ServiceDependencyAttribute |
LoLEngine.Core.ServiceManagement.Dependencies |
Documentation-of-intent only: the engine's dependency sorter never processes externally registered services, so the attribute has no runtime effect here. |
IEventManager, GameEvent |
LoLEngine.Core.Events.Interfaces / LoLEngine.Core.Events.GameEvents |
Pooled DeductionPropertyRevealedEvent/DeductionGuessCommittedEvent dispatch. |
ISaveSystem, PersistableData (via GameplayKitSaveData) |
LoLEngine.Core.DataPersistence.Interfaces / .Data |
Save domain registration and the BeforeSaveEvent/AfterLoadEvent hooks. |
UnityEngine.Debug |
UnityEngine |
Degrade-gracefully warnings when an optional dependency is absent. |
The content assets (DeductionPropertyDefinition, DeductionMatrix) additionally consume
UnityEngine.ScriptableObject/SerializeField (expected for authoring assets, out of scope for
the portability discipline, which concerns runtime logic) and LoLEngine.Core.Localization.LocString
for player-facing display names — the one deliberate exception doc 07's portability rules carve
out, same as every other module's SOs.