CoOp:Anything
Search any game and see how much of it you can play with friends: every co-op mod, its stage, quality and community rating.
Connect Four in the browser, on a Cloudflare Worker with one Durable Object per game
GamesJavaScript · ShellGitHub @AIssoufiOnlineOwner unconfirmed
Have you tried it? One tap is enough. No sign-in needed.
No updates detected yet. GitHub releases will appear here.
Owners of public repositories under a personal GitHub account can sign in to verify ownership and manage the introduction themselves. No administrator approval is needed.
Sign in, generate a verification file, and commit it to this repository’s default branch within 30 minutes. Files on other branches or forks do not count. Then check the file below to manage the project without administrator approval. Keep the file to retain your access.
If needed, you will go to sign-in. After signing in, return here and press your chosen verification button again.
Run /nomorevibe verify in your project folder to verify ownership and manage the introduction yourself.
Verify in one minuteAsk about other verification options
The maker's introduction consists of the line above. Read the repository README for more details. Repository README ↗
Loading the full README.
Read the full README ↗A two-player Connect Four game in the browser, played through a small REST API that runs on a Cloudflare Worker.
Two players share the same screen and take turns, red first. Click one of the seven dashed circles above the board to drop a token of the current player in that column: it falls to the lowest free cell. The first player who aligns four tokens, in a row, a column or a diagonal, wins. When the 42 cells are full with no alignment, the game is a draw.
Above the board, the page says whose turn it is, then who won or that the game is a draw. It shows why a move cannot be played, such as a full column, whose circle is grayed out, or a game that is over. Each visitor plays their own game: the Nouvelle partie button, or reloading the page, starts a new one.
The page can also be played on a phone, with the keyboard alone and with a screen reader:
nodejs_compat flag.docs/decisions/ records why, one decision per file.
/api/v1/ shows the docs of the API with Swagger UI, from the OpenAPI 3.1 document
public/api/v1/openapi.yaml. Keep that document in step with the routes of routes/index.js.
The page loads Swagger UI from jsDelivr, at the version of the swagger-ui-dist devDependency of
package.json, which Dependabot bumps in a pull request of its own. That pull request fails
index.test.js until the two links of public/api/v1/index.html take the new version and the
SRI hashes of its files.
POST /game starts a game. It returns 201 with the game, whose id the other routes take,
and a Location header to read it. Each client network may start 20 games a minute: beyond, it
returns 429 { "code": "too_many_starts", "message": "..." }.GET /game/{gameId}/status returns the game: its id, the players, the tokens left,
lastPlayerWhoPlayed, winner (no winner until a player aligns four tokens), state
(playing, won or draw), the board, top row first, and winningCells: the row and
column indexes in board of the tokens that won, in reading order, empty until a win.POST /game/{gameId}/play plays the move of its JSON body, { "playerId": "red", "column": 1 }:
a token of red or yellow in a column from 1 to 7. It returns the new status. A bad body or
a move that breaks a rule (game over, not the player's turn, full column) returns
400 { "code": "...", "message": "..." }, such as column_full, and leaves the game unchanged.An id that no game has returns 404 { "code": "game_not_found", "message": "..." }, and a route
that does not exist 404 { "code": "route_not_found", "message": "..." }. A game is deleted 7
days after its start or its last move, so its id then returns 404 too. So does an id whose
percent-encoding cannot be decoded, such as /game/%E0/status. On a route that does not exist, a
browser, whose Accept prefers text/html, gets instead a page in French with links to the game
and to the API docs; */*, JSON or no Accept at all keep the JSON 404.
A route called with a method it does not serve, such as GET /game/{gameId}/play or
DELETE /game, returns 405 { "code": "method_not_allowed", "message": "..." } with an Allow
header that lists its methods. OPTIONS on a route returns 204 with the same Allow header.
Every 4xx body carries a code from a fixed list, for a client to test, next to a message in
English, which may change. The OpenAPI document lists and describes each code.
The site is a Cloudflare Worker, described by
wrangler.jsonc:
public/ (the page, its scripts, its favicon and the API docs)
before the Worker runs. public/.assetsignore keeps the tests of the page helpers out of it.public/_headers adds security headers to these files. Its Content-Security-Policy only allows
the site itself and forbids other sites to frame the pages, and /api/v1/ has its own to load
Swagger UI from jsDelivr. Its Strict-Transport-Security keeps browsers on HTTPS for a year, for
this host alone. Its Permissions-Policy turns off the camera, the microphone, the geolocation,
payments and USB, and its Cross-Origin-Opener-Policy same-origin keeps the pages apart from
the windows of other sites. The answers of the Worker do not get these headers: the Express app
sends them only X-Content-Type-Options, Strict-Transport-Security, Permissions-Policy and
Cross-Origin-Opener-Policy, and its HTML 404 page
(routes/notFoundPage.js, styled by public/not-found.css) the same Content-Security-Policy
and Referrer-Policy as the files.public/main.js as an ES module, which runs the game. It draws the board and the
column buttons with public/board.js, moves the focus between the columns with
public/keyboard.js, and calls the API through public/api.js. These modules import their
rules from public/utils.js and the winning cells from public/winning-cells.js, and write
their texts with public/texts.js from the copy of public/constants.js.index.js runs the Express app of routes/ on the Worker, with the nodejs_compat flag.worker/Game.js), which saves its moves and plays them again in the engine of app/ when it
wakes up. The engine does not depend on Express or on Cloudflare. An alarm of the Durable
Object deletes the game 7 days after its start or its last move.GAME_START_LIMITER counts the games that each client network starts.docs/decisions/0002 explains these choices.
pnpm install --frozen-lockfile
pnpm dev
It needs pnpm: any installed version switches by itself to the
one that the packageManager field of package.json pins. pnpm only installs versions published
at least 7 days earlier, except the exclusions that pnpm-workspace.yaml lists, and runs no
install script of a dependency. The overrides of that file force the security fixes that a
dependency does not take yet: each one, and each exclusion made for such a fix, says when to
remove it.
pnpm dev runs the Worker, its files and its Durable Objects with wrangler dev, at
http://localhost:8787 by default, and keeps the games in .wrangler/.
Workers Builds deploys master on
Cloudflare after each merge, on the custom domain connect-four.issoufi.fr. It installs the
dependencies with pnpm, from pnpm-lock.yaml, and its PNPM_VERSION build variable must follow
the packageManager field of package.json.
pnpm run deploy deploys the Worker by hand with wrangler deploy (pnpm deploy is a command of
pnpm itself). It needs a Cloudflare account that holds the issoufi.fr zone: log in with
pnpm exec wrangler login, or set CLOUDFLARE_API_TOKEN and CLOUDFLARE_ACCOUNT_ID. The Worker
needs no build step, no variable and no secret.
Workers Builds deploys each push to another branch as a
Preview, with wrangler preview, when
Enable Preview Builds is on (Settings > Build > Branch control of the Worker). Its pull
request gets:
Workers Builds: connect-four check;<branch>-connect-four.<subdomain>.workers.dev, which serves
the last commit of the branch, and the URL of each deployment.These URLs are public and send an X-Robots-Tag: noindex header.
A Preview inherits none of the production bindings: the previews block of wrangler.jsonc
gives it GAMES and GAME_START_LIMITER bindings apart from those of production.
1002, which all the
Previews share, apart from the 1001 of production.preview_urls turns these workers.dev URLs on. It also turns on the
Version URLs
of the production versions, which run with the production games and limit, like the site.
pnpm exec wrangler preview deploys the current branch by hand, with the account of
pnpm run deploy, and pnpm exec wrangler preview delete --name <branch> deletes a Preview.
Past the limit of the plan (100 Previews on the Free plan), Cloudflare deletes the Preview
deployed to least recently.
pnpm test runs the Vitest tests:
app/), the routes (routes/) and the storage of the games (worker/), with fake
Durable Objects;public/utils.test.js, public/texts.test.js) and its calls to the
API (public/api.test.js), with a fake fetch;public/board.test.js, public/keyboard.test.js),
in a jsdom document;public/main.test.js), run on the elements of index.html in a
jsdom document with a fake fetch;index.test.js), run in the local Workers runtime by createTestHarness of
Wrangler, with its files and its Durable Objects;They need Node.js 22.22.2+, 24.15+ or 26+ (the engines field of package.json): the versions
that jsdom supports. .nvmrc gives the version used in development and in the CI: 24, an LTS
release.
pnpm lint runs both checks, and fails on any error or warning:
pnpm lint:js checks every JavaScript file with ESLint 10 and its flat configuration,
eslint.config.js: the recommended rules, plus the rules of Airbnb base that the code was
written against, the layout ones from ESLint Stylistic. The
scripts of public/ are linted with the browser globals, the page ones as ES modules, and the
other files with the Node.js ones.pnpm lint:api checks the OpenAPI document with the recommended rules of
Redocly CLI, each warning made an error (redocly.yaml).
.redocly.lint-ignore.yaml lists the problems ignored on purpose, each with its reason.GitHub Actions runs .github/workflows/ci.yml on every pull request, every push to master,
and on master every Monday at 05:23 UTC:
pnpm install --frozen-lockfile, pnpm audit --audit-level moderate, pnpm lint and
pnpm test on the Node.js version of .nvmrc;pnpm install --frozen-lockfile and pnpm test on Node.js 22 and 26: the oldest major release
that engines accepts and the newest one released. Dependabot does not update this matrix:
add each new major release of Node.js to it by hand.The audit fails the run on any advisory at or above moderate:
pnpm-lock.yaml, which pnpm 12 fills with pnpm itself
and its binaries alone;cron, and GitHub turns the schedule off after 60 days without activity: turn it back on with
"Enable workflow" in the Actions tab.Each job sets pnpm up with pnpm/action-setup, which reads the packageManager field. It is
pinned to a commit rather than its v6 tag, which lagged behind v6.1.0, the first release that
starts from pnpm 12 instead of 11.
.github/workflows/commit-messages.yml checks the commit messages of every pull request, and
its title unless a bot opened it, with commitlint (see Commit messages).
.github/workflows/live-check.yml runs scripts/live-check.sh against the live site:
src and
href attributes of the served pages, then the modules that each script imports, read from the
served files; Swagger UI on jsDelivr is left out), then /api/v1/openapi.yaml, and fails when
one of them does not answer 200;--files-only) starts no game: it
asks the status of the id made of 64 zeros, which reaches a real Game Durable Object that
holds no game, and expects the JSON 404 with the code game_not_found;host input
(connect-four.issoufi.fr by default) and a files-only box, or run
scripts/live-check.sh [--files-only] [host] with curl and jq; the host may be a URL with
its scheme, such as http://localhost:8787 for pnpm dev;cron of the workflow;Dependabot checks the npm packages and GitHub Actions every week, for releases at least a week old
(.github/dependabot.yml): one pull request for the npm minor and patch updates, one for the
GitHub Actions updates, and one alone for swagger-ui-dist (see API docs). It
proposes no npm major update: each one goes through its own ticket.
Every new commit follows Conventional Commits and names its ticket in a trailer:
feat: add a New game button
Refs: C4-14
Co-Authored-By: …
Refs: C4-<n> sits in the last paragraph, with no blank line before Co-Authored-By:.commit-msg Git hook, and in the CI. The CI also checks
the title of a pull request, without the trailer, unless a bot opened it.pnpm install sets up the hooks with Husky. On a branch named after a ticket, such as C4-20,
the prepare-commit-msg hook adds Refs: C4-20 to a message that has no Refs: line yet.docs/evolution.md tells how the stack changed, with figures, and docs/decisions/ keeps one record per decision. docs/ai-workflow/ explains how agents work on the project, and with which tools.