Chance Encounter System - UQcsse3200/2026-studio-3 GitHub Wiki

Overview

The Chance Encounter System handles non-combat EVENT rooms. Each encounter presents choices that can resolve to a health, currency, card, item, combined, or no-effect outcome; special encounters can advance through further choices or delegate to another flow.

Encounters are loaded from validated JSON configuration and selected using configured weights. The runtime integrates with persistent player state, the shared Card Library, the shared PlayerDeck, and the run-owned Item Inventory. Stable encounter and choice IDs allow display text to change without breaking configuration, resolution logic, or tests.

Documentation baseline: Team 2's Feature-2-Sprint4 at 70189e6d, checked on 17 October 2026. This includes Team 7's formal Curse Item dependency from PR #480. Sprint 4 extends the Sprint 2/3 system with Item acquisition, Shrine contracts, Mine risk, and acquisition-result data. Acquisition Display UI integration and complete player-visible acceptance remain pending.

Main Architecture and Components

Component Responsibility
ChanceEncounterConfigLoader Loads JSON configuration and creates encounter domain objects.
ChanceEncounterConfigValidator Validates encounter IDs, choices, outcomes, weights, optional card/item rewards, and Mine risk configuration.
ChanceEncounterFactory Provides the configured encounter pool.
ChanceEncounterSelector Selects an encounter according to its configured weight.
ChanceEncounter, ChanceChoice, ChanceOutcome Immutable domain model for encounters and outcomes. Runtime outcomes can contain ordered card and item rewards; a choice may carry optional ChanceCurseRisk.
ChanceEncounterBehaviourFactory Selects fixed ordinary, Abandoned Mine, Wishing Fountain, Dice, or Card Fusion choice behaviour for the selected encounter ID.
ChanceEncounterBehaviour, ChanceBehaviourResult Resolve a choice to an outcome, another choice stage, an invalid choice, or a delegated flow.
EncounterGameArea Creates production adapters, selects an encounter, starts its session, and supplies the selected Card Fusion flow to the encounter display, and exposes the acquisition-session adapter.
EncounterFlowController Coordinates Chance and Shop encounters and filters completion callbacks.
ChanceEncounterSession Controls choice resolution, staged choices, delegation, retry, cancellation, and completion.
ChanceOutcomeApplier Validates and applies health, currency, card, and item outcomes with rollback tracking.
AbandonedMineEncounterBehaviour, ChanceCurseRisk Resolve configured Mine risk and retain its complete rolled outcome for the behaviour's lifetime.
ItemRewardGateway, PlayerRunStateItemAdapter Validate Item IDs and track ownership additions, commit, and duplicate-safe rollback against Team 7's inventory.
ChanceResolution Records application status, resolved outcome, and health/currency before and after settlement.
ChanceAcquisitionResults Converts a successful resolution into immutable display data without applying rewards.
ChanceAcquisitionSession Exposes cached results and an optional encounter-scoped presentation lifecycle.
AcquisitionResult, AcquisitionPresentation, AcquisitionView Shared result contract, one-shot display coordination, and view boundary; not reward-granting APIs.
CardFusionEncounterFlow Bridges a delegated Event session to the existing Card Fusion domain service and completes the Event after success or Leave.
CardFusionService Owns Fusion validation, deck mutation, Rare reward selection, and the once-per-run rule; it is not reimplemented in the Event layer.
ChanceEncounterDisplay Displays normal choices/results, refreshes staged Dice choices, opens the Card Fusion selection view after delegation, and offers contextual rules Help in the Dice and Fusion intro scenes.
CardFusionSelectionView Shows eligible Common card instances, manages player selection, calls the existing Fusion flow, displays result or failure feedback, and offers delegated Leave and contextual rules Help.
ContextualHelpDialog, EventHelpContent Reuse a closable Scene2D rules dialog and source-checked text for Dice and Card Fusion without changing encounter state.
ComponentPlayerStateAdapter Connects outcomes to the player’s health and inventory components.
CardServiceCatalogAdapter Validates card IDs through the shared CardService.
PlayerDeckAdapter Adds rewards to the shared PlayerDeck and supports exact rollback.
RunState, PlayerRunState Preserve player state, owned item copies, and deck contents across game areas; RunState also tracks the Fusion allowance.
classDiagram
    class EncounterGameArea
    class ChanceEncounterFactory
    class ChanceEncounterConfigLoader
    class ChanceEncounterConfigValidator
    class ChanceEncounterSelector
    class ChanceEncounter
    class ChanceChoice
    class ChanceOutcome
    class ChanceEncounterBehaviourFactory
    class ChanceEncounterBehaviour {
        <<interface>>
    }
    class ChanceBehaviourResult
    class FixedChanceEncounterBehaviour
    class AbandonedMineEncounterBehaviour
    class WishingFountainEncounterBehaviour
    class DiceEncounterBehaviour
    class CardFusionEncounterBehaviour
    class CardFusionEncounterFlow
    class CardFusionService
    class ChanceEncounterDisplay
    class CardFusionSelectionView
    class EncounterFlowController
    class ChanceEncounterSession
    class ChanceOutcomeApplier
    class ChanceResolution
    class ChanceAcquisitionResults
    class ChanceAcquisitionSession
    class AcquisitionResult
    class ItemRewardGateway {
        <<interface>>
    }
    class PlayerRunStateItemAdapter

    class PlayerStateGateway {
        <<interface>>
    }
    class CardCatalogGateway {
        <<interface>>
    }
    class DeckGateway {
        <<interface>>
    }

    class ComponentPlayerStateAdapter
    class CardServiceCatalogAdapter
    class PlayerDeckAdapter
    class CombatStatsComponent
    class InventoryComponent
    class CardService {
        <<interface>>
    }
    class CardLibrary
    class PlayerDeck
    class RunState
    class PlayerRunState

    EncounterGameArea --> ChanceEncounterFactory
    EncounterGameArea --> ChanceEncounterSelector
    EncounterGameArea --> ChanceEncounterBehaviourFactory
    EncounterGameArea --> ChanceEncounterDisplay : session and Fusion flow
    EncounterGameArea --> EncounterFlowController
    EncounterGameArea --> CardFusionEncounterFlow : Fusion only
    EncounterGameArea --> RunState : active run
    EncounterGameArea --> PlayerRunState : shared state
    EncounterGameArea --> PlayerDeck : shared deck

    ChanceEncounterFactory --> ChanceEncounterConfigLoader
    ChanceEncounterConfigLoader --> ChanceEncounterConfigValidator
    ChanceEncounterConfigLoader --> CardCatalogGateway : semantic validation
    ChanceEncounterSelector --> ChanceEncounter

    ChanceEncounter "1" o-- "*" ChanceChoice
    ChanceChoice --> ChanceOutcome
    ChanceEncounterBehaviourFactory --> ChanceEncounterBehaviour
    FixedChanceEncounterBehaviour ..|> ChanceEncounterBehaviour
    AbandonedMineEncounterBehaviour ..|> ChanceEncounterBehaviour
    WishingFountainEncounterBehaviour ..|> ChanceEncounterBehaviour
    DiceEncounterBehaviour ..|> ChanceEncounterBehaviour
    CardFusionEncounterBehaviour ..|> ChanceEncounterBehaviour
    ChanceEncounterBehaviour --> ChanceBehaviourResult

    ChanceEncounterDisplay --> ChanceEncounterSession
    ChanceEncounterDisplay --> CardFusionSelectionView : opens after delegation
    CardFusionSelectionView --> CardFusionEncounterFlow : eligible cards, fuse, leave
    EncounterFlowController --> ChanceEncounterSession : creates
    EncounterFlowController --> ChanceOutcomeApplier
    ChanceEncounterSession --> ChanceEncounter
    ChanceEncounterSession --> ChanceEncounterBehaviour
    ChanceEncounterSession --> ChanceOutcomeApplier
    CardFusionEncounterFlow --> ChanceEncounterSession : completeDelegated
    CardFusionEncounterFlow --> CardFusionService : delegates Fusion
    CardFusionEncounterFlow --> RunState : active run
    CardFusionService --> RunState : deck and allowance

    ChanceEncounterSession --> ChanceResolution : retains successful settlement
    ChanceEncounterSession --> ChanceAcquisitionResults : cached mapping
    ChanceEncounterSession --> ChanceAcquisitionSession
    ChanceAcquisitionResults --> AcquisitionResult
    ChanceAcquisitionSession --> AcquisitionResult : presentation only
    ChanceOutcomeApplier --> ItemRewardGateway
    PlayerRunStateItemAdapter ..|> ItemRewardGateway
    PlayerRunStateItemAdapter --> PlayerRunState : owned items
    ChanceOutcomeApplier --> PlayerStateGateway
    ChanceOutcomeApplier --> CardCatalogGateway
    ChanceOutcomeApplier --> DeckGateway

    ComponentPlayerStateAdapter ..|> PlayerStateGateway
    ComponentPlayerStateAdapter --> CombatStatsComponent
    ComponentPlayerStateAdapter --> InventoryComponent

    CardServiceCatalogAdapter ..|> CardCatalogGateway
    CardServiceCatalogAdapter --> CardService
    CardLibrary ..|> CardService

    PlayerDeckAdapter ..|> DeckGateway
    PlayerDeckAdapter --> PlayerDeck

    RunState o-- PlayerRunState
    RunState o-- PlayerDeck
Loading

Source locations

Paths below are relative to the game repository:

Area Source
Configuration and encounter domain/behaviours source/core/assets/configs/chanceEncounters.json; source/core/src/main/com/csse3200/game/chance/ and its configs/ package
Session, application, gateways, and presentation adapter source/core/src/main/com/csse3200/game/encounters/integration/
Shared result contract and Chance mapper source/core/src/main/com/csse3200/game/encounters/results/
Production wiring and legacy UI source/core/src/main/com/csse3200/game/areas/EncounterGameArea.java; source/core/src/main/com/csse3200/game/components/chance/
Durable state and shared Item effects source/core/src/main/com/csse3200/game/maps/PlayerRunState.java; source/core/src/main/com/csse3200/game/rewards/
Unit and integration tests Corresponding packages under source/core/src/test/com/csse3200/game/

See also Map-to-Encounter Lifecycle, Non-Battle Encounter Framework, and Chance and Shop Encounter UI.

Encounter Configuration

The default encounter pool is defined in:

source/core/assets/configs/chanceEncounters.json

Each encounter contains:

  • a stable encounter id;
  • a player-facing description;
  • a positive integer weight;
  • one or more choices.

Each choice contains:

  • a stable choice id;
  • a player-facing description;
  • an outcome with required healthDelta and currencyDelta values;
  • an optional cardRewardId inside the outcome;
  • an optional itemRewardIds array inside the outcome;
  • for Mine exploration only, an optional choice-level curseRisk.

JSON retains the singular cardRewardId; runtime ChanceOutcome supports multiple cards through an ordered list. Fixed Item rewards use enum-style ID strings; repeated IDs represent multiple copies. For example, "itemRewardIds": ["BRAND_OF_THE_DAMNED"] with healthDelta: 0 and currencyDelta: 50 represents one Shrine contract.

{
  "id": "wandering-healer",
  "description": "...",
  "weight": 1,
  "choices": [
    {
      "id": "accept-bandage",
      "description": "...",
      "outcome": {
        "healthDelta": 0,
        "currencyDelta": 0,
        "cardRewardId": "bandage"
      }
    }
  ]
}

The zero-delta JSON outcomes for Wishing Fountain, Dice, and Card Fusion are catalogue placeholders. Their runtime behaviour, not those placeholder values, determines a wish result, a terminal Dice reward, or delegation to Fusion.

Configuration validation rejects:

  • missing or malformed configuration;
  • null encounters, choices, or outcomes;
  • blank IDs or descriptions;
  • duplicate encounter IDs;
  • duplicate choice IDs within an encounter;
  • empty choice lists;
  • missing, zero, or negative weights;
  • weights or outcome values that are not exact 32-bit integers;
  • blank card reward IDs;
  • a total encounter weight greater than Integer.MAX_VALUE.

loadEncounters() performs structural validation and validates the form of cardRewardId. Because it has no card catalog, it cannot reject an unknown but syntactically valid card ID.

loadEncountersWithCatalog(CardCatalogGateway) also verifies that every configured card reward exists. Production loading uses this catalog-aware path. Runtime-generated card rewards are validated through ChanceOutcomeApplier before addition to the deck.

Configured Mine risk

The production abandoned-mine/search-tunnels choice uses this shape:

{
  "id": "search-tunnels",
  "description": "Explore the Mine: gain 60 Gold; 50% chance of one random, lasting Curse.",
  "outcome": {"healthDelta": 0, "currencyDelta": 60},
  "curseRisk": {
    "triggerChance": 0.5,
    "items": [
      {"id": "BRAND_OF_THE_DAMNED", "weight": 1},
      {"id": "BLOOD_OATH_SIGIL", "weight": 1},
      {"id": "SHATTERED_AEGIS", "weight": 1},
      {"id": "ASHEN_EMBER", "weight": 1}
    ]
  }
}

The loader validates Item ID shape and list structure. Registered Item existence is checked at settlement by ItemRewardGateway, not by a loader-side copy of Team 7's enum. An unknown but syntactically valid Item ID cannot be acquired successfully.

Risk validation also requires a finite probability in [0, 1], a nonempty pool, unique valid Item IDs, positive exact-integer item weights, and a total pool weight no greater than Integer.MAX_VALUE. Risk is restricted to abandoned-mine/search-tunnels and cannot accompany fixed card/item rewards on that choice. Existing configurations without risk remain compatible.

Weighted Selection

ChanceEncounterSelector selects encounters in proportion to their configured positive weights.

It calculates the total pool weight, draws a value from that range using an injected Random, and finds the corresponding cumulative-weight interval. Injecting the random source allows deterministic boundary and seeded tests.

The seven configured Events have a total weight of 10. Wishing Fountain, Dice Game, and Card Fusion each have weight 2 and a 20% initial selection chance; each of the four ordinary Events has weight 1 and a 10% initial selection chance. Selection does not remove Card Fusion from the pool after its once-per-run allowance has been used; the Fusion service reports it unavailable if attempted again.

Current Encounter Pool

Encounter ID Weight Initial choice IDs and outcomes
wishing-fountain 2 make-wish: runtime Wishing Fountain outcome; leave: no effect
dice-game 2 low / high: begin a staged two-dice game; later stages offer take / double-down or cash-out / continue
card-fusion 2 fuse: delegate to the Fusion flow; leave: normal no-effect Event outcome before delegation
mysterious-shrine 1 make-offering: βˆ’10 HP, +25 Gold; leave: no effect; accept-brand: +50 Gold and one Brand; swear-blood-oath: +70 Gold and one Blood-Oath Sigil
wandering-healer 1 purchase-remedy: +20 health and βˆ’10 gold; accept-bandage: add bandage; decline: no effect
flooded-crossing 1 hire-ferryman: βˆ’8 gold; ford-river: βˆ’8 health
abandoned-mine 1 search-tunnels: +60 Gold, no immediate HP cost, 50% chance of one random Curse; leave: no effect

Mysterious Shrine β€” fixed contracts

The Shrine offers deterministic risk–reward choices, retaining its original offering and Leave alongside two contracts:

Choice ID Player-facing action Immediate HP delta Gold delta Item acquired
make-offering Make Offering βˆ’10 +25 None
leave Leave 0 0 None
accept-brand Accept the Brand 0 +50 BRAND_OF_THE_DAMNED Γ—1
swear-blood-oath Swear the Blood Oath 0 +70 BLOOD_OATH_SIGIL Γ—1

A contract is an alternative to the offering, not another execution of its HP-for-Gold trade. Choice descriptions identify the Gold, Item, and future battle penalty before selection. Four-choice configuration and settlement are implemented; the themed four-button layout still requires Guoqing's integration and manual verification.

Abandoned Mine β€” optional unknown risk

The Mine has exactly two configured choices: Explore the Mine (search-tunnels) and Leave (leave). Exploration awards 60 Gold and has a 50% chance to add one Curse. When triggered, the four listed Curses are equally likely: each has a 25% conditional probability, or 12.5% per exploration. Otherwise the player receives Gold alone.

There is no immediate HP cost. The former βˆ’12 HP/+30 Gold exploration and the intermediate fixed Mine contracts (claim-shattered-aegis, take-ashen-ember) are not current choices. Leave grants nothing and performs no risk roll.

Unlike the Shrine's known contract, the Mine asks the player to opt into an uncertain lasting penalty. The choice states the Gold and trigger probability; the encounter description names all four Curses and their per-copy effects. The 60 Gold, 0.5 probability, and equal weights are implemented initial balance parameters, not final balance acceptance. Event selection weights are unchanged.

Wishing Fountain

make-wish rolls once from 0–99: 0–49 (50%) gives +20 health, 50–79 (30%) gives one uniformly selected eligible card, and 80–99 (20%) gives no effect. Eligible rewards are the distinct nonblank IDs of registered base-card definitions returned by CardService.getAllCards(), sorted before selection; there is no rarity restriction. leave gives no effect without rolling. The health and card results use the existing outcome applier and persistent deck integration.

Dice Game

The player predicts low (total 2–6) or high (8–12) on two six-sided dice. A correct first-round prediction creates a 15-gold stake, an incorrect prediction ends with no reward, and a 7 opens take / double-down. Taking creates a 10-gold stake. Double-down can create a 30-gold stake or, after one final Lucky Seven tier, a 60-gold stake; incorrect predictions lose the stake. Every positive 10, 15, 30, or 60-gold path converges on cash-out / continue.

Cashing out awards the stake. Continuing risks it in round two, where successful paths preserve the stake and award one, two, or three cards according to the completed Lucky Seven tier; an incorrect prediction loses everything. The final tier treats another 7 as the jackpot and does not nest again. Dice rewards are applied only at terminal resolution. Dice card draws use the same registered-ID eligibility rule as Wishing Fountain and may produce duplicate card IDs.

Card Fusion

Card Fusion is selected by the same weighted EVENT selector as every other encounter. Its initial fuse choice delegates the session; the initial leave choice instead resolves a normal no-effect outcome, after which Continue completes the Event. These two exits must not be conflated with CardFusionEncounterFlow.leave() after delegation.

The delegated flow uses the active shared RunState and its persistent PlayerDeck. CardFusionEncounterFlow exposes eligible owned Common CardInstances and passes three selected, distinct instance IDs to CardFusionService. Different instances of the same card name are allowed. The Fusion service owns validation, removal of three Common instances, uniform random Rare reward selection, and the once-per-run allowance. The Event layer does not duplicate those rules.

Successful Fusion completes the delegated Event and returns through the normal encounter callback. Invalid selections, missing deck instances, or non-Common cards leave the deck and allowance unchanged and keep the delegated Event open for retry or Leave. If Fusion was already used or no Rare reward is available, Fusion cannot succeed but the player can still leave. CardFusionEncounterFlow.leave() completes the delegated Event without changing the deck or consuming the allowance.

Contextual Help

The Dice scene has a persistent ? button outside the staged choice table, so refreshing Low/High, Lucky Seven, and stake choices leaves Help available. Card Fusion provides the same contextual rules dialog on both its introductory Event scene and delegated card-selection scene. The dialog is modal and closable; opening or closing it does not submit an Event choice, alter the Dice stage, clear selected Fusion cards, change the deck or run allowance, or complete the encounter. Ordinary Events do not receive this contextual Help. The player-facing text explains the existing rules; it does not implement Dice or Fusion gameplay.

See Contextual Event Help and Tutorial Content for the tutorial and contextual-help documentation.

Runtime Flow

EncounterGameArea loads the catalog-validated pool, calls ChanceEncounterSelector.select(), creates the selected ChanceEncounterBehaviour, and asks EncounterFlowController.startChance(nodeId, encounter, behaviour) to start a session. Fixed trades use FixedChanceEncounterBehaviour; configured Mine exploration uses AbandonedMineEncounterBehaviour; Wishing Fountain and Dice use their existing runtime behaviours; Card Fusion uses a behaviour that returns a delegated result for fuse.

For an ordinary or terminal runtime outcome, the display submits a choice to the session, which applies the outcome. A successful result locks the choice buttons and reveals Continue. The map completion callback is sent only after Continue is selected. Dice instead returns AWAITING_CHOICE at intermediate stages; the session exposes the current choices, prompt, stage feedback, and latest dice roll, and ChanceEncounterDisplay refreshes the choices.

sequenceDiagram
    actor Player
    participant Area as EncounterGameArea
    participant Factory as ChanceEncounterFactory
    participant Catalog as CardCatalogGateway
    participant Selector as ChanceEncounterSelector
    participant Behaviours as ChanceEncounterBehaviourFactory
    participant Flow as EncounterFlowController
    participant UI as ChanceEncounterDisplay
    participant Session as ChanceEncounterSession
    participant Applier as ChanceOutcomeApplier
    participant Items as ItemRewardGateway
    participant Deck as DeckGateway
    participant State as PlayerStateGateway
    participant Callback as EncounterCallback

    Area->>Factory: createInitialEncounters(catalog)
    Factory->>Catalog: validate configured card rewards
    Factory-->>Area: encounter pool
    Area->>Selector: select()
    Selector-->>Area: weighted encounter
    Area->>Behaviours: create(encounter, random, cardService)
    Behaviours-->>Area: selected behaviour
    Area->>Flow: startChance(nodeId, encounter, behaviour)
    Flow-->>Area: session
    Area->>UI: display session

    Player->>UI: select choice
    UI->>Session: resolveChoice(choiceId)
    alt Intermediate Dice stage
        Session-->>UI: AWAITING_CHOICE
        UI->>Session: getAvailableChoices(), getChoicePrompt(), getLastDiceRoll()
        UI-->>Player: show next stage
    else Normal terminal outcome
        Session->>Applier: apply(outcome)
        loop Validate every card reward, if any
            Applier->>Catalog: validate card ID
        end
        opt Item rewards present
            Applier->>Items: validate all item IDs before mutation
        end
        loop Add each validated card reward
            Applier->>Deck: addCard(cardId)
            Note over Deck: Additions remain pending
        end
        loop Add each validated item reward
            Applier->>Items: addItem(itemId)
            Note over Items: Ownership only, pending rollback tracking
        end
        opt Currency change
            Applier->>State: setCurrency(target)
        end
        opt Health change
            Applier->>State: applyDirectHealthChange(delta)
            Note over State: Final irreversible gameplay mutation
        end
        alt Outcome applied
            Applier->>Deck: commit pending card additions, if any
            Applier->>Items: commit pending item additions, if any
            Applier-->>Session: APPLIED
            Session-->>UI: successful resolution
            Player->>UI: Continue
            UI->>Session: complete()
            Session->>Flow: completion
            Flow->>Callback: onEncounterComplete(nodeId, true)
        else Failure before health changed
            Applier->>Items: roll back pending items in reverse order
            Applier->>Deck: roll back exact added cards in reverse order
            Applier->>State: restore currency if required
            Applier-->>Session: failure, or ROLLBACK_FAILED if restoration fails
        else Health changed before failure
            Note over Applier,State: Do not assume an irreversible health event can be rolled back
            Applier-->>Session: ROLLBACK_FAILED
        end
    end
Loading

For Card Fusion, EncounterGameArea creates CardFusionEncounterFlow for the selected card-fusion session and passes that same flow to ChanceEncounterDisplay. The area also exposes it through EncounterGameArea.getCardFusionEncounterFlow() for the hosting screen. The introductory fuse choice makes the session report DELEGATED; the display then opens CardFusionSelectionView. That view calls getEligibleCards(), collects three distinct CardInstance.instanceId() values, and calls fuse(selectedInstanceIds) or leave() on the flow. It presents the returned success or failure feedback, while CardFusionService remains responsible for validating the selection, changing the persistent deck, and recording the once-per-run allowance. Successful Fusion or delegated Leave calls ChanceEncounterSession.completeDelegated() and returns through the normal encounter lifecycle. Initial leave remains a separate no-effect choice completed through Continue.

Mine random resolution and retry safety

ChanceEncounterBehaviourFactory creates a fresh AbandonedMineEncounterBehaviour for each selected, risk-configured Mine. On the first valid exploration, it tests random.nextDouble() < triggerChance. Only when this succeeds does it draw nextInt(totalItemWeight) and select a cumulative-weight interval.

The behaviour caches the complete ChanceOutcome before application: Gold alone, or Gold plus the selected Item. A failed transaction retry therefore reuses the same outcome, including a previously rolled no-Curse result. Leave does not draw. A new independent session receives a fresh behaviour and roll.

ChanceEncounterSession guards re-entrant resolution before calling the behaviour, and rejects further settlement after success. Presentation errors, repeated clicks, or repeated acquisition-data queries cannot invoke a second successful award. A fully rolled-back attempt can retry the cached outcome; an Item outcome with ROLLBACK_FAILED is blocked from further settlement rather than risking a second copy.

Persistence boundary: this is an in-memory, live-session guarantee. Save/Load does not serialize the unfinished behaviour, cached random outcome, or acquisition-result ID. Current save restoration returns to Map, not the middle of an Event. Recreating the session is not a continuation of the same cached draw; this documentation does not guarantee identical results across session reconstruction or loading.

Outcome Application and Transaction Safety

ChanceOutcomeApplier snapshots health and currency and validates the whole outcome before mutation:

  • exact currency arithmetic and sufficient Gold for a cost;
  • target health clamped between zero and maximum health;
  • every card reward ID and whether the deck can accept it;
  • every Item ID and availability of a compatible ItemRewardGateway.

The mutation order is:

  1. Add ordered card rewards as pending deck transactions.
  2. Add ordered item rewards as pending ownership transactions.
  3. Apply and verify the currency change.
  4. Apply direct health as the final irreversible gameplay mutation.
  5. Commit pending card and item rollback metadata.

Commit clears tracking; it does not grant rewards again or execute Item effects. Duplicate card/Item IDs represent separate copies. Pending additions are undone in reverse order when rollback is safe.

Direct health uses PlayerStateGateway.applyDirectHealthChange. Its production implementation delegates to CombatStatsComponent, bypassing Armour and Block for direct damage while preserving health clamping and normal death-event behaviour.

Failure handling

Failure Result
Validation fails Return a failure before changing any reward or resource. An unknown Item cannot leave Gold awarded.
Card or Item addition fails, including a partially completed addition Stop before resource changes; attempt rollback of tracked additions.
Currency update fails Attempt rollback of pending Items/cards and restore the original currency.
Health rejects or throws while health remains unchanged Attempt the same rollback of additions and currency.
Health changes before an exception or verification failure Treat health/death processing as potentially irreversible, retain preceding changes, and return ROLLBACK_FAILED.
Rollback or transaction-metadata finalisation cannot safely establish the required state Return failure, including ROLLBACK_FAILED where full restoration is not possible; never report a successful acquisition.

A fully restored failure remains unresolved and may retry without accumulating copies. A partial failure must not be assumed safe to retry: the session explicitly blocks subsequent settlement after an Item-bearing ROLLBACK_FAILED. This is not a general promise that every non-Item partial failure is automatically blocked or reversible.

Fusion continues to use the separate CardFusionService/CardFusionEncounterFlow transaction path, not a fabricated Chance outcome.

Player, Card, Deck, and Item Integration

Persistent player state

RunState owns the shared PlayerRunState and PlayerDeck. EncounterScreen supplies the active RunState, shared player state, and persistent deck to EncounterGameArea. The player factory restores state on each newly created player; the area does not call PlayerRunState.applyTo(player) a second time. It captures resulting health and currency when disposed. Owned Items already belong to the authoritative run state.

This allows Chance outcomes to persist across later rooms. Card Fusion uses the same active RunState to access that deck and its once-per-run Fusion flag.

Card catalog

CardServiceCatalogAdapter implements CardCatalogGateway using CardService.getCard. Production wiring supplies the shared Team 6 CardLibrary, which implements CardService.

This allows configuration loading and outcome application to reject unknown card reward IDs. Wishing Fountain and Dice build their random reward pools from registered CardService.getAllCards() definitions; Fusion's Rare selection remains owned by CardFusionService.

Shared deck

When a shared PlayerDeck is supplied, EncounterGameArea wraps it with PlayerDeckAdapter. Successful normal Event card rewards are added to that deck and persist through RunState. Fusion instead uses CardFusionService on the same run-scoped deck.

A fallback InventoryDeckAdapter remains available for construction paths without a shared deck. Production map encounters supply the shared deck and RunState.

Duplicate cards and exact rollback

Duplicate card IDs are permitted by the current deck model.

PlayerDeckAdapter records the instance ID and insertion index of each newly added CardInstance. Rollback removes only those exact instances, leaving pre-existing cards with the same card ID unchanged. Fusion selects distinct card instances, not necessarily distinct card IDs.

For a single card reward, the legacy Chance display produces text such as:

You receive the Bandage card.

Item acquisition versus battle effects

Production Event wiring supplies PlayerRunStateItemAdapter backed by runState.getOrCreatePlayerState(). It validates IDs with ItemType.valueOf, records the inventory before each attempted addition, and calls PlayerRunState.addOwnedItem(ItemType). Success is checked against the expected owned-item list.

Rollback calls removeOwnedItem(ItemType) for one copy, verifies the inventory, and restores the original acquisition order when needed and safe. It does not remove pre-existing copies indiscriminately or overwrite an inventory changed unexpectedly by another writer. Commit only clears pending-addition tracking.

Duplicate Curses are allowed: each acquisition adds a copy and the shared Item system counts them. There is no Event-level once-per-run or per-type cap. Later eligible EVENT rooms can select the same encounter again, so repeated visits can accumulate Gold and stacked Curses.

Event Item Acquisition and Curse Battle Effect Application are separate responsibilities. Chance settlement does not call ItemEffectApplier.applyItemEffect(), replay PlayerRunState.applyTo() on the living Event entity, or manually adjust combat penalties. Team 7 reconstructs derived Item effects for a newly created player and handles battle-time triggers.

Item ID Name Shared effect per owned copy
BRAND_OF_THE_DAMNED Brand of the Damned In future battles, each Strength gain is reduced by 1, with a minimum gain of 0. Fully cancelled gains do not clear existing Strength.
BLOOD_OATH_SIGIL Blood-Oath Sigil At the start of each player turn in battle, lose 2 HP, bypassing Block/Armour and retaining at least 1 HP.
SHATTERED_AEGIS Shattered Aegis In future battles, each Block gain is reduced by 1, with a minimum gain of 0.
ASHEN_EMBER Ashen Ember Maximum Energy is reduced by 1 when the player is reconstructed, with a minimum of 1.

PlayerRunState.applyTo() is a once-per-new-entity operation. Its current implementation combines Energy Crystal and Ashen Ember counts into one net Energy modifier before EnergyComponent clamps the maximum to 1–5. It reconstructs derived Energy rather than storing a repeatedly reduced base value. The Event must not apply Ashen Ember a second time.

The generic ownership path is suitable for these four Curses; it is not a claim that ownership alone implements every Item's special acquisition semantics. For example, Eternal Vitality has a separate grantEternalVitality() durable-health operation.

See Curse Items, Inventory and Item Persistence, and PR #480 for the Item subsystem.

Acquisition Result Integration

Committed data, not another reward operation

ChanceEncounterSession stores a successful ChanceResolution, including its actual before/after resources and applied outcome. ChanceAcquisitionResults.fromApplied(...) maps only such a successful resolution; it does not map an unexecuted JSON choice or award anything.

Entry kind Mapping
GOLD Entry.gold(currencyAfter - currencyBefore): the signed, actual settled change.
HP Entry.hp(healthAfter - healthBefore): the signed, actual change after clamping; e.g. a requested +20 that heals only 5 is +5.
CARD Applied card IDs grouped by ID, with actual acquired-copy counts and Card Service names/descriptions when available.
ITEM Applied Item IDs grouped by ID, with actual acquired-copy counts, shared ItemFormatting names, and effect descriptions.

Zero resource changes are omitted. Leave/no-effect outcomes, failed settlement, and outcomes with no valid entries produce no AcquisitionResult. A cost-only successful outcome can still produce a negative resource Entry. Existing HP/Gold/Card outcomes are supported by the mapper without forcing their dedicated UI to migrate.

A Shrine contract produces Gold plus one Item. Mine exploration produces Gold alone or Gold plus the actual selected Item. Each Curse has one ITEM entry, not an extra NEGATIVE_EFFECT entry for the same ownership. Descriptions explain future battle/player-reconstruction effects rather than immediate stat deductions.

Stable session data and optional presentation

The session owns a stable result ID and caches its mapped result. Repeated queries use the same committed data and ID without settling again. A premature query while pending or failed yields no result and does not prevent a later successful result.

ChanceAcquisitionSession.getResult() exposes this data. EncounterGameArea prepares and exposes the session adapter for a future UI owner. Its show(AcquisitionView, onContinue, onClose) delegates to Zixuan's AcquisitionPresentation, which rejects duplicate IDs, busy/disposed views, and stale or repeated dismissal callbacks. The same view/presentation is retained for that session.

Missing views, empty results, and display exceptions leave the old Outcome/Continue flow available; they never rerun ChanceOutcomeApplier. Closing the adapter invalidates presentation callbacks without completing the Event or changing rewards.

UI integration boundary: the new Event Acquisition Display popup is not wired into the current Chance display at this checkpoint. Guoqing owns the display and UI actions. The intended wiring is:

  • Continue: invoke the existing Event-completion and Map-return action.
  • Close: dismiss only the acquisition view and restore the already-settled Outcome page, awaiting its original Continue.
  • Screen disposal: clean up presentation only; do not synthesize completion.

These are the adapter contract and integration requirements, not a claim of accepted player-visible behaviour. Dice and Fusion retain their dedicated interaction flows. Team 7's battle reward/feedback UI is a separate source flow and must not be used to claim the same Event reward again.

For the shared API and ownership boundary, see docs/shop-refresh-and-acquisition-integration.md. #442 owns settlement and result mapping, Zixuan owns the shared contracts, and Guoqing owns display/layout/navigation wiring.

Sprint 4 player-visible result presentation (Guoqing, PR #499)

PR #499 proposes the read-only AcquisitionResultDisplay on the upstream task/439-guoqing-sp4-results branch. The view receives Zixuan's immutable AcquisitionResult through AcquisitionView; it lists actual signed Gold/HP changes, card and item quantities, and configured effect descriptions. Available cards use the existing CardWidget preview. It neither calculates a reward nor invokes a purchase.

After a successful Event choice, ChanceEncounterDisplay asks the retained ChanceAcquisitionSession to present the committed result. Continue dismisses the view and follows the original Event completion action; Close dismisses it and reveals the settled Outcome page with its original Continue. Empty and unsuccessful results retain the existing Outcome flow. Shop uses the same view only after a successful purchase receipt, and dismissing it does not purchase again. Four Mysterious Shrine choices occupy separate full-width rows; the existing two-choice presentation remains.

The PR adds AcquisitionResultDisplayTest, ChanceCurseChoiceDispatchTest, ChanceEncounterDisplayTest, and ShopDisplayTest checks for signed data, one-time dispatch, layout bounds, and purchase count. The branch is pending final automated checks, in-game visual/manual verification, review, and integration. Do not count this proposal as verified player-visible acceptance until those checks are recorded. A concise source note lists the manual paths.

Encounter Lifecycle and Completion

EncounterFlowController permits one active Chance or Shop encounter at a time.

For Chance encounters:

  • invalid choices do not resolve the session;
  • failed ordinary outcomes do not become successful resolutions;
  • fully rolled-back failures may be retried; Mine retries reuse its cached outcome;
  • re-entrant settlement is rejected, and unsafe partial Item rollback blocks further settlement;
  • staged Dice choices keep the session open and apply no stake until a terminal outcome;
  • a successful normal outcome can be resolved only once;
  • the display disables further choices after successful normal resolution;
  • Continue completes a normal outcome and forwards the integer node ID;
  • fuse enters delegated state without completing the Event or applying a Chance outcome;
  • successful Fusion or delegated CardFusionEncounterFlow.leave() completes the Event via completeDelegated();
  • the completion callback is sent once, and the controller clears its active encounter before forwarding completion;
  • cancellation before completion reports an unsuccessful completion once.

Initial Card Fusion leave is not delegated Leave: it is a normal no-effect choice that needs the display's Continue action to complete. The current Chance display does not expose a dedicated cancel button, although cancellation is supported by the session API.

Testing

Historical Sprint 2 verification

Sprint 2 tests covered:

  • valid and invalid configuration, numeric boundaries, duplicate IDs, and catalog-aware card validation;
  • weighted selection boundaries, deterministic random behavior, and weight overflow;
  • health, currency, combined, and no-effect outcomes;
  • card rewards and shared deck persistence;
  • exact duplicate-safe card rollback;
  • health rejection before mutation and failure after health mutation;
  • retry, single-resolution, cancellation, and completion behavior;
  • shared PlayerRunState and PlayerDeck integration;
  • production encounter wiring and exactly-once map progression;
  • compatibility with existing Shop transactions.

At that Sprint 2 checkpoint, the focused Chance, deck, Shop, encounter-flow, and shared-state regression suite recorded 135/135 passing tests before and after Team 2 integration. Verification used fresh JDK 21 compilation and standalone JUnit execution. This is retained as historical evidence and is not the current Sprint 4 test count.

Historical Sprint 3 verification

Sprint 3 tests additionally cover:

  • the exact seven-entry catalogue, weights, choice IDs, and all-event deterministic selection (ChanceEncounterConfigLoaderTest, ChanceEncounterFactoryTest, ChanceEncounterSelectorTest);
  • Wishing Fountain's 50/30/20 boundaries, +20 health, eligible-card selection, no effect, and Leave (WishingFountainEncounterBehaviourTest);
  • Dice stage-specific choices, finite Lucky Seven tiers, 10/15/30/60-gold stakes, round-two one/two/three-card rewards, duplicate cards, losses, and terminal-only application (DiceEncounterBehaviourTest, ChanceEncounterDisplayTest);
  • behaviour routing, delegated session completion, exact-once callbacks, and multi-card outcome rollback (ChanceEncounterBehaviourFactoryTest, ChanceEncounterSessionTest, EncounterFlowControllerTest, ChanceOutcomeApplierTest);
  • Card Fusion's owned-instance/rarity rules in the Fusion domain, and the Event bridge's shared deck, retry, unavailable, initial Leave, and delegated Leave paths (CardFusionServiceTest, CardFusionEncounterFlowTest, EncounterGameAreaTest).
  • player-facing Fusion selection by distinct instance ID, duplicate card names, the blocked fourth selection, success feedback and completion, stale-card retry, and already-used feedback followed by delegated Leave (CardFusionSelectionViewTest). Other failure reasons and initial Leave are covered by the backend tests above; the tests do not automate the complete real-map UI path.
  • contextual Help content, opening and closing across Dice stages, persistent Dice Help versus detached stale choice buttons, Fusion intro and selection Help without session/deck/run mutation, and absence on an ordinary Event (ChanceEncounterDisplayTest, CardFusionContextualHelpTest).

Yihan reported BUILD SUCCESSFUL for focused Card Fusion/Event integration tests and full :core:test after the Event backend integration commit, before the later UI merge. Those are earlier manually run Gradle results; this documentation update does not claim a new Gradle run. The earlier Sprint 3 notes report a limited manual smoke test of the player-facing Card Fusion flow, Leave paths, and staged Dice interaction in the development preview. That historical smoke test does not establish current Sprint 4 end-to-end, real-map, or failure-mode acceptance. An earlier Codex sandbox could not reach Gradle task execution because of a loopback initialization error; it was not represented as a successful sandbox run.

Sprint 4 coverage and integration checkpoint

Current test classes extend the earlier regression suite:

Coverage Representative tests
Fixed Item rewards, invalid IDs, duplicate copies, combined rewards, exact rollback, original choices ChanceCurseChoiceDispatchTest, ChanceCurseEncounterIntegrationTest, ChanceItemRewardIntegrationTest, PlayerRunStateItemAdapterTest, ChanceOutcomeApplierTest
Mine probability/pool parsing, invalid probabilities/weights/IDs, probability endpoints and weighted boundaries ChanceMineRiskConfigTest, AbandonedMineEncounterBehaviourTest
Four selectable Curses, cached Gold-only/Curse outcomes, failure retry, re-entry, new-session independence, Leave and unsafe rollback ChanceMineRiskIntegrationTest, behaviour tests above
Actual signed deltas, HP cap, repeated goods, no-effect/failed results, stable ID, presentation exceptions and duplicate callbacks ChanceAcquisitionResultsTest, ChanceAcquisitionSessionTest, ChanceEncounterSessionTest, AcquisitionPresentationTest
Formal Team 7 dependency, battle penalties, Energy reconstruction and shared state CurseItemEffectsTest, PlayerRunStateTest, EnergyComponentTest, CardEffectHandlerTest, BattleControllerTest

Verification evidence for the integration checkpoint is distinguished by execution method:

  • Independent Java/JUnit verification: the isolated integration rehearsal recorded 2,357 passing full Core tests and 478 passing focused Chance/Item/Acquisition/Curse tests. The focused run is a subset, not an additional full-suite total. Core/Desktop compilation and formatting/diff checks were also recorded.
  • Formal Windows Gradle: Yihan subsequently reported BUILD SUCCESSFUL in 38s, $LASTEXITCODE = 0, on the final Team 2 merge, covering Core/Desktop compilation, the full Core suite, and spotlessCheck. The independent runner counts above are not presented as Gradle's reported test counts.
  • Environment limitation: earlier Codex Gradle attempts failed at loopback initialization before tasks executed; those attempts are not Gradle passes.
  • Manual acceptance: the final Acquisition Display, four-button Shrine layout, Mine long-text rendering, and complete player-visible Event-to-Map flow have not been accepted by a new Sprint 4 end-to-end gameplay test.

The formal command is run from the repository's source directory in ordinary Windows PowerShell:

.\gradlew.bat :core:compileJava :desktop:compileJava :core:test spotlessCheck --offline --console=plain --max-workers=2

These are checkpoint records, not a claim that tests were rerun when preparing this Wiki update. Test counts can change with later commits.

Current Limitations and Deferred Work

  • Fusion availability in the pool: Card Fusion retains its configured selection weight even after a run has used its one Fusion. The service reports FUSION_ALREADY_USED when another fusion is attempted; the selection view then disables Fuse and keeps Leave available.

  • Card Upgrade: Chance-triggered card upgrading was deferred in Sprint 2 while the cross-team interaction boundary was still being finalised and remains outside the current encounter pool. Core card rewards and Fusion integration do not depend on it.

  • Partial health failure: If health changed before an exception was observed, earlier card, item, and currency changes are retained because reversing them could conflict with irreversible health or death processing.

  • Retry after partial failure: Duplicate-safe retry is guaranteed for fully restored failures. Item-bearing ROLLBACK_FAILED outcomes are blocked from further settlement; do not treat every partial failure as safely retryable.

  • Catalog-less loading: The loader overload without a card catalog cannot reject unknown but syntactically valid card IDs. Production uses catalog-aware loading.

  • Cancellation UI: Cancellation exists in the session API but is not exposed as a dedicated Chance display control.

  • Legacy result text: The existing Chance display derives card text from the stable ID and formats only the first card ID of a multi-card Dice outcome. The new mapper supports metadata and all copy counts, but this does not upgrade the legacy UI by itself.

  • Legacy deck fallback: Persistent rewards use the shared PlayerDeck; InventoryDeckAdapter remains as a fallback when no shared deck is supplied.

  • Acquisition UI pending: Shared result data and the session adapter are implemented; Guoqing's final display/layout and Continue/Close wiring are not yet integrated or manually accepted. Preserve the existing Outcome/Continue fallback.

  • Shrine/Mine presentation: Shrine has four configured choices awaiting themed layout verification. Mine has two choices, but its detailed risk/effect text still needs a rendering check. No new artwork or layout is implied by the data changes.

  • Pending-Event persistence: Save/Load records durable inventory and resources, not an unfinished Event's behaviour, cached Mine draw, or result ID. Stable results are guaranteed only within the retained session, not across loading or a newly constructed screen/session.

  • Balance and repeat visits: Mine's 60 Gold, 50% trigger, and equal weights are initial values. There is no per-run acquisition cap; repeated encounters and multiple Curse copies require later balance/playtest review.

Related Issue and Pull Requests

AI Assistance Disclosure

AI tools, including Codex, were used for implementation support, code review, verification troubleshooting, and preparation of documentation for this system. AI-generated suggestions and outputs were reviewed against repository source, tests, and Git history before incorporation. Sprint 2 AI-use details were disclosed in the Sprint 2 Achievement Form, and full Sprint 3 AI-use details, including prompts, incorporated outputs, attribution, and verification, are recorded in the team’s Sprint 3 AI Declaration and Achievement Form.

This Sprint 4 documentation update was prepared with AI assistance and checked against the stated source snapshot. Final author review and any required Sprint 4 course disclosure remain the author's responsibility; the historical disclosure statement above does not certify a new Sprint 4 submission.

⚠️ **GitHub.com Fallback** ⚠️