Compare commits
No commits in common. "24662060dffb19a35566fd663c57ee952c18e120" and "5df186d52d5a13b17f4776b82eb3a39d0ef9f67d" have entirely different histories.
24662060df
...
5df186d52d
11 changed files with 38 additions and 308 deletions
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "polaris-task-force",
|
"name": "polaris-task-force",
|
||||||
"version": "0.2.36",
|
"version": "0.2.35",
|
||||||
"description": "Web app for the Arma 3 unit Polaris Task Force. Uses Next.js for client- and server-side rendering, with a Postgres database and some other neat libraries.",
|
"description": "Web app for the Arma 3 unit Polaris Task Force. Uses Next.js for client- and server-side rendering, with a Postgres database and some other neat libraries.",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|
|
||||||
|
|
@ -1819,133 +1819,6 @@
|
||||||
overflow-wrap: anywhere;
|
overflow-wrap: anywhere;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Progressive hand-rank ladder: each tier a step brighter and glowier, from
|
|
||||||
brass hints through silver (straight, flush) and gold (full house, quads)
|
|
||||||
up to the royal flush prismatic treatment. */
|
|
||||||
.poker-rank-tier-1 {
|
|
||||||
border-color: rgb(138 109 59 / 80%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.poker-rank-tier-2 {
|
|
||||||
border-color: rgb(163 130 63 / 85%);
|
|
||||||
box-shadow: inset 0 0 0.4rem rgb(213 179 106 / 16%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.poker-rank-tier-3 {
|
|
||||||
border-color: rgb(185 138 62 / 90%);
|
|
||||||
box-shadow:
|
|
||||||
inset 0 0 0.5rem rgb(213 179 106 / 24%),
|
|
||||||
0 0 0.35rem rgb(213 179 106 / 18%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.poker-rank-tier-4 {
|
|
||||||
border-color: rgb(201 201 212 / 85%);
|
|
||||||
box-shadow:
|
|
||||||
inset 0 0 0.55rem rgb(226 232 240 / 22%),
|
|
||||||
0 0 0.45rem rgb(226 232 240 / 20%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.poker-rank-tier-5 {
|
|
||||||
border-color: rgb(214 214 228 / 95%);
|
|
||||||
box-shadow:
|
|
||||||
inset 0 0 0.7rem rgb(226 232 240 / 32%),
|
|
||||||
0 0 0.7rem rgb(226 232 240 / 30%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.poker-rank-tier-6 {
|
|
||||||
border-color: #cfa95c;
|
|
||||||
box-shadow:
|
|
||||||
inset 0 0 0.7rem rgb(214 179 106 / 30%),
|
|
||||||
0 0 0.6rem rgb(214 179 106 / 28%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.poker-rank-tier-7 {
|
|
||||||
border-color: #e0bd6f;
|
|
||||||
box-shadow:
|
|
||||||
inset 0 0 0.8rem rgb(224 189 111 / 44%),
|
|
||||||
0 0 0.85rem rgb(224 189 111 / 42%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.poker-rank-tier-8 {
|
|
||||||
border-color: #79a7f5;
|
|
||||||
box-shadow:
|
|
||||||
inset 0 0 0.9rem rgb(121 167 245 / 46%),
|
|
||||||
0 0 0.95rem rgb(121 167 245 / 48%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.poker-rank-tier-9 {
|
|
||||||
position: relative;
|
|
||||||
border-color: #ffd700;
|
|
||||||
background-image: linear-gradient(
|
|
||||||
115deg,
|
|
||||||
rgb(255 77 77 / 7%),
|
|
||||||
rgb(255 184 77 / 7%) 18%,
|
|
||||||
rgb(255 249 77 / 7%) 36%,
|
|
||||||
rgb(77 255 136 / 7%) 54%,
|
|
||||||
rgb(77 195 255 / 7%) 72%,
|
|
||||||
rgb(184 77 255 / 7%) 88%,
|
|
||||||
rgb(255 77 77 / 7%)
|
|
||||||
);
|
|
||||||
background-size: 300% 100%;
|
|
||||||
box-shadow:
|
|
||||||
inset 0 0 1rem rgb(255 215 0 / 46%),
|
|
||||||
0 0 1.1rem rgb(255 215 0 / 55%);
|
|
||||||
animation: poker-royal-wash 6s ease-in-out infinite alternate;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* The rainbow lives only on the border ring (masked pseudo-element) and the
|
|
||||||
near-transparent background wash, never on the element's content: the hue
|
|
||||||
filter animates the pseudo itself, so cards and text keep their true colors. */
|
|
||||||
.poker-rank-tier-9::before {
|
|
||||||
content: "";
|
|
||||||
position: absolute;
|
|
||||||
inset: 0;
|
|
||||||
z-index: 1;
|
|
||||||
border-radius: inherit;
|
|
||||||
padding: 1px;
|
|
||||||
background: linear-gradient(
|
|
||||||
115deg,
|
|
||||||
#ff4d4d,
|
|
||||||
#ffb84d,
|
|
||||||
#fff94d,
|
|
||||||
#4dff88,
|
|
||||||
#4dc3ff,
|
|
||||||
#b84dff,
|
|
||||||
#ff4d4d
|
|
||||||
);
|
|
||||||
background-size: 300% 100%;
|
|
||||||
-webkit-mask:
|
|
||||||
linear-gradient(#fff 0 0) content-box,
|
|
||||||
linear-gradient(#fff 0 0);
|
|
||||||
-webkit-mask-composite: xor;
|
|
||||||
mask:
|
|
||||||
linear-gradient(#fff 0 0) content-box,
|
|
||||||
linear-gradient(#fff 0 0);
|
|
||||||
mask-composite: exclude;
|
|
||||||
animation: poker-royal-border-hue 3.2s linear infinite;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes poker-royal-wash {
|
|
||||||
0% {
|
|
||||||
background-position: 0% 0;
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
background-position: 100% 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes poker-royal-border-hue {
|
|
||||||
0% {
|
|
||||||
filter: hue-rotate(0deg);
|
|
||||||
background-position: 0% 0;
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
filter: hue-rotate(360deg);
|
|
||||||
background-position: 300% 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.poker-console-economy {
|
.poker-console-economy {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
@ -2542,11 +2415,6 @@
|
||||||
animation: none;
|
animation: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.poker-rank-tier-9,
|
|
||||||
.poker-rank-tier-9::before {
|
|
||||||
animation: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.poker-card-inner,
|
.poker-card-inner,
|
||||||
.poker-card-deal {
|
.poker-card-deal {
|
||||||
animation: none;
|
animation: none;
|
||||||
|
|
|
||||||
|
|
@ -105,13 +105,9 @@ export function CardView({
|
||||||
<span className="font-semibold">{RANK_LABELS[card.rank]}</span>
|
<span className="font-semibold">{RANK_LABELS[card.rank]}</span>
|
||||||
<span>{SUIT_SYMBOLS[card.suit]}</span>
|
<span>{SUIT_SYMBOLS[card.suit]}</span>
|
||||||
</span>
|
</span>
|
||||||
{size !== "mini" && (
|
<span className={cn("poker-card-center", RED_SUITS.has(card.suit) && redSuitClass)}>
|
||||||
<span
|
{SUIT_SYMBOLS[card.suit]}
|
||||||
className={cn("poker-card-center", RED_SUITS.has(card.suit) && redSuitClass)}
|
</span>
|
||||||
>
|
|
||||||
{SUIT_SYMBOLS[card.suit]}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,6 @@ type SeatedSnapshot = PokerSnapshot & {
|
||||||
interface PokerActionConsoleProps {
|
interface PokerActionConsoleProps {
|
||||||
readonly snapshot: SeatedSnapshot;
|
readonly snapshot: SeatedSnapshot;
|
||||||
readonly handLabel: string | null;
|
readonly handLabel: string | null;
|
||||||
readonly handTier: number | null;
|
|
||||||
readonly hostSeatId: number | null;
|
readonly hostSeatId: number | null;
|
||||||
readonly starting: boolean;
|
readonly starting: boolean;
|
||||||
readonly leaving: boolean;
|
readonly leaving: boolean;
|
||||||
|
|
@ -81,7 +80,6 @@ function ViewerRoleBadges({ view }: { readonly view: SeatedSnapshot["you"] }) {
|
||||||
export function PokerActionConsole({
|
export function PokerActionConsole({
|
||||||
snapshot,
|
snapshot,
|
||||||
handLabel,
|
handLabel,
|
||||||
handTier,
|
|
||||||
hostSeatId,
|
hostSeatId,
|
||||||
starting,
|
starting,
|
||||||
leaving,
|
leaving,
|
||||||
|
|
@ -127,15 +125,7 @@ export function PokerActionConsole({
|
||||||
</div>
|
</div>
|
||||||
<ViewerRoleBadges view={view} />
|
<ViewerRoleBadges view={view} />
|
||||||
</div>
|
</div>
|
||||||
{handLabel && (
|
{handLabel && <span className="poker-hand-placard">You have: {handLabel}</span>}
|
||||||
<span
|
|
||||||
className={`poker-hand-placard${
|
|
||||||
handTier && handTier > 0 ? ` poker-rank-tier-${handTier}` : ""
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
You have: {handLabel}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,6 @@ interface PokerFeltCanvasProps {
|
||||||
readonly snapshot: SeatedSnapshot;
|
readonly snapshot: SeatedSnapshot;
|
||||||
readonly onSnapshot: (snapshot: PokerSnapshot) => void;
|
readonly onSnapshot: (snapshot: PokerSnapshot) => void;
|
||||||
readonly handLabel: string | null;
|
readonly handLabel: string | null;
|
||||||
readonly handTier: number | null;
|
|
||||||
readonly bestCardKeys: ReadonlySet<string>;
|
readonly bestCardKeys: ReadonlySet<string>;
|
||||||
readonly actionProps: FeltCanvasActionProps;
|
readonly actionProps: FeltCanvasActionProps;
|
||||||
readonly onActionFeedback: () => void;
|
readonly onActionFeedback: () => void;
|
||||||
|
|
@ -83,7 +82,6 @@ export function PokerFeltCanvas({
|
||||||
snapshot,
|
snapshot,
|
||||||
onSnapshot,
|
onSnapshot,
|
||||||
handLabel,
|
handLabel,
|
||||||
handTier,
|
|
||||||
bestCardKeys,
|
bestCardKeys,
|
||||||
actionProps,
|
actionProps,
|
||||||
onActionFeedback,
|
onActionFeedback,
|
||||||
|
|
@ -266,7 +264,6 @@ export function PokerFeltCanvas({
|
||||||
<PokerActionConsole
|
<PokerActionConsole
|
||||||
snapshot={snapshot}
|
snapshot={snapshot}
|
||||||
handLabel={handLabel}
|
handLabel={handLabel}
|
||||||
handTier={handTier}
|
|
||||||
hostSeatId={snapshot.hostSeatId}
|
hostSeatId={snapshot.hostSeatId}
|
||||||
starting={actionProps.starting}
|
starting={actionProps.starting}
|
||||||
leaving={actionProps.leaving}
|
leaving={actionProps.leaving}
|
||||||
|
|
|
||||||
|
|
@ -54,7 +54,6 @@ export function PokerGameTable({
|
||||||
[view.communityCards, view.phase, view.yourHoleCards],
|
[view.communityCards, view.phase, view.yourHoleCards],
|
||||||
);
|
);
|
||||||
const bestCardKeys = useMemo(() => new Set(handHint?.bestCardKeys ?? []), [handHint]);
|
const bestCardKeys = useMemo(() => new Set(handHint?.bestCardKeys ?? []), [handHint]);
|
||||||
const handTier = handHint?.tier ?? null;
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setMounted(true);
|
setMounted(true);
|
||||||
|
|
@ -88,7 +87,6 @@ export function PokerGameTable({
|
||||||
snapshot={snapshot}
|
snapshot={snapshot}
|
||||||
onSnapshot={onSnapshot}
|
onSnapshot={onSnapshot}
|
||||||
handLabel={handHint?.label ?? null}
|
handLabel={handHint?.label ?? null}
|
||||||
handTier={handTier}
|
|
||||||
bestCardKeys={bestCardKeys}
|
bestCardKeys={bestCardKeys}
|
||||||
actionProps={{
|
actionProps={{
|
||||||
starting,
|
starting,
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,7 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { BookOpen, ChevronDown } from "lucide-react";
|
import { BookOpen } from "lucide-react";
|
||||||
import type { Card, Rank, Suit } from "@/lib/minigames/poker";
|
import type { Card, Rank, Suit } from "@/lib/minigames/poker";
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
|
|
@ -18,8 +17,7 @@ interface PokerRanking {
|
||||||
readonly name: string;
|
readonly name: string;
|
||||||
readonly example: readonly Card[];
|
readonly example: readonly Card[];
|
||||||
readonly description: string;
|
readonly description: string;
|
||||||
/** 0 (no styling) through 9 (royal prismatic); see handRankStyle.ts. */
|
readonly color?: string;
|
||||||
readonly tier: number;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function card(rank: Rank, suit: Suit): Card {
|
function card(rank: Rank, suit: Suit): Card {
|
||||||
|
|
@ -31,61 +29,57 @@ const RANKINGS: readonly PokerRanking[] = [
|
||||||
name: "High card",
|
name: "High card",
|
||||||
example: [card(14, "s"), card(13, "h"), card(9, "d"), card(5, "c"), card(2, "s")],
|
example: [card(14, "s"), card(13, "h"), card(9, "d"), card(5, "c"), card(2, "s")],
|
||||||
description: "No matching ranks, the highest card wins.",
|
description: "No matching ranks, the highest card wins.",
|
||||||
tier: 0,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "One pair",
|
name: "One pair",
|
||||||
example: [card(9, "s"), card(9, "h"), card(13, "d"), card(7, "c"), card(3, "s")],
|
example: [card(9, "s"), card(9, "h"), card(13, "d"), card(7, "c"), card(3, "s")],
|
||||||
description: "Two cards share the same rank.",
|
description: "Two cards share the same rank.",
|
||||||
tier: 1,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Two pair",
|
name: "Two pair",
|
||||||
example: [card(11, "s"), card(11, "h"), card(4, "d"), card(4, "c"), card(14, "s")],
|
example: [card(11, "s"), card(11, "h"), card(4, "d"), card(4, "c"), card(14, "s")],
|
||||||
description: "Two different pairs plus a fifth card.",
|
description: "Two different pairs plus a fifth card.",
|
||||||
tier: 2,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Three of a kind",
|
name: "Three of a kind",
|
||||||
example: [card(7, "s"), card(7, "h"), card(7, "d"), card(13, "c"), card(2, "s")],
|
example: [card(7, "s"), card(7, "h"), card(7, "d"), card(13, "c"), card(2, "s")],
|
||||||
description: "Three cards share the same rank.",
|
description: "Three cards share the same rank.",
|
||||||
tier: 3,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Straight",
|
name: "Straight",
|
||||||
example: [card(5, "s"), card(6, "h"), card(7, "d"), card(8, "c"), card(9, "s")],
|
example: [card(5, "s"), card(6, "h"), card(7, "d"), card(8, "c"), card(9, "s")],
|
||||||
description: "Five cards run in rank order.",
|
description: "Five cards run in rank order.",
|
||||||
tier: 4,
|
color: "#ffff00",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Flush",
|
name: "Flush",
|
||||||
example: [card(14, "h"), card(10, "h"), card(7, "h"), card(4, "h"), card(2, "h")],
|
example: [card(14, "h"), card(10, "h"), card(7, "h"), card(4, "h"), card(2, "h")],
|
||||||
description: "Five cards share the same suit.",
|
description: "Five cards share the same suit.",
|
||||||
tier: 5,
|
color: "#ffff00",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Full house",
|
name: "Full house",
|
||||||
example: [card(13, "s"), card(13, "h"), card(13, "d"), card(8, "c"), card(8, "s")],
|
example: [card(13, "s"), card(13, "h"), card(13, "d"), card(8, "c"), card(8, "s")],
|
||||||
description: "Three of one rank and two of another.",
|
description: "Three of one rank and two of another.",
|
||||||
tier: 6,
|
color: "#ffff00",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Four of a kind",
|
name: "Four of a kind",
|
||||||
example: [card(3, "s"), card(3, "h"), card(3, "d"), card(3, "c"), card(14, "s")],
|
example: [card(3, "s"), card(3, "h"), card(3, "d"), card(3, "c"), card(14, "s")],
|
||||||
description: "Four cards share the same rank.",
|
description: "Four cards share the same rank.",
|
||||||
tier: 7,
|
color: "#ffff00",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Straight flush",
|
name: "Straight flush",
|
||||||
example: [card(5, "s"), card(6, "s"), card(7, "s"), card(8, "s"), card(9, "s")],
|
example: [card(5, "s"), card(6, "s"), card(7, "s"), card(8, "s"), card(9, "s")],
|
||||||
description: "Five cards run in order and share a suit.",
|
description: "Five cards run in order and share a suit.",
|
||||||
tier: 8,
|
color: "#ffff00",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Royal flush",
|
name: "Royal flush",
|
||||||
example: [card(10, "h"), card(11, "h"), card(12, "h"), card(13, "h"), card(14, "h")],
|
example: [card(10, "h"), card(11, "h"), card(12, "h"), card(13, "h"), card(14, "h")],
|
||||||
description: "The ace-high straight flush, the strongest hand.",
|
description: "The ace-high straight flush, the strongest hand.",
|
||||||
tier: 9,
|
color: "#ffff00",
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
@ -108,54 +102,32 @@ export function PokerGuide() {
|
||||||
|
|
||||||
<section className="flex flex-col gap-3">
|
<section className="flex flex-col gap-3">
|
||||||
<h2 className="text-sm font-semibold uppercase tracking-wider">Hand rankings</h2>
|
<h2 className="text-sm font-semibold uppercase tracking-wider">Hand rankings</h2>
|
||||||
<ol className="grid gap-2 sm:grid-flow-col sm:grid-cols-2 sm:grid-rows-[auto_1rem_auto_1rem_auto_1rem_auto_1rem_auto]">
|
<ol className="grid gap-3 sm:grid-cols-2">
|
||||||
{RANKINGS.flatMap((ranking, index) => {
|
{RANKINGS.map((ranking, index) => (
|
||||||
const isLast = index === RANKINGS.length - 1;
|
<li key={ranking.name} className="flex items-center gap-3 border border-border p-3" style={ranking.color ? ({
|
||||||
const columnBreakAfter = index === 4;
|
borderColor: ranking.color
|
||||||
const nodes = [
|
}) : {}}>
|
||||||
<li
|
<span className="w-5 shrink-0 font-mono text-xs text-muted-foreground">
|
||||||
key={ranking.name}
|
{index + 1}
|
||||||
className={`relative flex items-center gap-3 border border-border p-3${
|
</span>
|
||||||
ranking.tier > 0 ? ` poker-rank-tier-${ranking.tier}` : ""
|
<div className="flex min-w-0 flex-1 flex-col gap-1">
|
||||||
}`}
|
<div className="flex flex-wrap items-center justify-between gap-2">
|
||||||
>
|
|
||||||
<span className="w-5 shrink-0 font-mono text-xs text-muted-foreground">
|
|
||||||
{index + 1}
|
|
||||||
</span>
|
|
||||||
<div className="flex min-w-0 flex-1 flex-col justify-center gap-0">
|
|
||||||
<span className="font-medium">{ranking.name}</span>
|
<span className="font-medium">{ranking.name}</span>
|
||||||
<p className="text-xs leading-tight text-muted-foreground">
|
<div className="flex gap-0.5" aria-label={`${ranking.name} example`}>
|
||||||
{ranking.description}
|
{ranking.example.map((exampleCard, cardIndex) => (
|
||||||
</p>
|
<CardView
|
||||||
|
key={`${exampleCard.rank}-${exampleCard.suit}`}
|
||||||
|
card={exampleCard}
|
||||||
|
size="mini"
|
||||||
|
dealIndex={cardIndex}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex shrink-0 gap-0.5" aria-label={`${ranking.name} example`}>
|
<p className="text-xs text-muted-foreground max-w-1/2">{ranking.description}</p>
|
||||||
{ranking.example.map((exampleCard, cardIndex) => (
|
</div>
|
||||||
<CardView
|
</li>
|
||||||
key={`${exampleCard.rank}-${exampleCard.suit}`}
|
))}
|
||||||
card={exampleCard}
|
|
||||||
size="mini"
|
|
||||||
dealIndex={cardIndex}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</li>,
|
|
||||||
];
|
|
||||||
if (!isLast) {
|
|
||||||
nodes.push(
|
|
||||||
<div
|
|
||||||
key={`${ranking.name}-arrow`}
|
|
||||||
aria-hidden="true"
|
|
||||||
className={cn(
|
|
||||||
"flex items-center justify-center -my-3",
|
|
||||||
columnBreakAfter && "sm:hidden",
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<ChevronDown className="size-3.5 text-muted-foreground/70" />
|
|
||||||
</div>,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return nodes;
|
|
||||||
})}
|
|
||||||
</ol>
|
</ol>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,9 @@
|
||||||
import { HAND_CATEGORIES, HAND_CATEGORY_NAMES, evaluateHand } from "./evaluator";
|
import { HAND_CATEGORIES, HAND_CATEGORY_NAMES, evaluateHand } from "./evaluator";
|
||||||
import { handRankTier } from "./handRankStyle";
|
|
||||||
import type { Card, Rank } from "./types";
|
import type { Card, Rank } from "./types";
|
||||||
|
|
||||||
export interface HandStrengthHint {
|
export interface HandStrengthHint {
|
||||||
readonly label: string;
|
readonly label: string;
|
||||||
readonly bestCardKeys: string[];
|
readonly bestCardKeys: string[];
|
||||||
/** Presentation tier 0-9 driving the progressive rank styling. */
|
|
||||||
readonly tier: number;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const RANK_NAMES: Record<Rank, string> = {
|
const RANK_NAMES: Record<Rank, string> = {
|
||||||
|
|
@ -66,7 +63,7 @@ function postflopLabel(category: number, tiebreakers: readonly number[]): string
|
||||||
case HAND_CATEGORIES.QUADS:
|
case HAND_CATEGORIES.QUADS:
|
||||||
return `Four of a kind, ${pluralRankName(first)}`;
|
return `Four of a kind, ${pluralRankName(first)}`;
|
||||||
case HAND_CATEGORIES.STRAIGHT_FLUSH:
|
case HAND_CATEGORIES.STRAIGHT_FLUSH:
|
||||||
return first === 14 ? "Royal flush" : `Straight flush, ${highCardLabel(first)}`;
|
return `Straight flush, ${highCardLabel(first)}`;
|
||||||
default:
|
default:
|
||||||
return capitalize(HAND_CATEGORY_NAMES[category] ?? "unknown hand");
|
return capitalize(HAND_CATEGORY_NAMES[category] ?? "unknown hand");
|
||||||
}
|
}
|
||||||
|
|
@ -86,7 +83,6 @@ export function describeHandStrength(
|
||||||
? `Pair of ${pluralRankName(highCard.rank)}`
|
? `Pair of ${pluralRankName(highCard.rank)}`
|
||||||
: `High card ${rankName(highCard.rank)}`,
|
: `High card ${rankName(highCard.rank)}`,
|
||||||
bestCardKeys: pair ? holeCards.map(cardKey) : [cardKey(highCard)],
|
bestCardKeys: pair ? holeCards.map(cardKey) : [cardKey(highCard)],
|
||||||
tier: pair ? HAND_CATEGORIES.PAIR : HAND_CATEGORIES.HIGH_CARD,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -97,6 +93,5 @@ export function describeHandStrength(
|
||||||
return {
|
return {
|
||||||
label: postflopLabel(evaluation.rank.category, evaluation.rank.tiebreakers),
|
label: postflopLabel(evaluation.rank.category, evaluation.rank.tiebreakers),
|
||||||
bestCardKeys: evaluation.best.map(cardKey),
|
bestCardKeys: evaluation.best.map(cardKey),
|
||||||
tier: handRankTier(evaluation.rank),
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,18 +0,0 @@
|
||||||
import { HAND_CATEGORIES } from "./evaluator";
|
|
||||||
import type { HandRank } from "./types";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Presentation tier for a made hand: 0 (high card, no styling) through
|
|
||||||
* 9 (royal flush, full prismatic treatment). Each step up the ladder gets a
|
|
||||||
* more intense border and glow; see the poker-rank-tier-* rules in styles.css.
|
|
||||||
*/
|
|
||||||
export const ROYAL_RANK_TIER = 9;
|
|
||||||
|
|
||||||
export function handRankTier(hand: HandRank): number {
|
|
||||||
const royal = hand.category === HAND_CATEGORIES.STRAIGHT_FLUSH && hand.tiebreakers[0] === 14;
|
|
||||||
return royal ? ROYAL_RANK_TIER : hand.category;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function handRankTierClass(tier: number): string {
|
|
||||||
return `poker-rank-tier-${tier}`;
|
|
||||||
}
|
|
||||||
|
|
@ -9,7 +9,6 @@ describe("poker hand strength hints", () => {
|
||||||
expect(describeHandStrength([c(13, "s"), c(13, "h")], [])).toEqual({
|
expect(describeHandStrength([c(13, "s"), c(13, "h")], [])).toEqual({
|
||||||
label: "Pair of Kings",
|
label: "Pair of Kings",
|
||||||
bestCardKeys: ["13-s", "13-h"],
|
bestCardKeys: ["13-s", "13-h"],
|
||||||
tier: 1,
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -17,7 +16,6 @@ describe("poker hand strength hints", () => {
|
||||||
expect(describeHandStrength([c(14, "s"), c(9, "h")], [])).toEqual({
|
expect(describeHandStrength([c(14, "s"), c(9, "h")], [])).toEqual({
|
||||||
label: "High card Ace",
|
label: "High card Ace",
|
||||||
bestCardKeys: ["14-s"],
|
bestCardKeys: ["14-s"],
|
||||||
tier: 0,
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,66 +0,0 @@
|
||||||
import { describe, expect, it } from "vitest";
|
|
||||||
import type { Card, HandRank } from "@/lib/minigames/poker";
|
|
||||||
import { handRankTier, ROYAL_RANK_TIER } from "@/lib/minigames/poker/handRankStyle";
|
|
||||||
import { describeHandStrength } from "@/lib/minigames/poker/handHint";
|
|
||||||
|
|
||||||
function rank(category: number, tiebreakers: readonly number[]): HandRank {
|
|
||||||
return { category, tiebreakers };
|
|
||||||
}
|
|
||||||
|
|
||||||
describe("handRankTier", () => {
|
|
||||||
it("maps every category to its own tier", () => {
|
|
||||||
expect(handRankTier(rank(0, [14]))).toBe(0);
|
|
||||||
expect(handRankTier(rank(1, [9]))).toBe(1);
|
|
||||||
expect(handRankTier(rank(2, [11, 4]))).toBe(2);
|
|
||||||
expect(handRankTier(rank(3, [7]))).toBe(3);
|
|
||||||
expect(handRankTier(rank(4, [9]))).toBe(4);
|
|
||||||
expect(handRankTier(rank(5, [14]))).toBe(5);
|
|
||||||
expect(handRankTier(rank(6, [13, 8]))).toBe(6);
|
|
||||||
expect(handRankTier(rank(7, [3]))).toBe(7);
|
|
||||||
expect(handRankTier(rank(8, [9]))).toBe(8);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("promotes only the ace-high straight flush to the royal tier", () => {
|
|
||||||
expect(handRankTier(rank(8, [14]))).toBe(ROYAL_RANK_TIER);
|
|
||||||
expect(handRankTier(rank(8, [13]))).toBe(8);
|
|
||||||
expect(handRankTier(rank(4, [14]))).not.toBe(ROYAL_RANK_TIER);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("describeHandStrength tiers", () => {
|
|
||||||
it("keeps high card at tier zero and a pocket pair at tier one", () => {
|
|
||||||
const high = describeHandStrength(
|
|
||||||
[
|
|
||||||
{ rank: 14, suit: "s" },
|
|
||||||
{ rank: 13, suit: "h" },
|
|
||||||
],
|
|
||||||
[],
|
|
||||||
);
|
|
||||||
const pair = describeHandStrength(
|
|
||||||
[
|
|
||||||
{ rank: 9, suit: "s" },
|
|
||||||
{ rank: 9, suit: "h" },
|
|
||||||
],
|
|
||||||
[],
|
|
||||||
);
|
|
||||||
expect(high?.tier).toBe(0);
|
|
||||||
expect(pair?.tier).toBe(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("grades a royal flush at the top tier with the royal label", () => {
|
|
||||||
const hole: Card[] = [
|
|
||||||
{ rank: 10, suit: "h" },
|
|
||||||
{ rank: 11, suit: "h" },
|
|
||||||
];
|
|
||||||
const board: Card[] = [
|
|
||||||
{ rank: 12, suit: "h" },
|
|
||||||
{ rank: 13, suit: "h" },
|
|
||||||
{ rank: 14, suit: "h" },
|
|
||||||
{ rank: 2, suit: "s" },
|
|
||||||
{ rank: 3, suit: "d" },
|
|
||||||
];
|
|
||||||
const hint = describeHandStrength(hole, board);
|
|
||||||
expect(hint?.tier).toBe(ROYAL_RANK_TIER);
|
|
||||||
expect(hint?.label).toBe("Royal flush");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
Loading…
Reference in a new issue