Skip to content
Merged
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
46 changes: 34 additions & 12 deletions lucky7-app/frontend/src/app/components/GameScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@
double: boolean;
relance: boolean;
marchandSable: boolean;
legende: boolean;
}

// ---- Rules Dialog ----
Expand Down Expand Up @@ -165,6 +166,11 @@
active={rules.marchandSable}
desc="Score de 3 → immunité ce tour"
/>
<RuleItem
label="Légende"
active={rules.legende}
desc="Score de 11 (5+6) → tous les joueurs avec un dé à 5 ou 6 boivent"
/>
</div>
</div>
</div>
Expand Down Expand Up @@ -243,6 +249,9 @@
const [result, setResult] = useState<RoundResult | null>(null);
const [luckyProlongations, setLuckyProlongations] = useState(0);
const [looserProlongations, setLooserProlongations] = useState(0);
// Cumulative sip bonus from prolongations (sum of participants−1 per round)
const [luckyProlongBonus, setLuckyProlongBonus] = useState(0);

Check warning on line 253 in lucky7-app/frontend/src/app/components/GameScreen.tsx

View workflow job for this annotation

GitHub Actions / Lint & Format

'setLuckyProlongBonus' is assigned a value but never used

Check warning on line 253 in lucky7-app/frontend/src/app/components/GameScreen.tsx

View workflow job for this annotation

GitHub Actions / Lint & Format

'setLuckyProlongBonus' is assigned a value but never used
const [looserProlongBonus, setLooserProlongBonus] = useState(0);

Check warning on line 254 in lucky7-app/frontend/src/app/components/GameScreen.tsx

View workflow job for this annotation

GitHub Actions / Lint & Format

'setLooserProlongBonus' is assigned a value but never used

Check warning on line 254 in lucky7-app/frontend/src/app/components/GameScreen.tsx

View workflow job for this annotation

GitHub Actions / Lint & Format

'setLooserProlongBonus' is assigned a value but never used

// Prolongation state
const [prolongation, setProlongation] = useState<ProlongationState | null>(null);
Expand Down Expand Up @@ -462,7 +471,7 @@
? ` (+${luckyProlongations} prolongation${luckyProlongations > 1 ? 's' : ''})`
: '';
msgs.push(
`🏆 ${luckyPlayers[0].name} distribue ${drinks} gorgée${drinks > 1 ? 's' : ''}${ls.score === ann ? ' — score exact !' : ''}${extra}`
`🏆 ${luckyPlayers[0].name} distribue ${drinks} ${drinks > 1 ? 's' : ''}${ls.score === ann ? ' — score exact !' : ''}${extra}`
);
}

Expand All @@ -473,9 +482,7 @@
looserProlongations > 0
? ` (+${looserProlongations} prolongation${looserProlongations > 1 ? 's' : ''})`
: '';
msgs.push(
`💀 ${looserPlayers[0].name} boit ${drinks} gorgée${drinks > 1 ? 's' : ''}${extra}`
);
msgs.push(`💀 ${looserPlayers[0].name} boit ${drinks} ${drinks > 1 ? 's' : ''}${extra}`);
}

// Double — optionnel, suspendu pendant les prolongations (déjà géré : on affiche à la fin)
Expand All @@ -485,12 +492,10 @@
.forEach(s => {
const diceVal = s.dice1;
if (diceVal === 1) {
msgs.push(
`🎲 ${s.player.name} — Double 1 : distribue 1 gorgée ou fait relancer un joueur`
);
msgs.push(`🎲 ${s.player.name} — Double 1 : distribue 1 ou fait relancer un joueur`);
} else {
msgs.push(
`🎲 ${s.player.name} — Double ${diceVal} : distribue ${diceVal} gorgée${diceVal > 1 ? 's' : ''}`
`🎲 ${s.player.name} — Double ${diceVal} : distribue ${diceVal} ${diceVal > 1 ? 's' : ''}`
);
}
});
Expand All @@ -505,6 +510,22 @@
});
}

// Légende — optionnel
if (rules.legende) {
const legendeTriggered = playerScores.some(s => s.score === 11);
if (legendeTriggered) {
const affected = playerScores.filter(
s => s.dice1 === 5 || s.dice1 === 6 || s.dice2 === 5 || s.dice2 === 6
);
if (affected.length > 0) {
const names = affected.map(s => s.player.name).join(', ');
msgs.push(
`⭐ Légende ! ${names} boi${affected.length > 1 ? 'vent' : 't'} 1 (dé à 5 ou 6)`
);
}
}
}

return msgs;
}

Expand Down Expand Up @@ -720,12 +741,12 @@
)}
{roll && !showAnim && roll.score === 3 && rules.marchandSable && (
<Badge variant="secondary" className="text-xs shrink-0">
Marchand
Marchand de sable
</Badge>
)}
{relancedPlayerIds.has(player.id) && (
<Badge variant="outline" className="text-xs shrink-0">
Relancé
Relance
</Badge>
)}
</div>
Expand Down Expand Up @@ -810,7 +831,8 @@
const completedProlongations =
prolongation.type === 'lucky' ? luckyProlongations : looserProlongations;
const prolNumber = completedProlongations + 1;
const drinkStake = 1 + prolNumber;
const accumulatedBonus = prolongation.type === 'lucky' ? luckyProlongBonus : looserProlongBonus;
const drinkStake = 1 + accumulatedBonus + (prolongation.players.length - 1);
const isLuckyType = prolongation.type === 'lucky';

return (
Expand All @@ -837,7 +859,7 @@
<p className="text-xs text-muted-foreground">
Enjeu :{' '}
<span className="font-semibold text-foreground">
{drinkStake} gorgée{drinkStake > 1 ? 's' : ''}
{drinkStake} {drinkStake > 1 ? 's' : ''}
</span>
{' · '}Annonce : {announcement}
</p>
Expand Down
6 changes: 3 additions & 3 deletions lucky7-app/frontend/src/app/components/RulesPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -83,12 +83,12 @@ export default function RulesPage({ onBack }: RulesPageProps) {
<Rule
emoji="💀"
title="Looser"
desc="Le joueur le plus éloigné de l'annonce boit 1 gorgée."
desc="Le joueur le plus éloigné de l'annonce boit 1 gorgée. De manière générale, les gorgées à boire et à distribuer se compensent : un joueur peut choisir de les soustraire ou de les appliquer séparément. Exemple : 4 gorgées à donner + 1 à boire → donner 3 (4 − 1), ou donner 4 et boire 1."
/>
<Rule
emoji="⚖️"
title="Égalité"
desc="En cas d'égalité pour le Lucky ou le Looser, les joueurs concernés rejouent un tour supplémentaire."
title="Égalité & Prolongation"
desc="En cas d'égalité pour le Lucky ou le Looser, les joueurs concernés rejouent. La mise augmente de (nombre de participants − 1) gorgées à chaque prolongation. Exemple : 5 joueurs à égalité → mise passe à 1 + 4 = 5. Si 3 restent encore à égalité → 5 + 2 = 7."
/>
</div>
</Section>
Expand Down
92 changes: 57 additions & 35 deletions lucky7-app/frontend/src/app/components/SingleDeviceLobby.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,20 +33,24 @@ const TOGGLEABLE_RULES: ToggleableRule[] = [
},
{
key: 'relance',
label: 'Relance (Double 1)',
label: 'Relance',
desc: 'Score 1•1 → le joueur peut relancer ses dés une fois',
},
{
key: 'marchandSable',
label: 'Marchand de sable',
desc: 'Score de 3 (1+2) → immunité contre les gorgées ce tour',
desc: 'Score de 3 (1•2) → immunité contre les gorgées ce tour',
},
{
key: 'legende',
label: 'Légende',
desc: 'Score de 11 (5•6) → tous les joueurs avec un dé à 5 ou 6 boivent 1 gorgée',
},
];

const DISABLED_RULES: DisabledRule[] = [
{ label: 'Jeton', desc: 'Score de 7 → boit 1 gorgée' },
{ label: 'Jackpot', desc: 'Trois joueurs à 7 → relance pour distribuer un pot' },
{ label: 'Légende', desc: 'Score de 11 (5+6) → joueurs avec dé à 5 ou 6 boivent' },
{ label: 'Démon', desc: 'Trois joueurs à 6 → relance pour distribuer un pot' },
];

Expand All @@ -59,6 +63,7 @@ const DEFAULT_RULES: RulesConfig = {
double: true,
relance: false,
marchandSable: true,
legende: false,
};

// ---- Rules Config Dialog ----
Expand Down Expand Up @@ -95,55 +100,72 @@ function RulesConfigDialog({

<div className="flex flex-col gap-5 px-5 py-4">
{/* Règles activables */}
<div className="flex flex-col gap-3">
<div className="flex flex-col gap-2">
<p className="text-xs font-semibold text-muted-foreground uppercase tracking-wide">
Règles optionnelles
</p>
{TOGGLEABLE_RULES.map(rule => (
<label key={rule.key} className="flex items-start gap-3 cursor-pointer select-none">
<input
type="checkbox"
checked={rules[rule.key]}
onChange={() => onToggle(rule.key)}
className="mt-0.5 accent-primary shrink-0 w-4 h-4"
/>
<div>
<button
key={rule.key}
type="button"
onClick={() => onToggle(rule.key)}
className={`flex items-center gap-3 w-full text-left rounded-xl px-3 py-3 transition-colors select-none ${
rules[rule.key]
? 'bg-primary/10 ring-1 ring-primary/30'
: 'bg-muted/50 ring-1 ring-transparent'
}`}
>
<div className="flex-1 min-w-0">
<p className="text-sm font-medium leading-tight">{rule.label}</p>
<p className="text-xs text-muted-foreground mt-0.5">{rule.desc}</p>
</div>
</label>
{/* Toggle switch */}
<div
className={`relative shrink-0 w-11 h-6 rounded-full transition-colors duration-200 ${
rules[rule.key] ? 'bg-primary' : 'bg-foreground/20'
}`}
>
<div
className={`absolute top-0.5 w-5 h-5 bg-white rounded-full shadow transition-transform duration-200 ${
rules[rule.key] ? 'translate-x-5' : 'translate-x-0.5'
}`}
/>
</div>
</button>
))}
</div>

<Separator />

{/* Règles désactivées */}
<div className="flex flex-col gap-3">
<div className="flex flex-col gap-2">
<p className="text-xs font-semibold text-muted-foreground uppercase tracking-wide">
Bientôt disponible
</p>
<div className="flex flex-col gap-2">
{DISABLED_RULES.map(rule => (
<label
key={rule.label}
className="flex items-start gap-3 opacity-40 cursor-not-allowed select-none"
>
<input type="checkbox" disabled className="mt-0.5 shrink-0 w-4 h-4" />
<div>
<p className="text-sm font-medium leading-tight">{rule.label}</p>
<p className="text-xs text-muted-foreground mt-0.5">{rule.desc}</p>
</div>
</label>
))}
<label className="flex items-start gap-3 opacity-60 cursor-not-allowed select-none">
<input type="checkbox" disabled className="mt-0.5 shrink-0 w-4 h-4" />
<div>
<p className="text-sm font-medium leading-tight text-destructive">
{HARD_RULE.label}
</p>
<p className="text-xs text-destructive/70 mt-0.5">{HARD_RULE.desc}</p>
{DISABLED_RULES.map(rule => (
<div
key={rule.label}
className="flex items-center gap-3 rounded-xl px-3 py-3 bg-muted/30 opacity-40"
>
<div className="flex-1 min-w-0">
<p className="text-sm font-medium leading-tight">{rule.label}</p>
<p className="text-xs text-muted-foreground mt-0.5">{rule.desc}</p>
</div>
</label>
<div className="relative shrink-0 w-11 h-6 rounded-full bg-foreground/20">
<div className="absolute top-0.5 translate-x-0.5 w-5 h-5 bg-white rounded-full shadow" />
</div>
</div>
))}
<div className="flex items-center gap-3 rounded-xl px-3 py-3 bg-destructive/5 opacity-50">
<div className="flex-1 min-w-0">
<p className="text-sm font-medium leading-tight text-destructive">
{HARD_RULE.label}
</p>
<p className="text-xs text-destructive/70 mt-0.5">{HARD_RULE.desc}</p>
</div>
<div className="relative shrink-0 w-11 h-6 rounded-full bg-destructive/20">
<div className="absolute top-0.5 translate-x-0.5 w-5 h-5 bg-white rounded-full shadow" />
</div>
</div>
</div>
</div>
Expand Down
Loading