Skip to content

PlayerTrackerComponentSprint3

LI WANGXIN edited this page Oct 7, 2026 · 1 revision

Player Tracker Component

1. Overview

PlayerTrackerComponent is the event-observation component of the Enemy Memory System.

It is attached to the player entity and observes confirmed combat events. It converts runtime card and phase information into simplified player behaviour, then writes that information into EnemyMemoryComponent.

Its main responsibilities are:

  • Listening to successful card-play events;
  • Resolving a runtime card instance into its card configuration;
  • Identifying the card's CardType;
  • Detecting positive BLOCK effects;
  • Detecting when the player's real turn ends;
  • Ignoring temporary CARD_RESOLVING transitions;
  • Sending interpreted behaviour to EnemyMemoryComponent.

The tracker does not make enemy decisions and does not store long-term combat memory.

Player actions
      ↓
BattleController events
      ↓
PlayerTrackerComponent
      ↓
EnemyMemoryComponent
      ↓
PlayerMemory snapshot
      ↓
EnemyAIContext
      ↓
Enemy AI

2. File Locations

Implementation:

source/core/src/main/com/csse3200/game/components/enemy/Memory/PlayerTrackerComponent.java

Tests:

source/core/src/test/com/csse3200/game/components/enemy/Memory/PlayerTrackerComponentTest.java

Related files:

EnemyMemoryComponent.java
PlayerMemory.java
EnemyAIContext.java
EnemyBehaviourComponent.java
PlayerFactory.java
BattleScreen.java
BattleController.java
BattleDeck.java
CardService.java

3. Role in the Enemy Memory System

flowchart LR
    Player["Player Entity"] --> Tracker["PlayerTrackerComponent"]
    Player --> Memory["EnemyMemoryComponent"]

    Controller["BattleController"] --> Tracker
    Deck["BattleDeck"] --> Tracker
    CardService["CardService"] --> Tracker

    Tracker --> Memory
    Memory --> Snapshot["PlayerMemory"]
    Snapshot --> Context["EnemyAIContext"]
    Context --> EnemyAI["Enemy AI"]
Loading

The responsibilities are divided as follows:

Class Responsibility
PlayerTrackerComponent Observes and interprets player combat activity
EnemyMemoryComponent Stores accumulated per-combat behaviour
PlayerMemory Provides an immutable memory snapshot
EnemyAIContext Transfers battle and memory data to enemy AI
EnemyAI Uses the information to select an intent

This separation prevents enemy AI from depending directly on battle events, cards or player entities.


4. UML Class Diagram

classDiagram
    class Component {
        +create()
    }

    class PlayerTrackerComponent {
        -EnemyMemoryComponent enemyMemory
        -BattleController battleController
        -CardService cardService
        -BattleDeck battleDeck
        -boolean connected
        +create()
        +connect(battleController, cardService, battleDeck)
        -onCardPlayed(instanceId, targetId)
        -onPhaseChanged(previousPhase, nextPhase)
        -findCardConfig(instanceId) CardConfig
        -grantsPositiveBlock(config) boolean
    }

    class EnemyMemoryComponent {
        +recordCardPlayed(cardType, gainedBlock)
        +settlePlayerTurn()
        +snapshot() PlayerMemory
    }

    class BattleController {
        +addCardPlayedListener(listener)
        +addPhaseChangeListener(listener)
    }

    class BattleDeck {
        +getAllInstances() List
    }

    class CardService {
        +getCard(cardId) Optional
    }

    class CardInstance {
        +instanceId() String
        +cardId() String
    }

    class CardConfig {
        +CardType type
        +EffectConfig[] effects
    }

    Component <|-- PlayerTrackerComponent
    PlayerTrackerComponent --> EnemyMemoryComponent : writes
    PlayerTrackerComponent --> BattleController : observes
    PlayerTrackerComponent --> BattleDeck : resolves instance
    PlayerTrackerComponent --> CardService : resolves definition
    BattleDeck --> CardInstance : contains
    CardService --> CardConfig : provides
Loading

5. ECS Ownership

PlayerTrackerComponent and EnemyMemoryComponent are attached to the same player entity.

Player Entity
├── CombatStatsComponent
├── EnergyComponent
├── EnemyMemoryComponent
└── PlayerTrackerComponent

During ECS creation, the tracker retrieves the memory component:

@Override
public void create() {
  super.create();

  enemyMemory =
      entity.getComponent(
          EnemyMemoryComponent.class);

  if (enemyMemory == null) {
    throw new IllegalStateException(
        "PlayerTrackerComponent requires "
            + "EnemyMemoryComponent "
            + "on the same entity");
  }
}

The tracker cannot exist on a player without EnemyMemoryComponent.

This guarantees that every valid tracker has a memory destination.


6. Component Lifecycle

stateDiagram-v2
    [*] --> CONSTRUCTED
    CONSTRUCTED --> CREATED: Entity create
    CREATED --> CONNECTED: connect called
    CONNECTED --> OBSERVING: Battle events received
    OBSERVING --> OBSERVING: Continue tracking

    CREATED --> ERROR: Memory component missing
    CONNECTED --> ERROR: Second connection attempted
Loading

The lifecycle has two stages.

ECS construction

PlayerFactory attaches the tracker and memory components to the player.

Runtime connection

After BattleController, CardService and BattleDeck have been created, BattleScreen connects the tracker:

playerTracker.connect(
    battleController,
    cardService,
    battleDeck);

This separates entity ownership from battle-specific wiring.


7. Runtime Dependencies

The tracker receives three runtime dependencies:

public void connect(
    BattleController battleController,
    CardService cardService,
    BattleDeck battleDeck)
Dependency Purpose
BattleController Provides card-play and phase-change events
CardService Resolves a card ID into CardConfig
BattleDeck Resolves an instance ID into CardInstance

All dependencies must be non-null.

Objects.requireNonNull(
    battleController,
    "battleController cannot be null");

Objects.requireNonNull(
    cardService,
    "cardService cannot be null");

Objects.requireNonNull(
    battleDeck,
    "battleDeck cannot be null");

8. Duplicate Connection Protection

The tracker may only be connected once:

if (connected) {
  throw new IllegalStateException(
      "PlayerTrackerComponent is already connected");
}

This prevents duplicate listener registration.

Without this protection, one successful card play could be recorded multiple times.

After registering its listeners, the component sets:

connected = true;

9. Battle Listener Registration

The tracker registers two listeners:

battleController.addCardPlayedListener(
    this::onCardPlayed);

battleController.addPhaseChangeListener(
    this::onPhaseChanged);
sequenceDiagram
    participant Screen as BattleScreen
    participant Tracker as PlayerTrackerComponent
    participant Controller as BattleController

    Screen->>Tracker: connect(controller, cards, deck)
    Tracker->>Tracker: Validate dependencies
    Tracker->>Controller: Register card-play listener
    Tracker->>Controller: Register phase-change listener
    Tracker->>Tracker: connected = true
Loading

Card Tracking

10. Successful Card-Play Events

The card listener receives:

private void onCardPlayed(
    String instanceId,
    String targetId)
Parameter Meaning
instanceId Runtime identifier of the successfully played card
targetId Selected target identifier

Only successful card plays generate this event. A rejected card attempt therefore does not update memory.

targetId is currently unused because the memory system tracks how the player acts, rather than which enemy the player targets.


11. Card Resolution Process

The event provides an instance ID, but memory requires semantic card information.

The tracker resolves the card through:

instanceId
    ↓
BattleDeck
    ↓
CardInstance
    ↓
cardId
    ↓
CardService
    ↓
CardConfig
flowchart TD
    Event["Successful card event"] --> Id["Read instanceId"]
    Id --> Valid{"ID is valid?"}

    Valid -->|No| Ignore["Ignore event"]
    Valid -->|Yes| Deck["Search BattleDeck"]

    Deck --> Instance{"Instance found?"}
    Instance -->|No| Ignore
    Instance -->|Yes| CardId["Read cardId"]

    CardId --> Service["CardService.getCard"]
    Service --> Config{"CardConfig found?"}

    Config -->|No| Ignore
    Config -->|Yes| Classify["Classify card"]
Loading

12. Finding the Card Configuration

private CardConfig findCardConfig(
    String instanceId) {
  if (instanceId == null
      || instanceId.isBlank()) {
    return null;
  }

  for (CardInstance instance
      : battleDeck.getAllInstances()) {
    if (instance.instanceId()
        .equals(instanceId)) {
      return cardService
          .getCard(instance.cardId())
          .orElse(null);
    }
  }

  return null;
}

Invalid or unknown card events return null and are ignored safely.

They do not interrupt the battle.


13. Card Classification

After resolving the card, the tracker validates its type:

if (config == null
    || config.type == null) {
  return;
}

It then sends the interpreted data to memory:

enemyMemory.recordCardPlayed(
    config.type,
    grantsPositiveBlock(config));

EnemyMemoryComponent handles card types as follows:

Card type Attack total Skill total Current-turn total
ATTACK +1 No change +1
SKILL No change +1 +1
POWER No change No change +1
Other valid type No change No change +1

The tracker performs classification, while the memory component owns the counters.


14. Card Tracking Sequence

sequenceDiagram
    participant Battle as BattleController
    participant Tracker as PlayerTrackerComponent
    participant Deck as BattleDeck
    participant Cards as CardService
    participant Memory as EnemyMemoryComponent

    Battle->>Tracker: onCardPlayed(instanceId, targetId)
    Tracker->>Deck: getAllInstances()
    Deck-->>Tracker: CardInstance list
    Tracker->>Tracker: Match instanceId
    Tracker->>Cards: getCard(cardId)
    Cards-->>Tracker: CardConfig
    Tracker->>Tracker: Read CardType
    Tracker->>Tracker: Detect positive BLOCK
    Tracker->>Memory: recordCardPlayed(type, gainedBlock)
Loading

Block Detection

15. Positive Block Rule

The tracker considers a card defensive when at least one effect satisfies:

effect is not null
AND
effect.type == BLOCK
AND
effect.value > 0

Implementation:

private boolean grantsPositiveBlock(
    CardConfig config) {
  if (config.effects == null) {
    return false;
  }

  for (EffectConfig effect : config.effects) {
    if (effect != null
        && effect.type == EffectType.BLOCK
        && effect.value > 0) {
      return true;
    }
  }

  return false;
}

16. Block Detection Flow

flowchart TD
    Config["CardConfig"] --> Effects{"effects is null?"}

    Effects -->|Yes| False["Return false"]
    Effects -->|No| Next["Inspect next effect"]

    Next --> Null{"Effect is null?"}
    Null -->|Yes| More{"More effects?"}
    Null -->|No| Type{"Effect type is BLOCK?"}

    Type -->|No| More
    Type -->|Yes| Value{"Effect value above 0?"}

    Value -->|Yes| True["Return true"]
    Value -->|No| More

    More -->|Yes| Next
    More -->|No| False
Loading

17. Block Examples

Effects Counts as gaining block?
BLOCK 5 Yes
DAMAGE 5, BLOCK 3 Yes
BLOCK 0 No
BLOCK -2 No
HEAL 4 No
No effects No
Null effect followed by BLOCK 4 Yes
Null effects only No

A multi-effect card only needs one positive Block effect to mark the turn as defended.


Turn Settlement

18. Phase Change Listener

The tracker receives both the previous and next battle phase:

private void onPhaseChanged(
    BattlePhase previousPhase,
    BattlePhase nextPhase)

A player turn is settled when:

previousPhase == BattlePhase.PLAYER_TURN
    && nextPhase != BattlePhase.PLAYER_TURN
    && nextPhase != BattlePhase.CARD_RESOLVING

If the condition is true:

enemyMemory.settlePlayerTurn();

19. Why CARD_RESOLVING Is Excluded

Playing a card can temporarily change the phase:

PLAYER_TURN
    ↓
CARD_RESOLVING
    ↓
PLAYER_TURN

This is not the end of the player's turn.

If memory were settled during this transition:

  • cardsPlayedLastTurn would update after every card;
  • The current-turn card counter would reset too early;
  • No-block streaks could increase multiple times in one turn;
  • High-card-count behaviour could never be tracked correctly.

Therefore:

PLAYER_TURN -> CARD_RESOLVING

does not settle memory.


20. Turn Settlement Flow

flowchart TD
    Event["Phase changed"] --> Previous{"Previous phase is PLAYER_TURN?"}

    Previous -->|No| Ignore["Ignore transition"]
    Previous -->|Yes| Same{"Next phase is PLAYER_TURN?"}

    Same -->|Yes| Ignore
    Same -->|No| Resolving{"Next phase is CARD_RESOLVING?"}

    Resolving -->|Yes| Ignore
    Resolving -->|No| Settle["EnemyMemoryComponent.settlePlayerTurn"]
Loading

21. Phase Transition Examples

Previous phase Next phase Settle memory?
PLAYER_TURN CARD_RESOLVING No
CARD_RESOLVING PLAYER_TURN No
PLAYER_TURN PLAYER_END Yes
PLAYER_TURN ENEMY_TURN Yes
SETUP PLAYER_TURN No
PLAYER_END ENEMY_TURN No

Only a transition that genuinely leaves PLAYER_TURN settles the recorded turn.


22. Memory Changes During Settlement

Before settlement:

cardsPlayedThisTurn = 3
gainedBlockThisTurn = false
cardsPlayedLastTurn = 1
consecutiveTurnsWithoutBlock = 2

After settlement:

cardsPlayedThisTurn = 0
gainedBlockThisTurn = false
cardsPlayedLastTurn = 3
consecutiveTurnsWithoutBlock = 3

If the player gained positive Block during the turn:

consecutiveTurnsWithoutBlock = 0

Complete Event Flow

23. Per-Combat Data Flow

sequenceDiagram
    participant Player
    participant Battle as BattleController
    participant Tracker as PlayerTrackerComponent
    participant Deck as BattleDeck
    participant Cards as CardService
    participant Memory as EnemyMemoryComponent
    participant AI as EnemyAI

    Player->>Battle: Successfully play a card
    Battle->>Tracker: Card-play event
    Tracker->>Deck: Resolve runtime instance
    Tracker->>Cards: Resolve card definition
    Tracker->>Memory: recordCardPlayed(type, block)

    Player->>Battle: End player turn
    Battle->>Tracker: Phase-change event
    Tracker->>Memory: settlePlayerTurn()

    Memory-->>AI: Immutable PlayerMemory snapshot
Loading

24. Example Combat History

Suppose the player performs the following actions.

Turn One

Strike
Defend
End turn

Tracker output:

ATTACK card recorded
SKILL card recorded
Positive Block detected
Turn settled

Memory snapshot:

attackCardsPlayed = 1
skillCardsPlayed = 1
consecutiveTurnsWithoutBlock = 0
cardsPlayedLastTurn = 2

Turn Two

Strike
Strike
End turn

Memory snapshot:

attackCardsPlayed = 3
skillCardsPlayed = 1
consecutiveTurnsWithoutBlock = 1
cardsPlayedLastTurn = 2

Turn Three

Power card
End turn

Memory snapshot:

attackCardsPlayed = 3
skillCardsPlayed = 1
consecutiveTurnsWithoutBlock = 2
cardsPlayedLastTurn = 1

The Power card contributes to cardsPlayedLastTurn, but not to the attack or skill counters.


Error Handling

25. Invalid Input Behaviour

The tracker rejects programming errors during setup but safely ignores invalid runtime event data.

Setup errors

Condition Result
Missing EnemyMemoryComponent IllegalStateException
Null BattleController NullPointerException
Null CardService NullPointerException
Null BattleDeck NullPointerException
Calling connect() twice IllegalStateException

Runtime event problems

Condition Result
Null instance ID Event ignored
Blank instance ID Event ignored
Unknown instance ID Event ignored
Missing card definition Event ignored
Card with null type Event ignored
Null effects array Treated as no Block
Null effect entry Entry skipped

Runtime data problems do not terminate the battle.


26. Failure Handling Diagram

flowchart TD
    Input["Incoming operation"] --> Setup{"Setup operation?"}

    Setup -->|Yes| ValidDependency{"Dependencies valid?"}
    ValidDependency -->|No| Exception["Throw exception"]
    ValidDependency -->|Yes| Connect["Register listeners"]

    Setup -->|No| ValidEvent{"Runtime data resolvable?"}
    ValidEvent -->|No| Ignore["Ignore safely"]
    ValidEvent -->|Yes| Record["Write to EnemyMemoryComponent"]
Loading

Integration

27. Player Entity Composition

The player must contain both memory components:

new Entity()
    .addComponent(new EnemyMemoryComponent())
    .addComponent(new PlayerTrackerComponent());

EnemyMemoryComponent must be available when the entity is created so the tracker can locate it.


28. BattleScreen Wiring

After the battle controller, card service and battle deck exist, BattleScreen connects the tracker:

PlayerTrackerComponent playerTracker =
    Objects.requireNonNull(
        player.getComponent(
            PlayerTrackerComponent.class),
        "Battle player requires "
            + "PlayerTrackerComponent");

playerTracker.connect(
    controller,
    cardService,
    battleDeck);

Conceptually:

flowchart TD
    Factory["PlayerFactory"] --> Player["Create player entity"]
    Player --> Components["Attach tracker and memory"]
    Components --> Screen["BattleScreen"]
    Screen --> Runtime["Create controller, cards and deck"]
    Runtime --> Connect["Connect PlayerTrackerComponent"]
    Connect --> Observe["Begin observing battle events"]
Loading

The factory establishes ownership. The battle screen supplies runtime dependencies.


Testing

29. Test Architecture

The test suite mocks:

  • BattleController;
  • CardService;
  • BattleDeck.

It uses a real:

  • Entity;
  • EnemyMemoryComponent;
  • PlayerTrackerComponent.

Listeners registered with BattleController are captured using Mockito:

ArgumentCaptor<EventListener2<String, String>>
    cardPlayedCaptor;

ArgumentCaptor<
        EventListener2<
            BattlePhase,
            BattlePhase>>
    phaseChangeCaptor;

The tests invoke the captured listeners directly to simulate battle events.


30. Test Coverage

Component ownership

  • Requires EnemyMemoryComponent on the same entity.

Connection validation

  • Rejects a null battle controller;
  • Rejects a null card service;
  • Rejects a null battle deck;
  • Rejects a second connection;
  • Registers both required listeners.

Card classification

  • Records attack cards;
  • Records skill cards;
  • Counts other card types without classifying them as attack or skill;
  • Ignores a card with a null type.

Block detection

  • Detects a positive Block effect;
  • Finds Block among multiple effects;
  • Rejects zero Block;
  • Rejects negative Block;
  • Safely ignores null effects.

Invalid event data

  • Ignores null IDs;
  • Ignores blank IDs;
  • Ignores unknown instance IDs;
  • Ignores missing card definitions.

Phase handling

  • Does not settle during card resolution;
  • Settles when the player turn ends;
  • Ignores unrelated phase changes.

31. Test Coverage Diagram

flowchart TD
    Tests["PlayerTrackerComponentTest"] --> Ownership["ECS ownership"]
    Tests --> Connection["Connection validation"]
    Tests --> Classification["Card classification"]
    Tests --> Block["Block detection"]
    Tests --> Phases["Phase transitions"]
    Tests --> Invalid["Invalid runtime data"]

    Classification --> Attack["ATTACK"]
    Classification --> Skill["SKILL"]
    Classification --> Other["Other card types"]

    Block --> Positive["Positive Block"]
    Block --> Zero["Zero Block"]
    Block --> Negative["Negative Block"]
    Block --> Multiple["Multiple effects"]

    Phases --> Resolving["CARD_RESOLVING ignored"]
    Phases --> End["Real turn end settled"]
Loading

Current Scope and Limitations

32. Base Card Configuration

The current tracker resolves:

CardInstance
    ↓
cardId
    ↓
CardService
    ↓
CardConfig

It reads Block information from:

config.effects

This means the current implementation observes the base CardConfig.

It does not currently resolve the played card through CardResolver into a ResolvedCard.

Consequently, if a card upgrade changes its type or Block value only at runtime, the tracker may still observe the base card definition.

The Wiki documents this as the current implementation boundary rather than claiming upgraded effects are already supported.


33. Target Information

The card-play event includes targetId, but the current tracker does not store it.

This is intentional for the current player-memory model, which records:

  • Attack-card preference;
  • Skill-card preference;
  • Number of cards played;
  • Consecutive turns without gaining Block.

Possible future memory features may use the target ID to track:

  • Preferred enemy targets;
  • Focus-fire behaviour;
  • Repeated targeting of low-health enemies;
  • Avoidance of guarded enemies.

These features are outside the current component scope.


34. Per-Combat Lifetime

PlayerTrackerComponent and EnemyMemoryComponent belong to one battle.

They do not currently:

  • Persist data between battles;
  • Write to save data;
  • Aggregate achievement progress;
  • Track map decisions;
  • Track inventory history;
  • Track rewards collected outside combat.

The tracker observes the current combat only.


35. Design Summary

PlayerTrackerComponent is an event-driven adapter between the battle system and the Enemy Memory System.

Its complete responsibility is:

flowchart TD
    Events["Observe confirmed battle events"] --> Resolve["Resolve runtime card instance"]
    Resolve --> Classify["Classify card and Block"]
    Classify --> Write["Write semantic information to memory"]

    Phase["Observe battle phase"] --> Validate["Detect real player-turn exit"]
    Validate --> Settle["Settle per-turn memory"]
Loading

The component provides:

  • Clear separation between event observation and memory storage;
  • ECS ownership through the player entity;
  • Runtime dependency injection;
  • Protection against duplicate connections;
  • Safe handling of invalid event data;
  • Correct card-resolution phase handling;
  • Attack and skill classification;
  • Positive Block detection;
  • A stable input boundary for adaptive enemy AI.

Table of Contents

Home

Game Engine

Getting Started

Entities and Components

Service Locator

Loading Resources

Logging

Unit Testing

Debug Terminal

Input Handling

UI

Animations

Audio

AI

Physics

Game Screens and Areas

Terrain

Concurrency & Threading

Settings

Encounters
Tutorials and Contextual Help (Team 2)
Cards (Team 6)
Story (Team 6)
Level design, battle system, turn order, spawn (Team 3)
Cards, Save/Load & Bestiary (Team 5)
Rewards and Battle Settlement (Team 7)

Troubleshooting

MacOS Setup Guide

Clone this wiki locally