Chance Encounter System - UQcsse3200/2026-studio-3 GitHub Wiki
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.
| 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
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.
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
healthDeltaandcurrencyDeltavalues; - an optional
cardRewardIdinside the outcome; - an optional
itemRewardIdsarray 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.
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.
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.
| 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 |
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.
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.
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.
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 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.
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.
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
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.
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.
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:
- Add ordered card rewards as pending deck transactions.
- Add ordered item rewards as pending ownership transactions.
- Apply and verify the currency change.
- Apply direct health as the final irreversible gameplay mutation.
- 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 | 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.
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.
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.
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 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.
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.
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.
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.
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.
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;
-
fuseenters delegated state without completing the Event or applying a Chance outcome; - successful Fusion or delegated
CardFusionEncounterFlow.leave()completes the Event viacompleteDelegated(); - 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.
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
PlayerRunStateandPlayerDeckintegration; - 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.
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.
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, andspotlessCheck. 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=2These are checkpoint records, not a claim that tests were rerun when preparing this Wiki update. Test counts can change with later commits.
-
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_USEDwhen 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_FAILEDoutcomes 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;InventoryDeckAdapterremains 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.
- Issue #439 β Team 2 Sprint 4 feature
- Issue #442 β Chance Event Item Rewards and Curse Effect Integration
- PR #480 β Team 7 Curse Items and Battle Polish
- PR #453 β Shared acquisition-result integration
- PR #467 β Themed Event buttons
- Issue #265 β Team 2 Sprint 3 Special Events, Conditional Rewards and Campfire
- Issue #266 β Event System Refactor & Special Events
- Issue #392 β Dice and Card Fusion Contextual Help & Tutorial Prompts
- PR #318 β Event UI previews, Card Fusion selection, and Mine scene
- Issue #148 β Chance Encounter Expansion & Final Player/Card Integration (Sprint 2)
- PR #232 β Team 2 Sprint 2 integration into main
- PR #215 β Direct player-health API integration
- PR #216 β Shared player and run-state integration
- PR #155 β Shared PlayerDeck and CardService integration
- PR #77 β Card Library foundation
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.