Skip to content

Repository files navigation

🌍 EthnoGuessr

Guess where they're from. A geography guessing game where you identify a person's origin based on their photo — powered by Leaflet maps, PostGIS scoring, and Supabase.

Think GeoGuessr, but for people and cultures.


✨ Features

Feature Description
🎮 Core Game View a person's image, click a world map to guess their origin
📏 Haversine Scoring Exponential decay scoring (max 5000 pts) based on distance accuracy
🏆 Global Leaderboard Top 50 players, auto-aggregated scores, medal rankings
🔐 GitHub OAuth One-click sign-in via GitHub — no passwords needed
🛠️ Admin Panel Create levels: upload images, pick locations on map, set difficulty
🗺️ Leaflet + OpenStreetMap Interactive maps for both guessing and admin location selection
🛡️ Anti-Cheat Rate limiting (15 guesses/min), unique constraint (1 guess/level/user)
📍 PostGIS Spatial geography types, spatial indexes, precise coordinate storage
🔒 Row Level Security All tables protected with Supabase RLS policies

🛠️ Tech Stack

Layer Technology
Framework Next.js 15 (App Router)
Language TypeScript
Styling TailwindCSS v4
Maps Leaflet + OpenStreetMap
Database PostgreSQL + PostGIS (via Supabase)
Auth Supabase Auth (GitHub OAuth)
Storage Supabase Storage (level images)
Deployment Vercel / any Node.js host

📁 Project Structure

src/
├── app/
│   ├── layout.tsx                  # Root layout (Inter font, dark theme)
│   ├── page.tsx                    # Landing page (hero + features)
│   ├── globals.css                 # Tailwind + custom dark styles
│   ├── login/page.tsx              # GitHub OAuth sign-in
│   ├── game/page.tsx               # Game page (random unplayed level)
│   ├── leaderboard/page.tsx        # Global leaderboard (top 50, ISR)
│   ├── admin/page.tsx              # Admin panel (role-gated)
│   ├── auth/
│   │   ├── callback/route.ts       # OAuth code exchange
│   │   └── signout/route.ts        # Sign out handler
│   └── api/
│       ├── guess/route.ts          # Guess submission API
│       └── admin/levels/route.ts   # Level CRUD API (admin only)
├── components/
│   ├── Navbar.tsx                  # Navigation with auth state
│   ├── GameMap.tsx                 # Leaflet map for guessing
│   ├── GameClient.tsx              # Game UI (image, controls, score)
│   ├── AdminMap.tsx                # Leaflet map for admin location picker
│   └── AdminClient.tsx             # Admin UI (level creation + management)
├── lib/
│   ├── supabase/
│   │   ├── client.ts               # Browser Supabase client
│   │   ├── server.ts               # Server Supabase client
│   │   └── middleware.ts           # Session refresh + route protection
│   └── scoring.ts                  # Haversine distance + score calculation
├── types/
│   └── database.ts                 # TypeScript interfaces
└── middleware.ts                    # Root middleware
supabase/
└── schema.sql                      # Full database schema + triggers

🚀 Getting Started

Prerequisites

1. Clone & Install

git clone https://github.com/TAG-IIIT/Ethnoguessr.git
cd Ethnoguessr
npm install

2. Environment Variables

Copy the example and fill in your Supabase credentials:

cp .env.local.example .env.local
NEXT_PUBLIC_SUPABASE_URL=https://your-project.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key
SUPABASE_SERVICE_ROLE_KEY=your-service-role-key

Find these in Supabase Dashboard → Settings → API

3. Database Setup

Run supabase/schema.sql in Supabase Dashboard → SQL Editor. This creates:

  • profiles — user profiles (auto-created on GitHub login)
  • levels — game levels with PostGIS locations
  • guesses — user guesses with distance/score
  • leaderboard — auto-aggregated scores via trigger

Then create the coordinate extraction function:

create or replace function get_level_coords(p_level_id uuid)
returns table(lat double precision, lng double precision)
language sql security definer
as $$
  select
    ST_Y(location::geometry) as lat,
    ST_X(location::geometry) as lng
  from public.levels
  where id = p_level_id and is_active = true;
$$;

4. GitHub OAuth Setup

  1. Create a GitHub OAuth App at github.com/settings/developers
    • Homepage URL: http://localhost:3000
    • Callback URL: https://<your-supabase-url>/auth/v1/callback
  2. Enable GitHub in Supabase Dashboard → Authentication → Providers
    • Paste the Client ID and Client Secret

5. Storage Bucket

  1. Go to Supabase Dashboard → Storage
  2. Create a bucket named level-images (set to Public)
  3. Add storage policies (run in SQL Editor):
create policy "Allow authenticated uploads"
on storage.objects for insert to authenticated
with check (bucket_id = 'level-images');

create policy "Allow public reads"
on storage.objects for select to public
using (bucket_id = 'level-images');

create policy "Allow authenticated updates"
on storage.objects for update to authenticated
using (bucket_id = 'level-images');

create policy "Allow authenticated deletes"
on storage.objects for delete to authenticated
using (bucket_id = 'level-images');

6. Run

npm run dev

Open http://localhost:3000 and sign in with GitHub.

7. Become Admin

After your first login, run in SQL Editor:

update public.profiles set role = 'admin' where username = 'YOUR_GITHUB_USERNAME';

You'll now see the Admin link in the navbar.


🎮 How It Works

Scoring Formula

Uses the Haversine formula to calculate great-circle distance, then applies exponential decay:

score = round(5000 × e^(-distance_km / 2000))
Distance Score
0 km (exact) 5000
100 km 4756
500 km 3894
1000 km 3033
2000 km 1839
5000 km 410
10000 km 34

Game Flow

  1. Load → Server fetches a random active level the user hasn't played
  2. View → User sees the person's image (signed URL, 1hr expiry)
  3. Guess → User clicks on the Leaflet map to place their guess
  4. Score → API calculates Haversine distance and exponential decay score
  5. Result → Shows distance, score bar, correct/guessed markers with dashed line
  6. Next → Proceeds to the next unplayed level

🗄️ Database Schema

erDiagram
    auth_users ||--o| profiles : creates
    profiles ||--o{ guesses : makes
    profiles ||--o| leaderboard : aggregates
    levels ||--o{ guesses : receives

    profiles {
        uuid id PK
        text username UK
        text avatar_url
        text role
        timestamptz created_at
    }

    levels {
        uuid id PK
        text image_url
        geography location
        text hint
        text difficulty
        boolean is_active
        uuid created_by FK
        timestamptz created_at
    }

    guesses {
        uuid id PK
        uuid user_id FK
        uuid level_id FK
        geography guessed_location
        float distance_km
        int score
        timestamptz created_at
    }

    leaderboard {
        uuid user_id PK
        bigint total_score
        int games_played
        timestamptz updated_at
    }
Loading

🔒 Security

  • Row Level Security (RLS) on all tables
  • GitHub OAuth only — no password storage
  • Rate limiting — 15 guesses per minute per user
  • Unique constraint — one guess per level per user
  • Admin role check — server-side verification for all admin operations
  • Signed URLs — level images use 1-hour expiry signed URLs
  • Middleware — session refresh + route protection on every request

📜 License

MIT


Built with ❤️ using Next.js, Supabase, and Leaflet

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages