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

  1. Observe successful player actions.
  2. Convert battle events into meaningful behavioural information.
  3. Separate event tracking from memory storage.
  4. Provide immutable data to enemy AI.
  5. Allow multiple enemies to observe the same player history.
  6. Preserve compatibility with existing enemy AI.
  7. Keep enemy AI independent from card and battle infrastructure.
  8. 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:

  • BattleController
  • BattleDeck
  • CardService
  • CardConfig
  • CardInstance
  • BattlePhase

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:

  • POWER
  • STATUS
  • CURSE

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:

  1. Player entity is created.
  2. Memory begins empty.
  3. Tracker observes successful actions.
  4. Memory stores battle history.
  5. Enemies request snapshots.
  6. Battle ends.
  7. Player entity and memory are disposed.
  8. 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:

  1. An enemy behaviour requires it.
  2. A reliable event source exists.
  3. Its lifetime is clearly defined.
  4. Settlement behaviour is specified.
  5. 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