{"id":51699080,"url":"https://github.com/edycutjong/grudgeball","last_synced_at":"2026-07-16T08:33:44.399Z","repository":{"id":371420497,"uuid":"1300532221","full_name":"edycutjong/grudgeball","owner":"edycutjong","description":"🪤 Daily marble-drop gauntlet where the board is built from other players' grudges — Reddit Games with a Hook (Devvit)","archived":false,"fork":false,"pushed_at":"2026-07-15T07:31:46.000Z","size":882,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-07-16T08:33:42.513Z","etag":null,"topics":["canvas","devvit","games-with-a-hook","hackathon","marble-drop","physics-game","reddit","reddit-games","typescript","ugc"],"latest_commit_sha":null,"homepage":"https://edycutjong.github.io/grudgeball/","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/edycutjong.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":".github/CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":".github/CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":".github/SECURITY.md","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-07-14T13:35:38.000Z","updated_at":"2026-07-15T07:31:50.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/edycutjong/grudgeball","commit_stats":null,"previous_names":["edycutjong/grudgeball"],"tags_count":2,"template":false,"template_full_name":null,"purl":"pkg:github/edycutjong/grudgeball","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/edycutjong%2Fgrudgeball","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/edycutjong%2Fgrudgeball/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/edycutjong%2Fgrudgeball/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/edycutjong%2Fgrudgeball/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/edycutjong","download_url":"https://codeload.github.com/edycutjong/grudgeball/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/edycutjong%2Fgrudgeball/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":35537751,"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-07-16T02:00:06.687Z","response_time":83,"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":["canvas","devvit","games-with-a-hook","hackathon","marble-drop","physics-game","reddit","reddit-games","typescript","ugc"],"created_at":"2026-07-16T08:33:42.972Z","updated_at":"2026-07-16T08:33:44.392Z","avatar_url":"https://github.com/edycutjong.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003cdiv align=\"center\"\u003e\n  \u003cimg src=\"docs/icon.svg\" alt=\"Grudgeball\" width=\"144\" height=\"144\" /\u003e\n  \u003ch1\u003eGrudgeball\u003c/h1\u003e\n  \u003cp\u003e\u003cem\u003eDie, plant your revenge, count the bodies at dawn — a daily marble gauntlet on Reddit, built by the crowd.\u003c/em\u003e\u003c/p\u003e\n  \u003cimg src=\"docs/readme-hero.svg\" alt=\"Grudgeball — die, plant your revenge, count the bodies at dawn\" width=\"100%\" /\u003e\n\n  \u003cbr/\u003e\u003cbr/\u003e\n\n  [![Play on Reddit](https://img.shields.io/badge/Play_on-Reddit-EF4444?style=for-the-badge\u0026labelColor=0B0A07)](https://www.reddit.com/r/grudgeball_dev/)\n  [![Watch Demo](https://img.shields.io/badge/Watch-Demo-F43F5E?style=for-the-badge\u0026labelColor=0B0A07)](https://www.youtube.com/watch?v=ZHujf9Qudjw)\n  [![Devpost](https://img.shields.io/badge/View_on-Devpost-003E54?style=for-the-badge\u0026labelColor=0B0A07)](https://devpost.com/software/grudgeball)\n  [![Landing Page](https://img.shields.io/badge/Landing-Page-F59E0B?style=for-the-badge\u0026labelColor=0B0A07)](https://edycutjong.github.io/grudgeball/)\n  [![Pitch Deck](https://img.shields.io/badge/Pitch-Deck-F59E0B?style=for-the-badge\u0026labelColor=0B0A07)](https://edycutjong.github.io/grudgeball/pitch/)\n  [![Games with a Hook](https://img.shields.io/badge/Reddit-Games_with_a_Hook-8B5CF6?style=for-the-badge\u0026labelColor=0B0A07)](https://redditgameswithahook.devpost.com)\n\n  \u003cbr/\u003e\n\n  ![TypeScript](https://img.shields.io/badge/TypeScript_6-3178C6?style=flat\u0026logo=typescript\u0026logoColor=white)\n  ![Devvit](https://img.shields.io/badge/Devvit_0.13.6-FF4500?style=flat\u0026logo=reddit\u0026logoColor=white)\n  ![Hono](https://img.shields.io/badge/Hono-E36002?style=flat\u0026logo=hono\u0026logoColor=white)\n  ![Vite](https://img.shields.io/badge/Vite_8-646CFF?style=flat\u0026logo=vite\u0026logoColor=white)\n  ![Vitest](https://img.shields.io/badge/Vitest-235_passing-6E9F18?style=flat\u0026logo=vitest\u0026logoColor=white)\n  ![Coverage](https://img.shields.io/badge/coverage-100%25-6E9F18?style=flat\u0026logo=vitest\u0026logoColor=white)\n  [![CI](https://github.com/edycutjong/grudgeball/actions/workflows/ci.yml/badge.svg)](https://github.com/edycutjong/grudgeball/actions/workflows/ci.yml)\n\u003c/div\u003e\n\n---\n\n**The problem:** Reddit games get played once and abandoned, and daily games run\nout of hand-authored content. **The solution:** make the players the content\nengine — everyone plays one shared board, and spending your marbles earns you\n**one** attributed object placed into *tomorrow's* board. **What's built:** a\ngreen, tested Devvit app (Hono server + Canvas-2D client over a pure shared core)\nwhere every placement passes an in-transaction A\\* solvability check, a nightly\nscheduler compiles the board, and a personal **Grudge Report** turns yesterday's\ncrowd into your reason to return.\n\nYou get three marbles a day. You drop them through a machine of bumpers, fans,\nmagnets, and spikes — every object placed and *named* by another redditor, with\ntheir username and body count on it. Plant a spike and earn **Menace** credit for\nevery stranger it kills overnight; plant a booster or cushion and earn **Angel**\ncredit for every run it saves. In the morning: *\"Your spike 'Greg's Regret' claimed\n87 marbles. You are today's #3 Menace.\"*\n\nThe board compiles at midnight UTC and accretes new objects every hour, so a\n**Cruelty Multiplier** climbs all day. The traces of other players *are* the\ncontent — the demo post is alive at any hour a judge opens it.\n\n### 🔒 Sealed by architecture\n\nEverything runs on Reddit's own infrastructure — **no external services, no\nthird-party APIs, no data ever leaves the platform, no runtime AI.** The\n`devvit.json` `http` allowlist is **empty**; the client only calls same-origin\n`/api/*`; all state lives in Redis. Nothing to configure, no API keys to install,\nno privacy-policy friction — **privacy by architecture.** The board is\nmanufactured entirely by feed traffic and Redis, not by a model.\n\n---\n\n## 📸 The magic moment\n\n\u003e You drop your marble down the center. It threads the friendly top third, then\n\u003e dies on **\"Greg's Regret\"** — a spike a stranger named. A big **`312`** ticks up\n\u003e to **`313` bodies claimed**, `u/gb_founder_greg banks +1 Menace`, and you're told\n\u003e `You were victim #313`. Then you get to do the same to someone else.\n\nThis beat is **verified deterministic** on the seeded board: center drop → row 13 →\n`Greg's Regret` → score **3510** (`13 × 100 × cruelty 2.7`). It is witnessable **on\nload** — see [Offline demo mode](#-offline-demo-mode) and [`DEMO.md`](DEMO.md).\n\n---\n\n## 🕹️ How to play\n\n1. Open today's Grudgeball post. The inline **splash** card shows a live board\n   snapshot and a CTA — tap **ENTER THE GAUNTLET** to expand into the game.\n2. **Drop (×3):** tap a column to aim, then press **DROP**. Watch the marble\n   carom down. If it dies, a killer card names the object and its author, shows the\n   trap's accumulated body count **ticking up to include your marble**, and credits\n   that builder. If it survives, you bank depth + coins × the day's Cruelty Multiplier.\n3. **Place (×1):** once your three marbles are spent, the board dims and the\n   placement palette opens. Pick one of eight objects (Menace red / Angel green\n   / Neutral brass), tap a glowing legal cell, name your grudge (≤24 chars,\n   word-filtered), and **PLANT**. It returns at dawn in tomorrow's board.\n4. **Report:** come back the next day. The morning Grudge Report modal fires\n   once, showing what your object did overnight and your rank movement.\n5. **Leaderboards:** Depth / Menace / Angel / Streak, with your row pinned.\n\nScoring is server-authoritative: `score = (depth×100 + coins×25 + goal×500) ×\ncruelty`, where `cruelty(t) = 1.0 + 3.0 × (active_traps / trap_cap)`, clamped to\n[1.0, 4.0].\n\n---\n\n## 🏗️ Architecture\n\nDevvit Web app: a **Hono server** (`src/server`) + a **Canvas 2D client**\n(`src/client`) over a pure, platform-free **shared core** (`src/shared`).\nFull schema + endpoint tables in [`ARCHITECTURE.md`](ARCHITECTURE.md).\n\n```mermaid\nflowchart TD\n  subgraph Client [Canvas 2D webview]\n    Sp[Splash / game load] --\u003e|/api/board| S\n    A[Drop scene] --\u003e|/api/drop-result| S\n    B[Placement scene] --\u003e|/api/place| S\n    C[Report modal] --\u003e|/api/report| S\n    Lb[Leaderboards] --\u003e|/api/leaderboards| S\n    A \u003c--\u003e|subscribe board_live| RT[realtime]\n  end\n  subgraph Server [Devvit Node]\n    S[Hono routes] --\u003e R[(Redis)]\n    CR1[cron compile 00:00] --\u003e R\n    CR2[cron accrete hourly] --\u003e R\n    TR[onPostCreate] --\u003e R\n    M1[menu: Seed Demo Day] --\u003e R\n    M2[menu: Purge Object] --\u003e R\n    M3[menu: Create Today's Post] --\u003e R\n    S --\u003e|app comment / asUser| RED[Reddit API]\n    CR1 --\u003e|daily post| RED\n  end\n  S --\u003e|publish board_live| RT\n```\n\n```\nsrc/\n  shared/    pure domain logic — imported by client, server, and tests alike\n    constants · types · grid · terrain · solvability (A*) · cruelty · score\n    day (UTC math) · plausibility · names (word filter) · rng · protocol · fixtures/\n  server/    Hono adapters over the pure core (Redis + Reddit + scheduler)\n    routes/  api (board/drop-result/place/report/leaderboards) · cron · menu · forms · triggers\n    core/    placement (watch/multi/exec) · compile · dropResult · boardRead · report\n             leaderboards · seed · purge · post · keys (the whole Redis schema)\n  client/    splash.html (inline feed) + game.html (expanded) + Canvas renderer + drop sim\n             lib/  api (typed fetch, demo fallback) · sim (drop) · board-render · demo (offline)\n```\n\n**Data model — Redis hashes and sorted-sets only** (no plain lists/sets):\n\n- `board:{day}` **hash** — packed objects + `obj:{id}:kills|saves` counters + meta.\n- `queue:{day}` **zset** (by timestamp) + `queued:{day}` **hash** — tomorrow's material.\n- `density:{day}:{band}` **hash** — per-band category counts for the cap check (I3).\n- `lb:{depth|menace|angel}:{day}` + `lb:streak` **zsets** — score ladders.\n- `user:{id}:{day}`, `report:{day}:{id}`, `streak:{id}`, `shadow:{day}`, `postmap`, `daypost`.\n\n**The crown jewel** is the placement transaction: one `watch/multi/exec` guards\nevery placement, enforcing *in-transaction* — one placement per user per day\n(I1), cell vacancy, per-band density caps (I3), and an **A\\* solvability check**\n(I2: the player-built board can never become an impassable spike wall). The\ncompiler re-validates all of it as defense-in-depth and lays deterministic\nterrain from `seed = hash(day)`, so `compile(day)` is idempotent and\nbyte-reproducible.\n\n**Devvit surface exercised:** `redis.watch/multi/exec`, hashes + zsets,\nscheduler cron ×2 (`compile` 00:00, `accrete` hourly), `onPostCreate` trigger,\nmenu actions ×3 (seed, purge, manual post), realtime channel `board_live`\n(1 Hz-batched, best-effort), Reddit API app comment + consent-gated\n`asUser SUBMIT_COMMENT`. **Fetch allowlist: empty** (`devvit.json` `http.enable:\nfalse`). See `SPONSOR_DEFENSE.md` for the \"why only Reddit/Devvit\" table.\n\n---\n\n## ✅ Tests\n\n**235 tests across 23 files, all passing** (`npm test` → `vitest run`), with\n**100% statement/branch/function/line coverage** on `src/shared/**` +\n`src/server/core/**` + `src/server/routes/**` (`npm run test:coverage`).\n`src/client/**` (Canvas/Phaser, needs a real browser) and `src/server/index.ts`\n(process bootstrap) are intentionally excluded from that gate — same \"client\nis playable core, not full UI\" caveat as [Honest limitations](#-honest-limitations).\n\n| File | Cases | Covers |\n|------|------:|--------|\n| `tests/routes/api.test.ts` | 26 | `/api/{board,drop-result,place,report,leaderboards}` end-to-end via a mocked `@devvit/web/server` |\n| `tests/placement.test.ts` | 21 | one-per-day (I1), density caps (I3), solvability refusal (I2), contested-cell tx |\n| `tests/plausibility.test.ts` | 19 | anti-cheat gates: speed, coin ceiling, velocity continuity, event integrity |\n| `tests/boardRead.test.ts` | 17 | board parsing, live/preview `boardView`, player day-state |\n| `tests/compile.test.ts` | 16 | determinism, idempotency, band-cap defense, **1/24 accretion cohorts** |\n| `tests/redisStub.test.ts` | 13 | the in-memory `watch/multi/exec` + zset/hash stub the suite runs on |\n| `tests/pack.test.ts` | 13 | byte-deterministic (un)packing + every malformed-payload branch |\n| `tests/dropResult.test.ts` | 13 | anti-cheat ledger: marble spend, shadow-flagging, kill/save credit, streaks |\n| `tests/solvability.test.ts` | 10 | A\\* gravity pathfinding on blocked grids |\n| `tests/report.test.ts` | 10 | Grudge Report aggregation + headline generation |\n| `tests/purge.test.ts` | 9 | mod purge from board or queue, by id or name |\n| `tests/routes/triggers.test.ts` | 8 | `onPostCreate` day-binding, idempotent |\n| `tests/names.test.ts` | 8 | word filter + length clamp |\n| `tests/routes/menu.test.ts` | 7 | Seed Demo Day / Purge form / manual post-create menu actions |\n| `tests/routes/cron.test.ts` | 7 | `/internal/cron/{compile,accrete}` scheduler endpoints |\n| `tests/routes/forms.test.ts` | 6 | purge-confirmation form submission |\n| `tests/leaderboards.test.ts` | 6 | top-10 + pinned-neighbor windows across all 4 tabs |\n| `tests/cruelty.test.ts` | 6 | cruelty curve `1.0 + 3.0×(traps/cap)`, clamped |\n| `tests/grid.test.ts` | 5 | cells, bands, placement zones, legality |\n| `tests/day.test.ts` | 5 | UTC day/hour math, day-number, round-trip validation |\n| `tests/seed.test.ts` | 4 | demo-day seeding, deterministic + idempotent |\n| `tests/score.test.ts` | 4 | scoring formula + cruelty multiplier rounding |\n| `tests/post.test.ts` | 2 | daily post title + Reddit submission adapter |\n\nRun one file: `npx vitest run tests/compile.test.ts`.\n\nType-check everything (client + server + shared + tests):\n`npm run type-check` (`tsc --build \u0026\u0026 tsc -p tsconfig.test.json`).\n\n---\n\n## 🧪 Testing \u0026 CI\n\nThe submission repo root is this `build/` folder. CI runs the **real gates** — no\nNext.js scripts, no invented tooling.\n\n```bash\nnpm ci                    # clean install from package-lock\nnpm run lint              # eslint\nnpm run type-check        # tsc --build (client+server+shared) + test project\nnpm test                  # vitest run — 235 tests\nnpm run test:coverage     # vitest run --coverage — 100% on shared/core/routes\nnpm run build             # vite build → dist/client/{splash,game}.html + dist/server\nnpm run check:submission  # pre-submission gate (URLs, README sections, placeholders)\n```\n\n| Layer | Tool | Status |\n|---|---|---|\n| Code Quality | TypeScript 6, strict, `tsc --build` | ✅ |\n| Lint | ESLint 9 + typescript-eslint | ✅ |\n| Unit Testing | Vitest — **235 tests / 23 files** | ✅ |\n| Coverage | 100% stmts/branch/func/line on shared+core+routes | ✅ |\n| Build Verification | Vite (client + server → `dist/`) | ✅ |\n| CI/CD Pipeline | GitHub Actions, Node **20 + 22**, concurrency-guarded | ✅ |\n| Security (SAST) | CodeQL (`javascript-typescript`) | ✅ |\n| Security (SCA) | Dependabot (npm + github-actions, weekly) | ✅ |\n| E2E vs localhost | **N/A — Devvit adaptation** (see below) | — |\n| Performance (Lighthouse) | **N/A — Devvit adaptation** (see below) | — |\n\n**Devvit adaptations (deliberately omitted, documented):** a Devvit app runs\ninside Reddit's **webview** — there is no served `localhost:3000` origin. So\n**Playwright-against-localhost** and **Lighthouse CI** (both of which need a live\nHTTP server URL) do not apply here; the client is static HTML built to\n`dist/client/`, and the real end-to-end path is `devvit playtest` on a test\nsubreddit (which needs a Reddit login — see the checklist below). The magic-moment\nloop is instead verified deterministically by the shared-core unit tests and the\noffline demo mode.\n\n---\n\n## 🎬 Offline demo mode\n\nThe full loop is **witnessable on load with the seeded board even with no server**.\nEach `/api/*` call (`src/client/lib/api.ts`) tries the real Hono server first and\nonly falls back to `src/client/lib/demo.ts` when the fetch or JSON parse genuinely\nfails — i.e. there is no Devvit host answering (the built client opened directly, a\nstatic preview, or a screen recording). The fallback:\n\n- Renders the deterministic **60-object founder board** (`src/shared/fixtures/demoBoard.ts`).\n- Runs the **same** shared `simulateDrop` / `scoreRun` / `cruelty` the server uses —\n  it invents no game logic, so a center drop still dies on Greg's Regret for 3510.\n- Shows an honest `· demo` marker in the HUD/splash so it is never mistaken for the\n  live server. In the real Reddit webview the server answers first, so this code is\n  **inert** — a live `{status:\"error\"}` response is honored, never masked.\n\n---\n\n## 🌱 Seeding the demo board\n\nTwo equivalent paths produce the same deterministic 60-object founder board so a\njudge gets the identical experience every run. **Seed data is labeled as such**\n(authors are 12 clearly-named `u/gb_founder_*` accounts — no fake-user smell).\n\n- **In-app (writes to Redis):** moderator menu → **\"Grudgeball: Seed Demo Day\"**.\n  Loads the 60-object board, populated kill/save counters, 40 synthetic ghost\n  trails, yesterday's leaderboards, and — for the invoking mod — a pre-populated\n  Grudge Report so the morning modal demos immediately. Idempotent: re-seeding\n  the same day yields a byte-identical `board:{day}` hash.\n- **Offline (writes a repo fixture):** `npm run seed:local`. Materializes the\n  same fixture to `data/fixtures/demo-board.json` for inspection/diffing and\n  asserts the fixture is byte-deterministic.\n\nThe seeded board is a funnel: the centre-column drop line ends at **\"Greg's\nRegret\"** (a spike with 312 kills). See `DEMO.md` for the judge path.\n\n---\n\n## 🛡️ Anti-cheat \u0026 trust model (honest tier)\n\nGrudgeball states its trust model plainly, because pretending otherwise would be\nthe dishonest thing.\n\n- **Client physics is authoritative for *feel*; the server is authoritative for\n  *records*.** The client simulates the drop and reports a decimated trajectory\n  polyline (≤64 points), depth, coins, and collision events.\n- The server re-scores server-side and runs **plausibility gates** (min elapsed\n  time per depth, coin ceiling per fountain, velocity continuity / no teleports\n  or anti-gravity, event↔object type integrity, depth↔polyline reconciliation).\n- **Failures never hard-reject.** A failing run still consumes the marble, gets a\n  normal-looking ack, and lands in a `shadow:{day}` zset — leaderboard-hidden and\n  mod-reviewable — rather than being rejected. Records are plausibility-checked\n  and shadow-flagged, **not cryptographically proven**.\n- The per-day marble counter lives *inside* the same `watch/multi/exec` as the\n  score write, so the 3/day limit cannot be raced.\n\n**Known limitations:**\n1. A sophisticated cheater can shave leaderboard scores (shadow-flagged, not prevented).\n2. Realtime is best-effort garnish; players see landings, not each other's marbles mid-flight.\n3. Daily limits are per Reddit account; alts are a platform-level exposure shared by all Devvit games.\n\nModeration is a one-paragraph plan by design: a fixed 8-object palette (no free\ndrawing), a name word-filter, report-to-hide, and a mod **Purge Object** menu\naction.\n\n---\n\n## 💻 Local development\n\n```bash\nnpm install\nnpm run lint              # eslint\nnpm run type-check        # tsc --build (client+server+shared) + test project\nnpm test                  # vitest run — 235 tests\nnpm run test:coverage     # vitest run --coverage — 100% on shared/core/routes\nnpm run build             # vite build → dist/client/{splash,game}.html + dist/server\nnpm run seed:local        # regenerate data/fixtures/demo-board.json (deterministic)\nnpm run check:submission  # pre-submission gate (see below)\n```\n\nYou cannot run the actual game loop against Reddit without auth — that is the\nplaytest step below. `npm run build` verifies the client compiles and both\nentrypoints resolve; it does **not** exercise Redis/Reddit (those exist only at\nruntime). To witness the loop with zero auth, open the built client — the\n[offline demo mode](#-offline-demo-mode) takes over.\n\n---\n\n## 📋 First playtest checklist\n\nYou (the human) run these — they need a Reddit login this environment does not\nhave.\n\n1. **Front-load the subreddit-ban round-trip (do this first).**\n   New hackathon subreddits are currently being **auto-banned (\"Rule #2\") by\n   Reddit safety automation — including a re-ban immediately after you install a\n   Devvit app.** Reddit staff unban manually when you post your username +\n   subreddit in the Devpost forum thread. Mitigation:\n   - Create your test subreddit (e.g. **r/grudgeball_dev**) from your aged main\n     account on **day one**, and add a normal pinned post before installing anything.\n   - **Expect a re-ban at first app install.** Keep the unban-thread link handy\n     and install `dr-admin-approve` per the Devvit rules.\n   - Do this early so the ban/unban latency doesn't block your demo.\n2. **Log in:** `npm run login` (`devvit login`) with the account that owns the\n   test subreddit.\n3. **Playtest:** `npm run dev` (`devvit playtest`) — it builds and uploads to your\n   dev subreddit (`devvit.json` → `dev.subreddit: \"grudgeball_dev\"`) and hot-reloads.\n4. **Open the post and verify the two entrypoints:**\n   - The **splash** (`default`, inline) renders the board snapshot + CTA in the feed.\n   - Tapping the CTA calls `requestExpandedMode(event, 'game')` and the **game**\n     (`game.html`, tall) opens. Confirm the HUD (`DAY N · TRAPS · CRUELTY ×`),\n     marble pips, aim/drop, killer card (with the body-count tick), placement\n     palette, and (after a compile) the report modal all appear.\n5. **Seed for a live demo:** run the **\"Grudgeball: Seed Demo Day\"** mod menu\n   action, then open today's post and drop down the centre column → death to\n   Greg's Regret. Follow `DEMO.md` end to end.\n6. On a fresh board with no compile yet, `/api/drop-result` returns `closed` —\n   that's expected; seed (step 5) or wait for the midnight cron.\n\n---\n\n## 📮 Submission checklist\n\nRun `npm run check:submission` before submitting. The demo-post URL below stays a\nplaceholder (and the gate stays red) until the post is live.\n\n\u003e **The remaining `[ ]` items need no app approval — do them now.** The Reddit\n\u003e review only unlocks the *public* App Directory listing + icon; judging happens on\n\u003e your (public) test sub, which is entirely in your control.\n\n- App listing: https://developers.reddit.com/apps/grudgeball\n- Devpost project: https://devpost.com/software/grudgeball\n- Demo video: https://www.youtube.com/watch?v=ZHujf9Qudjw\n- Demo post: https://www.reddit.com/r/grudgeball_dev/s/1G2fhTJOq8\n- [x] `npm run lint` clean\n- [x] `npm run type-check` clean\n- [x] `npm test` green (235/235), `npm run test:coverage` at 100%\n- [x] `npm run build` succeeds (both entrypoints resolve)\n- [x] Published to the App Directory (`devvit publish`, in review)\n- [x] 60-second demo video recorded \u0026 published (link above)\n- [x] Public repo + Devpost project page linked\n- [x] `r/grudgeball_dev` set to **Public**, demo post seeded (`Seed Demo Day` → `Create Today's Post`) and verified against `DEMO.md`\n- [ ] Demo-post URL filled above + Devpost form submitted\n\n## ⚙️ Engineering harness\n\n| File | Purpose |\n|---|---|\n| `.github/workflows/ci.yml` | Node 22: `npm ci` → `lint` → `type-check` → `test:coverage` (100%) → `build` |\n| `.github/workflows/pages.yml` | Deploys `docs/` (landing + pitch deck) to GitHub Pages |\n| `.github/workflows/codeql.yml` | CodeQL SAST (`javascript-typescript`) |\n| `.github/dependabot.yml` | Weekly npm + github-actions dependency PRs |\n| `LICENSE` | MIT © 2026 Edy Cu |\n| `.github/CODE_OF_CONDUCT.md`, `CONTRIBUTING.md`, `SECURITY.md` | Community health + private vuln-reporting policy |\n| `.github/ISSUE_TEMPLATE/*`, `PULL_REQUEST_TEMPLATE.md` | Bug/feature templates, PR checklist |\n\n## ⚠️ Honest limitations\n\nThe shipped renderer is **Canvas 2D**, not Phaser (Phaser is a listed\ndependency but the MVP uses a lightweight, dependency-free canvas renderer and a\ndeterministic grid drop simulation — see `docs/friction-log.md`). The physics is\nplausible arcade feel, not a rigid-body simulation; the server treats it as\nuntrusted regardless.\n\n## 🔖 Versioning\n\nAutomatic semantic versioning via [semantic-release](https://semantic-release.gitbook.io/):\nevery push to `main` parses [Conventional Commits](https://www.conventionalcommits.org/)\n(`fix:` → patch, `feat:` → minor, `BREAKING CHANGE:` → major) and, when warranted,\nbumps `package.json`, updates `CHANGELOG.md`, tags the commit, and publishes a\nGitHub Release with generated notes (`.github/workflows/release.yml`). No manual\nversion bumps, no npm registry publish (private app).\n\n## 📄 License\n\n[MIT](LICENSE) © 2026 Edy Cu. Built for Reddit's *Games with a Hook*. Native\nDevvit — thank you to the Devvit team for the Redis, scheduler, realtime, and\nReddit APIs that make a crowd-built board possible.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fedycutjong%2Fgrudgeball","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fedycutjong%2Fgrudgeball","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fedycutjong%2Fgrudgeball/lists"}