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"
}
| Attribute | Description |
|---|---|
devices | List of device references ("device.name") or tag expressions ("tag.name"). |
window | Tumbling window anchored to the first event (e.g., "2s", "200ms"). |
threshold | Event count that triggers the signal. |
cooldown | Suppresses re-firing for this duration after a trigger. Optional. |
on_trigger | Block 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:
| Field | Description |
|---|---|
name | Label for the group (appears in trigger data). |
devices | Device references or tag expressions for this group. |
min_count | Minimum 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:
| Field | Description |
|---|---|
min_rate | Minimum events-per-window to enter this tier. Tiers are sorted ascending automatically. |
label | Name for the tier (available in trigger data as tier.label). |
multiplier | Scoring 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.
| Primitive | What happens during cooldown |
|---|---|
| Burst | Events continue accumulating. If the count still exceeds the threshold at cooldown expiry, it fires again with the accumulated count. |
| Coincidence | Bitmask resets on trigger. A new coincidence pattern must form after cooldown expires. |
| Rate | Rate 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:
- Per-hit scoring — a normal
score "event"rule awards base points for each bumper activation. - Burst bonus —
aggregation "burst"awards a cluster bonus when rapid hits cluster. - Rate tracking —
aggregation "rate"tiers the sustained bumper rate and adjustsvar.bumper_cluster_bonusvia anevent_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.