Skip to main content
Every match in Beast Card Clash is driven by a five-phase finite state machine (FSM). Rather than a monolithic battle script, each phase of the game — setup, bot turns, the human turn, scoring, and the results screen — is encapsulated in its own BattleState class. The BattleManager node owns the current state and triggers transitions. This design makes each phase independently testable and easy to extend without touching unrelated logic.

State diagram

Class hierarchy

All battle states share a common base. The inheritance chain is:
Node
└── BaseState
    └── BattleState
        ├── BattleStart
        ├── BattleLoop
        ├── BattleTurn
        ├── BattleReferee
        └── BattleEnd
BaseState is a generic state base that provides a controlled_node property. BattleState specialises it with a typed manager property that gives every state direct access to the BattleManager:
class_name BattleState extends BaseState

var manager: BattleManager:
    set(value):
        controlled_node = value
    get:
        return controlled_node
This means any state can reach shared battle data and trigger transitions through manager without coupling states to each other.

The five states

BattleStart — setup

File: assets/battle/states/start.gdBattleStart runs once at the beginning of every match. Its job is to build the initial game world before any player or bot acts.Responsibilities:
  • Calls MusicManager.play_music("battle") to start the battle music
  • Calls manager.setup_player() — creates the human player from PlayerStats, builds their deck
  • Calls manager.setup_bots() — creates 1–3 bots with random decks, shuffles turn order
  • Calls manager.setup_ui() — refreshes player stats panel, seeds hand display, hides end UI
  • Calls manager.setup_world() — disables dice via BattleWorld
Transitions:
ConditionNext state
Human player goes firstBattleTurn
A bot goes firstBattleLoop
File: assets/battle/states/loop.gdBattleLoop handles all automated (bot) turns. It runs without player interaction and loops until it is the human’s turn or the round ends.Responsibilities:
  • Determines whose turn is next in the turn order
  • Rolls the dice for the active bot
  • Selects a rock for the bot to move to
  • Chooses a card from the bot’s hand and plays it
  • Advances the turn counter
Transitions:
ConditionNext state
Next in turn order is a botStays in BattleLoop (loops)
Next in turn order is the humanBattleTurn
No turns remain in the roundBattleReferee
File: assets/battle/states/turn.gdBattleTurn is the interactive phase where the human player takes their action. The scene waits for input at each step.Responsibilities:
  • Enables the 3D dice — the player clicks it to roll
  • Highlights valid rock positions based on the dice result
  • Moves the player character to the chosen rock
  • Reveals the player’s hand and enables card selection
  • Plays the selected card
  • Passes the turn when the card has been played
Transitions:
ConditionNext state
Next in turn order is a botBattleLoop
Next in turn order is the humanStays in BattleTurn
No turns remain in the roundBattleReferee
The turn-passing logic uses the same criteria in both BattleTurn and BattleLoop. Any changes to turn order rules must be applied consistently in both states.
File: assets/battle/states/referee.gdBattleReferee runs after all players have taken their turns in a round. It evaluates the cards played, applies damage, and decides whether the match continues.Responsibilities:
  • Collects every card played during the round
  • Compares cards according to elemental rules and card values
  • Applies damage to the appropriate players
  • Removes defeated players (those whose health reaches zero)
  • Determines whether the match should continue
Transitions:
ConditionNext state
2 or more players remain, new roundBattleLoop or BattleStart (re-setup)
Only 1 player remainsBattleEnd
See Battle mechanics for the card comparison rules that this state enforces.
File: assets/battle/states/end.gdBattleEnd is the terminal state. It runs once after BattleReferee determines a winner.Responsibilities:
  • Calculates the final player ranking based on elimination order
  • Displays the end screen with results (winner, placements)
  • Waits for the player to press the back button
Transitions:
ConditionNext state
Player presses back / exitReturns to start menu via SceneManager

How BattleManager drives transitions

The BattleManager node holds a reference to the current active state. States do not transition themselves — they call a method on manager to request a transition, passing the name or reference of the next state. This keeps the state graph easy to audit in one place.
# Example: requesting a transition from inside a state
manager.transition_to(manager.state_loop)
See Battle manager for the full implementation details.

Adding a new state

Follow these steps to introduce a new phase to the battle:
1

Create the state script

Add a new GDScript file under assets/battle/states/. Extend BattleState and implement the enter(), exit(), and update() methods inherited from BaseState.
class_name BattleMyPhase extends BattleState

func enter() -> void:
    # Called when this state becomes active
    pass

func exit() -> void:
    # Called just before leaving this state
    pass
2

Register the state in BattleManager

Open assets/battle/battle_manager.gd (or the scene file) and add your new state as a child node or an exported variable, following the same pattern as the existing five states.
3

Define the transitions

In the states that should lead to your new state, add a call to manager.transition_to(manager.state_my_phase) at the appropriate point. Update any state that your new state should transition to as well.
4

Update the diagram

Keep the state diagram in .docs/ (and on this page) in sync with the new transition edges so the next developer has an accurate map.

Battle manager

The node that owns the state machine and drives transitions between states.

Battle mechanics

Player-facing rules that the Referee state enforces.

Architecture overview

How the state machine fits into the broader codebase design.

Card system

How cards are defined and what data the Referee state reads.

Build docs developers (and LLMs) love