Strategies

Who is this page for?

Integrators and strategy authors who decide how many points an event is worth. This is the conceptual + how-to view. The interpreter internals (AST, limits, sandbox) are in The DSL Strategy Engine; the per-block editor reference lives under docs/dsl/.

What a strategy is

A strategy is the scoring brain bound to a game (and optionally overridden per task). When an event arrives, GAME resolves the effective strategy and asks it for two things:

  • points - the integer reward, and

  • caseName - the human-readable label for why that amount was chosen (e.g. variable_basic_points, PeakPerformerBonus). The caseName flows into responses, analytics, and audit so a decision is always explainable.

Strategies come in two families:

Family

Behavior

Deterministic

Fixed, rule-based scoring. Same event → same points, always.

Adaptive

Scoring reacts to context: the user’s history, the task’s distribution, comparison to the global average, spatial state, etc.

Both families are deterministic given identical inputs - adaptivity means the inputs include behavioral/contextual analytics, not that the output is random. That is what makes GAME reproducible (see Overview).

Choosing a strategy: strategyId

Every game stores a strategyId; tasks inherit it unless they set their own. Two id shapes exist:

Shape

Meaning

<builtin-id>

A code-defined strategy registered in the engine (table below).

custom:<uuid>

A DSL strategy authored in the dashboard and stored in the database.

If no strategy is set, the built-in default applies.

Built-in strategies

These ship with the engine. Each registers a stable public id (persisted on games and returned by the API) via @register_strategy - the id is guaranteed stable across class renames:

strategyId

Version

Purpose

default

0.0.2

The baseline. Awards configurable basic/bonus points; the safe default for any game.

constantEffortStrategy

0.0.1

Rewards steady, sustained participation rather than bursts.

socio_bee

0.0.2

Tuned for the SOCIO-BEE citizen-science scenario.

greencrowdStrategy

1.0.0

Tuned for the GREENCROWD platform.

greengageStrategy

0.0.1

Tuned for the GREENGAGE scenario.

getis_ord_gi_star

0.0.1

Spatial strategy using the Getis–Ord \(G_i^*\) hot-spot statistic to shift incentives toward under-served areas.

Discover them at runtime:

GET /api/v1/strategies                 # list available strategies
GET /api/v1/strategies/{id}            # one strategy's metadata
GET /api/v1/strategies/{id}/schema     # its configurable variables
GET /api/v1/strategies/{id}/graph      # a rendered logic graph

Strategy variables (e.g. variable_basic_points) are the knobs; game/task params supply their values, so the same strategy behaves differently per game without code changes.

Extending the engine in code

New built-ins are plain classes that subclass BaseStrategy, implement the scoring method, and decorate themselves with @register_strategy(id="..."). Third-party packages can even contribute strategies through the game.strategies entry-point group - no fork required. See The DSL Strategy Engine and Codebase Reference.

Custom strategies (the DSL)

The dashboard’s Strategy Editor lets game designers build strategies visually (Blockly) with no Python. A strategy is a tree of blocks; when a scoring event arrives the engine walks the tree top-to-bottom and emits the first assign_points it reaches inside a matching rule.

Authoring modes

Mode

What you do

DSL_FULL

Write the whole strategy from scratch: a top-down list of rules (first match wins) plus a default fallback.

DSL_EXTEND

Start from a built-in (e.g. default) and layer pre_rules (may mutate the input or veto the award) and post_rules (may multiply points, override the case name, or add callback data), with optional per-realm parent_variables overrides.

Templates give authors a running start - both engine templates (default, constant_effort) and ready-made examples such as engagement_basico, recompensa_completar_tarea, bonus_por_velocidad, and bonus_extiende_default:

GET /api/v1/strategies/custom/templates

The custom-strategy lifecycle

Custom strategies are first-class, versioned resources:

Operation

Endpoint

Create a draft

POST /strategies/custom

List / get

GET /strategies/custom, GET /strategies/custom/{id}

Update (creates a new draft of a published strategy)

PUT /strategies/custom/{id}

Publish a version

POST /strategies/custom/{id}/publish

Archive

POST /strategies/custom/{id}/archive

List versions

GET /strategies/custom/{id}/versions

Roll back to a version

POST /strategies/custom/{id}/rollback/{version}

Where is it used?

GET /strategies/custom/{id}/usage

Import / export

POST /strategies/custom/import

Simulate (no persistence)

POST /strategies/custom/simulate, POST /strategies/custom/{id}/simulate

Versioning guarantees

  • Saving a published strategy creates a v+1 draft rather than overwriting. Production keeps running the published version until you explicitly publish the new one.

  • If a published version misbehaves, rollback restores a prior version. Rollback also rewrites the strategyId on every game/task that pointed at the archived version, so the cascade reaches all consumers.

This means you can experiment freely - nothing in production changes until you press Publish.

Simulate before you ship

Every strategy can be simulated: scoring runs and you get the full node-by-node trace, but no production data is touched - no UserPoints, no wallet movement.

Two ways to simulate:

  • Per-event - send "isSimulated": true to the points endpoint (Integrating with GAME).

  • Per-strategy - call POST /strategies/custom/{id}/simulate (the editor’s Test button) to dry-run a candidate strategy against sample input and inspect exactly which rule would fire and why.

There is also a per-user simulated view, GET /games/{gameId}/users/{externalUserId}/points/simulated (OAuth2-only, bound to the caller’s own subject), returning a simulationHash for integrity plus per-task projected points.

Use simulation liberally - it is the safe way to validate a scoring change.

Safety limits

Published DSL strategies run inside a sandbox with hard ceilings so a runaway rule cannot impact your tenant or others:

Limit

Default

Env var

Wall-clock per call

500 ms

DSL_EXECUTION_TIMEOUT_MS

AST nodes visited

1000

DSL_MAX_NODES

Recursion depth

32

DSL_MAX_DEPTH

Hitting a limit rejects the event with a clear error code rather than degrading the service. The full execution model - how the interpreter yields cooperatively so the timeout can actually cancel it - is in The DSL Strategy Engine.

Worked example: adaptive engagement

A common pattern (from the citizen-science deployments) layers cases by the user’s measurement count and performance:

Case

Condition

caseName

First/second measurement

No prior history

BasicEngagement

Slower than global avg

time > global

PerformancePenalty

Faster than global avg

time < global

PerformanceBonus

Beats own history and global

time < individual && time < global

PeakPerformerBonus

The full decision tree and points table is in the repository’s strategies.md; expressing it in the editor is a matter of nesting gd_rule blocks with gd_compare conditions and gd_assign_points leaves.

Observability

Every production run of a custom strategy emits Prometheus metrics (dsl_execution_duration_seconds, dsl_execution_nodes_total, dsl_execution_errors_total) and, on failure or by sampling, persists a StrategyExecutionLog row you can inspect later. Aggregations are available at GET /strategies/custom/{id}/metrics and an A/B view at GET /strategies/custom/compare. See Observability.