Skip to content

About

UCS Land Use Clinic 25-26

Resources

Stars

1 star

Watchers

0 watching

Forks

Repository files navigation

Common Ground

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.

Getting Started

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.

Requirements

  • Use Godot 4.5. to run the game. Older versions won't open the project.
  • This game has no external dependencies.

How to Run Locally

  1. Clone the repo.
  2. Open project.godot in Godot 4.5.
  3. 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.

Web export

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.

  1. Install Godot's Web export templates (Editor → Manage Export Templates).
  2. 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.

Game Flow

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)

Directory Structure

├── 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

Architecture Overview

Global state

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_ROUNDS is 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). -1 when unset.
  • Persisted board state
    • placed_cards: Array — every winning card from previous rounds, stored as { "card_data": LandOptionCardData, "local_position": Vector2 } so GridScene can 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 by RollToNegotiate (roll + votes ≥ 8).
    • winner: Array — [card_name, role_name, role_key] for the winning proposal.
    • veto: bool — true while the flow is unwinding from a farmer veto.
  • Unrepurposed tile counts (drive land-effect penalties in Summary)
    • open_corp_tiles: int
    • open_local_tiles: int
    • open_abandoned_tiles: int
  • Per-round event snapshot (set by EventScene before 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 from ROLES.
    • make_voted_dict() / make_ability_dict() — build a fresh {role_key → ""} or {role_key → false} dict for tracking per-round voting/ability state.

Core loop

  • Scripts:
    • scripts/game_structure/GameScreen.gd
    • scripts/game_structure/RoundManager.gd
    • scripts/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.

Pre-game scenes

  • Scripts:
    • scripts/exposition/exposition.gd
    • scripts/exposition/roles.gd
    • scripts/game_structure/start.gd
    • scripts/game_structure/MainMenu.gd
    • scripts/ui/how_to.gd
    • scripts/ui/menu_buttons.gd
  • Scenes:
    • scenes/exposition/Exposition.tscn
    • scenes/exposition/roles.tscn
    • scenes/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.

Proposal phase

  • Scripts:
    • scripts/game_structure/GameScreen.gd
    • scripts/grid/CardDragController.gd
    • scripts/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.

Discussion phase

  • Scripts:
    • scripts/voting/discussion_ui.gd
    • scripts/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.

Voting phase

  • 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.

Grid & board

  • Scripts:
    • scripts/grid/GridScene.gd
    • scripts/grid/GridManager.gd
    • scripts/grid/BoardIcon.gd
    • scripts/grid/CardDragController.gd
  • Scenes:
    • scenes/grid/GridScene.tscn
    • scenes/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 / Data

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 custom LandOptionCardData resource type used to represent a project. Each project lives as a .tres file in assets/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.
  • scripts/cards/EventCardData.gd — defines the EventCardData resource type for weather events. Each event (Drought, Flood, Heatwave) lives as a .tres file in assets/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+).

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 a BoardIcon on the grid. Each card reads its data from a LandOptionCardData resource.
  • scripts/cards/ProposalDetailCard.gd — the larger popup detail view shown when a player clicks a proposed card during discussion. Other scenes populate it by calling populate(card_data) with the relevant card.

Metrics

  • Scripts:
    • scripts/metrics/MetricsTable.gd
    • scripts/metrics/metric_chip.gd
  • Scenes:
    • scenes/metrics/MetricsTable.tscn
    • scenes/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).

Weather Events

  • 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.

Summary

  • 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.

  1. Card Effects — the winning card's metric effects, plus a Passives subsection summing per-round effects from all previously placed cards.
  2. Weather Event Effects — effects from the random event.
  3. Land Effects — per-round negative effects from unrepurposed land on the board. Unused Farm Co. tiles cause econ-1 / env-1 / comm-1 each. Unused Local tiles cause econ+1 / env-1 / lv+1 each.

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.

Outcomes

  • Scripts:
    • scripts/voting/won.gd
    • scripts/voting/RollToNegotiate.gd
    • scripts/voting/local_farmer_veto.gd
  • Scenes:
    • scenes/voting/Won.tscn
    • scenes/voting/RollToNegotiate.tscn
    • scenes/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 to Won.
  • 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 to Won.
  • 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.

End

  • 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.

Legacy code

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.

Signal Flow

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()

About

UCS Land Use Clinic 25-26

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages