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:
soroush.asadi
2026-06-04 10:11:00 +03:30
parent dff1a34f95
commit e2d0a602b6
41 changed files with 5766 additions and 93 deletions
@@ -0,0 +1,89 @@
"use client";
import { motion } from "framer-motion";
import { Coins, Trophy, Users } from "lucide-react";
import { useState } from "react";
import { ScreenHeader, ScreenShell } from "@/components/online/ScreenHeader";
import { useOnlineStore } from "@/lib/online-store";
import { useUIStore } from "@/lib/ui-store";
import { useI18n } from "@/lib/i18n";
import { cn } from "@/lib/cn";
const STAKES = [0, 100, 500, 1000];
export function OnlineLobbyScreen() {
const { t } = useI18n();
const createRoom = useOnlineStore((s) => s.createRoom);
const startMatchmaking = useOnlineStore((s) => s.startMatchmaking);
const go = useUIStore((s) => s.go);
const [stake, setStake] = useState(100);
const onCreate = async () => {
await createRoom({ targetScore: 7, stake, ranked: false });
go("room");
};
const onRandom = async () => {
await startMatchmaking({ ranked: true, stake });
go("matchmaking");
};
return (
<ScreenShell>
<ScreenHeader title={t("lobby.title")} />
{/* stake */}
<div className="glass rounded-2xl p-4 mb-4">
<div className="flex items-center gap-1.5 text-sm text-cream/70 mb-2.5">
<Coins className="size-4 text-gold-400" />
{t("room.stake")}
</div>
<div className="flex gap-2">
{STAKES.map((s) => (
<button
key={s}
onClick={() => setStake(s)}
className={cn(
"flex-1 rounded-xl py-2.5 text-sm font-bold transition",
stake === s ? "btn-gold" : "bg-navy-900/70 gold-border text-cream/70 hover:text-cream"
)}
>
{s === 0 ? t("menu.guest") : s.toLocaleString()}
</button>
))}
</div>
</div>
<div className="space-y-3">
<motion.button
whileHover={{ y: -2 }}
whileTap={{ scale: 0.98 }}
onClick={onRandom}
className="btn-gold w-full rounded-2xl p-5 flex items-center gap-4 text-start"
>
<span className="size-12 rounded-xl bg-black/15 flex items-center justify-center text-[#2a1f04]">
<Trophy className="size-6" />
</span>
<span>
<span className="block text-lg font-black text-[#2a1f04]">{t("lobby.random")}</span>
<span className="block text-xs text-[#2a1f04]/70">{t("lobby.randomDesc")}</span>
</span>
</motion.button>
<motion.button
whileHover={{ y: -2 }}
whileTap={{ scale: 0.98 }}
onClick={onCreate}
className="glass w-full rounded-2xl p-5 flex items-center gap-4 text-start hover:bg-navy-800/80 transition"
>
<span className="size-12 rounded-xl bg-navy-900 gold-border flex items-center justify-center text-gold-400">
<Users className="size-6" />
</span>
<span>
<span className="block text-lg font-black text-cream">{t("lobby.createRoom")}</span>
<span className="block text-xs text-cream/55">{t("lobby.createDesc")}</span>
</span>
</motion.button>
</div>
</ScreenShell>
);
}