Harvey Mudd College Clinic - Union of Concerned Scientists 2025–2026
This repository contains the scripts and files for Common Ground, an educational multiplayer strategy game about Multibenefit Land Repurposing, made by the Harvey Mudd College Clinic team in collaboration with the Union of Concerned Scientists. See below for instructions on how to run the game and how it was built.
Common Ground is a Godot 4.5 local game. Four players share a single computer to run the game, adopt community roles, and shape a shared landscape over four rounds: proposing projects, debating placements, voting, and handling random severe weather events.
- Use Godot 4.5. to run the game. Older versions won't open the project.
- This game has no external dependencies.
- Clone the repo.
- Open
project.godotin Godot 4.5. - Press Play (F5). The game starts at
MainMenu.tscn.
Two autoloads run for the whole session (set in project.godot):
| Autoload | Path | Purpose |
|---|---|---|
Global |
scripts/global.gd |
Shared game state. Emits metrics_changed. |
AudioManager |
scenes/audio/AudioManager.tscn |
Music, SFX, exposition voiceovers. |
If you'd like to host the game in a browser instead of running it from the Godot editor (for sharing, demoing, or self-hosting on a static site), use the Web export. A Web export preset named Common Ground is already configured; it builds to exports/final-export/index.html.
- Install Godot's Web export templates (Editor → Manage Export Templates).
- Project → Export → select Common Ground → Export Project.
Threads are on (variant/thread_support=true in export_presets.cfg), so the build uses SharedArrayBuffer. Browsers only enable that when the page is served with two cross-origin isolation headers:
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp
A _headers file in exports/final-export/ already sets them, which means hosts that read it (Netlify, Cloudflare Pages) work out of the box — just upload the export directory.
Don't open index.html directly via file:// — browsers won't fetch the .pck that way. The plain python3 -m http.server command also won't work, because it doesn't send the two headers above and the game will fail to start. For local testing, use a server that injects them, for example:
# from inside exports/final-export/
npx http-server -p 8000 \
--cors \
-H 'Cross-Origin-Opener-Policy: same-origin' \
-H 'Cross-Origin-Embedder-Policy: require-corp'Then open http://localhost:8000. For public deploys, any static host that respects _headers (Netlify, Cloudflare Pages) or lets you configure response headers will work; itch.io's HTML5 upload also has a "SharedArrayBuffer support" toggle that sets these headers for you.
Each round follows the same pattern: every player proposes a project, the group debates and votes, then a random event hits. After round 4 (or whenever funds drop below 5, the minimum of the project costs) the game ends and the metrics decide win/lose states.
MainMenu
→ Exposition (3-page story intro)
→ Roles (roll to assign roles, click for detail popups)
→ [Round 1–4]
Start (metrics overview w/ hover cards)
→ GameScreen
Proposal × 4 (pick a card & place it on the grid)
→ Discussion (2-min timer, can edit positions)
→ Voting (each player votes, abilities optional)
→ Outcome
Won (card passes)
RollToNegotiate (Corp land — 2d6 + votes ≥ 8)
LocalFarmerVeto (veto → EventScene, allow → Won)
→ EventScene (random event, tiered by protection)
→ Summary (round breakdown, apply changes)
→ [next round, or End if round 4 / fund < 5]
→ End (win/lose + role crowns)
├── project.godot # Entry point; main scene = MainMenu.tscn
├── default_bus_layout.tres # Audio bus layout
├── export_presets.cfg # "Common Ground" preset → exports/final-export/
├── README.md
├── assets/
│ ├── audio/
│ │ ├── music/ # Background tracks
│ │ ├── sfx/ # Sound effects
│ │ └── voiceovers2/ # Exposition voice lines
│ ├── cards/
│ │ ├── event_cards/data/ # EventCardData .tres (Drought, Flood, Heatwave)
│ │ ├── land_cards/ # Land-type card art
│ │ ├── option_cards/data/ # LandOptionCardData .tres (8 land options)
│ │ └── role_cards/ # Role display art
│ ├── dice/ # Dice sprites for Roll-to-Negotiate
│ ├── fonts/ # VT323, Tomorrow, Terminal-Grotesque
│ ├── map/ # Board art
│ ├── metric_icons/ # Metric chip icons
│ ├── themes/ # .tres theme files
│ ├── v1-pixel-art/ # Pixel art for cards and player icons
│ └── vfx/ # Visual effects
├── scenes/
│ ├── audio/ # AudioManager (autoload)
│ ├── cards/ # Card UI scenes
│ ├── events/ # EventScene
│ ├── exposition/ # Story intro + role assignment
│ ├── game_structure/ # MainMenu, GameScreen, Start, Summary, End
│ ├── grid/ # Board / grid scenes
│ ├── metrics/ # MetricsTable + MetricChip
│ ├── ui/ # HowTo, MenuButtons
│ ├── voting/ # VotingUI, DiscussionUI, outcome scenes
│ └── deprecated/ # Legacy scenes; not in current flow
├── scripts/
│ ├── global.gd # Autoload — shared game state
│ ├── audio/, cards/, events/, exposition/, game_structure/,
│ │ grid/, metrics/, ui/, voting/
│ └── deprecated/ # Legacy scripts
├── shaders/
├── simulation/ # (currently empty)
└── exports/
└── final-export/ # Web build output
scripts/global.gd
Global is an autoload singleton. That is Godot's term for a node that's instantiated once at startup and is accessible by name from any script for the lifetime of the game. We use it to hold all of the shared game state (metrics, proposals, the winning card, etc.) so scenes don't have to pass data through each other. Setters on the four metrics emit the metrics_changed signal, and MetricsTable re-renders based on that signal.
Defaults:
| Metric | Default |
|---|---|
community, economy, environment |
0 |
landvalue |
25 |
fund |
100 |
Goals:
| Type | Value | Source |
|---|---|---|
| Base goal | 50 |
community_goal, economy_goal, environment_goal, landvalue_goal |
| Role goal | 100 |
goal_value per role in the ROLES dict |
Role keys are lowercase: "comm", "econ", "env", "farm".
The singleton also tracks the following variables:
- Round / turn
current_round: int— current round number, 1–4.MAX_ROUNDSis 4.proposals: Array— array of{ "card_data": LandOptionCardData, "role_key": String, "role_name": String }, one entry per player's proposal in the current round.
- Winning card of the current round
winning_card_data: LandOptionCardData— the project that won the vote, including its metric effects, protections, cost, etc.winning_role_key: String— the role key ("comm","econ","env","farm") of the player who proposed the winning card. Used by the outcome scenes to drive role-specific behavior (e.g., farmer veto eligibility).winning_card_land: int— the land type the winning project was placed on (1 BG / 2 Corp / 3 Local / 4 Abandoned).-1when unset.
- Persisted board state
placed_cards: Array— every winning card from previous rounds, stored as{ "card_data": LandOptionCardData, "local_position": Vector2 }soGridScenecan re-spawn them at the start of each round.
- Ability flags (set during voting, applied during summary, then reset)
used_ability: Dictionary—{ "comm": bool, "env": bool, "econ": bool, "farm": bool }, tracks which roles have spent their once-per-game ability.double_environment: bool— Environmentalist ability is active for this round.halve_cost: bool— Green Economist ability is active for this round.add_land_value: bool— Local Farmer ability is active for this round.can_veto: bool— Local Farmer is eligible to veto this round's winner (the winning project landed on Local Farmer land and the farmer didn't vote for it).
- Voting
votes: int— votes the winning card received; used byRollToNegotiate(roll + votes ≥ 8).winner: Array—[card_name, role_name, role_key]for the winning proposal.veto: bool—truewhile the flow is unwinding from a farmer veto.
- Unrepurposed tile counts (drive land-effect penalties in Summary)
open_corp_tiles: intopen_local_tiles: intopen_abandoned_tiles: int
- Per-round event snapshot (set by
EventScenebefore Summary loads)last_event_name: String— name of the most recent event.last_event_protection_count: int— number of placed projects that protect against this event.last_event_effects: Dictionary—{ "env": int, "econ": int, "comm": int, "lv": int, "fund": int }, the metric deltas to apply.
- Static helper functions
role_name(key)/role_color(key)— look up display name and hex color fromROLES.make_voted_dict()/make_ability_dict()— build a fresh{role_key → ""}or{role_key → false}dict for tracking per-round voting/ability state.
- Scripts:
scripts/game_structure/GameScreen.gdscripts/game_structure/RoundManager.gdscripts/game_structure/TurnManager.gd
- Scenes:
scenes/game_structure/GameScreen.tscn
GameScreen.gd is the main scene controller. It runs proposal → discussion → voting → outcome inside a single round and owns the shared UI (top text, finalize/cancel buttons, card box, metrics table). It emits proposal_finalized when a player commits to a placement.
RoundManager.gd sequences the four rounds and the four turns inside each. When all four players have proposed, it kicks off discussion. TurnManager.gd is a small helper script that hands turn changes back to the proposal system.
All .tscn files are Godot scene files that contain all of the UI elements that players see and interact with.
- Scripts:
scripts/exposition/exposition.gdscripts/exposition/roles.gdscripts/game_structure/start.gdscripts/game_structure/MainMenu.gdscripts/ui/how_to.gdscripts/ui/menu_buttons.gd
- Scenes:
scenes/exposition/Exposition.tscnscenes/exposition/roles.tscnscenes/game_structure/start.tscn,scenes/game_structure/MainMenu.tscn,scenes/ui/HowTo.tscn,scenes/ui/MenuButtons.tscn
exposition.gd plays the three-page story intro, then loads the Roles screen. The Roles screen rolls through all 24 permutations with animation, assigns lowercase role keys to Global.player1 through Global.player4, and shows info popups on click.
start.gd is the round-start screen: staggered fade-ins, two-zone progress bars (✓ at 50, ♛ at 100), and hover cards for goal info, player number, and ability status.
The tutorial (how_to.gd, ui_how_to.gd) is a 7-topic hub plus a 14-page UI walkthrough that links out to the Google Doc rulebook.
- Scripts:
scripts/game_structure/GameScreen.gdscripts/grid/CardDragController.gdscripts/cards/LandOptionCard.gd
- Scene:
scenes/game_structure/GameScreen.tscn
Each player takes a turn proposing a project for the round. On their turn, the player chooses a repurposing option from the card box and drags it onto the grid. CardDragController.gd validates the placement (correct land type, no collisions, fits within grid_size) and tints the snap target green/red. Finalizing spawns a BoardIcon and emits proposal_finalized, which TurnManager.gd forwards to RoundManager.gd. Once all four players have proposed, RoundManager.gd transitions the screen to discussion.
- Scripts:
scripts/voting/discussion_ui.gdscripts/voting/DiscussionTimer.gd
- Scenes:
scenes/voting/DiscussionUI.tscn
This section is a two-minute timed deliberation to allow players to discuss which project they should agree on. The timer pulses red in the last 10 seconds. Each placed card becomes a preview overlay; clicking one opens its detail card and metric preview, then displays an "Edit Position" button. Editing unlocks the icon for re-drag and validates on confirm (logic in scripts/voting/discussion_ui.gd). The drag-and-drop, snap tinting, and rubber-band animation live in scripts/grid/CardDragController.gd and scripts/grid/BoardIcon.gd. An invalid drop or a cancel rubber-bands the icon back to its original spot. When the timer ends, discussion_ended fires, and the game transitions to voting.
- Scripts:
scripts/voting/voting_ui.gd
- Scenes:
scenes/voting/VotingUI.tscn
Once the discussion phase ends, each player casts one vote on which proposal should be built. Vote buttons spawn over each board icon. Every player picks one project. The Community Leader's ability lets them cast an extra vote, and ties are chosen randomly.
Ability effects apply when the winning card is stored to Global:
- Environmentalist: doubles the environment value.
- Green Economist: halves the cost.
- Local Farmer: land value metric effect of project adds to the local farmer's land value even if it is not built on the local farmer's land.
If the winning project was placed on Local Farmer land, the voting phase also flags the Local Farmer as eligible to veto for this round.
- Scripts:
scripts/grid/GridScene.gdscripts/grid/GridManager.gdscripts/grid/BoardIcon.gdscripts/grid/CardDragController.gd
- Scenes:
scenes/grid/GridScene.tscnscenes/grid/BoardIcon.tscn
The grid is the central play surface where all four players propose and place projects. It's divided into tiles of four land types, each with its own placement rules. Every project that wins a vote stays on the grid for the rest of the game, so the board accumulates state across rounds.
GridScene.gd wraps the board, re-spawns previous rounds' winners, and exposes spawn_card, is_card_placement_valid, get_land_type, and remove_last_card.
GridManager.gd owns the placement logic. Land types are encoded as BG=1, Corp=2, Local=3, and Abandoned=4, and the tile size is 128px. GridManager.gd also runs the validation that enforces single-layer footprints, dry-land vs. river-land building rules, and collision checks.
BoardIcon.gd is for placed cards. set_card_for_grid scales the sprite and collision shape to fit the card's grid_size. CardDragController.gd handles drag-and-drop with green/red snap tinting and rubber-bands invalid drops.
Cards are how the game represents much of the information in the game. Every land-repurposing project the players propose, inital unrepurposed types of land, and every weather event that hits at the end of a round, is represented as a card.
Data resources (typed data containers):
-
scripts/cards/LandOptionCardData.gd— defines a customLandOptionCardDataresource type used to represent a project. Each project lives as a.tresfile inassets/cards/option_cards/data/(eight projects total).- Fields:
name,description,image, metric effects (env,comm,econ,land_value),grid_size(footprint on the board),cost,protection(which weather event, if any, this project protects against), and per-round passive effects.
- Fields:
-
scripts/cards/EventCardData.gd— defines theEventCardDataresource type for weather events. Each event (Drought, Flood, Heatwave) lives as a.tresfile inassets/cards/event_cards/data/.- Fields:
name,description,image,icon_index, plus three tiers of metric effects (env/comm/econ/land_value/money), one per protection-count bucket (0 protections, 1, 2+).
- Fields:
UI scenes/scripts (the visual representations players actually interact with):
scripts/cards/LandOptionCard.gd— the in-game card a player drags from the card box. The card has a flip animation: the first click reveals it, and the second click spawns aBoardIconon the grid. Each card reads its data from aLandOptionCardDataresource.scripts/cards/ProposalDetailCard.gd— the larger popup detail view shown when a player clicks a proposed card during discussion. Other scenes populate it by callingpopulate(card_data)with the relevant card.
- Scripts:
scripts/metrics/MetricsTable.gdscripts/metrics/metric_chip.gd
- Scenes:
scenes/metrics/MetricsTable.tscnscenes/metrics/MetricChip.tscn
The four metrics are Community, Economy, Environment, and Land Value. These are the main numbers players are trying to grow to achieve their goals. Each project they build moves them, weather events decrease them, and the survival condition (every metric ≥ 50) is the whole objective of the game. MetricsTable is the persistent on-screen panel on the top right of the screen that displays them.
MetricsTable listens to Global.metrics_changed and re-renders whenever it fires. It runs in two modes: a full panel and a compact 80×220 strip. Metric value changes animate, and preview overlays show projected values with colored deltas. The progress bars use a red → yellow → green gradient with markers at 50 (✓ base goal) and 100 (♛ role goal).
- Scripts:
scripts/events/EventScene.gd
- Scenes:
scenes/events/EventScene.tscn
A random weather event takes place at the end of every round and applies negative effects to the metrics. The severity of the effect depends on how many of the projects built on the board offer protection against that specific event: the three tiers for effects correlate to having 0, 1, or 2+ protective projects. This rewards players for building defensively.
EventScene loads every EventCardData from assets/cards/event_cards/data/ and runs the sequence Start → Shuffle (4s) → Reveal → Effects. The Effects step shows three protection-tier panels (0, 1, 2+) and highlights the active tier based on how many placed cards have their protection data match the event name. When the player dismisses the screen, EventScene saves the resulting snapshot to Global and loads the Summary scene.
- Scripts:
scripts/game_structure/summary.gd
- Scenes:
scenes/game_structure/Summary.tscn
At the end of each round we give the players a three-column round breakdown. This summary scene provides an overview of everything that happened in the round and how the metrics were affected by each part.
- Card Effects — the winning card's metric effects, plus a Passives subsection summing per-round effects from all previously placed cards.
- Weather Event Effects — effects from the random event.
- Land Effects — per-round negative effects from unrepurposed land on the board. Unused Farm Co. tiles cause
econ-1 / env-1 / comm-1each. Unused Local tiles causeecon+1 / env-1 / lv+1each.
Once the per-round metric effects are applied, Summary clears the active ability flags (halve_cost, double_environment) so they don't carry into the next round. The screen routes to End when current_round == 4 or fund < 5.
- Scripts:
scripts/voting/won.gdscripts/voting/RollToNegotiate.gdscripts/voting/local_farmer_veto.gd
- Scenes:
scenes/voting/Won.tscnscenes/voting/RollToNegotiate.tscnscenes/voting/Vetoed.tscn
After players vote, the game switches to one of the following scenes:
- RollToNegotiate — used when the winning project landed on Farm Co. land, since the players have to "negotiate" with the corporation to build there. The scene animates a 2d6 dice roll and tests
roll + votes ≥ 8. If the roll fails, the card is removed; if it succeeds, the game continues toWon. - LocalFarmerVeto — used when the winning project was placed on the Local Farmer's land and the Local Farmer didn't vote for it. The Local Farmer then chooses whether to veto. A veto skips the card straight to
EventScene; allowing it routes toWon. - Won — used when the winning proposal has no special conditions to resolve. The scene shows a per-card-type confirmation message and then transitions to
EventScene.
- Scripts:
scripts/game_structure/end.gd
- Scenes:
scenes/game_structure/End.tscn
The End scene shows a summary of every metric, whether the players reached the survival threshold, and — if they did — which roles also hit their personal goals.
The players win when every metric is ≥ 50, and lose otherwise. Crown badges light up for any role whose individual metric is ≥ 100. playAgain() resets Global and returns to MainMenu.
scripts/deprecated/ and scenes/deprecated/ hold older flow scenes and test scaffolds (description.gd, chosen.gd, objective.gd, details.gd, and others). Nothing in the current flow reaches them.
Godot uses signals to connect different scripts and code together and to activate functions across them. The following shows the signals used to drive the game.
Proposal:
GameScreen.proposal_finalized → TurnManager → Global.proposals.append()
→ RoundManager.turn_finished() → next turn or deliberation
Discussion:
DiscussionTimer.timer_ended → discussion_ui cleanup → discussion_ended
→ GameScreen → transition to VotingUI
Voting:
voting_ui.voting_completed → GameScreen → route to outcome scene by land type
Metrics:
Global.metrics_changed → MetricsTable._refresh_from_global()