Build Hokm card game: offline vs-AI + online social/gamification (mock backend)
- Pure-TS Hokm engine (deal, hakem, trump, tricks, scoring, Kot) + AI bots - Persian-luxury RTL UI (Next 16 / React 19 / Tailwind v4 / Framer Motion / Zustand) - Online platform behind OnlineService seam (mock now, .NET SignalR later): auth (phone OTP + email/Google), profiles, friends, private rooms with partner pick, ranked matchmaking, leaderboard, shop - Gamification: ranks/leagues, coins, XP/levels, daily rewards, achievements - i18n fa/en, PWA manifest, engine + gamification sims Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,83 @@
|
||||
"use client";
|
||||
|
||||
import { cn } from "@/lib/cn";
|
||||
import { Card, SUIT_IS_RED, SUIT_SYMBOL, rankLabel } from "@/lib/hokm/types";
|
||||
|
||||
const SIZES = {
|
||||
sm: { w: 44, h: 62, rank: "text-base", pip: "text-lg", center: "text-2xl" },
|
||||
md: { w: 60, h: 84, rank: "text-lg", pip: "text-xl", center: "text-3xl" },
|
||||
lg: { w: 74, h: 104, rank: "text-xl", pip: "text-2xl", center: "text-4xl" },
|
||||
} as const;
|
||||
|
||||
export type CardSize = keyof typeof SIZES;
|
||||
|
||||
interface Props {
|
||||
card?: Card;
|
||||
faceDown?: boolean;
|
||||
size?: CardSize;
|
||||
className?: string;
|
||||
dimmed?: boolean;
|
||||
}
|
||||
|
||||
export function PlayingCard({
|
||||
card,
|
||||
faceDown,
|
||||
size = "md",
|
||||
className,
|
||||
dimmed,
|
||||
}: Props) {
|
||||
const s = SIZES[size];
|
||||
|
||||
if (faceDown || !card) {
|
||||
return (
|
||||
<div
|
||||
className={cn("card-back rounded-lg shrink-0", className)}
|
||||
style={{ width: s.w, height: s.h }}
|
||||
aria-hidden
|
||||
>
|
||||
<div className="h-full w-full rounded-lg flex items-center justify-center">
|
||||
<div className="text-gold-500/70 text-lg font-bold">✦</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const red = SUIT_IS_RED[card.suit];
|
||||
const color = red ? "text-rose-600" : "text-slate-900";
|
||||
const symbol = SUIT_SYMBOL[card.suit];
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"card-face rounded-lg shrink-0 relative select-none transition-opacity",
|
||||
dimmed && "opacity-45",
|
||||
className
|
||||
)}
|
||||
style={{ width: s.w, height: s.h }}
|
||||
>
|
||||
<div className={cn("absolute top-1 left-1.5 leading-none font-bold", color, s.rank)}>
|
||||
<div>{rankLabel(card.rank)}</div>
|
||||
<div className={s.rank}>{symbol}</div>
|
||||
</div>
|
||||
<div
|
||||
className={cn(
|
||||
"absolute inset-0 flex items-center justify-center font-bold",
|
||||
color,
|
||||
s.center
|
||||
)}
|
||||
>
|
||||
{symbol}
|
||||
</div>
|
||||
<div
|
||||
className={cn(
|
||||
"absolute bottom-1 right-1.5 leading-none font-bold rotate-180",
|
||||
color,
|
||||
s.rank
|
||||
)}
|
||||
>
|
||||
<div>{rankLabel(card.rank)}</div>
|
||||
<div className={s.rank}>{symbol}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user