Skip to content

03 09 Config play_stats

evets17 edited this page Apr 2, 2026 · 5 revisions

03 09 Config play_stats

This page explains play_stats in Sprint Boost.

What play_stats is

play_stats tracks selected values during play and can persist those values between sessions.

A common first use is high scores, but play_stats is not limited to high scores.

More than high scores

play_stats can track any value sourced from game_info or from expression, including:

  • score
  • lives-related counters
  • items collected/used
  • custom game-specific counters
  • derived values built from several live values
  • reusable true/false state checks used for save logic

If it can be represented through game_info directly, or built from game_info through expressions, it can usually be used by play_stats.

Why play_stats matters

Key benefits:

  • Persistence: keep tracked records between sessions
  • Leaderboards: display ranked historical values. play_stats is a pre-requisite for the leaderboards layout element.
  • Dynamic display layouts: show tracked values in custom overlays/screens
  • Realtime display values: show current tracked session values live

Where play_stats can be used

play_stats is relevant for:

  • boost_mode = "enhanced"

In current Sprint Boost behavior, play-stats runtime flow is used with enhanced display/runtime paths.

Basic play_stats example (high score)

[play_stats]
enabled = true
stats_file = "{gv.game_play_stats_path}"
auto_save_quit = true
auto_save_reset = true

[play_stats.players.p1]
label = "P1"

[play_stats.players.p1.tracked.score]
game_info = "p1_score"
mode = "final"

What this does:

  • Enables play-stats tracking
  • Saves to a persistent stats file
  • Tracks P1 score using game_info = "p1_score"

play_stats and expressions

Expressions are often the easiest way to make play_stats useful.

This is especially true when the game does not store a player-friendly value directly.

Common examples:

  • build one real score from several raw score digits
  • create a reusable game_over flag
  • create a reusable in_game or on_title_screen flag

High-level rule of thumb:

  • use game_info to read raw live values
  • use expression to clean up or combine those values
  • use play_stats to track, save, and display the result

Example using an expression as the tracked value:

[play_stats.players.p1.tracked.score]
expression = { int = "derived_score" }
mode = "final"

This means Sprint Boost is not tracking a raw memory value directly. It is tracking a score that was already built somewhere else in the config.

Players and labels

play_stats supports tracking for one or more players.

You can track the same stat pattern for multiple players, each under its own key:

  • play_stats.players.p1
  • play_stats.players.p2
  • or any player key name you choose

For single-player games, a simple key like player is perfectly fine:

[play_stats.players.player]
label = "Player"

players.<player>.label is used as the player name shown in leaderboards.

Practical tip:

  • Short labels like P1 / P2 are usually best, especially when leaderboard width is limited.

Save behavior

You can persist stats with:

  • auto_save_quit = true (save on quit)
  • auto_save_reset = true (save on reset)
  • manual save command (save_play_stats) from menu/hotkey

This gives you flexible control over when data is written.

You can also save when a configured session trigger fires:

  • play_stats.session_triggers.auto_save = true

Sessions and reset behavior

play_stats tracks values within a play-stats session.

Important reset setting:

  • clear_stats_on_reset controls whether tracked session values are cleared when reset is triggered through Sprint Boost reset flow.
  • In most cases, keeping this true (default) is best.

Another useful pattern has started to show up in real game configs:

  • set auto_save_reset = false
  • set clear_stats_on_reset = false
  • let session_triggers handle the save and session restart instead

This works well when both of these paths naturally return the game to the same title screen or menu:

  • starting a new game from inside the game
  • using reset

In that kind of setup, one in-game trigger can often handle both cases more cleanly than separate reset-based behavior.

High-level idea:

  • use the same session trigger for both "reset returned to title" and "the game naturally returned to title"
  • let that title-screen or menu transition be the thing that ends the run
  • if reset causes that transition, the save still happens right away, but through the session-trigger path
  • that means you often do not need separate reset auto-save or reset clear behavior

Thunder Castle is a good example of this pattern. Its config uses a bool expression for on_title_screen, then lets a session trigger handle the rollover:

[play_stats]
auto_save_reset = false
clear_stats_on_reset = false

[play_stats.session_triggers]
auto_save = true

[play_stats.session_triggers.back_to_menu]
expression = { bool = "on_title_screen" }
trigger = { mode = "changes_to", value = true }

In plain language, this means:

  • use the return-to-title moment as the single source of truth for ending the run
  • let reset reuse that same path when reset sends the game back to title
  • avoid split behavior where reset and in-game transitions try to manage sessions separately

Reset caveat:

  • If a reset happens outside Sprint Boost control flow (for example through an emulator-side kbdhackfile reset hotkey), Sprint Boost does not reliably know a reset occurred.
  • In that case, play-stats session values may not be cleared at that moment.

Session triggers (optional)

Use session_triggers when a game has a natural run-ending moment that does not always use emulator reset.

Common example: end the play-stats session when lives change to 0.

[play_stats.session_triggers]
auto_save = true

[play_stats.session_triggers.game_over]
game_info = "p1_lives"
trigger = { mode = "changes_to", value = 0 }

What this does:

  • Watches a game_info value.
  • Fires when that value changes to the target.
  • Starts a new play-stats session so the next run begins fresh.
  • If auto_save = true, saves immediately when it fires.

This is useful for games where the run ends inside the game itself instead of through a normal emulator reset.

It is also useful for games where reset and "start new game" both lead back to the same title-screen state, because one session trigger can cover both flows.

Common examples:

  • game over screen
  • return to title screen
  • back to menu after both players are dead
  • level or round boundaries when you want each segment saved separately

Session triggers can watch:

  • a game_info value
  • an expression.int value
  • an expression.bool value

That makes expressions especially helpful when the game-over signal is not stored as one clean memory value.

Example using a bool expression:

[play_stats.session_triggers.back_to_menu]
expression = { bool = "on_title_screen" }
trigger = { mode = "changes_to", value = true }

In plain language, that means:

  • watch a reusable true/false expression
  • when it changes from false to true
  • treat that as the end of the current run

Optional conditions:

[[play_stats.session_triggers.game_over.conditions]]
game_info = "p2_lives"
op = "eq"
value = 0

In plain terms, the trigger only fires when all conditions are true at that moment.

This lets you keep the watched trigger simple, then add extra checks only when needed.

Practical notes:

  • changes_to is edge-based: first observed value does not fire.
  • The watched value must leave the target and come back before the same trigger can fire again.
  • If auto_save = true, Sprint Boost saves the current session before it starts the next one.

Tracked modes (full supported list)

play_stats.players.<player>.tracked.<stat>.mode supports:

  • final: save the most recent value.
  • peak: save the highest value seen.
  • lowest: save the lowest value seen.
  • count_change: count every value change.
  • count_increase: count how many times value increased.
  • count_increase_reset_aware: count increases and reset-boundary carry-ins.
  • count_decrease: count how many times value decreased.
  • amount_increased: accumulate total increase amount.
  • amount_increased_reset_aware: accumulate increases with reset-boundary carry-in handling.
  • amount_decreased: accumulate total decrease amount.

Example beyond high score

[play_stats.players.p1.tracked.food_bags_collected]
game_info = "food_count"
mode = "count_increase"

[play_stats.players.p1.tracked.food_consumed]
game_info = "food_count"
mode = "amount_decreased"

This tracks two behavior stats from the same underlying value:

  • how many collection events occurred
  • how much total resource was consumed

Save conditions (optional)

Use save_conditions to skip trivial runs.

You can define more than one save condition for a player.

When multiple conditions are defined, all conditions must pass for that player record to be saved.

Each condition uses one source:

  • game_info = "..." checks a live game_info value.
  • play_stat = "..." checks that player's tracked play-stats value.
  • expression = { int = "..." } checks a numeric expression.
  • expression = { bool = "..." } checks a true/false expression.
  • expression = { string = "..." } checks a text expression.

Use one source per condition entry (do not combine both in the same entry).

[[play_stats.players.p1.save_conditions]]
game_info = "p1_score"
op = "gt"
value = 0

Meaning: only save when score is greater than 0.

Example using a tracked play stat:

[[play_stats.players.p1.save_conditions]]
play_stat = "score"
op = "gt"
value = 0

Meaning: only save when the tracked score stat is greater than 0.

Example using a bool expression:

[[play_stats.players.p1.save_conditions]]
expression = { bool = "has_started" }
op = "eq"
value = true

Meaning: only save when the reusable has_started expression says the run really began.

Update conditions (optional)

Use update_conditions to update a stat only when a condition is true (for example active player checks in 2-player games).

[play_stats.players.p1.tracked.score]
game_info = "active_score"
mode = "amount_increased"

[[play_stats.players.p1.tracked.score.update_conditions]]
game_info = "active_player"
op = "eq"
value = 1

Like save conditions, update conditions can also use expressions when that reads more naturally than repeating raw memory checks.

Realtime display usage

Live tracked values can be shown in display substitutions:

  • {ps.<player>.<stat_key>}
  • shorthand {ps.<stat_key>}

Example:

[[display.layouts.hud.elements]]
type = "text"
text = "P1 Total: {ps.p1.score}"
x = 20
y = 30
width = 300
height = 40
color = "#FFFFFF"
align = "left"
size = 26

Override behavior

play_stats follows normal config precedence:

  • folder-level can define shared tracking patterns
  • game-level can override tracked stats and save behavior

In practice, most tracked keys are game-specific because they depend on each game’s game_info setup.

Practical tips

  • Start with one tracked stat and confirm it saves correctly.
  • Add additional stats one at a time.
  • Use high score as a starter pattern, then expand into game-specific metrics.
  • Use manual save for intentional persistence workflows.
  • You can manually edit the play_stats save file's player_label attribute if wanting to put a person's identifier on a particular stat, then that name will display in leaderboard if not too long.

Next step

Continue to:

Clone this wiki locally