Map to Encounter Lifecycle - UQcsse3200/2026-studio-3 GitHub Wiki

Map to Encounter Lifecycle

How the map connects to the rest of the game, so a run behaves as one continuous session rather than a series of unrelated screens.

Related: Map Graph System, Map Display System, Node Pool and Room Distribution

Overview

Start run -> generate map -> pick a room -> the room's screen plays out
    -> report the result -> back to the same map -> pick the next room

The map is generated once when a run begins and kept for the whole run. Picking a room opens the screen that owns it. When that screen finishes it reports the result back, the room is marked completed, and the rooms it connects to become available. A failed encounter reports back too, but nothing unlocks.

Why run state exists

The engine disposes a screen as soon as the game switches away from it. GdxGame.setScreen disposes the outgoing screen, and MainGameScreen.dispose also clears the ServiceLocator. Anything a screen owns is destroyed when the player leaves it.

That is a problem for the map. If the map were owned by whichever screen draws it, entering a room would destroy it, and coming back would build a new map with no progress on it.

RunState solves this by being owned by GdxGame rather than by any screen. GdxGame lives for as long as the game is running, so the map survives every screen change. MapScreen reads the map out of RunState and only generates a new one when no run is in progress.

RunState also records which node the player is currently in. New screens are constructed without arguments, so an encounter screen cannot be told which node it belongs to. It reads the active node from RunState instead.

The ownership that makes this work:

classDiagram
    class GdxGame {
        -RunState runState
        +getRunState() RunState
        +setScreen(ScreenType)
    }
    class RunState {
        +startRun(MapGraph, Integer) boolean
        +enterEncounter(Integer)
        +completeEncounter(boolean)
        +abandonEncounter()
        +endRun()
        +isRunActive() boolean
        +getMapGraph() MapGraph
        +getActiveNodeId() Integer
    }
    class MapGraph {
        +moveToNode(Integer) boolean
        +getNode(Integer) MapNode
        +getCurrentNode() MapNode
    }
    class MapNode {
        -NodeState state
        -RoomType roomType
        +addConnection(MapNode)
    }
    class MapScreen

    GdxGame "1" *-- "1" RunState : lives as long as the game
    RunState "1" o-- "0..1" MapGraph : held for one run
    MapGraph "1" *-- "*" MapNode
    MapNode --> "*" MapNode : connections
    MapScreen ..> RunState : reads the run from
Loading

MapScreen reads from RunState, it doesn't hold it. That's the reason the screen can be thrown away without losing the run.

The round trip

sequenceDiagram
    actor Player
    participant Map as MapScreen
    participant Sel as MapSelectionController
    participant Game as GdxGame
    participant Run as RunState
    participant Graph as MapGraph
    participant Enc as Battle or Encounter screen

    Player->>Map: clicks a node
    Map->>Sel: onNodeClicked(nodeId)
    Sel->>Graph: is this move legal?
    Graph-->>Sel: allowed
    Sel-->>Map: nodeSelected(nodeId)

    Map->>Run: enterEncounter(nodeId)
    Map->>Graph: getNode(nodeId).getRoomType()
    Graph-->>Map: roomType
    Map->>Game: setScreen(BATTLE_SCREEN or ENCOUNTER)

    Game-xMap: dispose()
    Note right of Map: map screen destroyed here,<br/>RunState is not

    Game->>Enc: construct
    Enc->>Run: getActiveNodeId()
    Run-->>Enc: nodeId

    Enc->>Run: completeEncounter(success)
    Run->>Graph: on success, mark COMPLETED and unlock connections
    Enc->>Game: setScreen(MAP)
    Game-xEnc: dispose()

    Game->>Map: construct a new MapScreen
    Map->>Run: isRunActive()
    Run-->>Map: true, with the same MapGraph
    Note right of Map: same map comes back,<br/>nothing regenerated
Loading

Both screens get disposed in there. RunState and the MapGraph it holds don't, so the progress is still there when the map comes back.

What RunState holds

Since Sprint 1 this has become the shared place for anything that has to last a whole run, not just the map. Other teams store their own per-run values here so they survive the same screen changes.

Held Used by
The generated MapGraph and the active node Map progression
Player health, max health and max energy Team 7's player system
The player's persistent state (PlayerRunState) Team 2's encounters
The player's deck (PlayerDeck) Team 5 and Team 6's card systems

Where a room sends you

MapScreen reads the selected node's RoomType and opens the screen that owns it.

Room type Screen Owner
COMBAT, ELITE, FINAL BATTLE_SCREEN Team 3
EVENT, SHOP ENCOUNTER Team 2
START none, the run begins here —

Choosing a room

MapScreen does not decide whether a move is legal. Clicks go to MapSelectionController, which checks with MapGraph and fires one of four events:

  • nodeSelected when the move is accepted
  • nodeLocked when the room is not reachable yet
  • nodeCompleted when the room has already been played
  • nodeSelectionRejected when MapGraph refuses the move

MapScreen only listens for nodeSelected. The node id is stored in RunState and the game switches to the owning screen.

Finishing a room

The encounter reports its result through the interface agreed with Team 2:

void onEncounterComplete(Integer nodeId, boolean success);

RunState.completeEncounter(success) passes the active node and the result to MapGraph. On success the node becomes COMPLETED and its connections become AVAILABLE. On failure nothing changes on the map.

Losing a battle ends the run: EndBattleScreen calls RunState.endRun() and returns to the main menu. Winning returns to the map.

Leaving a battle through the pause menu is different from losing it. RunState.abandonEncounter() reverts the map to the player's position before they entered, so abandoning does not consume the room.

Testing

RunStateTest and MapGraphTest cover the classes individually. MapProgressionEndToEndTest walks a whole run across MapSelectionController, MapGraph and RunState together, since the problems in this feature have generally sat between those classes rather than inside one:

  • starting a run opens only the first choices
  • a locked node cannot be selected
  • completing a room returns the same map and unlocks what follows
  • failing a room leaves progress where it was
  • a completed room cannot be entered again
  • the branch not taken is closed off
  • a run can be played from the start through to the boss

MapNodeTest covers MapNode itself, including that connections point one way so the player cannot walk back into a room they skipped.

None of these need a graphics context. MapScreen and EncounterScreen are not unit tested, as libGDX screens require a live context and the logic they rely on sits in the classes above. To check them by hand, start a run and confirm the map is the same one after returning from a room, that completing a room unlocks its connections, and that a failed room unlocks nothing.

Known gaps

The save/load screen is only reachable from the main menu. There is currently no way to save from inside a run, so a run can be loaded but not created as a save.

⚠️ **GitHub.com Fallback** ⚠️