{"id":50542944,"url":"https://github.com/tchoukball/game-center","last_synced_at":"2026-06-03T21:30:44.973Z","repository":{"id":361620106,"uuid":"1254987090","full_name":"tchoukball/game-center","owner":"tchoukball","description":null,"archived":false,"fork":false,"pushed_at":"2026-05-31T14:23:13.000Z","size":75,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-05-31T15:14:59.331Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Vue","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/tchoukball.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-05-31T08:53:09.000Z","updated_at":"2026-05-31T14:23:17.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/tchoukball/game-center","commit_stats":null,"previous_names":["khodl/tchoukscorer"],"tags_count":null,"template":false,"template_full_name":null,"purl":"pkg:github/tchoukball/game-center","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tchoukball%2Fgame-center","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tchoukball%2Fgame-center/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tchoukball%2Fgame-center/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tchoukball%2Fgame-center/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/tchoukball","download_url":"https://codeload.github.com/tchoukball/game-center/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tchoukball%2Fgame-center/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33881107,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-03T02:00:06.370Z","response_time":59,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-06-03T21:30:44.905Z","updated_at":"2026-06-03T21:30:44.962Z","avatar_url":"https://github.com/tchoukball.png","language":"Vue","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Game Center\n\nAn offline-first tchoukball scoreboard (Vue 3 + Vite PWA).\n\nThe app records a match as a **`GameSheet`** — an append-only event log. This\ndocument specifies the `GameSheet` JSON format so it can be used as a shared\nstandard between the frontend, the backend, and any other app that produces or\nconsumes match data.\n\n---\n\n## The `GameSheet` format\n\nA sheet stores **only two things**: the teams and the chronological event log.\nEverything else a UI might show — current scores, the period number, the match\nphase, the start time — is **derived from `events`** and is never stored on the\nsheet. This keeps the event log the single source of truth.\n\n```jsonc\n{\n  \"teams\": [\n    { \"id\": \"italy\", \"name\": \"Italy\" },\n    { \"id\": \"switzerland-m15-bejune\", \"name\": \"Switzerland M15 BEJUNE\" }\n  ],\n  \"events\": [\n    /* TchoukEvent objects, in the order they happened */\n  ]\n}\n```\n\n### `GameSheet`\n\n| Field    | Type            | Notes                                              |\n| -------- | --------------- | -------------------------------------------------- |\n| `teams`  | `TchoukTeam[]`  | The teams in the match.                            |\n| `events` | `TchoukEvent[]` | Append-only log, ordered oldest → newest.          |\n\n### `TchoukTeam`\n\n| Field  | Type               | Notes                                                  |\n| ------ | ------------------ | ------------------------------------------------------ |\n| `id`   | `string \\| number` | Stable team identifier (`TeamId`). Unique in a sheet.  |\n| `name` | `string`           | Display name.                                          |\n\n### `TchoukEvent`\n\nA single recorded action.\n\n| Field         | Type                | Required | Notes                                                                       |\n| ------------- | ------------------- | -------- | --------------------------------------------------------------------------- |\n| `type`        | `TchoukEventType`   | yes      | What happened (see below).                                                  |\n| `actor`       | `ActorType`         | no       | Who performed the action. Omitted on corrections and on `time_*` events.    |\n| `target`      | `ActorType`         | no       | The team that receives the action. Set on `score_*` events.                 |\n| `scoreChange` | `ScoreChangeType`   | no       | Present on `score_*` events: the team and signed delta.                     |\n| `at`          | `string`            | yes      | ISO-8601 timestamp, e.g. `\"2026-05-31T14:02:31.000Z\"`.                      |\n\n\u003e The period an event belongs to is **not stored** — derive it from the log\n\u003e (see §2): the number of `time_period_start` events up to and including it,\n\u003e `null` before the first period (the first period is `1`).\n\n### `ActorType`\n\nA participant in an event. Modelled as an object so more information (player id,\nname, position, …) can be added later without changing the event shape.\n\n| Field    | Type     | Notes                       |\n| -------- | -------- | --------------------------- |\n| `teamId` | `TeamId` | The participant's team.     |\n\n### `ScoreChangeType`\n\n| Field       | Type     | Notes                                               |\n| ----------- | -------- | --------------------------------------------------- |\n| `teamId`    | `TeamId` | Team whose score changed (same as `target.teamId`). |\n| `increment` | `number` | Signed delta: `+1` for a point, `-1` for a fix.     |\n\n### Actor / target semantics\n\n- **`actor`** is who *did* it — the shooter, or the player/team who conceded a\n  given point. A cancelled point (`score_point_correction`) has **no actor**.\n- **`target`** is the team that *receives* the action — the team that benefits\n  from the point (the scoring team, or the team given the point), or whose\n  point is being cancelled.\n\n### `TchoukEventType`\n\nTwo families: **time** events (the match timeline; no `actor`/`target`) and\n**score** events (carry `target` + `scoreChange`).\n\n| `type`                    | Family | Meaning                                         | `actor`            | `target`         | `scoreChange` |\n| ------------------------- | ------ | ----------------------------------------------- | ------------------ | ---------------- | ------------- |\n| `time_game_start`         | time   | The match started.                              | —                  | —                | —             |\n| `time_period_start`       | time   | A period started (increments the period count). | —                  | —                | —             |\n| `time_period_end`         | time   | The current period ended.                       | —                  | —                | —             |\n| `time_game_end`           | time   | The match ended.                                | —                  | —                | —             |\n| `score_point_scored`      | score  | A team scored for itself.                       | scoring team       | scoring team     | `+1`          |\n| `score_point_given`       | score  | A team was given a point by an opponent.        | conceding opponent | benefiting team  | `+1`          |\n| `score_point_correction`  | score  | Manual correction (cancel a point).             | — (none)           | team losing pt.  | `-1`          |\n\n---\n\n## Worked example\n\n```json\n{\n  \"teams\": [\n    { \"id\": \"italy\", \"name\": \"Italy\" },\n    { \"id\": \"suisse\", \"name\": \"Suisse\" }\n  ],\n  \"events\": [\n    { \"type\": \"time_game_start\",   \"at\": \"2026-05-31T14:02:11.000Z\" },\n    { \"type\": \"time_period_start\", \"at\": \"2026-05-31T14:02:14.000Z\" },\n    { \"type\": \"score_point_scored\", \"at\": \"2026-05-31T14:02:31.000Z\",\n      \"actor\": { \"teamId\": \"italy\" }, \"target\": { \"teamId\": \"italy\" },\n      \"scoreChange\": { \"teamId\": \"italy\", \"increment\": 1 } },\n    { \"type\": \"score_point_given\", \"at\": \"2026-05-31T14:03:05.000Z\",\n      \"actor\": { \"teamId\": \"suisse\" }, \"target\": { \"teamId\": \"italy\" },\n      \"scoreChange\": { \"teamId\": \"italy\", \"increment\": 1 } },\n    { \"type\": \"score_point_correction\", \"at\": \"2026-05-31T14:03:40.000Z\",\n      \"target\": { \"teamId\": \"italy\" },\n      \"scoreChange\": { \"teamId\": \"italy\", \"increment\": -1 } },\n    { \"type\": \"time_period_end\",   \"at\": \"2026-05-31T14:14:00.000Z\" }\n  ]\n}\n```\n\nDerived from the log above: phase = `period_ended`, period = `1`,\nscores = `{ \"italy\": 1, \"suisse\": 0 }`, startedAt = `\"2026-05-31T14:02:11.000Z\"`.\n\n---\n\n## Rules\n\n### 1. The event log is the single source of truth\n\nProducers append events; they do not store derived values. Consumers compute\nwhat they need from `events`. The same algorithms must be used everywhere so\nall apps agree.\n\n### 2. Deriving values from the log\n\n**Scores** — fold the `scoreChange` deltas:\n\n```\nscores = {}\nfor event in events:\n    if event.scoreChange:\n        scores[event.scoreChange.teamId] += event.scoreChange.increment\n```\n\n**Current period** — the number of periods started:\n\n```\nperiod = count(events where type == \"time_period_start\")\n```\n\n**An event's period** — the number of `time_period_start` events up to and\nincluding it (`null` if none yet; the first period is `1`):\n\n```\neventPeriod(i) = count(events[0..i] where type == \"time_period_start\") or null\n```\n\n**Start time** — the timestamp of the first `time_game_start` (or `null`):\n\n```\nstartedAt = first(events where type == \"time_game_start\").at  // else null\n```\n\n**Phase** — determined by the most recent `time_*` event:\n\n| Last time event       | Phase            |\n| --------------------- | ---------------- |\n| (none)                | `pregame`        |\n| `time_game_start`     | `game_started`   |\n| `time_period_start`   | `period_started` |\n| `time_period_end`     | `period_ended`   |\n| `time_game_end`       | `game_ended`     |\n\n### 3. Match phase state machine\n\nTime events must follow these transitions. Scoring is only valid while the\nphase is `period_started`.\n\n```\npregame        --time_game_start--\u003e   game_started\ngame_started   --time_period_start--\u003e period_started\ngame_started   --time_game_end--\u003e     game_ended\nperiod_started --time_period_end--\u003e   period_ended\nperiod_started --time_game_end--\u003e     game_ended\nperiod_ended   --time_period_start--\u003e period_started   (next period; period + 1)\nperiod_ended   --time_game_end--\u003e     game_ended\n```\n\n### 4. Deletion rules\n\nEvents may be removed from the log under these constraints:\n\n- **Score events** (`score_point_*`) may be deleted at any position. Deleting\n  one simply removes its `scoreChange` from the fold.\n- **Time events** (`time_*`) may be deleted **only if they are the last event\n  in the log**, so the phase state machine stays consistent.\n\nA \"reset\" clears `events` entirely (the `teams` are kept).\n\n### 5. Validation guidance (backend)\n\nA backend accepting a sheet should verify:\n\n- `teams[].id` are unique; every `actor.teamId` / `target.teamId` /\n  `scoreChange.teamId` references an existing team.\n- `events` are ordered by non-decreasing `at`.\n- The sequence of `time_*` events is a valid walk of the state machine in §3.\n- `score_*` events occur only while the derived phase is `period_started`, and\n  carry a `target` and a `scoreChange` whose `teamId` equals `target.teamId`.\n- `score_point_scored` → `actor` and `target` are the same team, `increment == 1`;\n  `score_point_given` → `actor` is the conceding opponent, `target` the\n  beneficiary, `increment == 1`;\n  `score_point_correction` → no `actor`, `increment == -1`.\n\nBecause the sheet is an append-only log, a backend can store it as-is (e.g. a\nJSON column or an events table) and recompute any view on demand.\n\n---\n\n## Reference implementation\n\nThe canonical types and derivation helpers live in\n[`src/types.ts`](src/types.ts):\n\n- Types: `GameSheet`, `TchoukTeam`, `TchoukEvent`, `TchoukEventType`,\n  `ActorType`, `ScoreChangeType`, `TeamId`, `GamePhase`.\n- Helpers: `computeScores`, `currentPhase`, `currentPeriod`, `eventPeriod`,\n  `gameStartedAt`.\n\nThe runtime match state is managed by the store in\n[`src/stores/useMatchStore.ts`](src/stores/useMatchStore.ts), which holds the\nsingle reactive `sheet` and exposes the derived values plus the only mutations\nallowed (the actions that append events). The store persists the sheet to\n`localStorage` (key `tchoukscorer:sheet:v1`) on every change and restores it on\nload, so a page refresh resumes the same match.\n\n---\n\n## Development\n\n```bash\nnpm install\nnpm run dev          # start the dev server\nnpm run build        # type-check (vue-tsc) + production build\nnpm run type-check   # type-check only\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftchoukball%2Fgame-center","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ftchoukball%2Fgame-center","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftchoukball%2Fgame-center/lists"}