尚未偵測到更新。GitHub 發布版本後會顯示在這裡。
個人 GitHub 帳號下公開儲存庫的擁有者可以登入並驗證所有權,自行管理介紹,無需管理員核准。
登入並產生驗證檔案,在 30 分鐘內將其提交至此儲存庫的預設分支。其他分支或分叉儲存庫中的檔案無效。然後在下方檢查檔案,即可無需管理員核准直接管理專案。請保留檔案以維持管理權限。
如需登入,將前往登入頁面。登入後請回到此處,再次點擊所選的驗證按鈕。
在專案資料夾中執行 /nomorevibe verify,即可驗證所有權並自行管理介紹。
一分鐘完成驗證製作者的介紹僅有上面的一句話。 更多詳情請閱讀儲存庫 README。 儲存庫 README ↗
正在載入完整 README。
檢視完整 README ↗A browser party game for friends: everyone invents a fake answer to a trivia question, then tries to spot the real one among the bluffs. Play online with friends anywhere: one person creates a room and shares the 5-character code or the invite link, and everyone else joins from their phone or computer. It also runs on your own computer for players on the same Wi-Fi. Arabic-first (RTL) interface with an English mode.
Play online: kalaktn.pages.dev
Online, the game runs on Cloudflare Workers with one Durable Object per room; locally it runs on Node.js (npm start). Both use the same game code over plain WebSockets.
Inspired by the Arabic party game Kalak and the "fibbing" genre of games such as Fibbage. This is an independent fan-made project for learning; it is not affiliated with the original game.
Screenshots from a local 3-player session (three separate browser sessions on one machine), with the room set to English questions.
If you have never played a Kalak/Fibbage-style game, here is the idea:
Scoring
| Event | Points |
|---|---|
| You vote for the real answer | +2 |
| Another player votes for your bluff | +1 per player fooled |
You can't vote for your own bluff (it is greyed out). If you type the real answer by accident, the game tells you and lets you write a fake one instead. If two players write the same bluff (ignoring case, spacing and accents/harakat), it appears once and both authors get the points.
After the last round, a podium shows the top three (players with equal scores share a place) and the host can start a new game with the same group.
Host tools
During a game, the host (👑) has a bar at the bottom of the screen:
| Button | What it does |
|---|---|
| Pause / Resume | Stops the timer for everyone. All players see "Paused by the host" until the host taps Resume. |
| Skip | Ends the current step now: picks a random topic, or ends the writing or voting time. |
| Rounds | Adds 1 or 3 more rounds (30 at most). Everyone sees the new total. |
| End | After a confirm, ends the game and shows the final scores to everyone. |
The host is also a player, so these tools never show them any answers or bluffs early. If the host leaves, the next player becomes host and gets the bar. In the lobby, the host can also remove a player.
0/O or 1/I) and an invite link (/?room=CODE) shown in the lobby with a Copy link button. Opening the link goes straight to the join form with the code filled in.The questions are a hand-written, hard-coded list in lib/questions.js (the QUESTIONS object). There is no external trivia API or database. There are 13 categories × 25 questions = 325 questions, each stored in Arabic and English (q/a and qE/aE), so the host can switch the language of the questions. Each question has exactly one clean answer per language (the one shown in the vote list); other accepted spellings go in alt / altE and are only used to detect a player typing the real answer. A question is never asked twice in the same game: when the chosen topic runs out, another enabled topic is used, and when every enabled topic is used up the game ends normally with the podium.
Categories: General, Geography, History, Science, Sports, Entertainment, Technology, Food, Islam, Arab world, Literature, Animals, Flags.
phones / browsers (public/) one of two hosts, same game code
┌──────────────────────────┐ ┌───────────────────────────────────────┐
│ index.html (screens) │ POST │ Node (server.js) Cloudflare │
│ game.js (UI + events)│ /api/rooms │ Express static (worker/index.js) │
│ net.js (WebSocket, │ ─────────────► │ ws package Worker + assets, │
│ acks, reconnect) │ WebSocket │ Map<code, room> a Durable Object │
│ style.css │ /ws?room=CODE │ per room code │
└──────────────────────────┘ ◄────────────► │ lib/room.js: one room's game │
│ lib/logic.js: rules (pure) │
│ lib/questions.js: 325 Q/A │
└───────────────────────────────────────┘
lib/room.js owns one room and knows nothing about the transport: it takes a connection plus (event, data, ack) and replies with conn.send(...). server.js keeps a Map of rooms keyed by code (dropped once empty). On Cloudflare, the Worker routes /ws?room=CODE to the Durable Object idFromName(CODE), which runs the same module. To create a room, the browser first asks POST /api/rooms for an unused code (the room holds it for a minute), then connects to it and sends room:create.
The server is the single source of truth. Clients only send intents (create/join, answer, vote...). The server validates every payload (room codes; settings are whitelisted and clamped; text is trimmed and capped at 60 characters; votes for your own bluff are refused), advances the game and pushes the new state to everyone in the room. Messages are capped at 4 KB, unknown events and malformed frames are ignored, and every handler and timer callback is wrapped so that one bad event is logged instead of crashing the room. Correct answers are never sent to clients before the reveal. Vote options are sent as { id, text, own } only, without who wrote them.
lobby ──game:start──► picking ──category chosen / 15 s──► question
▲ ▲ │ all answered / answer timer
│ │ game:next (rounds left) ▼
│ results ◄──all voted / vote timer──── voting
│ │ game:next (last round)
└──game:restart── finished
A single server-side interval timer per room drives each phase (setTimer) and broadcasts timer:update every second. A phase also ends early as soon as every connected player has answered or voted; this is re-checked whenever someone disconnects or leaves, and if the topic picker leaves, a random topic is chosen right away.
The host tools act on the same machine. Pause stops the timer where it is and sets paused in room:state; while paused, answers, votes and the topic pick are refused and nothing moves on. Resume carries on with the time that was left. Skip sends the phase and round the host saw, so a double tap only skips once. End jumps to finished (the podium says the host ended the game). Any phase change clears the pause. Only the current host can use them; this is checked in lib/room.js, so the Node server and the Worker both get it.
Every message is JSON: { "t": event, "d": data, "id": ackId } (id only when the sender wants an answer), and answers are { "ack": ackId, "d": reply }. public/net.js wraps this in a socket.io-like emit(event, data, ack) / on(event, fn) API, pings every 25 s and reconnects with backoff.
| Direction | Event | Purpose |
|---|---|---|
| client → server | room:create, room:join, room:resume, room:leave | create / join / rejoin / leave a room (with an ack callback) |
| client → server | room:settings, room:kick | host changes settings / removes a player |
| client → server | game:start, game:next, game:restart | host controls the flow |
| client → server | host:pause, host:resume, host:skip, host:addRounds, host:endGame | host tools during a game (the ack only says yes or no, with an error code) |
| client → server | game:pickCategory | current picker chooses the topic |
| client → server | game:answer, game:vote | submit a bluff / a vote (the ack says why it was refused: real, own...) |
| client → server | chat:send | lobby chat |
| server → clients | room:state | full room snapshot (players, scores, settings, host) |
| server → clients | game:pick, game:question, game:vote, game:results, game:end | phase changes |
| server → clients | timer:update, game:answered, game:voted, chat:msg | live counters, timer, chat |
| server → clients | session:replaced, kicked | the seat was taken over by another tab / the host removed you |
A bluff equal to the real answer (or one of its accepted alternatives, in either language) is refused when it is submitted. When voting starts, bluffs that are equal once case, spacing, punctuation, accents/harakat, Arabic letter variants and leading articles are ignored are merged into one option that credits every author. The real answer and the bluffs are then shuffled (Fisher–Yates) before they are sent out.
Each browser gets a random player token stored in localStorage (kalak_token), together with the room it is in (kalak_session). The token is sent when creating or joining a room and is never shown to other players (they only see a separate public player id).
room:resume { code, token }. The server re-attaches the socket to the same seat (same score, same answer/vote if already given) and re-sends the current phase (topic pick, question, vote list, results or podium) plus the timer.Requirements: Node.js 18+ (tested with Node 22).
npm install
npm start # http://localhost:3000
To try the Cloudflare version locally (the Worker and the Durable Object run in workerd):
npx wrangler dev # http://localhost:8787
The port can be changed with the PORT environment variable:
PORT=3105 npm start # macOS / Linux / Git Bash
$env:PORT=3105; npm start # Windows PowerShell
npm test
Runs Node's built-in test runner (node:test, no test framework needed):
test/logic.test.js: unit tests for the pure rules in lib/logic.js (settings validation, answer normalisation and merging, scoring, tied ranks, question picking) and a check that every question has one clean answer.test/server.test.js: end-to-end tests that start the real server on port 3609 (override with KALAK_TEST_PORT) and play through it with a small WebSocket client (test/ws-client.js): malformed, junk and oversized messages, full rounds, reload and resume, grace-period expiry, players and the host leaving mid-game, kicks, running out of questions, invalid room codes, two rooms running side by side, empty rooms being cleaned up, and the host tools (only the host can use them, pause and resume keep the right time, skip, end game, add rounds, host hand-off, and no answers reach the host early).The server listens on all network interfaces, so phones on the same Wi-Fi can join:
npm start and find the host computer's local IP (ipconfig on Windows, ip addr / ifconfig on macOS/Linux), e.g. 192.168.1.20.http://192.168.1.20:3000.You may need to allow Node.js through the computer's firewall.
The repo is ready for Cloudflare Workers, and the free plan is enough. wrangler.jsonc serves public/ as static assets and declares the Room Durable Object (SQLite-backed, as the free plan requires).
kalak, leave the build command empty and set the deploy command to npx wrangler deploy.https://kalak.<your-subdomain>.workers.dev, and every push to main redeploys it.From your own computer instead: npx wrangler login, then npx wrangler deploy. npx wrangler deploy --dry-run checks the build without uploading anything.
.
├── server.js # Node host: Express static files, POST /api/rooms, ws at /ws?room=CODE
├── worker/
│ └── index.js # Cloudflare host: Worker (assets, /api/rooms) + Room Durable Object
├── wrangler.jsonc # Cloudflare config (assets, Durable Object binding, migration)
├── lib/
│ ├── room.js # one room: seats/reconnects, game flow, message handling (no transport)
│ ├── logic.js # pure rules: settings validation, answer matching, scoring, ranking
│ └── questions.js # question bank (Arabic + English)
├── public/
│ ├── index.html # all screens (landing, profile, lobby, pick, question, vote, results, final)
│ ├── game.js # client: translations, screens, game events, reconnect, rendering
│ ├── net.js # WebSocket client: emit/on/acks, heartbeat, reconnect with backoff
│ ├── style.css # dark, mobile-first RTL/LTR theme and animations
│ └── favicon.svg
├── test/ # node:test unit tests + WebSocket end-to-end tests
├── docs/screenshots/ # README images
└── package.json
ws locally; Cloudflare Workers + Durable Objects onlinenode:test + ws© 2026 Hamza Ben Ismail. All rights reserved.