Skip to content
LI WANGXIN edited this page Oct 6, 2026 · 8 revisions

Boss AI System

1. Overview

BossAI controls the decision-making behaviour of the Boss enemy, Vitium.

Unlike ordinary enemies that rely on short action cycles, the Boss combines several decision layers:

Health-based phase state machine
        +
Phase-specific action weights
        +
Global battle pressure
        +
Player-memory reaction
        +
Behaviour constraints
        +
Weighted random selection

The Boss currently supports four actions:

Boss move Result
ATTACK Deals the Boss's current attack value as damage
DEFEND Grants armour, with stronger defence in later phases
SILENCE Prevents the player from playing cards temporarily
DAMAGE_ON_CARD_PLAY Damages the player whenever a card is played

BossAI only decides which action should be used. It does not directly change health, armour or status effects. Instead, it returns an EnemyIntent, which is executed by EnemyBehaviourComponent.


2. File Locations

Implementation

source/core/src/main/com/csse3200/game/components/enemy/EnemyAI/BossAI.java

Tests

source/core/src/test/com/csse3200/game/components/enemy/EnemyAI/BossAITest.java

Related classes

EnemyAI.java
EnemyAIContext.java
EnemyAIFactory.java
EnemyBehaviourComponent.java
EnemyIntent.java
IntentEffectType.java
PlayerMemory.java
EnemyMemoryComponent.java

3. Boss Configuration

The Boss is defined in configs/enemies.json:

{
  "id": "boss_knight",
  "name": "Vitium",
  "health": 150,
  "baseAttack": 14,
  "armour": 8,
  "tier": "BOSS",
  "behaviour": "boss",
  "sprite": "images/enemies/boss_knight.atlas"
}

The important field for AI selection is:

"behaviour": "boss"

EnemyAIFactory maps this behaviour identifier to a new BossAI instance:

case BOSS -> new BossAI();

Every Boss entity therefore owns an independent AI instance with its own:

  • Phase;
  • Previous move;
  • Consecutive attack counter;
  • Debuff cooldown.

4. High-Level Architecture

flowchart LR
    Config["enemies.json"] --> EnemyFactory["EnemyFactory"]
    EnemyFactory --> Behaviour["EnemyBehaviourComponent"]
    Behaviour --> AIFactory["EnemyAIFactory"]
    AIFactory --> Boss["BossAI"]

    PlayerActions["Player actions"] --> Tracker["PlayerTrackerComponent"]
    Tracker --> MemoryComponent["EnemyMemoryComponent"]
    MemoryComponent --> Snapshot["PlayerMemory snapshot"]

    PlayerStats["Player combat stats"] --> Context["EnemyAIContext"]
    BossStats["Boss combat stats"] --> Context
    Snapshot --> Context

    Context --> Boss
    Boss --> Intent["EnemyIntent"]
    Intent --> Behaviour
    Behaviour --> Stats["CombatStatsComponent"]
Loading

The Boss does not directly access the player entity or the memory component.

Instead, it receives a read-only snapshot through EnemyAIContext.


5. UML Class Diagram

classDiagram
    class EnemyAI {
        <<interface>>
        +decide(context) EnemyIntent
    }

    class BossAI {
        -Random random
        -BossPhase currentPhase
        -BossMove previousMove
        -int consecutiveAttacks
        -int debuffCooldownRemaining
        +BossAI()
        ~BossAI(random)
        +decide(context) EnemyIntent
        -shouldPrioritiseSilence(context) boolean
        -updatePhase(context)
        -tickDebuffCooldown()
        -calculatePressure(context) PressureLevel
        -getBaseWeights(phase)
        -applyPressure(weights, pressure)
        -applyConstraints(weights, context)
        -selectWeightedMove(weights) BossMove
        -createIntent(move, context) EnemyIntent
        -recordMove(move)
    }

    class EnemyAIContext {
        -int playerHealth
        -int enemyHealth
        -int enemyMaxHealth
        -int enemyAttack
        -int enemyArmour
        -EnemyIntent previousIntent
        -int turnNumber
        -PlayerMemory playerMemory
        +getPlayerHealth() int
        +getEnemyHealthRatio() float
        +getEnemyAttack() int
        +getEnemyArmour() int
        +getTurnNumber() int
        +getPlayerMemory() PlayerMemory
    }

    class PlayerMemory {
        <<record>>
        +attackCardsPlayed() int
        +skillCardsPlayed() int
        +consecutiveTurnsWithoutBlock() int
        +cardsPlayedLastTurn() int
        +favoursAttack() boolean
        +isUndefended() boolean
    }

    class EnemyIntent {
        -IntentType type
        -int value
        -IntentEffectType effectType
        -int duration
        +attack(damage) EnemyIntent
        +defend(armour) EnemyIntent
        +debuff(effectType, value, duration) EnemyIntent
    }

    class BossPhase {
        <<enumeration>>
        PHASE_ONE
        PHASE_TWO
        ENRAGED
    }

    class BossMove {
        <<enumeration>>
        ATTACK
        DEFEND
        SILENCE
        DAMAGE_ON_CARD_PLAY
        ~isDebuff() boolean
    }

    class PressureLevel {
        <<enumeration>>
        LOW
        MEDIUM
        HIGH
        CRITICAL
    }

    BossAI ..|> EnemyAI : implements
    BossAI --> EnemyAIContext : reads
    BossAI --> EnemyIntent : creates
    BossAI --> BossPhase : maintains
    BossAI --> BossMove : selects
    BossAI --> PressureLevel : calculates
    EnemyAIContext o-- PlayerMemory : contains
Loading

6. Internal State

Each BossAI instance maintains four mutable state values:

private BossPhase currentPhase = BossPhase.PHASE_ONE;
private BossMove previousMove;
private int consecutiveAttacks;
private int debuffCooldownRemaining;
Field Responsibility
currentPhase Stores the highest phase reached by the Boss
previousMove Records the action selected during the previous decision
consecutiveAttacks Counts uninterrupted attack actions
debuffCooldownRemaining Prevents repeated applications of Boss Debuffs

These fields are instance fields rather than static fields. Multiple Boss instances therefore do not share decision state.


7. Complete Decision Pipeline

Every call to:

BossAI.decide(context)

performs the following process:

flowchart TD
    Start["decide(context)"] --> NullCheck{"Is context null?"}

    NullCheck -->|Yes| Error["Throw NullPointerException"]
    NullCheck -->|No| Phase["Update Boss phase"]

    Phase --> Cooldown["Tick Debuff cooldown"]
    Cooldown --> Pressure["Calculate battle pressure"]
    Pressure --> BaseWeights["Create phase weight table"]
    BaseWeights --> PressureWeights["Apply pressure adjustments"]
    PressureWeights --> Constraints["Apply behaviour constraints"]

    Constraints --> MemoryCheck{"At least 4 cards last turn and cooldown ready?"}

    MemoryCheck -->|Yes| Silence["Select SILENCE"]
    MemoryCheck -->|No| Weighted["Perform weighted selection"]

    Silence --> Record["Record selected move"]
    Weighted --> Record

    Record --> Convert["Convert move into EnemyIntent"]
    Convert --> Return["Return EnemyIntent"]
Loading

Equivalent implementation structure:

updatePhase(context);
tickDebuffCooldown();

PressureLevel pressure =
    calculatePressure(context);

EnumMap<BossMove, Integer> weights =
    getBaseWeights(currentPhase);

applyPressure(weights, pressure);
applyConstraints(weights, context);

BossMove selectedMove;

if (shouldPrioritiseSilence(context)) {
  selectedMove = BossMove.SILENCE;
} else {
  selectedMove = selectWeightedMove(weights);
}

recordMove(selectedMove);

return createIntent(selectedMove, context);

Phase System

8. Boss Phases

The Boss has three health-based phases:

public enum BossPhase {
  PHASE_ONE,
  PHASE_TWO,
  ENRAGED
}
Phase Health condition
PHASE_ONE Health above 65%
PHASE_TWO Health above 30% and at or below 65%
ENRAGED Health at or below 30%

Phase calculation:

if (healthRatio <= 0.30f) {
  desiredPhase = BossPhase.ENRAGED;
} else if (healthRatio <= 0.65f) {
  desiredPhase = BossPhase.PHASE_TWO;
} else {
  desiredPhase = BossPhase.PHASE_ONE;
}

9. Phase State Diagram

stateDiagram-v2
    [*] --> PHASE_ONE

    PHASE_ONE --> PHASE_TWO: Health at or below 65 percent
    PHASE_ONE --> ENRAGED: Health at or below 30 percent
    PHASE_TWO --> ENRAGED: Health at or below 30 percent

    PHASE_TWO --> PHASE_TWO: Boss is healed
    ENRAGED --> ENRAGED: Boss is healed
Loading

Phase transitions are irreversible.

The Boss only changes phase when the desired phase is later than the current phase:

if (desiredPhase.ordinal() > currentPhase.ordinal()) {
  currentPhase = desiredPhase;
}

Valid transitions:

PHASE_ONE -> PHASE_TWO
PHASE_ONE -> ENRAGED
PHASE_TWO -> ENRAGED

Invalid transitions:

PHASE_TWO -> PHASE_ONE
ENRAGED -> PHASE_TWO
ENRAGED -> PHASE_ONE

Healing the Boss cannot reduce its difficulty after a later phase has been reached.


Weighted Action System

10. Phase One Weights

Move Weight
ATTACK 45
DEFEND 55
SILENCE 0
DAMAGE_ON_CARD_PLAY 0

Phase One is normally defensive and does not randomly select Debuffs.

However, the memory-driven SILENCE response may still activate if the player used at least four cards during the previous turn.


11. Phase Two Weights

Move Weight
ATTACK 65
DEFEND 35
SILENCE 20
DAMAGE_ON_CARD_PLAY 10

Phase Two becomes more aggressive and introduces both Debuff actions.


12. Enraged Weights

Move Weight
ATTACK 90
DEFEND 10
SILENCE 15
DAMAGE_ON_CARD_PLAY 25

The Enraged phase strongly favours attacking and increases the likelihood of punishing repeated card usage.


13. Weight Selection

The final action weights are added together:

int totalWeight =
    weights.values()
        .stream()
        .mapToInt(Integer::intValue)
        .sum();

A random value is selected inside the total range:

int roll = random.nextInt(totalWeight);

The value is then consumed in enum order until the selected range is reached.

flowchart LR
    Roll["Random roll"] --> Attack["ATTACK range"]
    Attack --> Defend["DEFEND range"]
    Defend --> Silence["SILENCE range"]
    Silence --> CardDamage["DAMAGE_ON_CARD_PLAY range"]
Loading

A higher weight gives an action a larger section of the random range.


Global Pressure System

14. Pressure Inputs

The pressure system represents the urgency of the battle.

Pressure is calculated from:

  • Number of battle turns;
  • Percentage of Boss health lost;
  • Player health relative to the Boss's attack.
flowchart TD
    Turn["Battle duration"] --> Total["Pressure score"]
    BossHealth["Boss health lost"] --> Total
    PlayerHealth["Player health"] --> Total

    Total --> Level{"Classify pressure"}

    Level --> Low["LOW"]
    Level --> Medium["MEDIUM"]
    Level --> High["HIGH"]
    Level --> Critical["CRITICAL"]
Loading

15. Turn Pressure

Longer battles make the Boss more aggressive:

pressure +=
    Math.min(
        30,
        Math.max(0, context.getTurnNumber() - 1) * 3);
Turn Added pressure
1 0
2 3
3 6
5 12
8 21
11 or later 30

The turn contribution cannot exceed 30.


16. Boss Health Pressure

Pressure increases as the Boss loses health:

pressure +=
    Math.round(
        (1f - context.getEnemyHealthRatio()) * 40f);
Remaining Boss health Added pressure
100% 0
75% 10
50% 20
30% 28
10% 36
0% 40

17. Player Health Pressure

The Boss becomes more aggressive when it is close to defeating the player:

if (context.getPlayerHealth() > 0) {
  if (context.getPlayerHealth()
      <= context.getEnemyAttack()) {
    pressure += 30;
  } else if (context.getPlayerHealth()
      <= context.getEnemyAttack() * 2) {
    pressure += 15;
  }
}
Player condition Added pressure
More than two attacks from defeat 0
Within two attacks of defeat 15
Within one attack of defeat 30

18. Pressure Levels

Score Pressure level
0–19 LOW
20–44 MEDIUM
45–69 HIGH
70 or more CRITICAL
flowchart TD
    Score["Calculated pressure"] --> CriticalCheck{"Score at least 70?"}
    CriticalCheck -->|Yes| Critical["CRITICAL"]
    CriticalCheck -->|No| HighCheck{"Score at least 45?"}

    HighCheck -->|Yes| High["HIGH"]
    HighCheck -->|No| MediumCheck{"Score at least 20?"}

    MediumCheck -->|Yes| Medium["MEDIUM"]
    MediumCheck -->|No| Low["LOW"]
Loading

19. Pressure Adjustments

Pressure Attack adjustment Defence adjustment
LOW -10 +10
MEDIUM 0 0
HIGH +15 -15
CRITICAL +25 -25

Pressure only changes ATTACK and DEFEND.

It does not increase Debuff weights. This prevents high pressure from turning the Boss into a continuous Debuff loop.


Player Memory Integration

20. Memory Data Flow

The Boss reads the player's previous-turn card count through the Enemy Memory System:

sequenceDiagram
    participant Player
    participant Tracker as PlayerTrackerComponent
    participant Memory as EnemyMemoryComponent
    participant Behaviour as EnemyBehaviourComponent
    participant Context as EnemyAIContext
    participant Boss as BossAI

    Player->>Tracker: Successfully plays cards
    Tracker->>Memory: Record card information
    Tracker->>Memory: Settle completed player turn
    Memory-->>Behaviour: PlayerMemory snapshot
    Behaviour->>Context: Create context with snapshot
    Context-->>Boss: cardsPlayedLastTurn
    Boss->>Boss: Evaluate SILENCE reaction
Loading

The AI receives immutable memory data and cannot modify the memory component.


21. Memory-Driven SILENCE

The Boss reads:

context
    .getPlayerMemory()
    .cardsPlayedLastTurn();

The trigger condition is:

cardsPlayedLastTurn >= 4
AND
debuffCooldownRemaining == 0

Implementation:

private boolean shouldPrioritiseSilence(
    EnemyAIContext context) {
  return debuffCooldownRemaining == 0
      && context
          .getPlayerMemory()
          .cardsPlayedLastTurn()
          >= SILENCE_CARD_THRESHOLD;
}

The threshold is stored centrally:

private static final int SILENCE_CARD_THRESHOLD = 4;

22. Why SILENCE Is Directly Prioritised

The memory response does not merely increase the SILENCE weight.

When the trigger condition is satisfied, the Boss selects SILENCE before weighted random selection:

if (shouldPrioritiseSilence(context)) {
  selectedMove = BossMove.SILENCE;
} else {
  selectedMove = selectWeightedMove(weights);
}

This makes the response:

  • Deterministic;
  • Easy for the player to learn;
  • Easy to test;
  • Independent of random rolls;
  • Clearly connected to the player's previous behaviour.

The regular weighted system remains active whenever the memory trigger is not satisfied.


23. Memory Decision Diagram

flowchart TD
    Start["Read PlayerMemory"] --> Count["Read cardsPlayedLastTurn"]

    Count --> Threshold{"At least 4 cards?"}

    Threshold -->|No| Random["Use weighted selection"]
    Threshold -->|Yes| Cooldown{"Debuff cooldown ready?"}

    Cooldown -->|Yes| Silence["Select SILENCE"]
    Cooldown -->|No| Random

    Silence --> Record["Start shared Debuff cooldown"]
    Random --> Result["Return selected move"]
    Record --> Result
Loading

24. Phase One Exception

Phase One has a base SILENCE weight of zero.

Therefore, Phase One cannot randomly select SILENCE.

However, the memory response is an intentional exception:

Phase One
+
At least four cards played last turn
+
Debuff cooldown ready
=
SILENCE

This allows the Boss to react to excessive card usage even during the early stage of the encounter.

Without the memory trigger, Phase One retains its original attack-and-defend behaviour.


25. Backwards Compatibility

The original seven-argument EnemyAIContext constructor remains available.

When no memory is provided, it uses:

PlayerMemory.empty()

This produces:

attackCardsPlayed = 0
skillCardsPlayed = 0
consecutiveTurnsWithoutBlock = 0
cardsPlayedLastTurn = 0

Therefore:

cardsPlayedLastTurn >= 4

is false, and the Boss retains its original weighted behaviour.

Existing AI classes and older tests do not need to provide player memory.


Debuff System

26. SILENCE

SILENCE creates the following intent:

EnemyIntent.debuff(
    IntentEffectType.SILENCE,
    1,
    2);
Property Value
Effect SILENCE
Value 1
Duration 2 player turns

The effect prevents the player from playing cards while active.


27. Damage on Card Play

DAMAGE_ON_CARD_PLAY creates:

EnemyIntent.debuff(
    IntentEffectType.DAMAGE_ON_CARD_PLAY,
    3,
    3);
Property Value
Effect DAMAGE_ON_CARD_PLAY
Damage per card 3
Duration 3 player turns

While active, the player loses health whenever a card is played.


28. Shared Debuff Cooldown

Both Boss Debuffs share one cooldown:

SILENCE
DAMAGE_ON_CARD_PLAY

After either Debuff is selected:

debuffCooldownRemaining =
    DEBUFF_COOLDOWN_TURNS;

The configured cooldown is:

private static final int DEBUFF_COOLDOWN_TURNS = 3;

While the cooldown is active, all Debuff weights are set to zero:

if (debuffCooldownRemaining > 0) {
  for (BossMove move : BossMove.values()) {
    if (move.isDebuff()) {
      weights.put(move, 0);
    }
  }
}

The memory-trigger condition also checks the same cooldown. Therefore, player memory cannot bypass it.


29. Cooldown State Diagram

stateDiagram-v2
    [*] --> READY

    READY --> COOLDOWN_3: Debuff selected
    COOLDOWN_3 --> COOLDOWN_2: Next decision
    COOLDOWN_2 --> COOLDOWN_1: Next decision
    COOLDOWN_1 --> READY: Next decision

    READY --> READY: Attack or defend
    COOLDOWN_3 --> COOLDOWN_3: Debuffs unavailable
    COOLDOWN_2 --> COOLDOWN_2: Debuffs unavailable
Loading

Example:

Decision Cooldown before selection Result
1 0 Debuff selected; cooldown becomes 3
2 2 after ticking Debuffs unavailable
3 1 after ticking Debuffs unavailable
4 0 after ticking Debuffs available again

Behaviour Constraints

30. High-Armour Constraint

The Boss does not defend when it already has substantial armour:

if (context.getEnemyArmour()
    >= HIGH_ARMOUR_THRESHOLD) {
  weights.put(BossMove.DEFEND, 0);
}

Threshold:

private static final int HIGH_ARMOUR_THRESHOLD = 8;

This prevents the Boss from wasting turns repeatedly adding armour.


31. Repeated Defence Constraint

If the previous action was DEFEND, the next defence weight is divided by four:

if (previousMove == BossMove.DEFEND) {
  int defendWeight =
      weights.getOrDefault(
          BossMove.DEFEND,
          0);

  weights.put(
      BossMove.DEFEND,
      defendWeight / 4);
}

Defending remains possible, but becomes significantly less likely.


32. Consecutive Attack Constraint

After two or more consecutive attacks:

ATTACK -20
DEFEND +20

Implementation:

if (consecutiveAttacks >= 2) {
  adjustWeight(
      weights,
      BossMove.ATTACK,
      -20);

  adjustWeight(
      weights,
      BossMove.DEFEND,
      20);
}

This encourages action variation without completely forbidding another attack.


33. Safe Weight Handling

Weight adjustments are clamped to zero:

weights.put(
    move,
    Math.max(0, currentWeight + adjustment));

Negative action weights are therefore impossible.

After applying constraints, the Boss confirms that at least one move remains available:

if (!hasAvailableMove) {
  weights.put(BossMove.ATTACK, 1);
}

If every weight becomes zero, ATTACK becomes the safe fallback.


Intent Creation and Execution

34. Move-to-Intent Mapping

flowchart LR
    Attack["BossMove.ATTACK"] --> AttackIntent["EnemyIntent.attack"]
    Defend["BossMove.DEFEND"] --> DefendIntent["EnemyIntent.defend"]
    Silence["BossMove.SILENCE"] --> SilenceIntent["SILENCE Debuff intent"]
    CardDamage["BossMove.DAMAGE_ON_CARD_PLAY"] --> CardIntent["Card-play damage Debuff intent"]
Loading

Implementation:

return switch (move) {
  case ATTACK ->
      EnemyIntent.attack(
          context.getEnemyAttack());

  case DEFEND ->
      EnemyIntent.defend(
          getDefendAmount(currentPhase));

  case SILENCE ->
      EnemyIntent.debuff(
          IntentEffectType.SILENCE,
          SILENCE_VALUE,
          SILENCE_DURATION);

  case DAMAGE_ON_CARD_PLAY ->
      EnemyIntent.debuff(
          IntentEffectType.DAMAGE_ON_CARD_PLAY,
          CARD_PLAY_DAMAGE,
          CARD_PLAY_DAMAGE_DURATION);
};

35. Defence Strength

Defence becomes stronger in later phases:

Phase Armour granted
PHASE_ONE 4
PHASE_TWO 6
ENRAGED 8
private int getDefendAmount(
    BossPhase phase) {
  return switch (phase) {
    case PHASE_ONE -> 4;
    case PHASE_TWO -> 6;
    case ENRAGED -> 8;
  };
}

36. Execution Sequence

sequenceDiagram
    participant Battle
    participant Behaviour as EnemyBehaviourComponent
    participant Context as EnemyAIContext
    participant Boss as BossAI
    participant Intent as EnemyIntent
    participant Stats as CombatStatsComponent

    Battle->>Behaviour: rollIntent()
    Behaviour->>Context: Build combat snapshot
    Behaviour->>Boss: decide(context)
    Boss->>Boss: Update phase and cooldown
    Boss->>Boss: Calculate pressure and weights
    Boss->>Boss: Check PlayerMemory
    Boss->>Intent: Create selected intent
    Boss-->>Behaviour: Return intent
    Behaviour-->>Battle: Telegraph intent

    Battle->>Behaviour: executeIntent(player)
    Behaviour->>Stats: Apply damage, armour or status effect
Loading

Decision-making and action execution remain separate.


Randomness and Testability

37. Random Injection

Runtime instances use:

public BossAI() {
  this(new Random());
}

Tests can provide a controlled random source:

BossAI(Random random) {
  this.random =
      Objects.requireNonNull(
          random,
          "random cannot be null");
}

The package-private constructor allows tests to use:

  • FixedRollRandom;
  • SequenceRandom.

This makes weighted choices deterministic and repeatable.

The memory-triggered SILENCE does not depend on randomness.


38. Boss AI Test Coverage

BossAITest verifies the following behaviour.

Phase behaviour

  • Starts in PHASE_ONE;
  • Enters PHASE_TWO at 65% health;
  • Enters ENRAGED at 30% health;
  • Never returns to an earlier phase after healing.

Normal actions

  • Produces attack intents;
  • Produces defence intents;
  • Uses stronger defence in later phases;
  • Avoids defending at high armour;
  • Becomes more aggressive under pressure.

Internal state

  • Records the previous move;
  • Counts consecutive attacks;
  • Resets the attack counter after defending;
  • Resets the attack counter after using a Debuff;
  • Rejects a null context.

Debuff behaviour

  • Does not randomly use Debuffs in Phase One;
  • Can use SILENCE in Phase Two;
  • Can use DAMAGE_ON_CARD_PLAY while Enraged;
  • Cannot use Debuffs repeatedly;
  • Can use a Debuff again after cooldown expiration.

Memory behaviour

  • Exactly four cards trigger prioritised SILENCE;
  • Fewer than four cards do not trigger the response;
  • Memory-driven SILENCE records the correct move;
  • Memory-driven SILENCE starts the shared cooldown;
  • Memory cannot bypass an active cooldown;
  • Memory may trigger SILENCE again after cooldown expiration;
  • DAMAGE_ON_CARD_PLAY prevents immediate memory-driven SILENCE;
  • Empty memory preserves the original behaviour.

39. Memory Test Flow

flowchart TD
    Setup["Create PlayerMemory"] --> FourCards{"cardsPlayedLastTurn >= 4?"}

    FourCards -->|No| NormalTest["Expect normal weighted action"]
    FourCards -->|Yes| Ready{"Cooldown is zero?"}

    Ready -->|Yes| SilenceTest["Expect SILENCE"]
    Ready -->|No| BlockedTest["Expect non-Debuff action"]

    SilenceTest --> CooldownTest["Verify cooldown becomes 3"]
    BlockedTest --> SharedTest["Verify shared cooldown remains active"]
Loading

Behaviour Examples

40. Early Battle

Boss health: 100%
Turn: 1
Boss armour: 0
Cards played last turn: 1

Result:

Phase: PHASE_ONE
Pressure: LOW
Memory trigger: false

Weights:

Base:
ATTACK = 45
DEFEND = 55

After LOW pressure:
ATTACK = 35
DEFEND = 65

The Boss is more likely to defend.


41. Player Uses Four Cards

Boss health: 100%
Phase: PHASE_ONE
Cards played last turn: 4
Debuff cooldown: 0

Although the Phase One SILENCE weight is zero, the memory condition is satisfied.

Result:

Selected move: SILENCE
Cooldown after selection: 3

No random roll is needed.


42. Memory Trigger During Cooldown

Cards played last turn: 5
Debuff cooldown after ticking: 2

The card threshold is satisfied, but the cooldown is not ready.

Result:

Memory-driven SILENCE is blocked
SILENCE weight is zero
DAMAGE_ON_CARD_PLAY weight is zero
Boss selects ATTACK or DEFEND

43. Shared Cooldown Example

Decision one:

Phase: ENRAGED
Selected move: DAMAGE_ON_CARD_PLAY
Cooldown becomes 3

Decision two:

Cards played last turn: 4
Cooldown becomes 2 after ticking

Result:

SILENCE is not selected

This confirms that both Debuffs use the same cooldown.


44. Critical Pressure Example

Turn pressure: 24
Boss-health pressure: 28
Low-player-health pressure: 30
Total pressure: 82

Result:

Pressure level: CRITICAL
ATTACK weight: +25
DEFEND weight: -25

Unless the memory-driven SILENCE condition is active, the Boss strongly favours attacking.


Design Principles

45. Separation of Responsibilities

System Responsibility
PlayerTrackerComponent Observes player actions
EnemyMemoryComponent Stores per-combat behaviour data
PlayerMemory Provides an immutable memory snapshot
EnemyAIContext Transfers combat and memory data to AI
BossAI Selects an action
EnemyIntent Describes the selected action
EnemyBehaviourComponent Telegraphs and executes the intent
CombatStatsComponent Applies health, armour and status changes

The Boss AI is not coupled to battle events, the player entity or mutable memory storage.


46. Layered Decision Model

flowchart TD
    Phase["Phase layer"] --> Pressure["Pressure layer"]
    Pressure --> Constraints["Constraint layer"]
    Constraints --> Memory["Memory reaction layer"]
    Memory --> Selection["Weighted selection or direct priority"]
    Selection --> Intent["Intent conversion"]
Loading
Layer Purpose
Phase Defines broad Boss behaviour based on health
Pressure Responds to current battle urgency
Constraints Prevents repetitive or invalid decisions
Memory Responds to the player's previous behaviour
Selection Adds controlled variation when no direct response is needed
Intent conversion Connects AI decisions to the battle system

47. Extending the Boss

A new Boss action normally requires:

  1. Add a new BossMove;
  2. Add its phase weights;
  3. Add any required constraints;
  4. Map it to an EnemyIntent;
  5. Add execution support if the effect is new;
  6. Add an intent icon or animation;
  7. Add deterministic tests;
  8. Update this Wiki.

The architecture has already expanded from:

ATTACK
DEFEND

to:

ATTACK
DEFEND
SILENCE
DAMAGE_ON_CARD_PLAY

without replacing the phase, pressure or weighted-selection systems.


48. Summary

The complete Boss decision model is:

flowchart TD
    Health["Boss health"] --> Phase["Select irreversible phase"]
    Battle["Turn and health information"] --> Pressure["Calculate pressure"]
    Phase --> Weights["Create phase weights"]
    Pressure --> Weights
    Weights --> Constraints["Apply behaviour constraints"]

    Memory["PlayerMemory"] --> Reaction{"High card usage and cooldown ready?"}
    Constraints --> Reaction

    Reaction -->|Yes| Silence["Prioritise SILENCE"]
    Reaction -->|No| Random["Weighted action selection"]

    Silence --> History["Record move and cooldown"]
    Random --> History
    History --> Intent["Return EnemyIntent"]
Loading

BossAI provides:

  • Three irreversible combat phases;
  • Phase-specific action weights;
  • Global pressure calculation;
  • Controlled random behaviour;
  • Attack and defence variation;
  • Two status-effect actions;
  • A shared Debuff cooldown;
  • Memory-driven adaptation to player card usage;
  • Backwards compatibility through empty memory;
  • Deterministic and repeatable unit tests;
  • Clear separation between decision and execution.

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