Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Copy to .env.local and fill in from the Firebase console:
# Project settings -> General -> Your apps -> SDK setup and configuration.
#
# Vite only exposes variables prefixed with VITE_ to the client. These three are
# public identifiers, not secrets — the database is protected by the rules in
# firestore.rules, not by hiding these values.
VITE_FIREBASE_API_KEY=
VITE_FIREBASE_AUTH_DOMAIN=your-project.firebaseapp.com
VITE_FIREBASE_PROJECT_ID=your-project
34 changes: 20 additions & 14 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,24 +1,30 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.

# dependencies
/node_modules
/.pnp
.pnp.js
node_modules

# testing
/coverage
# build output
# Tailwind v4 auto-detects source files and skips git-ignored paths, so leaving
# the output directory untracked here also keeps built JS out of the CSS scan.
dist
dist-ssr
*.local

# production
/build
# testing
coverage

# misc
.DS_Store
# environment
.env
.env.local
.env.development.local
.env.test.local
.env.production.local
.env.*.local

# editors
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.swp

# logs
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
192 changes: 110 additions & 82 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,94 +1,122 @@
## Data Structure

```javascript
123456: {
id: [UID],
name: 'Joe',
team: ['success' | 'danger' | 'primary'],
active: [true | false],
position: [0,1,2,3,4,5]
}
```

## TODO

1. ~~Show token next to name~~
2. ~~Show start button after 2 people sit~~
3. ~~Start game~~
4. ~~Set random 1st player~~
5. ~~Show cards for current player~~
6. ~~Show back of cards for other players~~
7. ~~Remove a token~~
8. ~~Place a wild~~
9. ~~Indicate which users's turn~~
10. ~~Highlight cards that can have tokens placed on them~~
11. ~~Remove played card from players hand~~
12. Show decks for drawing
13. Draw a card
14. Fade out dead cards, Discard a dead card, draw a new card
15. Prevent player from playing twice quickly
16. Pass button
17. Determine if winner
18. Highlight winners
19. Play again button (same players, mix teams, add players)

## Nice to Have

1. Countdown timer

## Available Scripts

In the project directory, you can run:

### `yarn start`

Runs the app in the development mode.<br />
Open [http://localhost:3000](http://localhost:3000) to view it in the browser.

The page will reload if you make edits.<br />
You will also see any lint errors in the console.

### `yarn test`

Launches the test runner in the interactive watch mode.<br />
See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
# Sequence

### `yarn build`
A browser version of the board game [Sequence](https://en.wikipedia.org/wiki/Sequence_(game)),
for 2–6 players across three teams. Game state lives in Firestore, so every
player sees moves as they happen.

Builds the app for production to the `build` folder.<br />
It correctly bundles React in production mode and optimizes the build for the best performance.
Built with React 19, Vite, TypeScript, Tailwind CSS v4 and shadcn/ui.

The build is minified and the filenames include the hashes.<br />
Your app is ready to be deployed!
## Getting started

See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
Requires Node 22.12 or newer.

## Learn More

You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).

To learn React, check out the [React documentation](https://reactjs.org/).

### Code Splitting

This section has moved here: https://facebook.github.io/create-react-app/docs/code-splitting

### Analyzing the Bundle Size

This section has moved here: https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size

### Making a Progressive Web App
```sh
npm install
cp .env.example .env.local # then fill in your Firebase values
npm run dev
```

This section has moved here: https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app
Create a Firebase project with **Firestore** and **Anonymous authentication**
enabled, then copy the config values into `.env.local`. Without them the app
renders setup instructions instead of the game.

### Advanced Configuration
Publish the database rules once per project:

This section has moved here: https://facebook.github.io/create-react-app/docs/advanced-configuration
```sh
firebase deploy --only firestore:rules
```

### Deployment
### Scripts

| Script | What it does |
| ------------------- | --------------------------------------------- |
| `npm run dev` | Dev server on http://localhost:5173 |
| `npm run build` | Typecheck, then build to `dist/` |
| `npm run preview` | Serve the production build locally |
| `npm test` | Run the test suite once |
| `npm run test:watch`| Run tests in watch mode |
| `npm run typecheck` | `tsc` with no emit |
| `npm run lint` | ESLint |

## How a game runs

1. Enter a name and hit **Start**. The game id goes into the URL — share that
link to invite people.
2. Everyone picks a seat. Seats alternate between the green, blue and red teams.
3. Once two or more players are seated, the host presses the power button to
deal. The starting player is chosen at random.
4. On your turn, click a board space matching a card in your hand to place your
team's chip. You then draw a replacement card.
- **Two-eyed jacks** (`J♣` `J♦`) are wild — place a chip on any open space.
- **One-eyed jacks** (`J♥` `J♠`) remove one of an opponent's chips.
5. The four corners are free spaces belonging to everyone.

## Layout

This section has moved here: https://facebook.github.io/create-react-app/docs/deployment
```
src/
game/ Pure rules — board layout, deck, card parsing, move legality.
No React and no Firebase, so it is all unit tested.
services/ Firebase setup and every Firestore read/write.
hooks/ Auth, game subscription, and the game id <-> URL binding.
components/ UI. `ui/` holds shadcn primitives; `Card/cards.css` is a
vendored copy of CSS-Playing-Cards (CC BY-SA, see the header).
```

### `yarn build` fails to minify
The rules live in `src/game/` on purpose: it is the part worth testing, and
keeping it free of React and Firestore means the tests need neither.

## Data model

A game is one Firestore document in the `games` collection:

```jsonc
{
"createdBy": "<uid of the host>",
"createdAt": "<server timestamp>",
"isActive": false, // true once the cards are dealt
"cards": ["A♠", "2♥"], // the undealt draw pile
"players": {
"<uid>": {
"id": "<uid>",
"name": "Joe",
"position": 0, // 0-5, absent until they take a seat
"team": "green", // "green" | "blue" | "red"
"isActive": true, // whose turn it is
"cards": ["5♥", "J♦"] // their hand
}
},
"board": { // row -> column -> team holding that space
"3": { "7": "red" }
}
}
```

This section has moved here: https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify
Cards are two characters: rank then suit, with `T` for ten (`T♥`, `A♠`, `J♣`).
The four board corners are stored as `JB` and `JL` — the joker artwork from the
card stylesheet — and are never claimable.

## Known limitations

- **Hands are visible to anyone in the game.** The whole document, including
every player's cards, is streamed to every client; the UI just declines to
render other people's hands face up. Anyone willing to open devtools can read
them. Fixing this properly means moving hands into per-player subcollections
that only their owner can read.
- **Gameplay is not enforced server-side.** Moves are validated in
`src/game/moves.ts` and re-checked inside Firestore transactions, which stops
races and honest mistakes, but a player already in a game can still write an
arbitrary board by calling Firestore directly. Real enforcement needs Cloud
Functions. `firestore.rules` covers the rest: ownership, immutable
`createdBy`/`createdAt`, and no access at all when signed out.

## Not built yet

1. Draw pile shown on the table
2. Discarding a dead card and drawing a replacement
3. Pass button
4. Win detection, winner highlighting, and a play-again button
5. Turn countdown timer

Items 1–4 are the remaining gameplay gaps: a game currently has no end
condition, so players have to spot a completed sequence themselves.
21 changes: 21 additions & 0 deletions components.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york",
"rsc": false,
"tsx": true,
"tailwind": {
"config": "",
"css": "src/index.css",
"baseColor": "slate",
"cssVariables": true,
"prefix": ""
},
"iconLibrary": "lucide",
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
}
}
29 changes: 29 additions & 0 deletions eslint.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import js from "@eslint/js";
import reactHooks from "eslint-plugin-react-hooks";
import reactRefresh from "eslint-plugin-react-refresh";
import globals from "globals";
import tseslint from "typescript-eslint";

export default tseslint.config(
{ ignores: ["dist", "coverage", "src/components/ui/**"] },
{
extends: [js.configs.recommended, ...tseslint.configs.recommended],
files: ["**/*.{ts,tsx}"],
languageOptions: {
ecmaVersion: 2023,
globals: globals.browser,
},
plugins: {
"react-hooks": reactHooks,
"react-refresh": reactRefresh,
},
rules: {
...reactHooks.configs.recommended.rules,
"react-refresh/only-export-components": ["warn", { allowConstantExport: true }],
"@typescript-eslint/no-unused-vars": [
"error",
{ argsIgnorePattern: "^_", varsIgnorePattern: "^_" },
],
},
},
);
15 changes: 15 additions & 0 deletions firebase.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"firestore": {
"rules": "firestore.rules"
},
"hosting": {
"public": "dist",
"ignore": ["firebase.json", "**/.*", "**/node_modules/**"],
"rewrites": [
{
"source": "**",
"destination": "/index.html"
}
]
}
}
55 changes: 55 additions & 0 deletions firestore.rules
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
rules_version = '2';

// Deploy with: firebase deploy --only firestore:rules
//
// These rules close the worst of the open-database holes: an unauthenticated
// caller can do nothing, nobody can delete or hijack a game they did not create,
// and `createdBy`/`createdAt` are immutable once written.
//
// They deliberately do NOT try to enforce the rules of Sequence. Turn order,
// legal placements and hand contents are validated in the client
// (src/game/moves.ts) and re-checked inside the Firestore transactions in
// src/services/firestore.ts, but a determined player who is already in a game
// can still write an arbitrary board by calling Firestore directly. Enforcing
// gameplay server-side needs Cloud Functions or a real backend — see the
// "Known limitations" section of the README.
service cloud.firestore {
match /databases/{database}/documents {

function isSignedIn() {
return request.auth != null;
}

function isUnchanged(field) {
return request.resource.data[field] == resource.data[field];
}

match /games/{gameId} {
// Anyone signed in can read a game, because following an invite link has
// to work before you are one of its players.
allow read: if isSignedIn();

// A new game starts inactive, is owned by its creator, and contains
// exactly one player: the creator.
allow create: if isSignedIn()
&& request.resource.data.createdBy == request.auth.uid
&& request.resource.data.isActive == false
&& request.resource.data.players.keys().hasOnly([request.auth.uid]);

// Joining, seating and playing are all updates. Ownership and creation
// time can never change.
allow update: if isSignedIn()
&& isUnchanged('createdBy')
&& isUnchanged('createdAt');

// Only the host can bin the game.
allow delete: if isSignedIn()
&& resource.data.createdBy == request.auth.uid;
}

// Nothing else in the database is reachable from the client.
match /{document=**} {
allow read, write: if false;
}
}
}
Loading