Lesson 52 of 170

Design choices without hidden global writes

Martinez AI Studios Academy

Trace a narrative choice from intent through authoritative confirmation to domain-owned consequences, while specifying safe pointer-lock dialogue entry, exit, and fallback behavior.

754. Lesson identity

Module
2.8 — Interactive narrative
Lesson
Design choices without hidden global writes
Academic type
Case Study
Order
Lesson 2 in the module
Estimated time
35–45 minutes, including practice

755. Scope note

Pointer-lock dialogue is a documented CONTRABAND theme. The quartermaster, supply-transfer, and route-clue branches used below are hypothetical instructional examples. They do not describe a documented CONTRABAND implementation, incident, or game rule.

756. Learning objective

After this lesson, you can specify one narrative branch as a traceable choice contract, separate attempted intent from confirmed outcome, assign every durable consequence to its responsible system, and document safe pointer-lock entry, exit, observation, and fallback behavior.

757. Why this matters

A dialogue choice often touches more than dialogue. It may request a resource transfer, create a relationship consequence, or advance an objective. That does not make the dialogue controller authoritative over inventory, relationships, or quests.

A hidden global write occurs when one handler silently changes state owned by several domains. Such a branch is difficult to validate because selection, authorization, persistence, and presentation are collapsed into one operation. It may also leave inconsistent state: inventory rejects a transfer while a relationship or quest system has already recorded success.

A traceable branch separates two stages:

  1. Intent: what the player asked to do.
  2. Confirmed outcome: what the authoritative system validated and committed.

Only the confirmed outcome should authorize downstream consequences that depend on success.

Dialogue also creates an interaction-mode boundary. Pointer lock is controlled through an asynchronous browser lifecycle and is not guaranteed merely because code called requestPointerLock(). Restoration may require transient user activation, and success or failure must be observed rather than assumed.

758. Prior knowledge

You should already be able to:

  • distinguish dialogue state from dialogue presentation;
  • identify the system responsible for a durable fact;
  • describe dialogue nodes and choices;
  • apply the Stage 2 distinction between transient presentation and persistent game state;
  • recognize pointer lock as an interaction mode rather than a narrative fact.

The previous lesson established that a dialogue panel should not become the authority for durable world state.

759. Core concept: intent is not confirmation

A useful choice contract identifies the choice, its request, its authority, and the possible confirmed results.

Contract element Question Hypothetical example
Choice identity What did the player select? offer_one_crate
Intent or request What operation is being requested? Transfer one crate from the player
Authoritative handler Which system validates and commits that operation? Economy or inventory system
Confirmed success What event proves the operation completed? SuppliesTransferCompleted
Confirmed rejection What event reports that it did not complete? SuppliesTransferRejected
Downstream consumers Which systems may react to confirmed success? Relationship and quest systems

An intent event is not evidence that the requested change happened. Relationship and quest systems must not independently interpret an unvalidated SuppliesTransferRequested event as success. They consume SuppliesTransferCompleted when their consequences require a completed transfer.

760. A safe event sequence

The hypothetical quartermaster branch can be specified as follows:

1. Dialogue records selection:
   choiceId = offer_one_crate

2. Dialogue emits an intent:
   SuppliesTransferRequested
   correlationId = transfer-42
   quantity = 1
   source = player
   recipient = quartermaster

3. Economy system handles the request:
   - validate that the request is permitted
   - validate that one crate is available
   - commit the inventory removal if valid

4a. On successful commit, economy emits:
    SuppliesTransferCompleted
    correlationId = transfer-42
    quantity = 1

4b. On rejection, economy emits:
    SuppliesTransferRejected
    correlationId = transfer-42
    reasonCode = insufficient_supplies or another defined reason

5. Relationship and quest systems:
   - ignore the request as proof of success
   - consume the completed event when relevant
   - do not apply success consequences after a rejected result

The names are examples, but the distinction is required: selection produces a request; the responsible system produces the confirmed result.

761. Correlation, ordering, and no-partial-write rules

A branch specification must state more than event names.

Correlation

Every request and result needs a shared correlation identifier. This lets consumers distinguish two similar selections and associate a completion or rejection with the correct dialogue branch.

Ordering

A success-dependent consequence must not be processed before the authoritative completion event. The required logical order is:

choice selected
→ transfer requested
→ economy validates and commits
→ completed or rejected result
→ success-dependent consumers react to completed result

Delivery order may vary in an asynchronous implementation, so consumers should enforce the dependency rather than relying only on arrival timing.

No partial write on rejection

If the economy system rejects the transfer:

  • inventory remains unchanged;
  • no relationship reward for a completed transfer is persisted;
  • no quest progress that requires a completed transfer is persisted;
  • the player receives a rejection presentation rather than a success presentation.

Duplicate delivery and retry

A consumer should treat the confirmed event idempotently using its event or correlation identifier. Receiving the same completion more than once must not apply the same relationship or quest consequence repeatedly.

Cross-domain failure after confirmation

The confirmed transfer proves the economy operation completed; it does not prove that every downstream consumer has already persisted its own consequence. Each consumer records its own processing status and can retry safely. If the design requires all domains to succeed or fail together, the specification needs an explicit coordinator and compensation policy rather than an assumption of one invisible global transaction.

762. Ownership map

Fact Responsible system Allowed trigger
Inventory crate removed Economy or inventory Validated transfer request
Transfer completed or rejected Economy or inventory Result of validation and commit attempt
Relationship consequence recorded Relationship system Confirmed transfer completion
Objective progress recorded Quest system Confirmed transfer completion, if the objective requires it
Dialogue feedback displayed Dialogue presentation Confirmed completion or rejection result
Pointer-lock state observed Interaction-mode controller Pointer-lock lifecycle events

The dialogue layer may display a pending state while validation runs. It must not announce completed success merely because the player selected the option.

763. Pointer lock is an asynchronous interaction contract

Dialogue entry and exit should be specified independently from narrative persistence.

Entry

When pointer-locked gameplay enters dialogue:

  1. Record the prior interaction mode.
  2. Release pointer lock.
  3. Observe the resulting pointer-lock lifecycle change.
  4. Give focus and input interpretation to the dialogue interface.
  5. Do not use pointer-lock state as evidence about narrative success or failure.

Exit

When dialogue closes:

  1. Record the exit cause, such as a player click, cancellation, or an asynchronous narrative result.
  2. Close the dialogue presentation and restore gameplay input only when gameplay is actually resuming.
  3. Determine whether the prior gameplay mode required pointer lock.
  4. Determine whether the target canvas remains eligible.
  5. Request pointer lock only from a path compatible with browser user-activation requirements.
  6. Observe pointerlockchange to confirm the resulting state.
  7. Handle pointerlockerror or an unchanged unlocked state as a failed request, not as success.

requestPointerLock() is a request, not a synchronous state assignment. A browser may require transient user activation. If dialogue closes only after asynchronous processing, the activation from the original click may no longer be available.

Safe fallback

If automatic restoration is unavailable or rejected:

  • keep the camera-look path disabled while the pointer remains unlocked;
  • present an explicit control such as Click to resume or Click to recapture pointer;
  • call requestPointerLock() from that new player action;
  • continue observing lifecycle events;
  • permit non-pointer-lock navigation or cancellation where the design supports it.

Do not repeatedly request pointer lock in a hidden loop, assume success, or leave camera look active while the pointer is free.

764. Combined hypothetical case

Dialogue entry:
  priorMode = pointer_locked_gameplay
  release pointer lock
  wait for pointerlockchange
  activate dialogue input

Choice selection:
  emit SuppliesTransferRequested(correlationId)
  display pending feedback

Economy result:
  if committed:
    emit SuppliesTransferCompleted(correlationId)
  else:
    emit SuppliesTransferRejected(correlationId, reasonCode)

Downstream response:
  relationship and quest consume only the completed result
  each consumer deduplicates by event or correlation ID
  rejection produces no success-dependent durable writes

Dialogue exit after asynchronous result:
  restore gameplay input mode
  do not assume the earlier click still provides user activation
  attempt pointer lock only if the current exit action qualifies
  confirm through pointerlockchange
  on pointerlockerror, show an explicit click-to-resume control

This design separates the narrative result from the interaction result. A rejected pointer-lock request does not reverse a confirmed supply transfer, and a rejected supply transfer does not determine whether the browser can capture the pointer.

765. AI-native workflow

Use AI to inspect a contract, not to invent its authorities or rules.

  1. Write the choice request, authoritative handler, confirmed outcomes, and consumers.
  2. Add a correlation identifier and the required ordering.
  3. State the rejection and duplicate-delivery rules.
  4. Write pointer-lock entry, exit, lifecycle-event, user-activation, and fallback conditions.
  5. Ask AI to produce an event trace and flag consumers that react to intent as though it were confirmation.
  6. Ask it to identify cross-domain writes, missing owners, missing rejection paths, and pointer-lock requests whose success is assumed.
  7. Review the result yourself before requesting implementation.

A useful prompt is:

“Review this narrative choice and pointer-lock interaction contract. Separate intent from confirmed outcome, trace correlation and ordering, identify every durable write and its responsible system, test rejection and duplicate-delivery behavior, and flag pointer-lock transitions that assume user activation or synchronous success. Do not invent game rules or modify files.”

766. Common mistakes

Fan-out from unvalidated intent

onChoiceSelected():
  emit SuppliesTransferRequested

relationship.onRequested(): rewardRelationship()
quest.onRequested(): advanceObjective()
economy.onRequested(): maybeRemoveCrate()

This allows relationship and quest success to persist even when economy rejects the transfer.

Hidden global mutation

onChoiceSelected():
  inventory.crates -= 1
  relationship.quartermaster += 10
  quest.suppliesObjective.complete = true
  saveGame()

This hides validation, authority, ordering, and failure behavior inside dialogue code.

Assuming pointer-lock restoration

closeDialogue():
  await finishDialogueWork()
  canvas.requestPointerLock()
  enableCameraLook()

After asynchronous work, transient user activation may be gone. The request can fail, and camera look must not be enabled on the assumption that capture succeeded.

767. Guided practice

Design a hypothetical branch in which the player can share a route clue with a contact or keep the clue private. Do not attribute this scenario to CONTRABAND and do not implement it.

Complete this specification:

Field Your specification
Choice IDs and displayed text
Intent emitted by each choice
Correlation identifier
System responsible for validating each intent
Confirmed success and rejection events
Required event order
Consumers of confirmed success
Durable fact owned by each consumer
No-partial-write rule on rejection
Duplicate-delivery rule
Player feedback while pending
Player feedback after confirmation or rejection
Prior pointer-lock mode
Dialogue exit cause
User-activation availability at exit
Lifecycle events to observe
Fallback after a rejected pointer-lock request

Then check:

  1. Can every success-dependent consumer point to a confirmed event rather than an intent?
  2. Does the rejection path leave all success-dependent domains unchanged?
  3. Can duplicate completion delivery occur without duplicate consequences?
  4. Can a reviewer trace every durable fact to one responsible system?
  5. Does pointer-lock restoration depend on observed lifecycle state rather than a function call alone?
  6. If user activation has expired, is there an explicit player-action fallback?
  7. Are narrative completion and pointer-lock restoration treated as independent outcomes?

Revise the contract if any answer is no.

768. Validation evidence

Your completed branch specification should include:

  • at least two choices;
  • an intent for each choice;
  • an authoritative validator for each operation that can fail;
  • confirmed success and rejection results;
  • shared correlation data;
  • explicit ordering and no-partial-write rules;
  • an idempotency or duplicate-delivery rule;
  • one responsible system for each durable fact;
  • pointer-lock entry and exit conditions;
  • the relevant lifecycle observations;
  • user-activation analysis;
  • an explicit fallback after restoration failure.

The branch is traceable when a reviewer can determine what was requested, what was confirmed, which system persisted each consequence, and how rejection avoids false success. The interaction transition is safe when gameplay does not assume pointer capture and the player has a clear recovery action.

769. Key takeaways

  • A selected choice expresses intent; it does not necessarily prove completion.
  • The authoritative system emits the confirmed success or rejection result.
  • Success-dependent systems consume confirmation, not unvalidated intent.
  • Correlation, ordering, rejection, and idempotency rules prevent inconsistent consequences.
  • requestPointerLock() is asynchronous and may require transient user activation.
  • Pointer-lock success must be observed through lifecycle state, with an explicit fallback when restoration fails.
  • Narrative persistence and interaction-mode restoration are separate outcomes.

770. Next lesson

Continue to 2.9 — Faction systems. The confirmed relationship consequence identified here becomes an input to a faction-owned relationship contract. Dialogue remains the source of the choice or confirmed outcome, while the faction relationship system defines and owns the durable relationship rule.

771. Knowledge check

Answer these items for yourself before reading the answers.

A player selects “Offer one crate.” What should the dialogue system communicate first when the economy system must validate the transfer?

  • A. A completed quest objective
  • B. A global save that assumes every consequence succeeded
  • C. A confirmed relationship reward
  • D. A transfer request or intent with correlation data
Show answer and feedback

Answer: A transfer request or intent with correlation data

Why: Selection expresses intent. The economy system must validate and commit the transfer before emitting a confirmed completion or rejection.

Which sequence prevents relationship and quest systems from recording success after an invalid transfer?

  • A. Dialogue emits a request; all systems interpret it independently; economy may reject later
  • B. Dialogue edits all domains, then asks economy whether the edit was valid
  • C. Dialogue emits a correlated request; economy validates and commits; downstream systems consume only the confirmed completion
  • D. Relationship updates first, quest updates second, and inventory is checked when dialogue closes
Show answer and feedback

Answer: Dialogue emits a correlated request; economy validates and commits; downstream systems consume only the confirmed completion

Why: Success-dependent consumers need the authoritative completion event. A request alone does not prove that the transfer was valid or committed.

The economy system rejects a transfer because the required item is unavailable. Which rules belong in the branch contract?

  • A. Inventory remains unchanged
  • B. Success-dependent relationship and quest writes do not occur
  • C. The dialogue presents a rejection result associated with the original correlation identifier
  • D. Quest progress is applied temporarily so the branch still feels responsive
Show answer and feedback

Answer: Inventory remains unchanged; Success-dependent relationship and quest writes do not occur; The dialogue presents a rejection result associated with the original correlation identifier

Why: A rejection must not leave success-dependent partial state. Correlation allows the rejection feedback to be matched to the original choice.

What proves that a pointer-lock restoration request succeeded?

  • A. The call to requestPointerLock() returned
  • B. The dialogue panel became hidden
  • C. The observed pointer-lock lifecycle state confirms that the intended element owns the lock
  • D. The narrative consequence was persisted
Show answer and feedback

Answer: The observed pointer-lock lifecycle state confirms that the intended element owns the lock

Why: requestPointerLock() is asynchronous. Code must observe the pointer-lock lifecycle and handle errors or a remaining unlocked state.

Gameplay was pointer-locked before dialogue. The player clicks a choice, but dialogue closes only after an asynchronous result. The original user activation has expired, requestPointerLock() is rejected, and the pointer remains unlocked. Which exit transition is safest?

  • A. Enable camera look immediately and retry pointer lock continuously until it works
  • B. Keep the dialogue open forever because narrative completion depends on pointer lock
  • C. Return to an unlocked safe mode, keep camera look disabled, show an explicit click-to-resume control, and request pointer lock from that new action while observing lifecycle events
  • D. Reverse the confirmed narrative consequence because pointer-lock restoration failed
Show answer and feedback

Answer: Return to an unlocked safe mode, keep camera look disabled, show an explicit click-to-resume control, and request pointer lock from that new action while observing lifecycle events

Why: The safe transition does not assume pointer capture, does not expose camera-look behavior while unlocked, and obtains a new user action for another request. Pointer-lock failure remains separate from the confirmed narrative outcome.

Support