This commit is contained in:
lubiana 2025-07-12 15:38:06 +02:00
parent 1625579254
commit 1bef97490d
Signed by: lubiana
SSH key fingerprint: SHA256:vW1EA0fRR3Fw+dD/sM0K+x3Il2gSry6YRYHqOeQwrfk
4 changed files with 138 additions and 103 deletions

View file

@ -1,6 +1,7 @@
import { useState } from 'react' import React, { useState, useCallback, useMemo } from 'react';
import React from 'react'; // Added for useEffect
import Card from './components/Card'; import Card from './components/Card';
import TierCard from './components/TierCard';
import HistoryList from './components/HistoryList';
// --- Scratch Card Game Types and State --- // --- Scratch Card Game Types and State ---
export type Tier = { export type Tier = {
@ -24,23 +25,16 @@ const TIERS: Tier[] = [
{ name: 'Supreme', unlockPrice: 2000, buyPrice: 3000, gubblePointChance: 0.4 }, { name: 'Supreme', unlockPrice: 2000, buyPrice: 3000, gubblePointChance: 0.4 },
]; ];
// --- Card Component ---
function App() { function App() {
// --- Game State --- // --- Game State ---
const [money, setMoney] = useState(20); // User starts with $20 const [money, setMoney] = useState(20); // User starts with $20
const [unlockedTiers, setUnlockedTiers] = useState<string[]>([]); // Unlocked tier names const [unlockedTiers, setUnlockedTiers] = useState<string[]>([]); // Unlocked tier names
const [gubblePoints, setGubblePoints] = useState(0); const [gubblePoints, setGubblePoints] = useState(0);
// Remove selectedTier state
// --- Card State ---
const [card, setCard] = useState<{ const [card, setCard] = useState<{
winningNumbers: number[]; winningNumbers: number[];
fields: { value: number; scratched: boolean; won: number | null }[]; fields: { value: number; scratched: boolean; won: number | null }[];
tier: Tier | null; tier: Tier | null;
} | null>(null); } | null>(null);
// --- Play History State ---
const [history, setHistory] = useState<{ const [history, setHistory] = useState<{
tier: string; tier: string;
winnings: number; winnings: number;
@ -49,24 +43,19 @@ function App() {
}[]>([]); }[]>([]);
// --- Tier Unlock Logic --- // --- Tier Unlock Logic ---
const handleUnlockTier = (tier: Tier) => { const handleUnlockTier = useCallback((tier: Tier) => {
if (money >= tier.unlockPrice && !unlockedTiers.includes(tier.name)) { setMoney(m => {
setMoney(money - tier.unlockPrice); if (m < tier.unlockPrice) return m;
setUnlockedTiers([...unlockedTiers, tier.name]); setUnlockedTiers(prev => prev.includes(tier.name) ? prev : [...prev, tier.name]);
} return m - tier.unlockPrice;
}; });
}, []);
// --- Tier Select Logic --- // --- Buy Card Logic ---
// Remove handleSelectTier const handleBuyCard = useCallback((tier: Tier) => {
// const handleSelectTier = (tier: Tier) => { setMoney(m => {
// if (unlockedTiers.includes(tier.name)) { if (m < tier.buyPrice) return m;
// setSelectedTier(tier.name); if (!unlockedTiers.includes(tier.name)) return m;
// }
// };
// Update handleBuyCard to accept tier as argument
const handleBuyCard = (tier: Tier) => {
if (!unlockedTiers.includes(tier.name) || money < tier.buyPrice) return;
// Generate 3 unique winning numbers (1-9) // Generate 3 unique winning numbers (1-9)
const winningNumbers: number[] = []; const winningNumbers: number[] = [];
while (winningNumbers.length < 3) { while (winningNumbers.length < 3) {
@ -75,21 +64,23 @@ function App() {
} }
// Generate 6 random fields (1-9) // Generate 6 random fields (1-9)
const fields = Array.from({ length: 6 }, () => ({ value: Math.floor(Math.random() * 9) + 1, scratched: false, won: null })); const fields = Array.from({ length: 6 }, () => ({ value: Math.floor(Math.random() * 9) + 1, scratched: false, won: null }));
setMoney(money - tier.buyPrice);
setCard({ winningNumbers, fields, tier }); setCard({ winningNumbers, fields, tier });
// Add to history (winnings will be updated after all fields scratched) setHistory(h => [
setHistory((h) => [
{ tier: tier.name, winnings: 0, winningNumbers, fields: fields.map(f => f.value) }, { tier: tier.name, winnings: 0, winningNumbers, fields: fields.map(f => f.value) },
...h.slice(0, 9) ...h.slice(0, 9)
]); ]);
}; return m - tier.buyPrice;
});
}, [unlockedTiers]);
// --- Update history winnings after all fields scratched --- // --- Update history winnings after all fields scratched ---
React.useEffect(() => { // This effect only runs when card changes
// Use useCallback for setHistory logic
const updateHistoryWinnings = useCallback(() => {
if (!card) return; if (!card) return;
const allScratched = card.fields.every(f => f.scratched); const allScratched = card.fields.every(f => f.scratched);
if (allScratched) { if (allScratched) {
setHistory((h) => { setHistory(h => {
const [latest, ...rest] = h; const [latest, ...rest] = h;
if (!latest || latest.winningNumbers !== card.winningNumbers) return h; if (!latest || latest.winningNumbers !== card.winningNumbers) return h;
return [ return [
@ -100,6 +91,12 @@ function App() {
} }
}, [card]); }, [card]);
// Only run when card changes
// eslint-disable-next-line react-hooks/exhaustive-deps
React.useEffect(updateHistoryWinnings, [card]);
// --- Memoized winnings for current card ---
const currentWinnings = useMemo(() => card ? card.fields.reduce((sum, f) => sum + (f.won || 0), 0) : 0, [card]);
// --- UI --- // --- UI ---
return ( return (
@ -108,73 +105,29 @@ function App() {
<div className="mb-4 text-lg">Money: <span className="font-mono font-bold">${money}</span></div> <div className="mb-4 text-lg">Money: <span className="font-mono font-bold">${money}</span></div>
<div className="mb-4 text-lg">Gubble Points: <span className="font-mono font-bold">{gubblePoints}</span></div> <div className="mb-4 text-lg">Gubble Points: <span className="font-mono font-bold">{gubblePoints}</span></div>
<div className="flex flex-wrap gap-6 mb-8 justify-center"> <div className="flex flex-wrap gap-6 mb-8 justify-center">
{TIERS.map((tier) => { {TIERS.map((tier) => (
const unlocked = unlockedTiers.includes(tier.name); <TierCard
return ( key={tier.name}
<div key={tier.name} className="bg-white rounded-lg shadow p-4 flex flex-col items-center w-40"> tier={tier}
<div className="text-xl font-semibold mb-1">{tier.name}</div> unlocked={unlockedTiers.includes(tier.name)}
<div className="text-gray-500 text-sm mb-2">Unlock: ${tier.unlockPrice}</div> money={money}
<div className="text-gray-500 text-sm mb-2">Buy: ${tier.buyPrice}</div> onUnlock={handleUnlockTier}
{!unlocked ? ( onBuy={handleBuyCard}
<button />
className="bg-yellow-400 hover:bg-yellow-500 text-white font-bold px-4 py-1 rounded mb-1 disabled:opacity-50" ))}
disabled={money < tier.unlockPrice}
onClick={() => handleUnlockTier(tier)}
>
Unlock
</button>
) : (
<button
className="bg-green-500 hover:bg-green-600 text-white font-bold px-4 py-1 rounded mb-1 disabled:opacity-50"
disabled={money < tier.buyPrice}
onClick={() => handleBuyCard(tier)}
>
Buy Card
</button>
)}
</div> </div>
);
})}
</div>
{/* Tier Info and Card Summary */}
{/* Remove all selectedTier-dependent UI (selected tier info, buy button outside tier cards, etc.) */}
{card && ( {card && (
<> <>
<div className="mb-2 text-sm text-gray-600"> <div className="mb-2 text-sm text-gray-600">
<span className="font-semibold">Winnings this card: </span> <span className="font-semibold">Winnings this card: </span>
${card.fields.reduce((sum, f) => sum + (f.won || 0), 0)} ${currentWinnings}
</div> </div>
<Card card={card} setCard={setCard} setMoney={setMoney} setGubblePoints={setGubblePoints} /> <Card card={card} setCard={setCard} setMoney={setMoney} setGubblePoints={setGubblePoints} />
</> </>
)} )}
{/* Play History */} <HistoryList history={history} />
{history.length > 0 && (
<div className="w-full max-w-md mb-8">
<div className="font-semibold text-gray-700 mb-2">Play History</div>
<ul className="divide-y divide-gray-200 bg-white rounded-lg shadow">
{history.map((h, i) => (
<li key={i} className="p-3 flex flex-col sm:flex-row sm:items-center sm:justify-between">
<div>
<span className="font-bold text-blue-600">{h.tier}</span>
<span className="ml-2 text-gray-500">Win: ${h.winnings}</span>
</div> </div>
<div className="flex gap-2 mt-2 sm:mt-0"> );
{h.winningNumbers.map((n, j) => (
<span key={j} className="w-6 h-6 flex items-center justify-center rounded-full bg-blue-100 text-blue-700 text-xs font-bold border border-blue-300">{n}</span>
))}
</div>
<div className="flex gap-1 mt-2 sm:mt-0">
{h.fields.map((n, j) => (
<span key={j} className="w-5 h-5 flex items-center justify-center rounded bg-gray-100 text-gray-700 text-xs border border-gray-300">{n}</span>
))}
</div>
</li>
))}
</ul>
</div>
)}
</div>
)
} }
export default App export default App;

View file

@ -69,4 +69,4 @@ const Card: React.FC<CardProps> = ({ card, setCard, setMoney, setGubblePoints })
}; };
export type { CardProps }; export type { CardProps };
export default Card; export default React.memo(Card);

View file

@ -0,0 +1,43 @@
import React from 'react';
type HistoryItem = {
tier: string;
winnings: number;
winningNumbers: number[];
fields: number[];
};
type HistoryListProps = {
history: HistoryItem[];
};
const HistoryList: React.FC<HistoryListProps> = React.memo(({ history }) => {
if (history.length === 0) return null;
return (
<div className="w-full max-w-md mb-8">
<div className="font-semibold text-gray-700 mb-2">Play History</div>
<ul className="divide-y divide-gray-200 bg-white rounded-lg shadow">
{history.map((h, i) => (
<li key={i} className="p-3 flex flex-col sm:flex-row sm:items-center sm:justify-between">
<div>
<span className="font-bold text-blue-600">{h.tier}</span>
<span className="ml-2 text-gray-500">Win: ${h.winnings}</span>
</div>
<div className="flex gap-2 mt-2 sm:mt-0">
{h.winningNumbers.map((n, j) => (
<span key={j} className="w-6 h-6 flex items-center justify-center rounded-full bg-blue-100 text-blue-700 text-xs font-bold border border-blue-300">{n}</span>
))}
</div>
<div className="flex gap-1 mt-2 sm:mt-0">
{h.fields.map((n, j) => (
<span key={j} className="w-5 h-5 flex items-center justify-center rounded bg-gray-100 text-gray-700 text-xs border border-gray-300">{n}</span>
))}
</div>
</li>
))}
</ul>
</div>
);
});
export default HistoryList;

View file

@ -0,0 +1,39 @@
import React from 'react';
import type { Tier } from '../App';
type TierCardProps = {
tier: Tier;
unlocked: boolean;
money: number;
onUnlock: (tier: Tier) => void;
onBuy: (tier: Tier) => void;
};
const TierCard: React.FC<TierCardProps> = React.memo(({ tier, unlocked, money, onUnlock, onBuy }) => {
return (
<div className="bg-white rounded-lg shadow p-4 flex flex-col items-center w-40">
<div className="text-xl font-semibold mb-1">{tier.name}</div>
<div className="text-gray-500 text-sm mb-2">Unlock: ${tier.unlockPrice}</div>
<div className="text-gray-500 text-sm mb-2">Buy: ${tier.buyPrice}</div>
{!unlocked ? (
<button
className="bg-yellow-400 hover:bg-yellow-500 text-white font-bold px-4 py-1 rounded mb-1 disabled:opacity-50"
disabled={money < tier.unlockPrice}
onClick={() => onUnlock(tier)}
>
Unlock
</button>
) : (
<button
className="bg-green-500 hover:bg-green-600 text-white font-bold px-4 py-1 rounded mb-1 disabled:opacity-50"
disabled={money < tier.buyPrice}
onClick={() => onBuy(tier)}
>
Buy Card
</button>
)}
</div>
);
});
export default TierCard;