User Manual

The Electricity Strategy Game (ESG) is a web rebuild of the Berkeley EEP/ENVECON 147 "Electricity Strategy Game," a semester-long wholesale electricity market simulation. This page documents how the app works and, in §12, exactly where it matches, deviates from, or extends the original classroom implementation.

1. What is the ESG

The ESG is a semester-long simulation of a wholesale electricity market. Student teams each own a portfolio of power plants (a mix of technologies with different capacities, variable costs, and emissions rates). Every round, each team submits a price bid for each plant it owns. The app clears the market in merit order — cheapest bids run first — against that round's demand, computes a clearing price, and settles each team's profit. Team standings are ranked by cumulative cash balance, so the winning strategy is the one that earns the most money across the semester, not the one that dispatches the most energy.

Acknowledgment: this game is based on material from UC Berkeley's EEP/ENVECON C147 — Energy & Environmental Markets, taught by Meredith Fowlie (Department of Agricultural & Resource Economics; Energy Institute at Haas). In that course the game was run on a combination of Stata (market clearing), Excel (portfolio and bid data), and Jupyter notebooks hosted on the course's Datahub (student bidding-analysis sandbox and instructor lecture tools). This app reimplements that same game as a standalone web application so it can run without Datahub access, Stata, or manual GSI intervention between rounds.

2. The semester arc

An instructor composes a game as an ordered schedule of rounds. Round types, in the order they typically appear:

  1. Practice round. Every team (including teams that will later be reassigned) is given a temporary portfolio so they can learn the bidding interface. Results from the practice round do not count toward standings.
  2. Portfolio auction. Teams submit sealed bids for the portfolios that will define their fleet for the rest of the game (see §11 and the original process described in §12). Assignment goes to the highest bidder for each portfolio, subject to a reservation price (default $100,000, matching the original's reserve — see §12); any portfolios left unclaimed after all teams are assigned are played by AI teams (§9).
  3. Market rounds, grouped into eras that an instructor tags on the schedule:
    • 1990s era — only hydro, nuclear, and fossil plants may run; renewable technologies (solar, wind) are excluded from the merit order for these rounds, matching the original game's historical starting point.
    • Clean-transition era — solar, wind, and battery plants become eligible, and a carbon price can be introduced or ramped up round over round.
    • Capital-expansion era — rounds where the demand and cost environment reflects teams having invested (or not) via the capacity-planning exercise.
  4. Capacity-planning round. A one-shot exercise near the end of the game where teams act as grid planners rather than bidders (§10).

For reference, the original 2024 course ran this arc over roughly six weeks: a practice Round 0, then Rounds 1–6 (four hourly markets each, 24 hourly markets total), with the in-class portfolio auction held between the practice round and Round 1 (March 19 in 2024). Round 1 used pay-as-bid settlement; Rounds 2–6 used uniform pricing. Carbon policy was active in Rounds 4–6, followed by a Round 7 that existed only to true up permit compliance (no new electricity bids). Multiple discussion sections ran the game in parallel (e.g. one game per Wednesday-9am or Thursday-4pm section) — this app models that as one independent game per section (see "Parallel sections" in §12).

The status bar at the top of every page (once logged in) shows whether a round is currently open for bidding and the countdown to the next deadline. The Deadlines page lists every round's open/close time in Pacific time. An instructor can grant an individual team a per-team extension — a later effective deadline for that team only, used for excused circumstances — without moving the deadline for the rest of the class.

3. Portfolios and generators

Each generator (power plant) in the game is defined by:

  • Capacity (MW) — nameplate size.
  • Variable cost ($/MWh) — the fuel/O&M cost of producing one MWh; this is the plant's true marginal cost.
  • Daily fixed cost ($) — an O&M cost owed every round regardless of whether the plant runs (see §6).
  • CO2 rate (tons/MWh) — emissions per MWh generated, used for the carbon price adjustment and emissions accounting.
  • Availability profile — which load blocks the plant can offer capacity into, and how much (§7). Baseload plants (the original game's default) are available at full capacity in every block.

A portfolio is a fixed bundle of generators assigned to one team. Two built-in fleets ship with the app:

Original ESG fleet — 42 plants, 7 portfolios, 22,050 MW total

Transcribed from the original game's ESGPorfolios.csv (see §12).

Portfolio# plantsTotal capacity (MW)
Big Coal63,900
Big Gas73,600
Bay Views52,650
Beachfront83,800
East Bay63,000
Old Timers52,750
Fossil Light52,350
CAISO-2026 fleet — 45 plants, 8 portfolios, 5,200 MW total

A present-day CAISO generation mix scaled to game size (roughly 1/18th of the real CAISO fleet); see docs/research/caiso-fleet-2026.md for the full sourcing and scaling methodology.

Portfolio# plantsTotal capacity (MW)Character
Diablo Bay4665Nuclear + coastal gas CC + battery
Big Gas – South Coast4560LA-basin gas CC + peakers
Valley Combined Cycle6610Central Valley gas CC + peakers
Sierra Hydro & Pumped Storage5475Reservoir hydro + pumped storage
Desert Solar Belt4770Big-name utility solar
Central Coast & Valley Solar5620Remaining solar fleet
Tehachapi Wind & Geothermal Corridor7525CA wind + geothermal
Storage & Biomass Reserve10975Batteries, biomass, import contracts

Portfolio sizes are intentionally unequal in MW because dollar value per MW differs a lot by technology (a nuclear+gas portfolio and a battery+biomass portfolio are sized to land in a similar range of expected value, not equal capacity). Admins are not limited to these two fleets — the generator-set composer lets an instructor build a custom set of portfolios and plants from scratch.

4. Bidding

Each round, a team submits one bid price per plant, per load block. A round is split into 4 load blocks (analogous to the original game's 4 "hours" per round, e.g. night/morning/afternoon/evening), each covering a slice of the round's total demand. A team may bid the same price in every block for a plant, or a different price per block.

  • Bounds. Every price must be between $0 and the game's price cap (negative bids are disallowed by default; an admin setting can relax the floor for advanced play). The original game's cap was $500/MWh (ESG_2024.pdf p.2); this app defaults to $500 and admins can change it per game.
  • Validation — hard errors (submission is rejected until fixed):
    • Missing bid for a generator in the team's portfolio.
    • Non-numeric, NaN/infinite, or negative price (unless negative bids are allowed).
    • Price above the price cap.
  • Validation — soft warnings (submission is accepted, but flagged):
    • Bid below the plant's effective marginal cost (variable cost + carbon cost) — if it sets the price, the plant sells at a loss.
    • Bid more than 10× marginal cost — likely a typo.
    • Bid within 1% of the price cap — the plant will almost never be dispatched.
    • Every plant in the portfolio bid at the identical price — a possible copy-paste error.
  • Reset to marginal cost. A one-click button on the bid form fills every price with that plant's effective marginal cost, as a starting point.
  • Submission history is append-only and visible to the whole team: every submission (valid or not) is kept, timestamped, and never overwritten, so teammates can see who submitted what and when.
  • If a team never submits a valid bid before its deadline, every plant in its portfolio is bid at marginal cost automatically (the competitive default) so the market still clears. This fallback is flagged in the round's clearing report so instructors can see which teams didn't submit — the original game had GSIs chase down missing bids by hand.
  • Late submissions are only accepted if an instructor has granted that team an extension for that round (§2); otherwise the deadline is firm.

5. Market clearing, step by step

The market-clearing algorithm is merit-order dispatch against inelastic demand:

  1. Collect every plant's bid price and available capacity for the load block.
  2. Sort plants ascending by bid price (cheapest first — the "merit order").
  3. Walk down the sorted list, accumulating capacity, until cumulative capacity meets or exceeds the block's demand.
  4. The clearing price is the bid price of the last plant needed to meet demand (the marginal unit). Every plant that sorts before it (cheaper) is inframarginal and runs at full offered capacity.
Worked example

Three plants, demand of 250 MW (strictly between the second and third plant's cumulative capacity):

PlantBid ($/MWh)Capacity (MW)Cumulative (MW)Status
Nuclear A10100100Inframarginal — runs 100 MW
Gas CC B32120220Inframarginal — runs 120 MW
Gas CT C5080300Marginal — runs 30 of 80 MW

Cumulative capacity crosses 250 MW inside Gas CT C's block, so C is the marginal unit: it runs only the 30 MW needed to cover the remaining demand (250 − 220), and the clearing price is C's bid of $50/MWh — not the average of all three bids.

(a) Supply exactly equal to demand

When cumulative capacity lands exactly on demand (no partial dispatch needed), every plant at or below the marginal price runs at its full offered capacity, and the clearing price is still the marginal (last-accepted) plant's bid. This sounds obvious, but the original Stata implementation's tie-rationing block (esgV20_deterministic_eep147.do, lines 336–358) has only a marginalHorizDemand > 0 branch and an else that conflates "exactly zero" (demand fully met by strictly-cheaper units, with no tied units needed) with "negative" (excess supply) — in the exact-equality case it walks the price one step too low instead of stopping. This app makes that case first-class in esg/engine/clearing.py and covers it with dedicated tests, so an exact match never produces an error or an undefined price.

(b) Ties at the margin

When two or more plants bid the identical price and that price is the marginal price, the residual demand (what's left after all strictly-cheaper plants have run) is split pro-rata by offered capacity across all tied plants, regardless of which team owns them. For example, if two 100 MW plants tie at the margin and only 60 MW of demand remains, each runs 30 MW (50% of its capacity) — not "whichever plant happens to sort first gets to run." This is a deliberate improvement over the original notebook's arbitrary sort-order tie-break (see §12) and matches standard ISO practice.

(c) Shortage

If total offered capacity is less than demand, every plant runs at full offered capacity (no one is held back) and the market cannot fully serve demand. The clearing price becomes the administrative price cap (scarcity pricing) if one is configured; otherwise it is the highest accepted bid. The unserved amount is recorded as shortage.

(d) Uniform-price vs. pay-as-bid settlement

Two settlement rules exist, chosen per round by a game/round setting:

  • Uniform price (the default, and the rule for every round in the original game except round 1): every dispatched MW — inframarginal or marginal — is paid the single clearing price.
  • Pay-as-bid (used for round 1 in the original game, offered here as a general setting): every dispatched MW is paid that plant's own bid price.

Numeric comparison for the worked example above (clearing price $50/MWh):

SettlementNuclear A revenue (100 MW)Gas CC B revenue (120 MW)
Uniform price100 × $50 = $5,000120 × $50 = $6,000
Pay-as-bid100 × $10 = $1,000120 × $32 = $3,840

(Per hour of the load block; multiply by block hours for round totals.)

(e) Carbon price

When a game or round has a carbon price set, it is added to each plant's marginal-cost benchmark (variable cost + carbon price × CO2 rate) — this is what the "reset to marginal cost" button fills in, what missing bids default to, and what validation compares submitted bids against. The carbon price does not automatically change a team's own bid — a team can still bid whatever it wants — but it does raise the competitive floor. Separately, at settlement, dispatched plants are charged a carbon cost equal to their emissions (dispatched MWh × CO2 rate) × the carbon price, deducted from profit (§6).

(f) Demand realization

Each load block has a published demand forecast. The original game's documented rule (ESG_2024.pdf p.5) is that realized demand is drawn independently per location × hour from a Normal distribution with mean = the forecast and standard deviation = 3% of the forecast; the 2024 run was actually played with that shock zeroed out (esgV20_deterministic) or with a fully manual demand_real.csv supplied by the instructor. At clearing time, this app draws the realized demand from the forecast plus a seeded normal shock, truncated to [50%, 150%] of the forecast so an extreme draw can never produce a nonsensical negative or runaway number. The shock's size is a game setting: demand_sigma_pct (a fraction of the forecast — 0.03 reproduces the documented 3% rule), an absolute demand_sigma, or 0 to reproduce the deterministic 2024 behavior exactly. The draw is seeded by the round and block, so re-running a clearing reproduces the same result exactly (auditability).

6. Money

Each round, a team's profit is:

profit = energy revenue
       − variable (fuel) cost of dispatched energy
       − carbon cost (emissions × carbon price)
       − daily fixed costs of every plant in the portfolio (paid whether it runs or not)
       − interest on a negative cash balance (rate × |balance|, charged only when negative)

A team's portfolio is paid for once, as a single auction-payment ledger entry when the portfolio auction clears (§11). Teams may borrow unlimited funds — there's no credit limit — at a default 5% per-round interest rate on any negative balance; interest only starts accruing after Round 1, so the portfolio-auction debt itself is not charged interest during Round 1, matching the original game. Every game has a configurable starting cash balance, defaulting to $0 as in the original. Standings are ranked by cumulative cash balance across all rounds played so far — not by energy sold, market share, or emissions.

The original course grade combined this market performance with more than pure profit: 50% of the grade was team performance (energy-market results and a team's contribution to an in-class carbon-policy deliberation) and 50% individual performance (weekly quizzes and ESG-based problem sets). This app reports cash-balance standings only and leaves how those standings translate into a grade to the instructor.

7. Temporal availability profiles

A plant's availability profile determines what fraction of its nameplate capacity it can offer in each load block:

ProfileNightMorningAfternoonEvening
Baseload1.01.01.01.0
Solar0.00.51.00.1
Wind0.60.40.30.7
Hydro1.01.01.01.0
Battery (4-hr)0.00.01.01.0

Hydro's daily energy budget: hydro can offer full capacity in any block, but its total energy across the round's blocks is capped at capacity × 12 hours (of the 24-hour day the four blocks represent), reflecting a limited reservoir/snowpack energy supply rather than an unlimited fuel. Once a hydro plant's daily energy budget is used up, its offered capacity in later blocks shrinks accordingly.

Battery's 4-hour discharge: batteries are modeled as offering capacity only in the two blocks with the highest expected prices (afternoon and evening by default), reflecting a battery that charges off-peak and discharges for a limited number of hours at peak.

Setting every generator's profile to "baseload" — the default for the original-ESG fleet — makes every plant available at full capacity in every block, reproducing the original game's behavior exactly (the original game has no availability profiles at all; see §12).

8. The simulator

Each team has access to a private "what-if" simulator: it runs the exact same clearing engine as the real market, but writes nothing to the database and is never a submission. By default, every rival team is assumed to bid at marginal cost (including the carbon adjustment), and demand defaults to the round's published forecast — but a team can override any rival plant's bid, the demand quantity, or its own trial bids, and instantly see the resulting clearing price, its own dispatch, and its estimated profit. This lets teams test a bidding strategy before committing to a real submission.

9. AI teams

AI teams hold any portfolios left over after the auction, and can also fill out a single-player game. Three strategies are available:

  • Marginal cost — bids every plant at its true variable cost (plus carbon). The purely competitive benchmark.
  • Markup — bids marginal cost plus a fixed percentage markup on every plant.
  • Adaptive — a heuristic best response: plants that are comfortably inframarginal at the forecast demand bid close to cost (to guarantee dispatch); plants near the expected margin shade their bid up toward the estimated market-clearing price, informed by the previous round's actual clearing price. All strategies respect the price cap/floor and are deterministic given a seed, so a given game's AI behavior is reproducible.

Because AI teams can fill any number of portfolios, a game can be run with as few as one human team.

10. Capacity planning

A one-shot exercise, typically run near the end of the semester: each team acts as a grid planner rather than a bidder, choosing which existing plants to retire and how many new plants to build, then sees whether its plan meets a full year of representative demand and an emissions target.

Technology catalog (fixed block sizes — teams pick an integer count of blocks, not arbitrary MW):

TechnologyBlock size (MW)Variable cost ($/MWh)CO2 (tons/MWh)Notes
Coal (new)40030.950.7791Original catalog
Natural gas (new)10036.380.5245Original catalog
Nuclear (new)1,00012.980.0000Original catalog
Wind (new)1000.000.0000Original catalog
Solar (new)1000.000.0000Original catalog
4-hour battery1005.000.0000Extension — admins can hide it
Geothermal1005.000.0000Extension — admins can hide it

Each new plant's capital expenditure is annualized via a capital-recovery-factor (CRF) annuity, at a 2% discount rate over a 30-year horizon (both configurable) — the same rate and horizon the original notebook used:

CRF = r(1+r)^T / ((1+r)^T − 1),  r = 0.02, T = 30
annualized capital cost = capacity_MW × capex_per_MW × CRF

Capacity payments: any incumbent plant (not a new build) whose annual profit at competitive (marginal-cost) prices comes out negative is paid a subsidy equal to exactly that shortfall, modeling a capacity-market backstop that keeps money-losing-but-needed incumbents in the fleet. New builds receive no such subsidy.

Emissions goal: the plan's total annual emissions are compared against a configured target; meeting it is reported but does not by itself change the plan's cost.

Unserved-energy penalty: if a plan cannot meet demand in some block, the unserved MWh is charged a large per-MWh penalty (a value-of-lost-load proxy) when computing the plan's ranking score, so a shortfall shows up as a graded outcome rather than an undefined result.

Cross-team ranking is an equal-weight composite of three ranks: total cost (including the unserved-energy penalty), total emissions, and unserved energy (reliability) — each team's plan is ranked on all three, and the ranks are summed to produce a single composite score.

11. For instructors (summary)

Full detail lives in the admin portal; in outline, an instructor:

  1. Uploads a class roster (CSV) — accounts are created automatically, with each student's university ID used as their initial password (they are prompted to change it on first login).
  2. Organizes students into teams, and (optionally) multiple parallel sections/games.
  3. Composes a generator set — either of the two built-in fleets, or a fully custom set of portfolios and plants.
  4. Composes the round-by-round schedule: round types, eras, open/close times, and per-round settings (carbon price, demand shock, excluded technologies, etc.).
  5. Clears rounds manually or on an automatic schedule, reviews the clearing report (including which teams fell back to marginal-cost bids), and publishes results to students.
  6. Grants per-team deadline extensions and manages the audit log of every clearing and settings change.
  7. Relies on scheduled backups to S3 for game data durability.

See /demo for a guided walkthrough with no login required.

12. Provenance: what matches the original, what differs

This is the key section for anyone evaluating whether the rebuild is faithful to the original game. "Original" below refers to the Stata implementation (esgV20_deterministic_eep147.do) and the live-game analysis notebook (ESG_Analysis_v2025.1.ipynb) and capacity-expansion notebook (ESG_CapacityExpansion_v2025.13.ipynb) from the course's GitHub repository (ds-modules/EEP-147-SP24); see docs/research/github-repo-review.md for the full source review this table is drawn from.

MechanicOriginal (Stata/notebook)This appStatus
Merit-order clearing & marginal pro-rata dispatch The notebook's price_calc/marginal_proportion: sort bids ascending, cumulative capacity vs. demand, tied units split pro-rata by capacity. The live Stata engine (market program) actually clears by bisection search of a (near-vertical) linear demand curve (load = intercept + loadslope × price) against the step supply curve, then rations tied units pro-rata — the student-facing docs describe demand as perfectly inelastic (vertical) Same sort/cumulative-capacity algorithm as the notebook, esg/engine/clearing.py::clear_block MATHEMATICALLY EQUIVALENT — as loadslope → 0 (the limit the 2024 game actually used, i.e. perfectly inelastic demand), the Stata engine's bisection search and this app's sort/cumulative-capacity dispatch yield the identical clearing price and dispatch
Supply exactly equal to demand Undefined — only strict < and > cases were handled in code Explicit third case: full dispatch at the marginal bid, no error DELIBERATE CHANGE — fixes an undefined edge case
Per-hour bids (4 load blocks/round) PRICE1..PRICE4 columns, one row per plant per round Same concept: one price (or per-block dict) per plant per round IDENTICAL
Round-1 pay-as-bid, other rounds uniform-price Global PAB_PERIODS = [1] switch in profit_calc Per-round pricing_rule setting (uniform/pay-as-bid), same capability IDENTICAL capability, generalized to any round
Carbon-price adjustment adjust_by_cp: variable cost += carbon price × CO2 rate effective_marginal_cost — identical formula IDENTICAL
Demand realization Documented rule: realized demand ~ Normal(mean = forecast, sd = 3% of forecast), drawn per location × hour (ESG_2024.pdf p.5); the 2024 run actually zeroed this shock out (deterministic) or substituted a manual demand_real.csv Seeded truncated-normal shock around the forecast, sized by demand_sigma_pct (0.03 reproduces the documented rule) or an absolute demand_sigma; 0 reproduces the 2024 deterministic behavior exactly MATHEMATICALLY EQUIVALENT — sigma_pct=0.03 matches the documented rule, sigma=0 matches how 2024 was actually run
Fixed cost accounting (per-block vs. per-round) FixedCst_OandM_perDay / 4, charged each of the round's 4 hour-blocks Full daily fixed cost charged once per round, summed from the portfolio MATHEMATICALLY EQUIVALENT — same daily total, computed once instead of accumulated across 4 blocks
Startup costs The Stata engine supported a per-plant startup cost charged when a unit switches on within the day (the $startup flag), disabled in the 2024 run ($startup==0) Not implemented DEFERRED — matches how the original's 2024 run was actually played (startup costs disabled there too)
Missing-bid handling GSIs manually chased down teams that hadn't submitted Automatic fallback to marginal-cost bids, flagged in the clearing report DELIBERATE CHANGE — removes manual intervention, keeps behavior explicit and auditable
Portfolio auction Open-outcry, ascending-price (English) auction: portfolios sold one at a time in class; each team buys exactly one; $100,000 reservation price; the bidder pool shrinks by one after each sale (ESG_2024.pdf p.6) Sealed-bid sequential highest-bid assignment: same $100,000 reserve price and one-portfolio-per-team outcome, run automatically instead of live in class, with seeded tie-breaks DELIBERATE CHANGE — auction format (open-outcry is impractical for an async/online class); reserve price and outcome structure preserved
Availability profiles (solar/wind/hydro/battery shapes) None — every plant can run in every hour Configurable per-block coefficients; off by default for the original-ESG fleet (all "baseload") NEW EXTENSION
AI teams (marginal-cost / markup / adaptive bidding) Not present — leftover portfolios were unplayed or reassigned manually Automated bidding strategies fill leftover or single-player portfolios NEW EXTENSION
Capacity planning: new-plant catalog, CRF annuity, capacity payments, emissions goal ESG_CapacityExpansion_v2025.13: same 5 technologies, block sizes, CRF (r=0.02, T=30), capacity-payment backstop, emissions goal Identical catalog, block counts, and formulas; competitive cross-team ranking and a value-of-lost-load penalty are added Catalog/CRF/payments: IDENTICAL; ranking + VOLL penalty: NEW EXTENSION
CO2 permit double-auction market Cap-and-trade permit market, active Rounds 4–6 with a Round-7 permits-only true-up. Each round, free permits are grandfathered directly; a double auction (price floor $24.04, ceiling $88.24, government supply offered at the floor) clears computationally from Google-Form buy/sell tiers; bilateral trades are manually validated Google-Form entries that are only summed, never algorithmically cleared. At the Round-7 true-up: offsets cover up to 5% of a team's emissions at $32/ton, then a $300/ton penalty applies to any remaining shortfall Not implemented DEFERRED to a later version
Parallel sections Up to 3 independent sections sharing one demand table, separate bid histories Supported: run one independent game per section IDENTICAL capability

13. Glossary

Merit order
Ranking generators from cheapest to most expensive bid; the market dispatches from the top of this order down until demand is met.
Clearing price
The bid price of the marginal (last-needed) unit; under uniform pricing, every dispatched unit is paid this price.
Inframarginal / marginal
A plant is inframarginal if it is strictly cheaper than the clearing price (runs at full offered capacity); it is marginal if its bid equals the clearing price (may run only partially, or split pro-rata with other tied plants).
Uniform price
A settlement rule in which every dispatched MW, regardless of its own bid, is paid the single market clearing price.
Pay-as-bid
A settlement rule in which every dispatched MW is paid exactly its own bid price.
Capacity factor
The share of a plant's nameplate capacity it actually produces on average, over some period — driven here by its availability profile and (for hydro) its energy budget.
VOLL (value of lost load)
A per-MWh penalty applied to unserved demand, used in the capacity-planning ranking score to make a shortfall a graded (worse) outcome rather than an undefined one.