Enemy_Memory_System - UQcsse3200/2026-studio-3 GitHub Wiki
Enemy Memory System
1. Overview
The Enemy Memory System observes the player's behaviour during one combat encounter and exposes that information to enemy AI through immutable snapshots.
It allows enemies to respond to both:
- Current combat state, such as health and armour
- Historical player behaviour, such as card usage and defensive habits
The system currently records:
- Total attack cards played during the battle
- Total skill cards played during the battle
- Cards played during the previous player turn
- Consecutive turns in which the player gained no block
- Whether the player strongly favours attack cards
- Whether the player has remained undefended
All enemies in the same battle read the same player memory, while retaining independent AI instances and internal decision state.
Memory lasts for one battle only. A new battle begins with empty memory.
2. Goals and Scope
Goals
- Observe successful player actions.
- Convert battle events into meaningful behavioural information.
- Separate event tracking from memory storage.
- Provide immutable data to enemy AI.
- Allow multiple enemies to observe the same player history.
- Preserve compatibility with existing enemy AI.
- Keep enemy AI independent from card and battle infrastructure.
- Support future adaptive enemy behaviours.
Current Scope
The system tracks player combat behaviour only.
It does not currently:
- Persist memory between battles
- Track map exploration
- Track inventory history
- Track failed card plays
- Replace the battle controller
- Directly decide enemy intents
- Implement an achievement system
3. System Context Diagram
flowchart LR
Player["Player Entity"]
Controller["BattleController"]
Tracker["PlayerTrackerComponent"]
Memory["EnemyMemoryComponent"]
Snapshot["PlayerMemory"]
Behaviour["EnemyBehaviourComponent"]
Context["EnemyAIContext"]
AI["EnemyAI"]
Intent["EnemyIntent"]
Controller --> Tracker
Player --> Tracker
Player --> Memory
Tracker --> Memory
Memory --> Snapshot
Snapshot --> Behaviour
Behaviour --> Context
Context --> AI
AI --> Intent
The main information flow is:
Player behaviour
↓
PlayerTrackerComponent
↓
EnemyMemoryComponent
↓
PlayerMemory
↓
EnemyBehaviourComponent
↓
EnemyAIContext
↓
EnemyAI
4. ECS Ownership
PlayerTrackerComponent and EnemyMemoryComponent are attached to the player entity.
flowchart TB
Factory["PlayerFactory"]
Player["Player Entity"]
Stats["CombatStatsComponent"]
Energy["EnergyComponent"]
Actions["PlayerActions"]
Tracker["PlayerTrackerComponent"]
Memory["EnemyMemoryComponent"]
Factory --> Player
Player --> Stats
Player --> Energy
Player --> Actions
Player --> Tracker
Player --> Memory
PlayerFactory is responsible for component ownership:
.addComponent(new EnemyMemoryComponent())
.addComponent(new PlayerTrackerComponent())
Both PlayerFactory.createPlayer overloads must add the components.
PlayerFactory only establishes ownership. BattleScreen performs runtime wiring after BattleController, BattleDeck and CardLibrary have been created.
5. Source Structure
The logical structure of the system is:
components/enemy/
├── EnemyBehaviourComponent.java
├── PlayerTrackerComponent.java
├── EnemyMemoryComponent.java
├── PlayerMemory.java
└── EnemyAI/
├── EnemyAI.java
├── EnemyAIContext.java
├── ArmourScalingAI.java
└── EnrageLowHealthAI.java
Related integration files include:
entities/factories/
└── PlayerFactory.java
screens/
└── BattleScreen.java
components/combat/
└── BattleController.java
Related tests include:
components/enemy/
├── PlayerMemoryTest.java
├── EnemyMemoryComponentTest.java
├── PlayerTrackerComponentTest.java
├── EnemyBehaviourComponentTest.java
└── EnemyAI/
├── ArmourScalingAITest.java
└── EnrageLowHealthAITest.java
The exact physical folder layout may group memory files inside an additional Memory folder, while their Java package remains:
package com.csse3200.game.components.enemy;
6. Core UML Class Diagram
classDiagram
class Component
class PlayerTrackerComponent {
-EnemyMemoryComponent enemyMemory
-BattleController battleController
-CardService cardService
-BattleDeck battleDeck
-boolean connected
+create()
+connect()
+onCardPlayed()
+onPhaseChanged()
+findCardConfig()
+grantsPositiveBlock()
}
class EnemyMemoryComponent {
-int attackCardsPlayed
-int skillCardsPlayed
-int cardsPlayedThisTurn
-int cardsPlayedLastTurn
-int consecutiveTurnsWithoutBlock
-boolean gainedBlockThisTurn
+recordCardPlayed()
+settlePlayerTurn()
+snapshot()
}
class PlayerMemory {
+int attackCardsPlayed
+int skillCardsPlayed
+int consecutiveTurnsWithoutBlock
+int cardsPlayedLastTurn
+favoursAttack()
+isUndefended()
+empty()
}
class EnemyBehaviourComponent {
-EnemyMemoryComponent enemyMemory
+setEnemyMemory()
+rollIntent()
+buildContext()
}
class EnemyAIContext {
-PlayerMemory playerMemory
+getPlayerMemory()
}
class EnemyAI {
+decide()
}
Component <|-- PlayerTrackerComponent
Component <|-- EnemyMemoryComponent
Component <|-- EnemyBehaviourComponent
PlayerTrackerComponent --> EnemyMemoryComponent : writes
EnemyMemoryComponent --> PlayerMemory : creates
EnemyBehaviourComponent --> EnemyMemoryComponent : reads
EnemyBehaviourComponent --> EnemyAIContext : creates
EnemyAIContext --> PlayerMemory : contains
EnemyAI --> EnemyAIContext : reads
7. Component Responsibility Diagram
flowchart TB
subgraph Observation
Controller["BattleController"]
Tracker["PlayerTrackerComponent"]
Deck["BattleDeck"]
Cards["CardService"]
end
subgraph Storage
Memory["EnemyMemoryComponent"]
Snapshot["PlayerMemory"]
end
subgraph Decision
Behaviour["EnemyBehaviourComponent"]
Context["EnemyAIContext"]
AI["EnemyAI"]
end
Controller --> Tracker
Deck --> Tracker
Cards --> Tracker
Tracker --> Memory
Memory --> Snapshot
Snapshot --> Behaviour
Behaviour --> Context
Context --> AI
Responsibilities are divided as follows:
| Class | Responsibility |
|---|---|
BattleController |
Publishes successful card-play and phase-change events |
PlayerTrackerComponent |
Interprets battle events |
EnemyMemoryComponent |
Stores mutable per-battle memory |
PlayerMemory |
Provides an immutable read model |
EnemyBehaviourComponent |
Builds the current AI context |
EnemyAIContext |
Carries combat state and memory |
EnemyAI |
Selects the next enemy intent |
8. PlayerMemory Data Model
PlayerMemory is an immutable Java record:
public record PlayerMemory(
int attackCardsPlayed,
int skillCardsPlayed,
int consecutiveTurnsWithoutBlock,
int cardsPlayedLastTurn)
Field Definitions
| Field | Description | Update Pattern |
|---|---|---|
attackCardsPlayed |
Attack cards played during the battle | Accumulates |
skillCardsPlayed |
Skill cards played during the battle | Accumulates |
consecutiveTurnsWithoutBlock |
Consecutive completed turns without positive block | Increases or resets |
cardsPlayedLastTurn |
Cards played in the previous completed turn | Replaced each turn |
Data Lifetime Diagram
flowchart TB
PlayerMemory["PlayerMemory"]
Attack["Attack cards played"]
Skill["Skill cards played"]
NoBlock["Turns without block"]
LastTurn["Cards played last turn"]
BattleLifetime["Battle lifetime"]
TurnHistory["Turn history"]
PlayerMemory --> Attack
PlayerMemory --> Skill
PlayerMemory --> NoBlock
PlayerMemory --> LastTurn
Attack --> BattleLifetime
Skill --> BattleLifetime
NoBlock --> TurnHistory
LastTurn --> TurnHistory
9. Immutable Snapshot Model
Enemy AI never modifies the live memory component.
flowchart LR
Live["EnemyMemoryComponent"]
SnapshotA["PlayerMemory A"]
Action["New player action"]
SnapshotB["PlayerMemory B"]
Live -->|"snapshot"| SnapshotA
Action --> Live
Live -->|"snapshot"| SnapshotB
Example:
PlayerMemory before =
enemyMemory.snapshot();
enemyMemory.recordCardPlayed(
CardType.ATTACK,
false);
PlayerMemory after =
enemyMemory.snapshot();
before remains unchanged after the component is updated.
This prevents:
- AI code modifying shared memory
- Decision state changing midway
- Enemies observing partially modified records
- Tests depending on mutable shared objects
10. PlayerMemory Derived Rules
10.1 Attack Preference
favoursAttack() returns true when:
attackCardsPlayed >= 4
and:
attackCardsPlayed > skillCardsPlayed * 2
flowchart TD
Start["Check attack preference"]
Minimum{"At least four attacks"}
Ratio{"More than twice skills"}
Yes["Return true"]
No["Return false"]
Start --> Minimum
Minimum -->|"No"| No
Minimum -->|"Yes"| Ratio
Ratio -->|"Yes"| Yes
Ratio -->|"No"| No
Examples:
| Attack Cards | Skill Cards | Result |
|---|---|---|
| 3 | 0 | false |
| 4 | 0 | true |
| 4 | 1 | true |
| 4 | 2 | false |
| 7 | 3 | true |
| 6 | 3 | false |
10.2 Undefended Player
isUndefended() returns true when:
consecutiveTurnsWithoutBlock >= 3
flowchart TD
Start["Check defence history"]
Threshold{"Three turns without block"}
Yes["Return true"]
No["Return false"]
Start --> Threshold
Threshold -->|"Yes"| Yes
Threshold -->|"No"| No
10.3 Null Object
PlayerMemory.empty() returns:
new PlayerMemory(0, 0, 0, 0)
It is used instead of null.
flowchart TD
Build["Build EnemyAIContext"]
Available{"Memory component available"}
Current["Use current snapshot"]
Empty["Use empty memory"]
Context["Create context"]
Build --> Available
Available -->|"Yes"| Current
Available -->|"No"| Empty
Current --> Context
Empty --> Context
11. EnemyMemoryComponent
EnemyMemoryComponent is the mutable storage centre of the system.
Public API
public void recordCardPlayed(
CardType cardType,
boolean gainedBlock)
public void settlePlayerTurn()
public PlayerMemory snapshot()
Internal State
classDiagram
class EnemyMemoryComponent {
-int attackCardsPlayed
-int skillCardsPlayed
-int cardsPlayedThisTurn
-int cardsPlayedLastTurn
-int consecutiveTurnsWithoutBlock
-boolean gainedBlockThisTurn
}
The component deliberately does not depend on:
BattleControllerBattleDeckCardServiceCardConfigCardInstanceBattlePhase
These dependencies belong to the tracker.
12. Card Recording Activity Diagram
Every successfully played card increases cardsPlayedThisTurn.
Only attack and skill cards increase their dedicated lifetime totals.
flowchart TD
Card["Successful card"]
Count["Increment current turn count"]
Type{"Card type"}
Attack["Increment attack total"]
Skill["Increment skill total"]
Other["No category total"]
Block{"Positive block gained"}
Mark["Mark block gained"]
Done["Complete"]
Card --> Count
Count --> Type
Type -->|"Attack"| Attack
Type -->|"Skill"| Skill
Type -->|"Other"| Other
Attack --> Block
Skill --> Block
Other --> Block
Block -->|"Yes"| Mark
Block -->|"No"| Done
Mark --> Done
Other card types still count as cards played during the turn:
POWERSTATUSCURSE
They do not increase attack or skill totals.
13. Turn Settlement Activity Diagram
When the player finishes a real turn:
flowchart TD
End["Player turn ends"]
Copy["Copy current count to last turn"]
Block{"Block gained this turn"}
Reset["Reset no-block streak"]
Increase["Increase no-block streak"]
ClearCount["Clear current turn count"]
ClearFlag["Clear block flag"]
Complete["Settlement complete"]
End --> Copy
Copy --> Block
Block -->|"Yes"| Reset
Block -->|"No"| Increase
Reset --> ClearCount
Increase --> ClearCount
ClearCount --> ClearFlag
ClearFlag --> Complete
Equivalent behaviour:
cardsPlayedLastTurn = cardsPlayedThisTurn;
if (gainedBlockThisTurn) {
consecutiveTurnsWithoutBlock = 0;
} else {
consecutiveTurnsWithoutBlock++;
}
cardsPlayedThisTurn = 0;
gainedBlockThisTurn = false;
14. PlayerTrackerComponent
PlayerTrackerComponent observes raw battle events and converts them into semantic memory updates.
flowchart LR
Event["Raw battle event"]
Tracker["PlayerTrackerComponent"]
Resolution["Resolve card information"]
Classification["Classify behaviour"]
Memory["EnemyMemoryComponent"]
Event --> Tracker
Tracker --> Resolution
Resolution --> Classification
Classification --> Memory
It is responsible for:
- Listening to successful card-play events
- Listening to phase-change events
- Resolving
CardInstance - Retrieving
CardConfig - Reading
CardType - Detecting positive block
- Calling the memory component
It does not store lifetime combat statistics itself.
15. Runtime Initialisation Sequence
sequenceDiagram
participant PF as PlayerFactory
participant PE as PlayerEntity
participant BS as BattleScreen
participant BC as BattleController
participant PT as PlayerTracker
participant EM as EnemyMemory
participant EB as EnemyBehaviour
PF->>PE: Add tracker
PF->>PE: Add memory
BS->>BC: Create controller
BS->>PE: Get tracker
BS->>PT: Connect battle systems
BS->>PE: Get memory
loop Each enemy
BS->>EB: Set shared memory
end
PlayerTrackerComponent.connect() receives:
connect(
BattleController battleController,
CardService cardService,
BattleDeck battleDeck)
A tracker may only be connected once.
This prevents duplicate event listeners and duplicated memory updates.
16. Successful Card Play Sequence
sequenceDiagram
participant P as Player
participant BC as BattleController
participant PT as PlayerTracker
participant BD as BattleDeck
participant CS as CardService
participant EM as EnemyMemory
P->>BC: Request card play
BC->>BC: Validate and resolve
BC->>PT: Card played event
PT->>BD: Find runtime instance
BD-->>PT: CardInstance
PT->>CS: Find card definition
CS-->>PT: CardConfig
PT->>PT: Detect type and block
PT->>EM: Record card
Only successful card plays produce the event consumed by the tracker.
The following actions are not recorded:
- Insufficient energy
- Invalid target
- Missing card
- Card not in hand
- Cancelled selection
- Failed card resolution
17. Runtime Card Resolution
The card event supplies a runtime instance ID.
The tracker resolves it into a reusable card definition.
flowchart LR
InstanceId["Runtime instance ID"]
Deck["BattleDeck"]
Instance["CardInstance"]
CardId["Card definition ID"]
Service["CardService"]
Config["CardConfig"]
InstanceId --> Deck
Deck --> Instance
Instance --> CardId
CardId --> Service
Service --> Config
Example:
Runtime instance ID: strike-7f3a
Card definition ID: strike
BattleDeck.getAllInstances() is used because the played card may already be in the discard pile when the event is handled.
18. Positive Block Detection
A card counts as providing defence when it contains:
EffectType.BLOCK
with:
value > 0
flowchart TD
Config["CardConfig"]
Effects{"Effects exist"}
Inspect["Inspect effect"]
IsBlock{"Block effect"}
Positive{"Positive value"}
True["Block gained"]
More{"More effects"}
False["No block gained"]
Config --> Effects
Effects -->|"No"| False
Effects -->|"Yes"| Inspect
Inspect --> IsBlock
IsBlock -->|"No"| More
IsBlock -->|"Yes"| Positive
Positive -->|"Yes"| True
Positive -->|"No"| More
More -->|"Yes"| Inspect
More -->|"No"| False
The tracker safely handles:
- Null effect arrays
- Null effect entries
- Zero block values
- Negative block values
19. Battle Phase Filtering
Playing one card temporarily produces:
PLAYER_TURN
↓
CARD_RESOLVING
↓
PLAYER_TURN
This must not settle the turn.
flowchart TD
Change["Phase changed"]
Previous{"Previous is player turn"}
Same{"Next is player turn"}
Resolving{"Next is card resolving"}
Ignore["Ignore"]
Settle["Settle memory"]
Change --> Previous
Previous -->|"No"| Ignore
Previous -->|"Yes"| Same
Same -->|"Yes"| Ignore
Same -->|"No"| Resolving
Resolving -->|"Yes"| Ignore
Resolving -->|"No"| Settle
Valid settlement:
PLAYER_TURN to PLAYER_END
Ignored transitions:
PLAYER_TURN to CARD_RESOLVING
CARD_RESOLVING to PLAYER_TURN
SETUP to PLAYER_TURN
PLAYER_END to ENEMY_TURN
20. AI Decision Sequence
sequenceDiagram
participant BC as BattleController
participant EB as EnemyBehaviour
participant EM as EnemyMemory
participant PM as PlayerMemory
participant EC as EnemyAIContext
participant AI as EnemyAI
BC->>EB: Roll intent
EB->>EM: Request snapshot
EM-->>EB: PlayerMemory
EB->>EC: Build context
EB->>AI: Decide with context
AI->>EC: Read player memory
AI-->>EB: Return EnemyIntent
A new snapshot is requested for every decision.
This ensures enemy AI receives the latest player history rather than a snapshot captured only at battle setup.
21. EnemyAIContext Integration
EnemyAIContext contains both current battle state and player memory.
classDiagram
class EnemyAIContext {
-int playerHealth
-int enemyHealth
-int enemyMaxHealth
-int enemyAttack
-int enemyArmour
-EnemyIntent previousIntent
-int turnNumber
-PlayerMemory playerMemory
+getPlayerHealth()
+getEnemyHealth()
+getEnemyMaxHealth()
+getEnemyAttack()
+getEnemyArmour()
+getPreviousIntent()
+getTurnNumber()
+getPlayerMemory()
+getEnemyHealthRatio()
+canDefeatPlayer()
}
class PlayerMemory {
+favoursAttack()
+isUndefended()
}
EnemyAIContext --> PlayerMemory
Backwards Compatibility
The original seven-argument constructor remains available.
flowchart LR
Old["Seven argument constructor"]
Empty["PlayerMemory empty"]
New["Full constructor"]
Context["EnemyAIContext"]
Old --> Empty
Empty --> New
New --> Context
Old AI classes automatically receive empty memory and retain their original behaviour.
22. Multi-Enemy Object Diagram
One player memory is shared by all enemies.
flowchart TB
Player["Player Entity"]
Memory["EnemyMemoryComponent"]
EnemyOne["Enemy Entity One"]
EnemyTwo["Enemy Entity Two"]
EnemyThree["Enemy Entity Three"]
BehaviourOne["EnemyBehaviour One"]
BehaviourTwo["EnemyBehaviour Two"]
BehaviourThree["EnemyBehaviour Three"]
AIone["EnemyAI One"]
AItwo["EnemyAI Two"]
AIthree["EnemyAI Three"]
Player --> Memory
EnemyOne --> BehaviourOne
EnemyTwo --> BehaviourTwo
EnemyThree --> BehaviourThree
Memory --> BehaviourOne
Memory --> BehaviourTwo
Memory --> BehaviourThree
BehaviourOne --> AIone
BehaviourTwo --> AItwo
BehaviourThree --> AIthree
The enemies share only the observed player history.
They do not share:
- Previous intents
- Enemy health
- Enemy armour
- AI-specific counters
- Boss phase
- Random generators
- Debuff cooldowns
Shared observation does not mean shared decision state.
23. Memory-Driven Enemy Behaviour
Enemy AI may ignore memory or consume only the fields relevant to its design.
flowchart TD
Context["EnemyAIContext"]
Current["Current combat state"]
Memory["PlayerMemory"]
Rule["Enemy behaviour rule"]
Intent["EnemyIntent"]
Context --> Current
Context --> Memory
Current --> Rule
Memory --> Rule
Rule --> Intent
23.1 ArmourScalingAI
Normal behaviour:
DEFEND
ATTACK
When favoursAttack() is true:
DEFEND
DEFEND
ATTACK
flowchart TD
Start["ArmourScalingAI"]
Preference{"Player favours attack"}
Normal["Two turn cycle"]
Reactive["Three turn cycle"]
Intent["Return intent"]
Start --> Preference
Preference -->|"No"| Normal
Preference -->|"Yes"| Reactive
Normal --> Intent
Reactive --> Intent
The reactive cycle allows Dark Acolyte to build more armour against aggressive players.
23.2 EnrageLowHealthAI
Void Knight becomes enraged when either:
Enemy health is below 50 percent
or:
Player is undefended
flowchart TD
Start["EnrageLowHealthAI"]
Base["Create base intent"]
Health{"Below half health"}
Memory{"Player undefended"}
Attack{"Base intent is attack"}
Double["Double attack value"]
Normal["Keep base intent"]
Result["Return intent"]
Start --> Base
Base --> Health
Health -->|"Yes"| Attack
Health -->|"No"| Memory
Memory -->|"Yes"| Attack
Memory -->|"No"| Normal
Attack -->|"Yes"| Double
Attack -->|"No"| Normal
Double --> Result
Normal --> Result
Enrage only modifies attack damage. Defensive intents remain defensive.
23.3 BossAI
BossAI receives PlayerMemory through EnemyAIContext.
Memory-driven Boss behaviour is currently deferred because the Boss already contains:
- Phase transitions
- Weighted actions
- Global pressure
- Debuff cooldown
- Consecutive-action constraints
Deferring this behaviour does not affect completion of the memory infrastructure.
24. Memory Lifecycle
flowchart LR
Created["Memory created"]
Tracking["Tracking battle"]
Settlement["Turn settlement"]
Decision["Enemy reads snapshot"]
Finished["Battle finished"]
Destroyed["Memory discarded"]
Created --> Tracking
Tracking --> Settlement
Settlement --> Decision
Decision --> Tracking
Tracking --> Finished
Settlement --> Finished
Finished --> Destroyed
Lifecycle summary:
- Player entity is created.
- Memory begins empty.
- Tracker observes successful actions.
- Memory stores battle history.
- Enemies request snapshots.
- Battle ends.
- Player entity and memory are disposed.
- The next battle starts with new empty memory.
25. Full Example
Turn One
The player uses:
Attack
Attack
Skill with block
Before settlement:
attackCardsPlayed = 2
skillCardsPlayed = 1
cardsPlayedThisTurn = 3
gainedBlockThisTurn = true
After settlement:
cardsPlayedLastTurn = 3
consecutiveTurnsWithoutBlock = 0
Turn Two
The player uses:
Attack
Attack
Attack
After settlement:
attackCardsPlayed = 5
skillCardsPlayed = 1
cardsPlayedLastTurn = 3
consecutiveTurnsWithoutBlock = 1
Derived result:
favoursAttack = true
isUndefended = false
Later Turns
If the player completes three consecutive turns without positive block:
consecutiveTurnsWithoutBlock = 3
isUndefended = true
flowchart LR
Block["Turn with block"]
TurnOne["No block turn one"]
TurnTwo["No block turn two"]
TurnThree["No block turn three"]
Undefended["Player is undefended"]
Block -->|"Streak zero"| TurnOne
TurnOne -->|"Streak one"| TurnTwo
TurnTwo -->|"Streak two"| TurnThree
TurnThree -->|"Streak three"| Undefended
26. Error Handling
| Situation | Result |
|---|---|
| Blank card instance ID | Event ignored |
| Unknown runtime card | Event ignored |
| Unknown card definition | Event ignored |
| Null card type | Event ignored |
| Null effects array | Treated as no block |
| Null effect entry | Entry skipped |
| Zero block | Not considered defence |
| Negative block | Not considered defence |
| Tracker connected twice | IllegalStateException |
| Tracker missing memory | IllegalStateException |
| Enemy has no memory reference | PlayerMemory.empty() |
| Null context memory | NullPointerException |
| Negative memory values | IllegalArgumentException |
Failure Handling Diagram
flowchart TD
Input["Tracking input"]
Valid{"Input valid"}
Required{"Required setup present"}
Process["Update memory"]
Ignore["Ignore invalid runtime event"]
Fail["Fail fast for setup error"]
Input --> Valid
Valid -->|"No"| Ignore
Valid -->|"Yes"| Required
Required -->|"No"| Fail
Required -->|"Yes"| Process
Invalid runtime data is ignored safely.
Missing required components are treated as development-time setup errors.
27. Testing Architecture
flowchart TB
Full["Complete Gradle test suite"]
Integration["Behaviour integration tests"]
Tracker["Tracker event tests"]
Component["Memory component tests"]
Model["PlayerMemory tests"]
Full --> Integration
Integration --> Tracker
Tracker --> Component
Component --> Model
PlayerMemoryTest
Covers:
- Empty memory
- Negative-value rejection
- Attack preference threshold
- Attack-to-skill ratio
- Undefended threshold
- Boundary values
EnemyMemoryComponentTest
Covers:
- Empty initial state
- Attack and skill counting
- Other card types
- Current-turn settlement
- No-block streak increase
- No-block streak reset
- Per-turn state reset
- Lifetime totals
- Immutable snapshots
- Null card-type rejection
PlayerTrackerComponentTest
Covers:
- Required memory component
- Null dependencies
- Duplicate connections
- Listener registration
- Runtime card resolution
- Card classification
- Positive block detection
- Multiple effects
- Null effects
- Invalid IDs
- Unknown definitions
- Phase filtering
- Turn settlement
EnemyBehaviourComponentTest
Covers:
- Empty-memory fallback
- Memory transfer into context
- Latest snapshot per decision
- Clearing memory
- Existing enemy behaviour regression
Memory-Driven AI Tests
Covers:
- Dark Acolyte normal cycle
- Dark Acolyte reactive cycle
- Void Knight normal enrage
- Void Knight memory-triggered enrage
- Defence while enraged
- Threshold boundaries
28. Verification Commands
Compile production code:
.\gradlew.bat core:compileJava
Run memory tests:
.\gradlew.bat core:test --tests "*EnemyMemoryComponentTest"
Run tracker tests:
.\gradlew.bat core:test --tests "*PlayerTrackerComponentTest"
Run behaviour tests:
.\gradlew.bat core:test --tests "*EnemyBehaviourComponentTest"
Run all tests:
.\gradlew.bat test
Check formatting:
.\gradlew.bat spotlessCheck
Apply formatting:
.\gradlew.bat spotlessApply
29. Design Rationale
Tracking and Storage Are Separate
flowchart LR
Raw["Raw battle events"]
Tracker["PlayerTrackerComponent"]
Meaning["Player behaviour"]
Memory["EnemyMemoryComponent"]
Snapshot["PlayerMemory"]
Raw --> Tracker
Tracker --> Meaning
Meaning --> Memory
Memory --> Snapshot
This separation provides:
- Simpler unit tests
- Fewer mocks for memory tests
- No battle dependencies in memory storage
- No card dependencies in enemy AI
- Clear class responsibilities
- Easier future extensions
One Shared Memory per Battle
One player
One memory
Many enemy readers
Creating one memory per enemy could produce inconsistent observations and duplicated tracking work.
Immutable AI Input
The mutable component remains private to the tracking side.
Enemy AI receives immutable values only.
Backwards Compatibility
Existing AI can continue using the original EnemyAIContext constructor and automatically receives empty memory.
30. Extension Model
Future player behaviour fields may include:
- Damage dealt last turn
- Healing used last turn
- Cards drawn last turn
- Energy left unused
- Repeated card types
- Status effects applied
- Largest single attack
- Turns spent at low health
A new field should be added only when:
- An enemy behaviour requires it.
- A reliable event source exists.
- Its lifetime is clearly defined.
- Settlement behaviour is specified.
- Unit tests can verify it.
flowchart TD
Need["New AI information"]
Source{"Reliable source"}
Lifetime{"Clear lifetime"}
Settlement{"Clear settlement"}
Testable{"Testable"}
Add["Add field"]
Wait["Do not add yet"]
Need --> Source
Source -->|"No"| Wait
Source -->|"Yes"| Lifetime
Lifetime -->|"No"| Wait
Lifetime -->|"Yes"| Settlement
Settlement -->|"No"| Wait
Settlement -->|"Yes"| Testable
Testable -->|"No"| Wait
Testable -->|"Yes"| Add
31. Future Achievement Integration
The Enemy Memory System is not itself an achievement system because memory is discarded after every battle.
However, the final snapshot can be converted into persistent progress.
flowchart LR
Memory["EnemyMemoryComponent"]
Snapshot["Final PlayerMemory"]
Summary["BattleSummary"]
Progress["AchievementProgress"]
Save["Save System"]
Unlock["Achievement Unlock"]
Memory --> Snapshot
Snapshot --> Summary
Summary --> Progress
Progress --> Save
Progress --> Unlock
Example:
Battle one attack cards = 23
Battle two attack cards = 31
Battle three attack cards = 46
Persistent total = 100
Achievement unlocked
Only the final battle snapshot should be committed.
Intermediate snapshots must not be added together because their counters are cumulative.
32. Current Limitations
- Memory lasts for one battle.
- Memory is not persisted in save data.
- Positive block is detected from card configuration.
- Final armour actually received is not measured.
- Damage dealt is not tracked.
- Card draw and discard history are not tracked.
- Target-specific card history is not tracked.
- Map behaviour is not tracked.
- Inventory changes are not tracked.
- Boss memory-driven behaviour is deferred.
- Dynamically spawned enemies must receive the shared memory reference.
These limitations define the current scope and do not prevent the completed system from supporting adaptive enemy AI.
33. Final Architecture
flowchart TB
subgraph Creation
Factory["PlayerFactory"]
Player["Player Entity"]
end
subgraph Tracking
Controller["BattleController"]
Tracker["PlayerTrackerComponent"]
Deck["BattleDeck"]
Cards["CardService"]
end
subgraph MemorySystem
Memory["EnemyMemoryComponent"]
Snapshot["PlayerMemory"]
end
subgraph EnemyDecision
Behaviour["EnemyBehaviourComponent"]
Context["EnemyAIContext"]
AI["EnemyAI"]
Intent["EnemyIntent"]
end
Factory --> Player
Player --> Tracker
Player --> Memory
Controller --> Tracker
Deck --> Tracker
Cards --> Tracker
Tracker --> Memory
Memory --> Snapshot
Snapshot --> Behaviour
Behaviour --> Context
Context --> AI
AI --> Intent
The final responsibility chain is:
BattleController reports what happened
PlayerTrackerComponent interprets what happened
EnemyMemoryComponent remembers what happened
PlayerMemory describes the stored history
EnemyBehaviourComponent builds the decision context
EnemyAI reacts to the player's behaviour