Skip to content

Add new Briscola card game example #1131

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 17 commits into
base: main
Choose a base branch
from
Draft
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
3 changes: 2 additions & 1 deletion biome.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@
"**/ios/**",
"**/android/**",
"packages/jazz-svelte/**",
"examples/*svelte*/**"
"examples/*svelte*/**",
"examples/briscola/src/routeTree.gen.ts"
]
},
"formatter": {
Expand Down
2 changes: 2 additions & 0 deletions examples/briscola/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
VITE_JAZZ_WORKER_ACCOUNT=
JAZZ_WORKER_SECRET=
21 changes: 21 additions & 0 deletions examples/briscola/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Local
.DS_Store
*.local
*.log*

# Dist
node_modules
dist/
.vinxi
.output
.vercel
.netlify
.wrangler

# IDE
.vscode/*
!.vscode/extensions.json
.idea


.env
38 changes: 38 additions & 0 deletions examples/briscola/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# Jazz Briscola

This is a simplified implementation of the Italian card game [Briscola](https://en.wikipedia.org/wiki/Briscola), written using Jazz.

While most Jazz apps don't need workers, in this game players must not be able to see each other's cards. This is a good example of when a worker is useful. In this case, the worker acts as a dealer, revealing the cards to each player as needed.

In general this showcases how workers can be used to moderate access to coValues.

The communication between the dealer and the players is done using the [Inbox API](#), which is an abstraction over the Jazz API that allows for easy communication between workers and clients.

## Setup

First of we need to create a new account for the dealer:

```bash
pnpx jazz-run account create --name "Dealer"
```

This will print an account ID and a secret key:

```
# Credentials for Jazz account "Dealer":
JAZZ_WORKER_ACCOUNT=co_xxxx
JAZZ_WORKER_SECRET=sealerSecret_xxx
```
use these to create a `.env` file based on the `.env.example` file and fill in the `VITE_JAZZ_WORKER_ACCOUNT` and `JAZZ_WORKER_SECRET` fields.

We can then start the dealer worker:

```bash
pnpm dev:worker
```

and the client:

```bash
pnpm dev
```
21 changes: 21 additions & 0 deletions examples/briscola/components.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "default",
"rsc": false,
"tsx": true,
"tailwind": {
"config": "tailwind.config.js",
"css": "src/index.css",
"baseColor": "neutral",
"cssVariables": true,
"prefix": ""
},
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
},
"iconLibrary": "lucide"
}
12 changes: 12 additions & 0 deletions examples/briscola/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<!doctype html>
<html lang="en" class="bg-green-800">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Jazz Briscola</title>
</head>
<body>
<div id="app" class="min-h-screen"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
47 changes: 47 additions & 0 deletions examples/briscola/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
{
"name": "jazz-example-briscola",
"version": "0.0.0",
"private": true,
"type": "module",
"scripts": {
"typecheck": "tsc --noEmit",
"dev": "vite --port=3001",
"dev:worker": "tsx --env-file=.env ./src/worker.ts",
"build": "vite build",
"serve": "vite preview",
"start": "vite"
},
"devDependencies": {
"@tanstack/router-plugin": "^1.87.13",
"@types/node": "^22.10.2",
"@types/react": "^18.3.12",
"@types/react-dom": "^18.3.1",
"@vitejs/plugin-react": "^4.3.4",
"autoprefixer": "^10.4.20",
"postcss": "^8.4.27",
"tailwindcss": "^3.4.15",
"typescript": "~5.6.2",
"vite": "^5.4.10"
},
"dependencies": {
"@radix-ui/react-dialog": "^1.1.4",
"@radix-ui/react-label": "^2.1.1",
"@radix-ui/react-radio-group": "^1.2.2",
"@radix-ui/react-separator": "^1.1.1",
"@radix-ui/react-slot": "^1.1.1",
"@tanstack/react-router": "^1.87.12",
"@tanstack/router-devtools": "^1.87.12",
"class-variance-authority": "^0.7.1",
"clsx": "^2.0.0",
"jazz-nodejs": "workspace:*",
"jazz-react": "workspace:*",
"jazz-tools": "workspace:*",
"lucide-react": "^0.468.0",
"motion": "^11.14.3",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"tailwind-merge": "^1.14.0",
"tailwindcss-animate": "^1.0.7",
"tsx": "^4.19.2"
}
}
6 changes: 6 additions & 0 deletions examples/briscola/postcss.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};
119 changes: 119 additions & 0 deletions examples/briscola/src/components/how-to-play-content.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
export function HowToPlayContent() {
return (
<div className="">
<h3 className="font-semibold text-lg">Objective:</h3>
<p className="mt-2">
The goal is to score the most points by winning tricks containing
high-value cards. The game is played until all card are played.
</p>

<h3 className="font-semibold text-lg mt-6">The deck</h3>
<p className="mt-2">
A deck with 40 cards is used, split into four suits:
<ul className="list-disc list-inside">
<li className="list-item">Coins (Denari)</li>
<li className="list-item">Cups (Coppe)</li>
<li className="list-item">Swords (Spade)</li>
<li className="list-item">Clubs (Bastoni)</li>
</ul>
</p>
<p className="mt-2">Each suit has cards numbered from 1 to 10.</p>

<h3 className="font-semibold text-lg mt-6">Card values</h3>
<p className="mt-2">
Each card has a point value:
<ul className="list-disc list-inside">
<li className="list-item">Ace (1): 11 points</li>
<li className="list-item">Three (3): 10 points</li>
<li className="list-item">Eight (8): 2 points</li>
<li className="list-item">Nine (9): 3 points</li>
<li className="list-item">Ten (10): 4 points</li>
<li className="list-item">All others (2, 4-7): 0 points</li>
</ul>
</p>
<p className="mt-2">
There are <span className="font-semibold">120 total points</span> in the
deck.
</p>

<h3 className="font-semibold text-lg mt-6">Gameplay</h3>
<p className="mt-2">
<ol className="list-inside list-decimal">
<li>
<span className="font-semibold">Starting the game:</span>
<ul className="list-inside list-disc">
<li className="list-item ml-4">
3 cards are dealt to each player.
</li>
<li className="list-item ml-4">
1 card is placed face-up on the table, on the bottom of the draw
pile, indicating the trump suit. (Briscola)
</li>
<li className="list-item ml-4">
One player is randomly chosen to start the game.
</li>
</ul>
</li>

<li>
<span className="font-semibold">Starting a trick:</span>
<ul className="list-inside list-disc">
<li className="list-item ml-4">
Players play one card each in turn, trying to win the trick.
</li>
</ul>
</li>

<li>
<span className="font-semibold">Winning a trick:</span>
<ul className="list-inside list-disc">
<li className="list-item ml-4">
The highest card of the trump suit wins the trick.
</li>
<li className="list-item ml-4">
If no trump cards are played, the highest card of the leading
suit wins.
</li>
<li className="list-item ml-4">
The leading suit is the suit of the first card played in the
current trick.
</li>
<li className="list-item ml-4">
The winner collects the cards, which are placed face-up in their
scoring pile.
</li>
</ul>
</li>

<li>
<span className="font-semibold">Drawing cards:</span>
<ul className="list-inside list-disc">
<li className="list-item ml-4">
After each trick, a new card is dealt to each player (starting
with the trick winner).
</li>
</ul>
</li>

<li>
<span className="font-semibold">Continuing play:</span>
<ul className="list-inside list-disc">
<li className="list-item ml-4">
The winner of the previous trick leads the next round.
</li>
</ul>
</li>

<li>
<span className="font-semibold">End of the game:</span>
<ul className="list-inside list-disc">
<li className="list-item ml-4">
Play continues until all cards are played.
</li>
</ul>
</li>
</ol>
</p>
</div>
);
}
75 changes: 75 additions & 0 deletions examples/briscola/src/components/playing-card.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { cn } from "@/lib/utils";
import type { Card, Suit } from "@/schema";
import type { co } from "jazz-tools";
import { motion } from "motion/react";

import bastoni from "../img/bastoni.svg?url";
import coppe from "../img/coppe.svg?url";
import denari from "../img/denari.svg?url";
import spade from "../img/spade.svg?url";

interface Props {
card: co<Card>;
faceDown?: boolean;
className?: string;
layoutId?: string;
}
export function PlayingCard({
card,
className,
faceDown = false,
layoutId,
}: Props) {
const cardImage = getCardImage(card.data?.suit!);
if (!faceDown && card.data?.value === undefined && card.data?.suit) {
return null;
}

return (
<motion.div
className={cn(
"block aspect-card w-[150px] bg-white touch-none rounded-lg shadow-lg p-2 border",
className,
)}
style={{
...(faceDown && {
backgroundImage: `url(https://placecats.com/150/243)`,
backgroundSize: "cover",
}),
}}
layoutId={layoutId}
>
<div className="border-zinc-400 border rounded-lg h-full px-1 flex flex-col ">
{!faceDown && (
<>
<div className="text-4xl font-bold text-black self-start">
{card.data?.value}
</div>
<div className="grow flex justify-center items-center">
<img
src={cardImage}
className="pointer-events-none max-h-[140px]"
/>
</div>
<div className="text-4xl font-bold text-black rotate-180 transform self-end">
{card.data?.value}
</div>
</>
)}
</div>
</motion.div>
);
}

function getCardImage(suit: typeof Suit) {
switch (suit) {
case "C":
return coppe;
case "D":
return denari;
case "S":
return spade;
case "B":
return bastoni;
}
}
Loading
Loading