尚未偵測到更新。GitHub 發布版本後會顯示在這裡。
個人 GitHub 帳號下公開儲存庫的擁有者可以登入並驗證所有權,自行管理介紹,無需管理員核准。
登入並產生驗證檔案,在 30 分鐘內將其提交至此儲存庫的預設分支。其他分支或分叉儲存庫中的檔案無效。然後在下方檢查檔案,即可無需管理員核准直接管理專案。請保留檔案以維持管理權限。
如需登入,將前往登入頁面。登入後請回到此處,再次點擊所選的驗證按鈕。
在專案資料夾中執行 /nomorevibe verify,即可驗證所有權並自行管理介紹。
一分鐘完成驗證Jace Games: quick browser games with Sign in with Jace and scores synced to your Jace account. Snake, Memory Match, 2048, Minesweeper and more.
翻譯準備中
取自儲存庫 README
正在載入完整 README。
檢視完整 README ↗Quick browser games from Jace. Sign in with your Jace account to keep your scores, including the Snake and Memory Match scores from the old Jace site, and compete on leaderboards shared across Jace.
Next.js 16 (App Router), TypeScript and Tailwind 4, deployed on Vercel.
| id | Game | Score | Better |
|---|---|---|---|
snake | Snake | Food eaten | Higher |
memory | Memory Match | Moves to clear | Lower |
2048 | 2048 | Points | Higher |
tetris | Block Stacker | Points | Higher |
minesweeper | Minesweeper | Seconds (9×9, 10 mines) | Lower |
flappy | Flappy Jace | Pipes passed | Higher |
breakout | Breakout | Points | Higher |
wordle | Word Guess | Guesses used (wins only) | Lower |
reaction | Reaction Time | Average of 5, in ms | Lower |
typing | Typing Speed | WPM over 30 s | Higher |
snake and memory keep the ids the original Jace site used, so old scores carry over. Never change an existing id.
Every game works with a keyboard (arrow keys/WASD, Enter to restart) and on a touchscreen (swipes, taps, on-screen pads).
Every game can be played with friends, in one of three ways:
| Mode | Games | How it works |
|---|---|---|
| Race | 2048, Minesweeper, Word Guess, Typing, Reaction, Flappy, Breakout, Block Stacker (with garbage attacks) | Everyone plays the same seeded board at once, sees each other's live score, and the server ranks the results. Timed races count your score when time runs out. |
| Turns | Memory Match, Connect Four, Chess, Checkers, Battleship, Dots and Boxes, Draw & Guess | The server referees every move with a rules module in lib/multiplayer/rules and only sends each player what they may see. Also playable by passing one device around (except Draw & Guess). |
| Live | Pong, Snake (2–4 player arena) | The host's browser runs the game and broadcasts it about 30 times a second; the others send their input. |
Create a room with Play with friends on a game page, then share the link (/r/<code>), the code, or invite Jace Social friends from the lobby. Wins are kept per game (the "Most wins" leaderboards and the profile); they don't go to Jace's single-player leaderboards. Rooms expire after a day.
Every game page also has a Fullscreen button (or press F). Where the browser can't go fullscreen (iPhone), it covers the page instead.
GAMES in lib/games.ts: id, name, description, icon, better ("higher" or "lower"), how to format the score, addedAt (shows a "New" badge for a month), and limits for server-side validation.components/games/. It receives { onFinish(score, durationMs), best } and calls onFinish once when a game ends.components/games/index.ts.For multiplayer, add multiplayer: { mode, min, max } to the entry. A race needs nothing else as long as the game uses useGameRandom(seed) for randomness and calls onProgress/onFail. A turn-based game needs a rules module (registered in lib/multiplayer/registry.ts, with tests in tests/rules.test.ts) and a board in components/multi. A live game needs a component in components/multi/live.ts.
You need Node.js 20.9 or newer.
npm install
cp .env.example .env.local
Fill in .env.local, then run npm run dev and open http://localhost:3000.
| Name | Required | What it is |
|---|---|---|
JACE_CLIENT_ID | yes | The OAuth client id from Jace. |
JACE_CLIENT_SECRET | yes | The OAuth client secret from Jace. Server only, never sent to the browser. |
SESSION_SECRET | yes | 32+ random characters used to encrypt the session cookie. Generate with openssl rand -base64 32. Changing it signs everyone out. |
APP_URL | no | This app's public URL, used to build the redirect URI <APP_URL>/api/auth/callback. Defaults to the URL of the incoming request. Set it in production if you use more than one domain. |
JACE_URL | no | The Jace base URL. Defaults to https://jaceo.vercel.app. |
SUPABASE_URL | yes* | The Jace Games Supabase project URL. Rooms, match results and community games live there. |
SUPABASE_SECRET_KEY | yes* | That project's secret key (server only). |
NEXT_PUBLIC_SUPABASE_URL | yes* | The same project URL, for Realtime in the browser. |
NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY | yes* | That project's publishable key, for Realtime in the browser. |
JACE_SOCIAL_URL | no | Defaults to https://jace-social.vercel.app. |
JACE_SOCIAL_APP_KEY | no | Shared secret with Jace Social (its JACE_GAMES_APP_KEY). Without it, friend invites show "coming soon". |
CRON_SECRET | no | Vercel sends it to the daily room cleanup cron; set it so nobody else can call that route. |
SITE_OWNER_JACE_ID | no | The Jace user id (the sub from userinfo) of the site owner. That account can take community games down and put them back, and sees reports. Without it nobody can. |
* Required in production for multiplayer and the Game Studio. In development without them, rooms use an in-memory store and a local live relay (lib/realtime/devBus.ts), so everything can be tried locally.
Put the same variables in Vercel → Project → Settings → Environment Variables. Only the two NEXT_PUBLIC_SUPABASE_* values reach the browser, and both are safe to expose; every secret stays on the server. .env* files are gitignored (except .env.example).
supabase/migrations in order (SQL editor, or supabase db push). Every table has row-level security on and no policies, so only the server's secret key can read or write.Live channels are named with an unguessable per-room secret that only room members get. Anything that matters (moves, results, wins) goes through the server; the channels only carry "something changed" pings, live scores, drawing strokes and live game frames.
The lobby lists your Jace Social friends and sends invites as Jace Social DMs, through Jace Social's server-to-server API (/api/v1/integrations/games/{friends,invite,presence}, see its README). Jace Games calls it from the server with the player's Jace token and JACE_SOCIAL_APP_KEY. While you're in a game, your friends see "Playing on Jace Games".
scores scope).https://<your-vercel-domain>/api/auth/callbackhttp://localhost:3000/api/auth/callback (for local development)JACE_CLIENT_ID and JACE_CLIENT_SECRET.Plain OAuth 2.0 authorization code flow (no OIDC):
/api/auth/signin stores a random state in a short-lived encrypted cookie and redirects to GET /oauth/authorize?scope=profile scores./api/auth/callback checks state, exchanges the code at POST /api/oauth/token with the client secret (on the server), and calls GET /api/oauth/userinfo.jg_session). See lib/session.ts.Every page load re-checks userinfo through /api/session. If Jace says banned, the person is signed out and sees "This Jace account is banned." A 401 from any Jace call also signs them out.
localStorage first, with a UUID as its external_id (lib/localScores.ts).POST /api/scores. The server validates each score against the game's limits (whole number, in range, and a plausible game length) before forwarding it to Jace's import endpoint. Because external_id makes the import idempotent, retries never create duplicates.components/ScoresProvider.tsx)./api/leaderboard → GET /api/oauth/leaderboard, order=asc for lower-is-better games). Jace's API needs a user token, so signed-out players see their own device's best instead.The scores scope and the /api/oauth/scores and /api/oauth/leaderboard endpoints are a contract Jace hasn't shipped yet. Until it does, those calls return 404, the app shows "Sync with Jace is coming soon", and scores stay on the device as pending. They upload on their own once the endpoints are live. If someone signed in before Jace accepted the scores scope (Jace answers 403 insufficient_scope), the app asks them to sign in again.
scripts/mock-jace.mjs implements the whole contract locally, with an account ("oldplayer") that already has old Snake and Memory Match scores:
node scripts/mock-jace.mjs
JACE_URL=http://localhost:4000 npm run dev
The mock's authorize endpoint lets you pick one of two accounts, "oldplayer" or "friend". To be both at once, open the app on localhost in one tab and 127.0.0.1 in another (they keep separate cookies). The mock also stands in for Jace Social's friends API: set JACE_SOCIAL_URL to the mock and JACE_SOCIAL_APP_KEY=dev-app-key. Start it with MOCK_SCORES=off to behave like Jace before its scores API shipped (scores endpoints return 404), or MOCK_BANNED=1 to test the banned path.
People make their own games at /studio, with a code editor (CodeMirror, HTML with inline JavaScript and CSS) or Scratch-style blocks (Blockly 13.3.0, the same version Jace Social's bot editor uses). Published games are listed at /community, and multiplayer ones can be played in rooms like the built-in games. Data lives in Supabase (supabase/migrations/002_studio.sql): row-level security on with no policies, so only the server (secret key) can read or write. Without Supabase, development falls back to memory.
lib/studio/blockRuntime.ts): a 480×360 canvas, emoji or shape sprites with position, size and speed, events (start, keys, taps, every frame, timers, messages), score and game over. "Convert to code" turns a blocks game into a code game (one way).SITE_OWNER_JACE_ID) sees reports on the game's page and can take it down.Every game gets window.jace:
jace.player.name | Who's playing. |
jace.submitScore(n) | A whole-number score, higher is better. Solo, the player's best goes on the game's leaderboard (labelled unverified). In a race, it's the player's live score. |
jace.gameOver() | The round is over. In a race, the last submitted score becomes the result (none = did not finish). |
jace.onReady(cb) | Calls cb once the page is ready. |
jace.seed, jace.random() | A seed and a seeded random function; everyone in a room gets the same ones. |
jace.room | null when playing alone. In a room: mode ("race" or "live"), isHost, mySeat, players ({ seat, name, you, host }). |
jace.room.send(event, data) | Sends to the other players over the room's channel. |
jace.room.on(event, (data, fromSeat) => …) | Hears them. Returns an unsubscribe function. |
jace.gameOver({ winners: [seat] }) | Live games: the host ends the game and names the winners. |
In race rooms everyone plays their own copy and the highest result wins. In live rooms the game shares its own state with jace.room (usually the host runs the game and sends it out); only the host's gameOver counts.
Community games are untrusted code, so they run in <iframe sandbox="allow-scripts allow-pointer-lock" srcdoc> (components/studio/GameFrame.tsx):
allow-same-origin: the game runs on an opaque origin, so document.cookie, localStorage and IndexedDB throw, and it can't touch this site's pages or cookies. No allow-top-navigation, allow-popups, allow-forms or allow-modals.default-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline'; img-src data: blob:; media-src data: blob:; font-src data:; connect-src 'none'; form-action 'none'; base-uri 'none'), so fetch, XHR, WebSocket, EventSource and loading anything from another site all fail. Images, sounds and fonts go in the game as data: URLs.postMessage only. The page accepts messages only when event.source is its own iframe, and checks every one: scores must be finite numbers (at most one a second); room messages need an event name matching [A-Za-z0-9_:.-]{1,32} and JSON data up to 8 KB, at most 20 a second.npm run dev: development servernpm run build: production buildnpm run lint: ESLintnpm run typecheck: TypeScriptnpm test: tests for the turn-based rules modules