-
Notifications
You must be signed in to change notification settings - Fork 0
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.jsonschema/episode.v0.schema.json
| 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.
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.
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.
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.
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.
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.
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.
Scripted by the pack. There is no randomness.
-
conservation_appealis an offer. It pays a credit per MWh of demonstrated reduction over stated turns, and it is opt-in throughaccept_offer. Reduce nothing and earn nothing. -
curtailment_orderis 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.
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.
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.
Exedra
Why
How it is built
What you write
Boundaries
For reviewers