Skip to content

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 (zero UnityEngine/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 a KnowledgeBook from a DeductionMatrix, adds a save domain (DeductionSaveData), and forwards book changes as pooled DeductionPropertyRevealedEvent/DeductionGuessCommittedEvents on the engine's IEventManager.

Quickstart

  1. Create one or more DeductionPropertyDefinition assets (Create → LoLGameplayKit → Deduction → Deduction Property Definition) — each is one deducible property (e.g. "author", "section") with a closed list of allowed value ids.
  2. Create a DeductionMatrix asset (Create → LoLGameplayKit → Deduction → Deduction Matrix), reference your property assets in Properties, and add one subject entry per subject (e.g. one per book) in Subjects, assigning exactly one allowed value per property as that subject's truth. Place the asset in a Resources folder under the exact name DeductionMatrix.
  3. Create a GameplayKitConfig asset if you don't already have one, leave enableDeduction on, and place it in a Resources folder under the exact name GameplayKitConfig.
  4. That's it — registration is automatic. The module ships DeductionServiceRegistrar, an IServiceRegistrar the engine's initializer discovers by assembly scan and constructs parameterlessly after all engine services are up. The registrar loads the config and matrix from Resources, checks the toggle, builds a KnowledgeService (which builds a KnowledgeBook from the matrix — malformed matrix content throws here, fail-loud, the same posture as CalendarService constructing a CalendarModel from its config), calls Initialize() itself, and registers IKnowledgeService.
  5. 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 */ }
  1. React to changes made anywhere in the game via the pooled events (see Samples/Scripts/DeductionSample.cs for 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

  • GetTruth is gated on reveal, never throws. The alternative (throw for an unrevealed pair) would force every catalog-UI read through a try/catch or an IsRevealed precondition check. Instead, "not revealed" and "doesn't exist" both collapse into null — a catalog UI can query freely. Use IsRevealed if a caller ever needs to distinguish those cases (no current consumer does).
  • Reveal/CommitGuess throw on an unknown subject, fail-loud. Unlike WorldFlagStore (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 mirrors CalendarModel.ScheduleAt's invalid-phase-index throw, not WorldFlagStore's never-throws posture.
  • CommitGuess ignores 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 — CommitGuess replaces, 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.
  • revealOnCorrectCommit defaults to false. 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. RestoreSnapshot is a clear-then-load, not a sequence of transitions a listener should react to — same reasoning as WorldFlagStore.RestoreSnapshot/CalendarModel.RestoreSnapshot.
  • A schema-mismatched save entry is dropped, not thrown. If a DeductionMatrix is edited after a save was written (a subject or property removed), RestoreSnapshot silently drops the now-dangling revealed/guessed entries rather than crashing the load — the same graceful-degradation posture as CalendarModel.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>() in Initialize().
  • Subscribes to the domain's inherited BeforeSaveEvent/AfterLoadEvent — capturing a KnowledgeBook.CaptureSnapshot() into the domain before serialization, and calling KnowledgeBook.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.