A fan-made statistics tracker for the board game Terraforming Mars. Records game sessions, tracks scores over time, and surfaces insights about players, corporations, cards, milestones, and awards.
Fan project — not affiliated with, endorsed by, or sponsored by FryxGames in any way. All Terraforming Mars intellectual property belongs to FryxGames.
A web application built for physical (and digital) Terraforming Mars groups who want to keep a proper record of their games. Once you log a session you get:
- Full score breakdowns per game (TR, milestones, awards, greeneries, cities, cards)
- Win rates and score trends per player and corporation
- Card play frequency and affinity stats
- Milestone and award claim/funding rates
- A printable score sheet to fill in during the game
It started as a passion project — our group never wrote down scores during physical games, so stats just disappeared. This fixes that.
Note: The card reference is still being filled in, some cards display oddly, and the app is under active development. Stats are fun, but take them with a grain of salt.
If someone has shared a link to a running instance, everything is publicly readable — no account needed. You can explore all games, players, cards and stats freely. Only adding or editing data requires a login.
This section is for people who want to run the app for their own playgroup. It requires some basic familiarity with the command line and web development tools. If that sounds daunting, feel free to reach out — a more beginner-friendly guide may come later.
- Node.js 20+ — the runtime that builds the app (nodejs.org)
- A Supabase account — free tier is more than enough. Supabase is a hosted database service that the app uses to store all game data (supabase.com)
- A GitHub account — if you want to deploy to GitHub Pages for free
Fork this repository to your own GitHub account, then clone it locally:
git clone https://github.com/<your-username>/tm-scoring-statistics.git
cd tm-scoring-statistics
npm install- Go to supabase.com and create a free account
- Create a new project
- Once it's ready, go to Project Settings → API and copy your Project URL and anon public key
In your Supabase project, go to SQL Editor and run the schema below to create all the tables the app needs:
-- Core game tables
create table game_sessions (
id uuid primary key default gen_random_uuid(),
date date not null,
map_name text,
player_count int,
generations int,
turn_order text[],
game_number int
);
create table player_results (
id uuid primary key default gen_random_uuid(),
game_id uuid references game_sessions(id) on delete cascade,
player_name text,
position int,
corporation text,
corporation_2 text,
tr int, milestone_vp int, award_vp int,
greenery_vp int, city_vp int, card_vp int,
total_vp int, mc int,
key_notes text
);
create table game_expansions (
id uuid primary key default gen_random_uuid(),
game_id uuid references game_sessions(id) on delete cascade,
expansion_name text
);
create table game_colonies (
id uuid primary key default gen_random_uuid(),
game_id uuid references game_sessions(id) on delete cascade,
colony_name text
);
create table game_milestones (
id uuid primary key default gen_random_uuid(),
game_id uuid references game_sessions(id) on delete cascade,
milestone_name text,
player_name text,
claimed_order int
);
create table game_awards (
id uuid primary key default gen_random_uuid(),
game_id uuid references game_sessions(id) on delete cascade,
award_name text,
funder_name text,
funded_order int,
winner_name text,
winner_name_2 text,
second_name text,
second_name_2 text
);
create table parameter_contributions (
id uuid primary key default gen_random_uuid(),
game_id uuid references game_sessions(id) on delete cascade,
player_name text,
temperature_steps int default 0,
oxygen_steps int default 0,
ocean_steps int default 0,
venus_steps int default 0,
habitat_steps int default 0,
mining_steps int default 0,
logistics_steps int default 0
);
create table cards_played (
id uuid primary key default gen_random_uuid(),
game_id uuid references game_sessions(id) on delete cascade,
player_name text,
card_name text,
vp_from_card int,
generation int
);
-- Card reference library
create table card_reference (
id uuid primary key default gen_random_uuid(),
card_name text unique,
card_type text,
cost int,
tags text,
base_vp int,
vp_type text,
vp_per text,
resource_type text,
description text
);
create table card_expansions (
id uuid primary key default gen_random_uuid(),
card_id uuid references card_reference(id) on delete cascade,
expansion text
);
-- Player profiles and notes
create table player_profiles (
id uuid primary key default gen_random_uuid(),
player_name text unique,
preferred_color text,
display_name text
);
create table site_notes (
id uuid primary key default gen_random_uuid(),
category text,
content text,
created_at timestamptz default now()
);Still in the SQL Editor, run this to lock down writes to authenticated users only while keeping everything publicly readable:
-- Enable RLS
alter table game_sessions enable row level security;
alter table player_results enable row level security;
alter table game_expansions enable row level security;
alter table game_colonies enable row level security;
alter table game_milestones enable row level security;
alter table game_awards enable row level security;
alter table parameter_contributions enable row level security;
alter table cards_played enable row level security;
alter table card_reference enable row level security;
alter table card_expansions enable row level security;
alter table player_profiles enable row level security;
alter table site_notes enable row level security;
-- Public read
create policy "public read" on game_sessions for select using (true);
create policy "public read" on player_results for select using (true);
create policy "public read" on game_expansions for select using (true);
create policy "public read" on game_colonies for select using (true);
create policy "public read" on game_milestones for select using (true);
create policy "public read" on game_awards for select using (true);
create policy "public read" on parameter_contributions for select using (true);
create policy "public read" on cards_played for select using (true);
create policy "public read" on card_reference for select using (true);
create policy "public read" on card_expansions for select using (true);
create policy "public read" on player_profiles for select using (true);
create policy "public read" on site_notes for select using (true);
-- Authenticated write
create policy "auth insert" on game_sessions for insert with check (auth.role() = 'authenticated');
create policy "auth insert" on player_results for insert with check (auth.role() = 'authenticated');
create policy "auth insert" on game_expansions for insert with check (auth.role() = 'authenticated');
create policy "auth insert" on game_colonies for insert with check (auth.role() = 'authenticated');
create policy "auth insert" on game_milestones for insert with check (auth.role() = 'authenticated');
create policy "auth insert" on game_awards for insert with check (auth.role() = 'authenticated');
create policy "auth insert" on parameter_contributions for insert with check (auth.role() = 'authenticated');
create policy "auth insert" on cards_played for insert with check (auth.role() = 'authenticated');
create policy "auth insert" on card_reference for insert with check (auth.role() = 'authenticated');
create policy "auth insert" on card_expansions for insert with check (auth.role() = 'authenticated');
create policy "auth insert" on player_profiles for insert with check (auth.role() = 'authenticated');
create policy "auth insert" on site_notes for insert with check (auth.role() = 'authenticated');
create policy "auth update" on game_sessions for update using (auth.role() = 'authenticated');
create policy "auth update" on player_results for update using (auth.role() = 'authenticated');
create policy "auth update" on game_expansions for update using (auth.role() = 'authenticated');
create policy "auth update" on game_colonies for update using (auth.role() = 'authenticated');
create policy "auth update" on game_milestones for update using (auth.role() = 'authenticated');
create policy "auth update" on game_awards for update using (auth.role() = 'authenticated');
create policy "auth update" on parameter_contributions for update using (auth.role() = 'authenticated');
create policy "auth update" on cards_played for update using (auth.role() = 'authenticated');
create policy "auth update" on card_reference for update using (auth.role() = 'authenticated');
create policy "auth update" on card_expansions for update using (auth.role() = 'authenticated');
create policy "auth update" on player_profiles for update using (auth.role() = 'authenticated');
create policy "auth update" on site_notes for update using (auth.role() = 'authenticated');
create policy "auth delete" on game_sessions for delete using (auth.role() = 'authenticated');
create policy "auth delete" on player_results for delete using (auth.role() = 'authenticated');
create policy "auth delete" on game_expansions for delete using (auth.role() = 'authenticated');
create policy "auth delete" on game_colonies for delete using (auth.role() = 'authenticated');
create policy "auth delete" on game_milestones for delete using (auth.role() = 'authenticated');
create policy "auth delete" on game_awards for delete using (auth.role() = 'authenticated');
create policy "auth delete" on parameter_contributions for delete using (auth.role() = 'authenticated');
create policy "auth delete" on cards_played for delete using (auth.role() = 'authenticated');
create policy "auth delete" on card_reference for delete using (auth.role() = 'authenticated');
create policy "auth delete" on card_expansions for delete using (auth.role() = 'authenticated');
create policy "auth delete" on player_profiles for delete using (auth.role() = 'authenticated');
create policy "auth delete" on site_notes for delete using (auth.role() = 'authenticated');Also go to Authentication → Providers → Email and disable "Allow new users to sign up" — then create your admin account manually in Authentication → Users before disabling it. This ensures only you can log in.
Create a .env.local file in the project root:
VITE_SUPABASE_URL=https://<your-project>.supabase.co
VITE_SUPABASE_ANON_KEY=<your-anon-key>
npm run devOpen http://localhost:5173/tm-scoring-statistics/ in your browser.
npm run buildThen push to your fork and enable GitHub Pages in your repository settings, pointing at the dist folder or using a GitHub Actions workflow.
If your group plays on terraforming-mars.herokuapp.com, you can import game data directly from the game log instead of entering it manually.
How it works:
- After a game ends, open the game log in the online app — it contains a full play-by-play of every card played, milestone claimed, and award funded
- Copy the entire log text
- In the admin panel, go to Parse Log and paste it in
- The parser extracts players, cards played per generation, milestones, and awards and shows you a preview before saving
- You still fill in the score breakdown (VP totals) manually on the Add Game screen — the log does not contain final scores
The parser handles known encoding issues and card name differences between the online app and the card reference. Some milestone variants require a manual disambiguation step since the log output is ambiguous.
| Route | Page | What it shows |
|---|---|---|
/ |
Dashboard | Recent games, top stats, quick links |
/games |
Games | Full game log with filters |
/games/:id |
Game detail | Complete breakdown of one session |
/players |
Players | All players, win rates, scores |
/players/:name |
Player detail | Card affinity, corp history, score trends |
/cards |
Cards | Card reference with play and win rate stats |
/cards/:name |
Card detail | Who played it, in which games, VP contributed |
/corporations |
Corporations | Win rates and score averages per corporation |
/ceos |
CEOs | CEO card stats |
/ma |
Milestones & Awards | Claim and funding rates |
/leaderboard |
Leaderboard | All-time player standings |
/scoresheet |
Score sheet | Printable score sheet for physical games |
/notes |
Notes | Session notes |
| Route | Page | What it does |
|---|---|---|
/admin |
Dashboard | Links to all admin tools |
/admin/add-game |
Add game | Record a new session |
/admin/cards/reference |
Card reference | Add and edit the card library |
/admin/parse-log |
Parse log | Import game data from a log format |
/admin/player-profile |
Player profiles | Edit player names and profile info |
The source code in this repository is released under the MIT license — see LICENSE for details.
Some visual assets used in the app (icons, frames, and graphical elements) are derived from or inspired by the Terraforming Mars board game and the open-source community project terraforming-mars/terraforming-mars. These assets are not covered by the MIT license and remain the property of their respective owners.
This project is non-commercial and fan-made. It is not affiliated with FryxGames in any way.
A heartfelt thank you to FryxGames for creating Terraforming Mars — one of the greatest board games ever made. This project exists entirely because of the joy their game has brought to our table. If you haven't played it, go get a copy.
This application is a fan project and is not affiliated with, endorsed by, or sponsored by FryxGames in any way. All Terraforming Mars intellectual property belongs to FryxGames.
The card icons, tag icons, expansion icons, and resource icons used throughout this application were sourced from and inspired by the incredible open-source community project:
terraforming-mars/terraforming-mars
This project would not look the way it does without the work of every contributor there. Thank you.
This is a personal, non-commercial fan project. All game content, card names, corporation names, and related intellectual property belong to their respective owners.