I’m working on a multi-player 2D board game that players similar to board games like Betrayl at house on the hill. I don’t have any pictures yet to show as I’m in the early stages of coding still. But wanted to share the dice script singleton I wrote in hopes it could help people out. Its right below… I’ll try and post an update when I get a working test scene!
extends Node
## Global dice roller singleton
## Supports:
## - arbitrary dice (roll(n, sides))
## - named dice (d4, d6, d8, d10, d12, d20, d100)
## - percentile (int + float version)
## - advantage / disadvantage
## - history with timestamps
## - last-roll inspection
## Drop this in Autoloads as "DiceRoller"
#region Signals --------------------------------------------------------
## Emitted every time a roll happens.
## result = final total (after advantage/disadvantage applied)
## num_dice = how many dice were rolled
## sides = sides per die
## roll_type = DiceRoller.RollType.*
signal dice_rolled(result: int, num_dice: int, sides: int, roll_type: int)
#endregion
#region Enums ----------------------------------------------------------
## How to roll each die:
## NONE → roll once
## ADVANTAGE → roll twice, keep highest
## DISADVANTAGE → roll twice, keep lowest
enum RollType { NONE, ADVANTAGE, DISADVANTAGE }
#endregion
#region Variables ------------------------------------------------------
## RNG used for all rolls (can be seeded)
var rng: RandomNumberGenerator = RandomNumberGenerator.new()
## Stores the dice values from the *most recent* roll
## Example: [4, 6, 2]
var last_rolls: Array[int] = []
## Stores *all* rolls in this session
## key: int (auto-increment)
## value: {
## "num_dice": int,
## "sides": int,
## "rolls": Array[int],
## "total": int,
## "roll_type": int,
## "time": Dictionary (from Time.get_datetime_dict_from_system())
## }
var roll_history: Dictionary = {}
#endregion
#region Lifecycle ------------------------------------------------------
func _ready() -> void:
## Randomize once on startup
rng.randomize()
#endregion
#region Core Roll Logic ------------------------------------------------
## Main roll function.
## Rolls `num_dice` of `sides`, applying roll_type (none/adv/dis).
## Returns the TOTAL of all dice.
func roll(num_dice: int, sides: int, roll_type: int = RollType.NONE) -> int:
# clear previous roll
last_rolls.clear()
# roll each die
for i in range(num_dice):
match roll_type:
RollType.NONE:
last_rolls.append(rng.randi_range(1, sides))
RollType.ADVANTAGE:
# roll twice, take higher
var a := rng.randi_range(1, sides)
var b := rng.randi_range(1, sides)
last_rolls.append(max(a, b))
RollType.DISADVANTAGE:
# roll twice, take lower
var c := rng.randi_range(1, sides)
var d := rng.randi_range(1, sides)
last_rolls.append(min(c, d))
# get total for this roll
var total: int = get_last_rolls_total()
# notify listeners (UI, logs, etc.)
dice_rolled.emit(total, num_dice, sides, roll_type)
# store in history
add_to_history(num_dice, sides, last_rolls.duplicate(), total, roll_type)
return total
## Adds a completed roll to the history dictionary.
func add_to_history(num_dice: int, sides: int, rolls: Array[int], total: int, roll_type: int = RollType.NONE) -> void:
roll_history[roll_history.size()] = {
"num_dice": num_dice,
"sides": sides,
"rolls": rolls,
"total": total,
"roll_type": roll_type,
"time": Time.get_datetime_dict_from_system()
}
#endregion
#region History Access -------------------------------------------------
## Get a roll by index (safe).
## If index out of range or history empty → returns empty dict {}.
func get_roll_from_history(ind: int) -> Dictionary:
if roll_history.is_empty():
return {}
ind = clamp(ind, 0, roll_history.size() - 1)
return roll_history[ind]
## Returns the *most recent* roll entry from history.
func get_last_roll() -> Dictionary:
if roll_history.is_empty():
return {}
return roll_history[roll_history.size() - 1]
## Convert roll_type → string for UI/logging.
## NONE → "" ; ADVANTAGE → "adv" ; DISADVANTAGE → "dis"
func get_roll_type_as_string(roll_type: int) -> String:
match roll_type:
RollType.NONE:
return ""
RollType.ADVANTAGE:
return "adv"
RollType.DISADVANTAGE:
return "dis"
return ""
## Get the last N rolls, NEWEST first.
## Returns a Dictionary: 0 → newest, 1 → 2nd newest, ...
func get_roll_history_range_from_end(search_length: int) -> Dictionary:
var history: Dictionary = {}
var total_entries := roll_history.size()
if total_entries == 0:
return history
# clamp requested length
if search_length > total_entries:
search_length = total_entries
var out_index := 0
# walk backwards through history
for i in range(total_entries - 1, total_entries - search_length - 1, -1):
history[out_index] = roll_history[i]
out_index += 1
return history
## Get the first N rolls, OLDEST first.
## Returns a Dictionary: 0 → oldest, 1 → 2nd oldest, ...
func get_roll_history_range_from_start(search_length: int) -> Dictionary:
var history: Dictionary = {}
var total_entries := roll_history.size()
if total_entries == 0:
return history
if search_length > total_entries:
search_length = total_entries
for i in range(search_length):
history[i] = roll_history[i]
return history
#endregion
#region Helpers --------------------------------------------------------
## Sum up the current last_rolls array.
## Called after every roll.
func get_last_rolls_total() -> int:
var total := 0
for roll in last_rolls:
total += roll
return total
#endregion
#region Named Dice Rolls -----------------------------------------------
## All of these just call the main roll(...) with appropriate sides.
func roll_d4(num_dice: int = 1, roll_type: int = RollType.NONE) -> int:
return roll(num_dice, 4, roll_type)
func roll_d6(num_dice: int = 1, roll_type: int = RollType.NONE) -> int:
return roll(num_dice, 6, roll_type)
func roll_d8(num_dice: int = 1, roll_type: int = RollType.NONE) -> int:
return roll(num_dice, 8, roll_type)
func roll_d10(num_dice: int = 1, roll_type: int = RollType.NONE) -> int:
return roll(num_dice, 10, roll_type)
func roll_d12(num_dice: int = 1, roll_type: int = RollType.NONE) -> int:
return roll(num_dice, 12, roll_type)
func roll_d20(num_dice: int = 1, roll_type: int = RollType.NONE) -> int:
return roll(num_dice, 20, roll_type)
func roll_d100(num_dice: int = 1, roll_type: int = RollType.NONE) -> int:
return roll(num_dice, 100, roll_type)
#endregion
#region Percentile Rolls -----------------------------------------------
## “Integer percentile” → standard 1..100 roll
func roll_ipercentile(roll_type: int = RollType.NONE) -> int:
return roll(1, 100, roll_type)
## “Float percentile” → 0.01..1.00
func roll_percentile(roll_type: int = RollType.NONE) -> float:
return roll(1, 100, roll_type) / 100.0
#endregion
#region Extra Helpers --------------------------------------------------
## Roll with a bonus/penalty applied to the final total
func roll_with_modifier(num_dice: int, sides: int, modifier: int = 0, roll_type: int = RollType.NONE) -> int:
return roll(num_dice, sides, roll_type) + modifier
## Set a specific RNG seed (for deterministic tests / replays)
func set_seed(seed_value: int) -> void:
rng.seed = seed_value
## Randomize RNG again
func get_new_seed() -> void:
rng.randomize()
#endregion