Skip to content

The Competitor Contract

Marty McEnroe edited this page Aug 6, 2026 · 2 revisions

The Competitor Contract

The human at a browser and the agent at an API are two implementations of one interface. This is that interface.

The machine-readable companions are normative. Where this page and a schema disagree, the schema is right.

  • schema/scenario-pack.v0.schema.json
  • schema/episode.v0.schema.json

Roles

Role Owns Never touches
Competitor Actions, one per turn The grid model, other players' state
Platform The pack, turn resolution, scoring, the episode log The competitor's decision process

The platform is server-authoritative. A competitor cannot observe anything not present in an observation, and cannot do anything not expressible as an action.

Episode lifecycle

join → brief → [ observe → act → resolve ] × N → settlement

Join. An episode of a scenario begins. The platform pins pack_version, engine_version, and score_version into the episode at creation, so the episode is immutable against later edits to any of them.

Brief. The pack's brief_md, plus the scoring declaration. The player sees exactly what is scored and how, before the first turn. There are no hidden objectives.

Turn loop. N discrete turns, each step_minutes long, both declared by the pack. Per turn the platform issues an observation, the competitor returns an action, and the platform appends a resolution. One action per turn. No revisions after commit.

Settlement. Both metrics with their denominators, and the full episode log available for download.

Time

Turn-based and discrete. Turns map one to one onto co-simulation time steps for phase two. There is no wall-clock pressure, because a workshop session has to be able to breathe.

Observation

Everything a competitor may know at turn t:

Field Content
turn, local_time Where we are in the episode
price_usd_per_mwh Grid energy price this turn
system_status normal, watch, or emergency
forecast Next three turns of price and status
site IT demand this turn, battery state of charge in MWh, generator permit-hours remaining
events Active event notices
ledger Running cost, MWh shed so far, relief delivered and asked so far

The forecast is currently exact. That is a property of the data, not of the contract: forecast error arrives as a scenario-data upgrade, and the contract does not change when it does.

Action

One action object per turn. Every field is clamped by the platform to physical and regulatory limits, and every clamp is reported back. Silent correction is forbidden, because learning where the limits bite is part of the curriculum.

Field Range Meaning
curtail_mw 0 to IT demand Shed IT load, at the SLA price per MWh
battery_mw -power to +power Positive discharges to serve load, negative charges from the grid
diesel_mw 0 to capacity Generator output, limited by permit hours
accept_offer boolean Only when an offer event is active

diesel_mw names a natural-gas generator. The field name is a legacy wire identifier and is kept for compatibility.

Resolution

grid_import_mw = max(0, it_demand - curtail_mw - max(0, battery_mw) - diesel_mw)
                 + max(0, -battery_mw)

turn_cost      = grid_import·price + gas·fuel + curtail·sla - credits

Charging adds load, which is the second term of the import line. The battery's round-trip efficiency is charged entirely against filling. See The Engine for what that means in practice, because it surprises almost everyone the first time.

Grid response

A pack may declare grid.price_slope_usd_per_mwh_per_mw. When it does, the strip price is defined as the price at nominal campus draw, and the competitor pays:

effective_price = price[t] + slope[t] · (grid_import_mw - it_demand_mw)

so a competitor's own deviation moves the price it pays. Relief during scarcity earns a softer price. Filling the battery into a tight hour pays a harder one.

The slope is derived offline as the analytic derivative of the compiler's documented curve, which keeps the modelling in the compiler and the bookkeeping in the engine. A pack without a slope behaves as a price taker, bit-identically to the engine version that predates the feature. That equivalence is enforced by a test.

Events

Scripted by the pack. There is no randomness.

  • conservation_appeal is an offer. It pays a credit per MWh of demonstrated reduction over stated turns, and it is opt-in through accept_offer. Reduce nothing and earn nothing.
  • curtailment_order is a directive. It requests MW over stated turns. Delivery is measured, not optional, and shortfall shows up in the reliability metric.

Severity encodes obligation throughout the platform. An order is mandatory and wears danger red. An appeal is voluntary and wears warning yellow.

New event types extend the enum. They never change the turn loop.

Scoring

The pack declares exactly two metrics in tension, never a four-term composite, with a score_version. See Scoring and Stars.

Replayability is contractual:

score(replay(episode_log)) === stored_score

Arguing with the scoring function is the instrument working. The point of publishing and versioning it is that the argument is against something fixed.

Structural moves

A vocabulary is reserved and not yet exposed: build_onsite_generation, interconnection_request, islanding_agreement, network_change_request, der_contract. These are the moves that change the shape of the problem, with capital cost, lead time, and consequences across episodes.

They are named in the contract rather than invented later inside a scenario, so that the first scenario to need them does not force a contract change.

Clone this wiki locally