.. role:: raw-html-m2r(raw)
:format: html
Cards game for developers
=========================
.. image:: Image_intro.png
:target: Image_intro.png
:alt: Intro
Goal of the demonstrator
------------------------
The DestinE Earth Pulse demonstrator aims to illustrate the capabilities of the DestinE Platform environment through an interactive game that combines local climate, socio-economic, and satellite information. Designed for non-expert users, the game allows players to explore data and create their own customised versions without any coding skills.
By leveraging DestinE Platform services to access and process Climate Digital Twin data, Eurostat statistics, and Copernicus satellite imagery at NUTS-3 level, the demonstrator showcases how complex information can be transformed into engaging, accessible experiences.
Packaged as a mobile-friendly game that can be easily shared and played, Earth Pulse serves as an entry point for new audiences, raising awareness and visibility of the Destination Earth initiative beyond its traditional user community.
Game architecture
-----------------
Components overview
^^^^^^^^^^^^^^^^^^^
* **Frontend:** Nuxt 3 web application, user interface, game client.
* **Dashboard:** Vite web application, game stats monitoring.
* **Backend:** FastAPI server, database, game logic, API endpoints.
* **Database**\ : PostgreSQL, stores game data, city stats, and indicators.
Structure overview
^^^^^^^^^^^^^^^^^^
.. code-block::
cards-game/
├── apiserver/ # Backend API and logic
├── frontend/ # Frontend UI, the "Game App"
├── frontend-dashboard/ # Frontend dashboard app
├── .gitlab-ci.yml # GitLab CI configuration
├── docker-compose.yml # Sets up the database using Docker container
Prerequisites
-------------
Installed tools
^^^^^^^^^^^^^^^
* Docker (for containerized setup)
* Python 3.9+ (for backend)
* Node.js 18+ (for frontend)
* PostgreSQL (for database)
* See ``.env.template`` and ``.env.example`` for environment variables
Accounts
^^^^^^^^
* **DestinE platform**\ : An account on the DestinE platform is necessary, which is authorized to access the Climate Digital Twin data.
* **OpenStreetMap**\ : To make requests via OverPass API & Nominatim API, it is not necessary to create an account.
Installation & Run
------------------
Local deployment (development)
..
ℹ️ You need to ensure a connection to the database
Game app
~~~~~~~~
.. code-block:: bash
cd frontend
npm install
npm run dev
# Visit http://localhost:3000
Server
~~~~~~
.. code-block:: bash
cd apiserver
pip install -r requirements.txt
uvicorn main:app --host localhost --reload --port 8000
# API available at http://localhost:8000
# Swagger available at http://localhost:8000/docs
Dashboard
~~~~~~~~~
.. code-block:: bash
cd frontend-dashboard
npm install
npm run dev
# Visit http://localhost:5173
Development
-----------
Environment
^^^^^^^^^^^
Game app
~~~~~~~~
* Located in the ``frontend/`` directory.
* Built with Nuxt 3, Vue 3, Tailwind CSS, PrimeVue, TypeScript.
Server
~~~~~~
* Located in the ``apiserver/`` directory.
* Built with FastAPI, SQLAlchemy/SQLModel, asyncpg, Boto3 (S3 integration).
Dashboard
~~~~~~~~~
* Located in the ``frontend-dashboard/`` directory.
* Template for a dashboard application, built with Vite, Vue 3, and Tailwind CSS.
Game app code structure
^^^^^^^^^^^^^^^^^^^^^^^
* ``components/``\ : Vue components (UI, cards, modals, etc.)
* ``pages/``\ : Nuxt pages (routes)
* ``composables/``\ : Reusable logic (websocket game logic)
* ``assets/``\ : Static assets (images, styles)
* ``public/``\ : Static assets (images, video, favicon, etc.)
* ``const/``\ : Constants and configuration
* ``api/``\ : API client logic
* ``types/``\ : TypeScript interfaces
* ``store/``\ : Pinia store for state management
Cards creation
~~~~~~~~~~~~~~
* Card data is generated from open datasets (see frontend/pages/data-information.vue).
* Each card represents a location, enriched with climate indicators and demographic data.
**Bounding Box Calculation**
* We use OpenStreetMap’s Nominatim/Overpass APIs (via our ``bbox.py`` service) to determine each city’s administrative boundary.
* Process:
#. Query Nominatim by city name (and optionally country) to get a place ID or polygon geometry.
#. If needed, fetch the full boundary via Overpass with that place ID.
#. Compute the axis-aligned bounding box:
.. code-block:: python
lats = [pt[1] for pt in polygon_coords]
lons = [pt[0] for pt in polygon_coords]
lat_min, lat_max = min(lats), max(lats)
lon_min, lon_max = min(lons), max(lons)
Note: The bbox is only used for creating the satellite image.
**Indicators Calculation**
* Population and area figures are retrieved from OpenStreetMap via the ``osm-cities`` dataset.
*
These indicators are retrieved via CacheB service in DestinE plateform, they are computed from DestinE datasets via xarray/zarr. Unless noted, values are computed for year 2039.
*
Tropical Nights (count)
* Formula: days where (daily min > 20)
* Unit: count
* Mapping: ``IndicatorTypeEnum.TROPICAL_NIGHT``
*
Mean Temperature (°C)
* Formula: annual mean of ``t2m`` (Kelvin) converted to Celsius.
* Unit: °C
* Mapping: ``IndicatorTypeEnum.MEAN_TEMP``
*
Total Precipitation (mm)
* Formula: sum of ``tp`` over the period; meters → millimeters (× 1000).
* Unit: mm
* Mapping: ``IndicatorTypeEnum.PRECIPITATION``
*
Cloud Cover (%)
* Formula: mean of ``tcc`` over the period; fraction → percent (× 100).
* Unit: %
* Mapping: ``IndicatorTypeEnum.CLOUD_COVER``
*
Wind Speed (km/h)
* Formula: sqrt(mean(u10)^2 + mean(v10)^2) then m/s → km/h (× 3.6).
* Unit: km/h
* Mapping: ``IndicatorTypeEnum.WIND_SPEED``
*
Total Snowfall (cm)
* Formula: sum of ``sf`` over the period; meters → centimeters (× 100).
* Unit: cm
* Mapping: ``IndicatorTypeEnum.SNOWFALL``
*
Dry days (days)
* Formula: count of days where daily max of ``tp`` equals 0.
* Unit: days
* Mapping: ``IndicatorTypeEnum.DAYS_WITHOUT_RAIN``
*
Rainy days (days)
* Formula: count of days where daily sum ``tp`` × 1000 > 1 mm
* Unit: days
* Mapping: ``IndicatorTypeEnum.DAYS_WITH_RAIN``
*
Sunny days (days)
* Formula: days where (daily max ``tp`` == 0) AND (daily mean ``tcc`` × 100 < 20%).
* Unit: days
* Mapping: ``IndicatorTypeEnum.SUNNY_DAYS``
*
Snowy days (days)
* Formula: days where daily min of ``sf`` > 0.
* Unit: days
* Mapping: ``IndicatorTypeEnum.SNOWY_DAYS``
*
Stormy days (days)
* Formula: days where (daily mean wind magnitude^2 > 10) AND (daily sum ``tp`` × 1000 > 5 mm).
* Unit: days
* Mapping: ``IndicatorTypeEnum.STORMY_DAYS``
*
Heatwaves (count)
* Formula: number of heatwave events where (daily min ``t2m`` ≥ 20°C) AND (daily max ``t2m`` ≥ 35°C) for ≥ 3 consecutive days.
* Unit: count
* Mapping: ``IndicatorTypeEnum.HEATWAVES``
*
Heavy precipitation days (days)
* Formula: number of Heavy precipitation where (daily min ``tp`` ≥ 20mm)
* Unit: days
* Mapping: ``IndicatorTypeEnum.HEAVY_PRECIP_DAYS``
*
Frost days (days)
* Formula: number of Frost days where (daily min ``t2m`` < 0°C)
* Unit: days
* Mapping: ``IndicatorTypeEnum.FROST_DAYS``
*
July 2039 temperature (°C)
* Formula: mean of ``t2m`` (Kelvin) converted to Celsius in July 2039.
* Unit: °C
* Mapping: ``IndicatorTypeEnum.MEAN_TEMP_JULY``
Notes
* All computations are idempotent at the value level: existing non‑null values are not recomputed during personalized harvest unless missing.
* OSM population is intentionally excluded from default gameplay indicators (see “Hide population”).
**Workflow**
#. Open the full-year 2039 Zarr with **xarray** + **dask** (chunked by time).
#. Subset by each city’s position (\ ``lat``\ , ``lon``\ ).
#. Compute annually:
* **Tropical Nights**\ : count of days where the nightly minimum temperature > 20 °C (293.15 K)
* **Total Precipitation**\ : sum of ``tp`` (in m, converted to mm)
* **Mean Temperature**\ : annual mean of ``t2m`` (in K, converted to °C)
#. Return the results as simple integers/floats for the game logic.
**Optimization**
CacheB handles global data aggregation and caching, so no local downloads or heavy processing are required.
**Satellite images sources**
* ESA "Earth from space image collection": https://www.esa.int/ESA_Multimedia/Sets/Earth_from_Space_image_collection/(result_type)/images
* Alia Space images using NUPSI service
* Copernicus Sentinel-2 images
Card creation by players
~~~~~~~~~~~~~~~~~~~~~~~~
Players can create a new city card from the UI (Create Card screen). This triggers a protected backend endpoint that validates the requested OpenStreetMap place, creates the card row and indicator placeholders, starts an asynchronous climate‑indicator harvest, generates a satellite photo, and immediately returns a preliminary card payload.
* **Endpoint**\ : ``POST /cards``
Request body
* **osmid** (string, required): OpenStreetMap ID of the place
* **osm_type** (string, required): One of ``R`` (relation), ``W`` (way), ``N`` (node)
* **name** (string, optional): City name hint; enables province‑aware flow when paired with ``countrycode``
* **countrycode** (string, optional): ISO 3166‑1 alpha‑2 code used with ``name`` to locate the province via GISCO
* **latitude** (number, optional) and **longitude** (number, optional): Geographic center used for satellite image generation and, when province mode is enabled, for province lookup
Notes
* If ``osm_type`` is not one of ``R | W | N``\ , the request is rejected (400).
Workflow (backend)
#. **Auth & validation**\ : Token is validated; ``osmid`` and ``osm_type`` are required; ``osm_type`` must be ``R|W|N``.
#. **De‑duplication**\ : If a card with the same ``place_id`` (\ ``osm_type + osmid``\ ) already exists, a 400 error is returned.
#. **OSM fetch**\ : City metadata and bounding box are fetched from OpenStreetMap (\ ``OSMService``\ ).
#. **Province mode (optional)**\ : If both ``name`` and ``countrycode`` are provided, the province is resolved via GISCO. If another card already exists in that province, its stored values can be reused to speed up initialization.
#. **Persistence**\ : A new ``Card`` row is created. Indicator rows are created for the supported types; population is skipped in province mode.
#. **Image**\ : A satellite image is generated via Copernicus WMS (\ ``download_wms``\ ) using the provided center, uploaded to S3, and a presigned URL is returned.
#. **Background task**\ : A task is enqueued to compute the 2039 climate indicators with Dask/xarray (\ ``harvest_indicators_and_save``\ ). This can take up to ~10 minutes.
#. **Notification**\ : When computation finishes, the card’s ``updated_at`` is set and a user notification is sent via the notifications channel.
Operational notes
* While the background computation runs, the API returns a card with a valid image and indicator metadata; most indicator values are ``null`` until the task finishes.
* Environment variables required: ``COPERNICUS_API_KEY`` for WMS image, ``DESTINE_USERNAME`` and ``DESTINE_PASSWORD`` for DestinE data access.
* You can poll ``GET /cards?latest=true`` or subscribe to notifications to know when the card is fully ready.
Deck creation & usage
~~~~~~~~~~~~~~~~~~~~~
Backend behavior (on deck creation)
"""""""""""""""""""""""""""""""""""
* Name validation: must be alphanumeric, 3..100 chars. Reserved names ``tutorial`` and ``Default`` are rejected.
* Uniqueness per owner: the same user cannot create two decks with the same name.
* Ownership: the authenticated user is stored on the deck (\ ``owner_id``\ , ``owner_name``\ ).
* Persistence: the server inserts a ``Deck`` row with
* ``name`` and ``public`` as provided,
* ``completed = false``\ ,
* timestamps (\ ``created_at``\ , ``updated_at``\ ).
* Initial content: no cards and no deck indicators are created at this stage; the deck is empty by design.
* Side effects: no background computation, no notifications, no S3 operations are triggered during creation.
Operational notes
"""""""""""""""""
* A deck becomes usable for games only after later updates add cards and default indicators; the deck is marked complete when it has 32 cards and ≥ 4 deck indicators.
* Indicator values are computed asynchronously after updates (not on creation). A background job fills or updates values for the deck’s selected indicators across all its cards, and a notification is sent when finished.
* Room creation validates deck usage: non‑tutorial games require a complete deck; private decks can only be used by their owner.
* System decks ``tutorial`` and ``Default`` are immutable: you cannot create, rename, or delete decks with these names, and those decks cannot be modified.
Personalized indicators creation
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Backend behavior (when computing personalized indicators for a deck)
* Trigger: occurs after a deck update (\ ``PUT /decks/{id}``\ ) that changes cards, indicators, or default indicators. A background task is queued for that deck.
* Data sources: DestinE CacheB Zarr dataset for climate variables, accessed via xarray/dask; city coordinates resolved from OSM metadata and date.
* Selection scope: for each card in the deck and each deck indicator (by type, year, month), compute the missing value only; existing non‑null values are reused.
* Computation pipeline:
1) Open Zarr dataset once per deck job and select relevant variables per indicator type.
2) Time slice using the indicator’s month/year or full year for annual metrics.
3) Spatial selection by nearest point at the city’s lat/lon.
4) Apply the indicator‑specific aggregation (e.g., mean temperature, total precipitation, wind speed magnitude, count thresholds like tropical nights, frost days, etc.).
5) Upsert the result into the database by creating/finding the ``Indicator`` row and creating/updating its ``IndicatorValue``.
* Idempotency: if an indicator already has a stored non‑null value, it is skipped; missing or null values are recalculated.
* Performance: uses dask for lazy loading and chunked computation; reuses a single opened dataset for the full deck job.
* Side effects:
* Updates ``updated_at`` for the card when card‑level harvest runs; for deck jobs, checks deck completeness (32 cards and ≥ 4 indicators) and flips ``completed=true`` accordingly.
* Sends a notification when all cards in the deck are processed; if < 32 cards, informs that the deck isn’t complete yet though values are updated.
* Errors & resilience: any computation error for a given indicator rolls back that indicator’s transaction but continues the deck job for other indicators/cards.
Operational notes
* Personalized indicators are not computed at deck creation time, only on deck updates that alter indicator configuration or card membership.
* The job runs asynchronously; consumers should wait for the completion notification before sharing/playing with updated values.
* Year and month are stored per deck indicator; when ``month`` is null, annual metrics are computed.
Notifications
~~~~~~~~~~~~~
Backend behavior
* Channel: WebSocket at ``/notify/connect?token=``.
* Authentication: token is decoded server‑side; if the user is unknown, it is created, and the socket is registered in an in‑memory ``connections`` map keyed by internal ``user.id``.
* On connect: server loads unseen notifications for the user and pushes them one‑by‑one as JSON ``{ id, message }``.
* Produce: services call ``notify_user(user, message)`` to persist (\ ``notifications`` table) and, if connected, push to the user’s socket immediately.
* Persistence: each notification row stores ``user_id``\ , ``message``\ , ``seen`` (0/1), and timestamps.
* Mark as read: the client sends a JSON message on the socket ``{ "action": "read", "id": [, , ...] }``\ ; backend sets ``seen=1`` and updates ``updated_at``.
When notifications are sent
* Card creation background completes (city indicators harvested): user receives “The city card :raw-html-m2r:`` ready to played”.
* Deck personalized indicators complete: user receives either “The deck :raw-html-m2r:`` is ready to played” (32 cards) or a partial‑update message if fewer than 32 cards.
Operational notes
* The notification socket is separate from game WebSockets; reconnect with the same token to resume and fetch any unseen notifications.
* Notifications are stored server‑side; on reconnect, any ``seen=0`` entries are replayed to the client.
* Ensure the client acknowledges reads to prevent repeated delivery on subsequent connects.
Users storage in database
~~~~~~~~~~~~~~~~~~~~~~~~~
Backend behavior
* Source of truth: users are derived from the OIDC token (Keycloak). The backend does not implement password auth.
* Persistence model: table ``users`` with
* ``id`` (PK, autoincrement),
* ``preferred_username`` (string, indexed),
* ``sub`` (string, unique, indexed) — the stable subject identifier from the token.
* Creation & lookup: whenever a feature needs a DB user (e.g., notifications), the backend resolves the current user by ``sub``. If absent, it inserts a new row with the current ``preferred_username``.
* Relationships: one‑to‑many with ``notifications`` (\ ``users.id`` → ``notifications.user_id``\ ).
Operational notes
* Username changes in the IdP: future logins keep the same ``sub`` so the same DB user is reused; ``preferred_username`` is not auto‑updated unless you add that logic.
* No roles/permissions are stored in the ``users`` table; authorization is enforced from the JWT claims at request time.
* If you later extend the profile (e.g., avatar, locale), add nullable columns or a separate profile table keyed by ``users.id``.
CacheB requests/Queue
~~~~~~~~~~~~~~~~~~~~~
Backend behavior
* Purpose: serialize and offload heavy DestinE CacheB requests (Zarr opens, array computations) from the request path.
* Queue: an in‑process ``asyncio.Queue`` holds tasks; each task is ``(task_id, func, args, kwargs)`` with status tracked in ``tasks_status``.
* Worker: a background coroutine started at app startup continuously pulls tasks and executes them; it awaits async functions or runs sync ones in a thread pool.
* Enqueue: producers call ``add_task_in_queue(func, **payload)`` and receive a ``task_id``\ ; for card creation, the system enqueues ``harvest_indicators_and_save(card_id, city, bbox, user)``.
* Introspection: ``GET /tasks`` returns the live ``tasks_status`` map for observability.
Task lifecycle
* pending → running → finished | failed: the worker updates ``tasks_status`` accordingly; errors are captured into the failed status string.
* Side effects: indicator values are computed and persisted; upon completion, user notifications are sent (card ready, deck ready/partial).
Operational notes
* This lightweight queue avoids blocking API responses; it is suitable for a single‑process deployment. For multi‑replica or long‑running workloads, consider a durable queue (Redis/Celery/RQ/Arq) and idempotent tasks.
* Ensure tight timeouts and retries inside background functions when calling external services (CacheB, WMS, OSM). Re‑enqueue or mark failed with diagnostics on repeated errors.
NUTS‑3 regions
~~~~~~~~~~~~~~
Backend behavior
* Region resolution: when creating a card with both ``name`` (city) and ``countrycode``\ , the backend resolves the administrative region (province‑level) using the EU GISCO service.
* Primary method: reverse lookup with the city center (\ ``/reverse``\ ), extracting the ``L1`` label as the province/region name.
* Fallback: ``/provinces?country=&city=`` if reverse lookup fails.
* Storage: the resolved province name is stored on the card (\ ``cards.province``\ ). No numeric NUTS code is persisted at this time.
* Province reuse: if any card already exists with the same ``province``\ , new cards for that province initialize their indicator values by reusing existing values (idempotent speed‑up). Background harvesting may still run later if needed.
* Province bounding boxes: when ``province`` is known, the harvest area uses GISCO’s province bbox; otherwise, the area falls back to the city bbox derived from OSM.
Data & standards
* GISCO endpoints used: ``/reverse``\ , ``/provinces``\ , ``/cities``\ , ``/countries``\ , and ``/bbox`` (service‑specific paths as implemented in ``GiscoService``\ ).
* The ``L1`` label from GISCO responses is used as a province/NUTS‑level name for storage and comparisons.
* A reference dataset ``resources/nuts_3_population_2021.json`` is included for NUTS‑3 populations (ESTAT); it is not directly persisted but can support analytics.
Operational notes
* Scope: GISCO NUTS coverage is Europe; non‑EU locations may skip province mode and use city‑level bbox only.
* Disambiguation: reverse lookup uses the provided latitude/longitude to disambiguate homonymous city names.
* Future improvements: consider persisting official NUTS codes alongside labels to avoid string comparison drift and enable robust joins with external datasets.
Population
~~~~~~~~~~
Backend behavior
* Card initialization response: OSM population is deliberately excluded from the returned indicator metadata. Implementation filters out ``IndicatorTypeEnum.POPULATION`` when building the initial indicators list.
* Province cards: when a card is created in province mode, the population indicator row is not created for that card.
* Personalized computations: population is not mapped for deck‑level personalized indicators and is never computed in the background job; deck indicator configuration must not include population.
* Gameplay usage: population is not used for bounds computation or turn comparisons since gameplay relies on deck‑selected indicators (which should exclude population).
Operational notes
* Keep population out of deck indicator selection. If included, the personalized harvest has no mapping for population and will not compute values for it.
* Population may exist in the database for some cards, but it is not exposed in the initial creation response and is not used in gameplay logic.
Architecture/services — Optimization & refactoring
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Backend-focused proposals to improve maintainability, performance, and reliability. The items below are actionable and scoped to existing modules.
*
**Service boundaries & dependency injection**
* Split responsibilities in ``CardService`` (creation, WMS image, indicator bootstrapping) into smaller services (e.g., ``ImageService`` for WMS/S3; keep ``OSMService`` and ``IndicatorService`` dedicated).
* Avoid opening DB sessions inside services that already operate within a session (e.g., ``CardService.download_wms_image`` calls ``get_db()``\ ); pass the session or isolate side effects behind a repository layer.
* Standardize constructor injection for all external deps (S3, OSM, GISCO, Value/Indicator services) to simplify testing and mocking.
*
**Transactions & database performance**
* Use a single atomic transaction per use‑case; minimize scattered ``flush()`` calls inside loops (batch inserts for indicators/values where safe).
* Index hot paths (if not already): ``cards.place_id``\ , ``(indicator.card_id, indicator.type_id, indicator.year, indicator.month)``\ , ``(deck_indicator.deck_id, indicator_type_id, year, month)``.
* In ``DeckService._apply_cards_change``\ , fix validation logic for unknown card IDs (raise only when ``missing`` is non‑empty) and reduce redundant queries by selecting once and building a map.
* Prefer projection queries over loading full ORM rows when only IDs are needed; avoid N+1 where possible by eager loading.
*
**Background compute & tasking**
* Create the Dask client lazily at startup and monitor health; avoid instantiating global clients at import‑time in request workers.
* Keep heavy I/O/CPU (WMS download, Zarr reads) off the request path; for card creation, consider deferring WMS generation to background (with a temporary placeholder) to reduce tail latency.
* Add bounded retries with backoff to external calls (Copernicus WMS, OSM/Nominatim, CacheB) and set strict timeouts.
* Consider a dedicated task runner (RQ/Celery/Arq) for stronger reliability, visibility, and retry semantics; keep the current queue abstraction if you prefer minimal footprint.
*
**Caching & reuse**
* Cache immutable reference data (indicator types list) within a request/job to avoid repeated queries.
* Reuse a single opened Zarr dataset per deck job (already done); extend with an in‑process cache with eviction to avoid reopen on rapid successive jobs.
* Memoize OSM lookups and bounding‑box computations for the lifetime of the job; persist short‑lived caches if rate limits are tight.
*
**Model consistency & enums**
* Keep ``IndicatorTypeEnum`` and DB ``IndicatorType`` in a single source of truth; generate one from the other or validate at startup to prevent drift.
* Centralize the mapping between ``IndicatorTypeEnum`` and ``IndicatorTypeForPersonnal`` to avoid name‑based coupling and runtime KeyErrors.
*
**Observability**
* Replace ``print`` statements with structured logs; add request/task correlation IDs to tie logs across services and background jobs.
* Emit metrics for task durations, retries, and cache hit‑rates (Prometheus counters/histograms).
*
**Configuration & security**
* Move magic constants (e.g., year 2039) to typed config; make ranges selectable per deck/room when needed.
* Centralize timeouts, retry budgets, and external URLs in ``config.py``\ ; validate at startup.
*
**Error model & notifications**
* Prefer domain exceptions in services and translate once at router/controller boundaries; avoid raising ``HTTPException`` from deep layers.
* Route user notifications through a small event bus (domain events) decoupled from services; keep side effects observable and testable.
Game mechanics
~~~~~~~~~~~~~~
* Players create or join a room and receive cards.
* Each turn, players select a statistic to compare.
* The player with the highest or lowest value (choice of the active player) wins the round.
* A score is calculated for each player at each round in function of played cards.
* The player with the highest score wins the game.
Tutorial mode
~~~~~~~~~~~~~
*
Purpose
* One-turn guided match against a bot to onboard new players.
*
Frontend flow
* Entry: on Home → New game, if ``localStorage['tutorial-done']`` is absent, the app creates a tutorial room (\ ``POST /create_room`` with ``mode: "tutorial"``\ , ``max_players: 2``\ ), instantiates a local ``Game``\ , opens the standard game WebSocket, then navigates to ``/game-board``.
* Completion: when the tutorial game ends, the client sets ``localStorage['tutorial-done'] = 'true'``\ , resets the store, and redirects to ``/`` with ``?endedtutorial=true`` to display a completion popup.
* Restart: the side menu provides "Restart tutorial" which clears the flag and starts a new tutorial session.
*
Backend behavior
* Deck: the tutorial room uses the ``tutorial`` deck (looked up by name); users cannot rename or delete the ``tutorial`` or ``Default`` decks.
* On first WebSocket join in tutorial mode: a bot (\ ``LuckyBot``\ ) is auto-added, ``turn_duration`` is extended (≈ 1500 s), and ``max_turn`` is set to 1.
* Turn order: the human player always starts in tutorial mode.
* Persistence: tutorial games do not write scores to the leaderboard.
*
Notes
* Authentication is not required to play the tutorial; a nickname (preferred_username) is sufficient.
* The standard game WebSocket endpoint is used for tutorial sessions as well.
Solo mode
~~~~~~~~~
Backend behavior
* Room startup: when a room is created with ``mode="solo"`` and the first human joins, a non‑interactive bot (\ ``LuckyBot``\ ) is auto‑added. Turn duration is extended (~1500 s).
* Start and turns: on start, the current player is always the first human. For each turn, 4 candidate indicators are selected from the deck. The human chooses the indicator and direction; the bot does not make choices. The bot “turn” is skipped by design; the human drives the turn flow.
* Dealing and values: the deck is split equally among the human and the bot. The bot always holds and draws a card; its indicator values come from precomputed deck indicators. Missing values are treated as 0 for comparisons.
* Finish conditions: the game ends at the configured ``max_turn`` or when only one player remains (e.g., the bot or the human). If the human disconnects mid‑game, the room is closed.
* Persistence: the persisted game record carries ``mode='solo'``. On finish, only non‑bot players have their scores recorded in the leaderboard.
Operational notes
* Solo mode starts immediately; no waiting for a second human.
* Scoring and indicator bounds are computed exactly like multiplayer; the difference is only that indicator selection is human‑driven and the bot is passive.
* The bot has no WebSocket and is not persisted as a user; it serves as an in‑memory opponent using the same deck data.
WebSocket
"""""""""
This project uses WebSockets to enable real-time, bidirectional communication between clients and the game server. When a player connects, a persistent WebSocket connection is established, allowing instant transmission of game events and state updates.
Typical WebSocket events
########################
Below are common events exchanged between client and server:
.. list-table::
:header-rows: 1
* - Event
- Description
* - ``card``
- Sends a card to the player.
* - ``error``
- Notifies about an error, with a message.
* - ``info``
- General informational message.
* - ``game_start``
- Indicates the game has started, with total number of turns.
* - ``game_finish``
- Signals the end of the game, including the reason, winner, and leaderboard.
* - ``player_join``
- A player has joined the game; includes the updated player list.
* - ``player_leave``
- A player has left the game.
* - ``player_eliminated``
- Notifies that a player has been eliminated.
* - ``turn_info``
- Provides information about the current turn, including which player's turn it is.
* - ``turn_result``
- Announces the result of a turn, including the winner and all players’ cards and scores.
* - ``game_context``
- Shares context about the game, such as the expected and current player.
* - ``turn_status``
- Gives the status of all players for the current turn (cards remaining, elimination status).
Each event typically includes a payload with relevant data (such as player ID, card details, or updated game state), allowing clients to update their UI and logic in real time.
Game progress
~~~~~~~~~~~~~
General
"""""""
* Game state managed on the backend.
* Real-time updates via WebSocket.
Actions proposed
""""""""""""""""
* Create/join room
* Play a card/statistic
* View results and scores
Score computation
~~~~~~~~~~~~~~~~~
Points are awarded each turn based on the chosen indicator value relative to the deck. The calculation follows these steps:
#.
**Indicator bounds**\ :raw-html-m2r:`
`
Determine the minimum (\ ``minVal``\ ) and maximum (\ ``maxVal``\ ) values of the selected indicator across all active cards in deck.
#.
**Logarithmic normalization**
Compute a raw score:
ε = 1e-6
rel = val - minVal + ε
span = maxVal - minVal + ε
raw_score = math.log(rel) / math.log(span) * 100
#.
**Direction adjustment**
If the direction is 1 use the raw score as is, if the direction is -1 invert the score:\ :raw-html-m2r:`
`
raw_score = 100 - raw_score
#.
**Score adjustment**
If ``raw_score < 0``\ , set ``raw_score = 0`` and if ``raw_score > 100``\ , set ``raw_score = 100``.
Database MCD
^^^^^^^^^^^^
.. image:: MCD.png
:target: MCD.png
:alt: Database
Server code structure
^^^^^^^^^^^^^^^^^^^^^
* ``main.py``\ : FastAPI entrypoint
* ``routers/``\ : API route definitions
* ``models/``\ : Database models
* ``services/``\ : Business logic
* ``utils/``\ : Utility functions
Server communication
^^^^^^^^^^^^^^^^^^^^
.. image:: Communication_diagramme.png
:target: Communication_diagramme.png
:alt: Overview
Library
~~~~~~~
The backend relies on the following key Python libraries:
* **fastapi** – high-performance ASGI web framework
* **uvicorn** – lightning-fast ASGI server
* **pydantic** – data validation and settings management
* **databases** – async support for SQL databases
* **asyncpg** – fast PostgreSQL client for asyncio
* **sqlalchemy** – core ORM and SQL toolkit
* **sqlmodel** – Pydantic + SQLAlchemy integration
* **python-jose** – JSON Web Token implementation
* **websockets** – WebSocket client/server library
* **requests** – HTTP for humans (sync client)
* **python-dotenv** – load environment variables from ``.env``
* **psycopg2-binary** – PostgreSQL driver (sync)
* **boto3** – AWS SDK for Python (S3 access)
* **python-multipart** – support for form & file uploads
* **xarray** – N-D labeled arrays & datasets
* **xarray[io]** – I/O engines for netCDF, raster, Zarr, etc.
* **numpy** – numerical computing library
* **zarr** – chunked, compressed, mutable N-dim arrays
* **fsspec** – Filesystem specification for local & remote
* **s3fs** – S3 backend for fsspec
* **dask** – parallel computing with task scheduling
* **aiohttp** – async HTTP client/server framework
*For adding dependencies, see* ``requirements.txt``.
Server endpoints
~~~~~~~~~~~~~~~~
*
**POST** ``/create_room``\ :raw-html-m2r:`
`
Create a new game room.\ :raw-html-m2r:`
`
*Request body:* JSON with room settings (e.g. maxPlayers, mode).\ :raw-html-m2r:`
`
*Response:* ``{ "room_id": "...", "settings": { … } }``
*
**GET** ``/rooms``\ :raw-html-m2r:`
`
List all active game rooms with their current player counts and settings.
*
**GET** ``/playedGame``\ :raw-html-m2r:`
`
List all played games with their results and player scores stored in the database.
*
**GET** ``/playedGame/now``
List today active games with their result and player stored in the database.
*
**WebSocket** ``/ws/{room_id}/{player}``\ :raw-html-m2r:`
`
Real-time endpoint for game events: join notifications, turn submissions, results, game start/end.
*
**GET** ``/card/``\ :raw-html-m2r:`
`
Retrieve metadata for all cards (including presigned image URLs).
*
**GET** ``/score/leaderboard``\ :raw-html-m2r:`
`
Fetch the all-time high‐scores leaderboard.
*
**GET** ``/score/leaderboard/now``\ :raw-html-m2r:`
`
Fetch today’s high‐scores leaderboard.
..
**Development-Only Endpoints**
* **GET** ``/era5/era5``
Retrieve metadata for one deck and assemble card data with climate indicators
* **POST** ``/era5/era5``\ :raw-html-m2r:`
`
Query climate indicators from ERA5/DEM Zarr for a given bounding box.
* **GET** ``/s3/buckets``\ :raw-html-m2r:`
`
Retrieve the names of all s3 buckets accessible by the application.
* **GET** ``/s3/buckets/{bucket_name}``
Retrieve information about a specific S3 bucket, including its metadata and ACL.
* **GET** ``/s3/buckets/{bucket_name}/all``
Retrieve a list of all object keys in the specified S3 bucket.
* **POST** ``/s3/buckets/{bucket_name}/upload``
Upload a file to the specified S3 bucket.
* **GET** ``/s3/buckets/{bucket_name}/url``
Generate a presigned URL to upload a file to the specified S3 bucket.
Configuration
~~~~~~~~~~~~~
* Environment variables in ``.env`` (see ``.env.template``\ )
* Database and S3 credentials required
Identity & Access Management (IAM)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Modes: visitor vs identified
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
*
Visitor (unauthenticated)
* Access: home, rules, data information, leaderboard, and gameplay (solo/multi) without an account.
* No access: create/edit deck and card screens, and "My decks".
*
Identified user (authenticated via Keycloak OIDC)
* Additional access: create/edit cards and decks, card library, "My decks".
* The session exposes ``username``\ , ``roles`` and ``sub`` (user identifier) to associate actions on the API side.
Authentication overview
~~~~~~~~~~~~~~~~~~~~~~~
Frontend (Nuxt 3 + Auth.js)
"""""""""""""""""""""""""""
* Authentication handled by ``@sidebase/nuxt-auth`` with a Keycloak (OIDC) provider.
* Protected routes use ``definePageMeta({ auth: true })`` (e.g., ``create-card``\ , ``create-deck``\ , ``card-library``\ , ``my-decks``\ ). Gameplay, home, rules, data information, and leaderboard remain publicly accessible.
* Token handling:
* Retrieves ``access_token``\ /\ ``refresh_token`` at sign-in; extracts ``username`` and roles from the JWT.
* Silent ``access_token`` refresh via the JWT callback; auto-refresh on window focus and periodically (~60 s).
* Main environment variables: ``NUXT_PUBLIC_KEYCLOAK_ISSUER``\ , ``NUXT_PUBLIC_KEYCLOAK_CLIENT_ID``\ , ``NUXT_KEYCLOAK_CLIENT_SECRET``\ , ``NUXT_AUTH_SECRET``\ , ``NUXT_AUTH_ORIGIN``.
Backend (FastAPI)
"""""""""""""""""
* Validates ``Authorization: Bearer `` against the Keycloak realm public keys.
* Protected endpoints: ``Depends(get_current_user)`` (token required). Public or visitor-tolerant endpoints: ``Depends(get_optional_current_user)`` (token optional).
* Game WebSocket: authentication not mandatory; a nickname ( preferred_username ) is enough to play. If a token is provided, events can be linked to an identified user.
* Main environment variables: ``IAM_URL``\ , ``IAM_REALM``\ , ``ADDR_PUBLIC_KEY`` (optional; otherwise JWKs are fetched automatically).
Calling protected API from the frontend
"""""""""""""""""""""""""""""""""""""""
* Include ``Authorization: Bearer ${session.access_token}`` when calling protected routes.
* Or WebSocket: ``ws(s):///notify?token=``
Cloud deployment (production)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Production environment run on dedicated Kubernetes cluster. Build and deployment are performed using a Gitlab pipeline (not supplied here) however, there's nothing to prevent manual deployment.
..
ℹ️ You need to ensure a functional postgre database and a kubernetes cluster
Create a kubernetes namespace to isolate the application in your cluster :
.. code-block:: bash
kubectl create namespace climate-cards-game
**Backend (apiserver)**
You can adapt the file ``apiserver/deploy/production.yaml`` to fit your deployment goal.
.. code-block:: bash
cd apiserver
# STEP 1: Build docker image
docker build -t apiserver:latest .
# STEP 2: Create kuberntes ressources
kubectl -n climate-cards-game create secret generic apiserver-db \
--from-literal=DB_HOST= \
--from-literal=DB_PORT= \
--from-literal=DB_NAME= \
--from-literal=DB_USER= \
--from-literal=DB_PASSWORD= \
--dry-run=client -o yaml | kubectl apply -f -
kubectl -n climate-cards-game create secret generic apiserver-aws \
--from-literal=AWS_ACCESS_KEY= \
--from-literal=AWS_SECRET_KEY= \
--dry-run=client -o yaml | kubectl apply -f -
kubectl -n climate-cards-game create secret generic apiserver-destine \
--from-literal=EDH_API_KEY= \
--dry-run=client -o yaml | kubectl apply -f -
kubectl -n climate-cards-game apply -f deploy/production.yaml
**Frontend**
You can adapt the file ``frontend/deploy/production.yaml`` to fit your deployment goal.
.. code-block:: bash
cd frontend
# STEP 1: Build docker image
docker build -t frontend:latest .
# STEP 2: Create kuberntes ressources
kubectl -n climate-cards-game apply -f deploy/production.yaml
**Dashboard**
You can adapt the file ``dashboard-frontend/deploy/production.yaml`` to fit your deployment goal.
.. code-block:: bash
cd frontend-dashboard
# STEP 1: Build docker image
docker build -t dashboard:latest -f Dockerfile.prod .
# STEP 2: Create kuberntes ressources
kubectl -n climate-cards-game apply -f deploy/production.yaml
License
-------
This game is free software, developed by ATOS for ESA DestinE. Licensed under the Apache License, Version 2.0 (see LICENSE).