Event Aggregation

Event Aggregation

Event aggregation lets you score on patterns of events rather than individual hits — bumper bursts, cross-device “hot playfield” moments, and spinner rate tiers. Aggregation signals flow through the same pipeline as any other signal and can be consumed in score "signal" blocks, event_handler blocks, and conditions.

Three aggregation primitives are available: burst, coincidence, and rate. Each is declared at the top level of a .cade file.

Burst — Rapid Hits on a Device or Tag

Fires when a device (or tagged group of devices) produces a threshold number of events within a tumbling time window.

aggregation "burst" "rapid_bumper" {
  devices   = ["device.bumper_top", "device.bumper_left", "device.bumper_right"]
  window    = "2s"
  threshold = 5
  cooldown  = "3s"

  on_trigger {
    emit = "aggregation.rapid_bumper.burst"
    data = {
      count   = "aggregation.count"
      rate_hz = "aggregation.rate_hz"
    }
  }
}

score "signal" "rapid_bumper_bonus" {
  when   = "aggregation.rapid_bumper.burst"
  points = "var.bumper_cluster_bonus * signal.count"
}
AttributeDescription
devicesList of device references ("device.name") or tag expressions ("tag.name").
windowTumbling window anchored to the first event (e.g., "2s", "200ms").
thresholdEvent count that triggers the signal.
cooldownSuppresses re-firing for this duration after a trigger. Optional.
on_triggerBlock specifying what signal to emit when the threshold is met.

Coincidence — Multiple Device Groups Active Together

Fires when every named device group registers at least one activation inside the shared window.

aggregation "coincidence" "hot_playfield" {
  device_groups = [
    { name = "bumpers",  devices = ["device.bumper_top", "device.bumper_left", "device.bumper_right"], min_count = 1 },
    { name = "ramps",    devices = ["device.ramp_left", "device.ramp_right"],   min_count = 1 },
    { name = "targets",  devices = ["device.target_1", "device.target_2", "device.target_3"], min_count = 1 }
  ]

  window = "5s"

  on_trigger {
    emit = "aggregation.hot_playfield.coincidence"
    data = {
      device_count = "aggregation.device_count"
    }
  }
}

score "signal" "hot_playfield_bonus" {
  when   = "aggregation.hot_playfield.coincidence"
  points = 25000
}

Each device group object supports these fields:

FieldDescription
nameLabel for the group (appears in trigger data).
devicesDevice references or tag expressions for this group.
min_countMinimum activations required from this group (default 1).

Use coincidence for chaos detection: “bumpers AND ramps AND targets all saw action within 5 seconds.”

Rate — Tiered Event Density

Tracks a smoothed events-per-second rate and emits a signal each time the current tier changes. Use this for escalating multipliers on sustained activity such as spinner frenzies.

aggregation "rate" "spinner_speed" {
  devices = ["device.spinner"]
  window  = "1s"

  rate_tiers {
    tier {
      min_rate   = 0
      label      = "idle"
      multiplier = 1.0
    }
    tier {
      min_rate   = 5
      label      = "active"
      multiplier = 1.5
    }
    tier {
      min_rate   = 15
      label      = "fast"
      multiplier = 2.5
    }
    tier {
      min_rate   = 25
      label      = "frenzy"
      multiplier = 5.0
    }
  }

  on_tier_change {
    emit = "aggregation.spinner_speed.tier_change"
    data = {
      rate       = "aggregation.rate_hz"
      multiplier = "tier.multiplier"
    }
  }
}

event_handler "spinner_tier_scoring" {
  when = "aggregation.spinner_speed.tier_change"
  actions {
    spinner_frenzy_multiplier = "tier.multiplier * 100"
  }
}

The rate_tiers block contains one or more tier blocks, each with:

FieldDescription
min_rateMinimum events-per-window to enter this tier. Tiers are sorted ascending automatically.
labelName for the tier (available in trigger data as tier.label).
multiplierScoring multiplier associated with this tier (available as tier.multiplier).

Tier transitions include hysteresis — downgrades require the rate to drop about 10% below the tier’s min_rate before the tier changes. This keeps rules stable when rates hover at a boundary.

Cooldown Behavior

cooldown inhibits re-firing for a fixed duration. During cooldown, the rule still tracks state but does not emit.

PrimitiveWhat happens during cooldown
BurstEvents continue accumulating. If the count still exceeds the threshold at cooldown expiry, it fires again with the accumulated count.
CoincidenceBitmask resets on trigger. A new coincidence pattern must form after cooldown expires.
RateRate keeps updating. If the tier differs from the pre-cooldown tier at expiry, a tier change fires.

Combining Aggregations with Other Scoring

Aggregation signals do not replace per-event scoring — they layer on top of it. A typical bumper scoring stack uses three layers:

  1. Per-hit scoring — a normal score "event" rule awards base points for each bumper activation.
  2. Burst bonusaggregation "burst" awards a cluster bonus when rapid hits cluster.
  3. Rate trackingaggregation "rate" tiers the sustained bumper rate and adjusts var.bumper_cluster_bonus via an event_handler.

Aggregation signals can also be used as conditions on unrelated scoring rules. For example, only award an orbit bonus when the playfield is “hot”:

score "signal" "hot_orbit_bonus" {
  signal    = "shot.left_orbit.complete"
  condition = "signal.aggregation.hot_playfield.active"
  points    = 5000
}

See Signal vs Event Handler for the broader rules on consuming signals.