mirror of
https://github.com/averel10/crypto_clash.git
synced 2026-03-12 19:08:11 +01:00
Implement timeout handling and commit resolution in Commit and Reveal components
- Added state management for opponent's move and commit time left in Commit component. - Implemented automatic checks for opponent's move and commit timeout. - Introduced timeout resolution functionality allowing players to claim victory if the opponent fails to commit in time. - Updated UI to display timeout results and allow players to resolve timeouts. - Enhanced Reveal component with timeout checks and resolution logic. - Added necessary contract methods in config.json for commit time left and timeout resolution.
This commit is contained in:
@@ -45,9 +45,12 @@ export default function Commit({
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [playMove, setPlayMove] = useState<string>("");
|
||||
const [selfPlayed, setSelfPlayed] = useState<string>("");
|
||||
const [opponentPlayed, setOpponentPlayed] = useState<string>("");
|
||||
const [bothPlayed, setBothPlayed] = useState<string>("");
|
||||
const [autoCheckInterval, setAutoCheckInterval] = useState<NodeJS.Timeout | null>(null);
|
||||
const [moveSubmitted, setMoveSubmitted] = useState(false);
|
||||
const [commitTimeLeft, setCommitTimeLeft] = useState<number>(0);
|
||||
const [timeoutExpired, setTimeoutExpired] = useState(false);
|
||||
|
||||
// Update encrypted move when move or secret changes
|
||||
useEffect(() => {
|
||||
@@ -82,6 +85,18 @@ export default function Commit({
|
||||
|
||||
checkSelfPlayed();
|
||||
|
||||
const checkOpponentPlayed = async () => {
|
||||
try {
|
||||
const opponentKey = whoAmI === "player1" ? "playerB" : "playerA";
|
||||
const encrMove = gameDetails[opponentKey].encrMove;
|
||||
setOpponentPlayed(Number(encrMove) !== 0 ? "true" : "false");
|
||||
} catch (err: any) {
|
||||
console.error("Auto-check opponent played failed:", err.message);
|
||||
}
|
||||
};
|
||||
|
||||
checkOpponentPlayed();
|
||||
|
||||
// Check immediately on mount or when dependencies change
|
||||
const checkBothPlayed = async () => {
|
||||
try {
|
||||
@@ -100,8 +115,27 @@ export default function Commit({
|
||||
|
||||
checkBothPlayed();
|
||||
|
||||
// Check commit timeout
|
||||
const checkCommitTimeout = async () => {
|
||||
try {
|
||||
const timeLeft = await contract.methods.commitTimeLeft(gameDetails.returnGameId).call();
|
||||
console.log("Commit time left:", timeLeft);
|
||||
setCommitTimeLeft(Number(timeLeft));
|
||||
if (Number(timeLeft) <= 0) {
|
||||
setTimeoutExpired(true);
|
||||
}
|
||||
} catch (err: any) {
|
||||
console.error("Commit timeout check failed:", err.message);
|
||||
}
|
||||
};
|
||||
|
||||
checkCommitTimeout();
|
||||
|
||||
// Set up interval to check every 2 seconds
|
||||
const interval = setInterval(checkBothPlayed, 2000);
|
||||
const interval = setInterval(() => {
|
||||
checkBothPlayed();
|
||||
checkCommitTimeout();
|
||||
}, 2000);
|
||||
setAutoCheckInterval(interval);
|
||||
|
||||
return () => {
|
||||
@@ -151,99 +185,207 @@ export default function Commit({
|
||||
setSelectedMove(move);
|
||||
};
|
||||
|
||||
const handleResolveTimeout = async () => {
|
||||
if (!contract || !web3 || !account) return;
|
||||
setLoading(true);
|
||||
try {
|
||||
const tx = contract.methods.resolveTimeout(gameDetails?.returnGameId);
|
||||
const gas = await tx.estimateGas({ from: account });
|
||||
const result = await (globalThis as any).ethereum.request({
|
||||
method: "eth_sendTransaction",
|
||||
params: [
|
||||
{
|
||||
from: account,
|
||||
to: config?.GAME_CONTRACT_ADDRESS,
|
||||
data: tx.encodeABI(),
|
||||
gas: web3.utils.toHex(gas),
|
||||
chainId: web3.utils.toHex(await web3.eth.net.getId()),
|
||||
},
|
||||
],
|
||||
});
|
||||
showToast("Timeout resolved: " + result, "success");
|
||||
} catch (err: any) {
|
||||
console.error("Timeout resolution failed:", err);
|
||||
showToast("Timeout resolution failed: " + err.message, "error");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Check if current player can resolve timeout (they committed, opponent didn't, and game is still active)
|
||||
const canResolveTimeout = timeoutExpired && selfPlayed === "true" && gameDetails?.isActive;
|
||||
|
||||
// Check if game is finished due to timeout
|
||||
const isGameFinishedByTimeout = !gameDetails?.isActive && (Number(gameDetails?.outcome) === 4 || Number(gameDetails?.outcome) === 5);
|
||||
|
||||
// Determine if current player won/lost the timeout
|
||||
const didIWinTimeout =
|
||||
(whoAmI === "player2" && Number(gameDetails?.outcome) === 4) ||
|
||||
(whoAmI === "player1" && Number(gameDetails?.outcome) === 5);
|
||||
|
||||
return (
|
||||
<div className="border p-6 rounded-lg bg-gradient-to-br from-slate-50 to-slate-100 dark:from-slate-700 dark:to-slate-800">
|
||||
<h2 className="font-semibold text-lg mb-6 text-slate-900 dark:text-white">
|
||||
Select Your Move
|
||||
</h2>
|
||||
|
||||
{moveSubmitted || selfPlayed === "true" ? (
|
||||
// Waiting animation after move is submitted
|
||||
{/* Show timeout result after game is inactive */}
|
||||
{isGameFinishedByTimeout ? (
|
||||
<div className="flex flex-col items-center justify-center py-16">
|
||||
<div className="mb-6">
|
||||
<div className="w-16 h-16 border-4 border-slate-300 dark:border-slate-500 border-t-blue-500 rounded-full animate-spin"></div>
|
||||
</div>
|
||||
<p className="text-lg font-semibold text-slate-700 dark:text-slate-300 mb-2">
|
||||
Waiting for opponent...
|
||||
</p>
|
||||
<p className="text-sm text-slate-500 dark:text-slate-400">
|
||||
Your move has been submitted. Stand by while the other player commits.
|
||||
</p>
|
||||
{didIWinTimeout ? (
|
||||
<div className="mb-6 p-4 bg-green-50 dark:bg-green-900 border-2 border-green-400 dark:border-green-600 rounded-lg w-full">
|
||||
<p className="text-green-700 dark:text-green-200 font-semibold mb-3 text-center">
|
||||
🎉 Victory by Timeout!
|
||||
</p>
|
||||
<p className="text-sm text-green-600 dark:text-green-300 text-center">
|
||||
Your opponent failed to commit in time. You claimed victory!
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="mb-6 p-4 bg-red-50 dark:bg-red-900 border-2 border-red-400 dark:border-red-600 rounded-lg w-full">
|
||||
<p className="text-red-700 dark:text-red-200 font-semibold mb-3 text-center">
|
||||
⏱️ Timeout Loss
|
||||
</p>
|
||||
<p className="text-sm text-red-600 dark:text-red-300 text-center">
|
||||
You failed to commit in time. Your opponent claimed victory!
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{/* Move Selection */}
|
||||
<div className="mb-8">
|
||||
<p className="text-sm text-slate-600 dark:text-slate-300 mb-4 font-medium">
|
||||
Choose your move:
|
||||
</p>
|
||||
<div className="flex gap-4 justify-center">
|
||||
{(["1", "2", "3"] as const).map((move) => (
|
||||
<button
|
||||
key={move}
|
||||
onClick={() => handleMoveSelect(move)}
|
||||
className={`flex flex-col items-center justify-center p-6 rounded-lg transition-all transform ${
|
||||
selectedMove === move
|
||||
? "bg-blue-500 text-white shadow-lg scale-110"
|
||||
: "bg-white dark:bg-slate-600 text-slate-700 dark:text-slate-200 shadow hover:shadow-md hover:scale-105"
|
||||
}`}
|
||||
{/* Timeout Warning - Only show to eligible player */}
|
||||
{timeoutExpired && canResolveTimeout ? (
|
||||
<div className="flex flex-col items-center justify-center py-16">
|
||||
<div className="mb-6 p-4 bg-red-50 dark:bg-red-900 border-2 border-red-400 dark:border-red-600 rounded-lg w-full">
|
||||
<p className="text-red-700 dark:text-red-200 font-semibold mb-3 text-center">
|
||||
⏱️ Commit phase timeout expired!
|
||||
</p>
|
||||
<p className="text-sm text-red-600 dark:text-red-300 mb-4 text-center">
|
||||
The opponent failed to commit in time. You can claim victory!
|
||||
</p>
|
||||
<Button
|
||||
onClick={handleResolveTimeout}
|
||||
disabled={loading || !account || !contract}
|
||||
variant="primary"
|
||||
className="w-full py-3 text-lg"
|
||||
>
|
||||
<span className="text-5xl mb-2">{MOVES[move].icon}</span>
|
||||
<span className="font-semibold text-sm">{MOVES[move].name}</span>
|
||||
</button>
|
||||
))}
|
||||
{loading ? "Processing..." : "⚡ Resolve Timeout"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Secret Input */}
|
||||
<div className="mb-8 bg-white dark:bg-slate-700 p-4 rounded-lg">
|
||||
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-3">
|
||||
Secret:
|
||||
</label>
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
type="text"
|
||||
value={secret}
|
||||
onChange={(e) => handleSecretChange(e.target.value)}
|
||||
placeholder="Your secret passphrase"
|
||||
className="flex-1"
|
||||
/>
|
||||
<Button
|
||||
onClick={regenerateSecret}
|
||||
variant="secondary"
|
||||
disabled={loading}
|
||||
>
|
||||
🔄 New
|
||||
</Button>
|
||||
) : timeoutExpired && !canResolveTimeout ? (
|
||||
// Show waiting message to opponent
|
||||
<div className="flex flex-col items-center justify-center py-16">
|
||||
<div className="mb-6">
|
||||
<div className="w-16 h-16 border-4 border-slate-300 dark:border-slate-500 border-t-red-500 rounded-full animate-spin"></div>
|
||||
</div>
|
||||
<p className="text-lg font-semibold text-slate-700 dark:text-slate-300 mb-2">
|
||||
Opponent claiming victory...
|
||||
</p>
|
||||
<p className="text-sm text-slate-500 dark:text-slate-400">
|
||||
The timeout has expired. Waiting for the opponent to resolve.
|
||||
</p>
|
||||
</div>
|
||||
<p className="text-xs text-slate-500 dark:text-slate-400 mt-2">
|
||||
Keep this secret safe! It's needed to reveal your move later.
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{/* Time Left Display */}
|
||||
{commitTimeLeft > 0 && opponentPlayed === "true" && (
|
||||
<div className="mb-6 p-3 bg-blue-50 dark:bg-blue-900 border border-blue-200 dark:border-blue-700 rounded-lg">
|
||||
<p className="text-sm text-blue-700 dark:text-blue-300 text-center font-semibold">
|
||||
⏱️ Time Left: {commitTimeLeft}s
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Encrypted Move Display */}
|
||||
<div className="mb-8 bg-blue-50 dark:bg-blue-900 p-4 rounded-lg">
|
||||
<label className="block text-sm font-medium text-slate-700 dark:text-blue-200 mb-2">
|
||||
Encrypted Move (to be sent):
|
||||
</label>
|
||||
<div className="bg-white dark:bg-slate-700 p-3 rounded border border-blue-200 dark:border-blue-700 overflow-x-auto">
|
||||
<code className="text-xs text-slate-600 dark:text-slate-300 font-mono break-all">
|
||||
{playMove || "Select a move and enter a secret"}
|
||||
</code>
|
||||
</div>
|
||||
</div>
|
||||
{moveSubmitted || selfPlayed === "true" ? (
|
||||
// Waiting animation after move is submitted
|
||||
<div className="flex flex-col items-center justify-center py-16">
|
||||
<div className="mb-6">
|
||||
<div className="w-16 h-16 border-4 border-slate-300 dark:border-slate-500 border-t-blue-500 rounded-full animate-spin"></div>
|
||||
</div>
|
||||
<p className="text-lg font-semibold text-slate-700 dark:text-slate-300 mb-2">
|
||||
Waiting for opponent...
|
||||
</p>
|
||||
<p className="text-sm text-slate-500 dark:text-slate-400">
|
||||
Your move has been submitted. Stand by while the other player commits.
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{/* Move Selection */}
|
||||
<div className="mb-8">
|
||||
<p className="text-sm text-slate-600 dark:text-slate-300 mb-4 font-medium">
|
||||
Choose your move:
|
||||
</p>
|
||||
<div className="flex gap-4 justify-center">
|
||||
{(["1", "2", "3"] as const).map((move) => (
|
||||
<button
|
||||
key={move}
|
||||
onClick={() => handleMoveSelect(move)}
|
||||
className={`flex flex-col items-center justify-center p-6 rounded-lg transition-all transform ${
|
||||
selectedMove === move
|
||||
? "bg-blue-500 text-white shadow-lg scale-110"
|
||||
: "bg-white dark:bg-slate-600 text-slate-700 dark:text-slate-200 shadow hover:shadow-md hover:scale-105"
|
||||
}`}
|
||||
>
|
||||
<span className="text-5xl mb-2">{MOVES[move].icon}</span>
|
||||
<span className="font-semibold text-sm">{MOVES[move].name}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Action Buttons */}
|
||||
<div className="flex flex-col gap-3">
|
||||
<Button
|
||||
onClick={handlePlay}
|
||||
disabled={loading || !account || !contract || !selectedMove || !secret}
|
||||
variant="primary"
|
||||
className="w-full py-3 text-lg"
|
||||
>
|
||||
{loading ? "Submitting..." : "Submit Move"}
|
||||
</Button>
|
||||
</div>
|
||||
{/* Secret Input */}
|
||||
<div className="mb-8 bg-white dark:bg-slate-700 p-4 rounded-lg">
|
||||
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-3">
|
||||
Secret:
|
||||
</label>
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
type="text"
|
||||
value={secret}
|
||||
onChange={(e) => handleSecretChange(e.target.value)}
|
||||
placeholder="Your secret passphrase"
|
||||
className="flex-1"
|
||||
/>
|
||||
<Button
|
||||
onClick={regenerateSecret}
|
||||
variant="secondary"
|
||||
disabled={loading}
|
||||
>
|
||||
🔄 New
|
||||
</Button>
|
||||
</div>
|
||||
<p className="text-xs text-slate-500 dark:text-slate-400 mt-2">
|
||||
Keep this secret safe! It's needed to reveal your move later.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Encrypted Move Display */}
|
||||
<div className="mb-8 bg-blue-50 dark:bg-blue-900 p-4 rounded-lg">
|
||||
<label className="block text-sm font-medium text-slate-700 dark:text-blue-200 mb-2">
|
||||
Encrypted Move (to be sent):
|
||||
</label>
|
||||
<div className="bg-white dark:bg-slate-700 p-3 rounded border border-blue-200 dark:border-blue-700 overflow-x-auto">
|
||||
<code className="text-xs text-slate-600 dark:text-slate-300 font-mono break-all">
|
||||
{playMove || "Select a move and enter a secret"}
|
||||
</code>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Action Buttons */}
|
||||
<div className="flex flex-col gap-3">
|
||||
<Button
|
||||
onClick={handlePlay}
|
||||
disabled={loading || !account || !contract || !selectedMove || !secret}
|
||||
variant="primary"
|
||||
className="w-full py-3 text-lg"
|
||||
>
|
||||
{loading ? "Submitting..." : "Submit Move"}
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -48,6 +48,8 @@ export default function Reveal({
|
||||
const [opponentRevealed, setOpponentRevealed] = useState(false);
|
||||
const [bothRevealed, setBothRevealed] = useState(false);
|
||||
const [outcome, setOutcome] = useState<number>(0);
|
||||
const [revealTimeLeft, setRevealTimeLeft] = useState<number>(0);
|
||||
const [timeoutExpired, setTimeoutExpired] = useState(false);
|
||||
|
||||
const clearMove = selectedMove && secret ? `${selectedMove}-${secret}` : "";
|
||||
|
||||
@@ -77,6 +79,25 @@ export default function Reveal({
|
||||
};
|
||||
|
||||
setStateFromGameDetails();
|
||||
|
||||
// Check reveal timeout
|
||||
const checkRevealTimeout = async () => {
|
||||
if (!contract || !gameDetails) return;
|
||||
try {
|
||||
const timeLeft = await contract.methods.revealTimeLeft(gameDetails.returnGameId).call();
|
||||
setRevealTimeLeft(Number(timeLeft));
|
||||
if (Number(timeLeft) <= 0) {
|
||||
setTimeoutExpired(true);
|
||||
}
|
||||
} catch (err: any) {
|
||||
console.error("Reveal timeout check failed:", err.message);
|
||||
}
|
||||
};
|
||||
|
||||
checkRevealTimeout();
|
||||
const interval = setInterval(checkRevealTimeout, 2000);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, [gameDetails, contract, account, whoAmI]);
|
||||
|
||||
const handleReveal = async () => {
|
||||
@@ -99,6 +120,7 @@ export default function Reveal({
|
||||
});
|
||||
showSuccessToast("Reveal tx sent: " + result);
|
||||
} catch (err: any) {
|
||||
console.error("Reveal failed:", err);
|
||||
showErrorToast("Reveal failed: " + err.message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
@@ -132,230 +154,320 @@ export default function Reveal({
|
||||
}
|
||||
};
|
||||
|
||||
const handleResolveTimeout = async () => {
|
||||
if (!contract || !web3 || !account) return;
|
||||
setLoading(true);
|
||||
try {
|
||||
const tx = contract.methods.resolveTimeout(gameDetails?.returnGameId);
|
||||
const gas = await tx.estimateGas({ from: account });
|
||||
const result = await (globalThis as any).ethereum.request({
|
||||
method: "eth_sendTransaction",
|
||||
params: [
|
||||
{
|
||||
from: account,
|
||||
to: config?.GAME_CONTRACT_ADDRESS,
|
||||
data: tx.encodeABI(),
|
||||
gas: web3.utils.toHex(gas),
|
||||
chainId: web3.utils.toHex(await web3.eth.net.getId()),
|
||||
},
|
||||
],
|
||||
});
|
||||
showSuccessToast("Timeout resolved: " + result);
|
||||
} catch (err: any) {
|
||||
console.error(err);
|
||||
showErrorToast("Timeout resolution failed: " + err.message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const outcomeData = OUTCOMES[outcome] || OUTCOMES[0];
|
||||
|
||||
// Check if game is finished due to timeout
|
||||
const isGameFinishedByTimeout = !gameDetails?.isActive && (Number(gameDetails?.outcome) === 4 || Number(gameDetails?.outcome) === 5);
|
||||
|
||||
// Determine if current player won/lost the timeout
|
||||
const didIWinTimeout =
|
||||
(whoAmI === "player2" && Number(gameDetails?.outcome) === 4) ||
|
||||
(whoAmI === "player1" && Number(gameDetails?.outcome) === 5);
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Your Move Section - Hidden when both revealed */}
|
||||
{!bothRevealed && (
|
||||
<div className="border p-6 rounded-lg bg-gradient-to-br from-slate-50 to-slate-100 dark:from-slate-700 dark:to-slate-800">
|
||||
<h2 className="font-semibold text-lg mb-4 text-slate-900 dark:text-white">
|
||||
Your Move
|
||||
</h2>
|
||||
{selectedMove ? (
|
||||
<div className="flex items-center justify-center gap-4">
|
||||
<div className="flex flex-col items-center">
|
||||
<span className="text-6xl mb-2">{MOVES[selectedMove].icon}</span>
|
||||
<span className="font-semibold text-lg">
|
||||
{MOVES[selectedMove].name}
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-3xl text-slate-400">→</div>
|
||||
<div className="bg-white dark:bg-slate-600 p-4 rounded-lg">
|
||||
<p className="text-xs text-slate-600 dark:text-slate-300 mb-1">
|
||||
Clear Move:
|
||||
</p>
|
||||
<code className="text-sm font-mono text-slate-700 dark:text-slate-200">
|
||||
{clearMove}
|
||||
</code>
|
||||
</div>
|
||||
{/* Show timeout result after game is inactive */}
|
||||
{isGameFinishedByTimeout && (
|
||||
<div className="flex flex-col items-center justify-center py-16">
|
||||
{didIWinTimeout ? (
|
||||
<div className="mb-6 p-4 bg-green-50 dark:bg-green-900 border-2 border-green-400 dark:border-green-600 rounded-lg w-full">
|
||||
<p className="text-green-700 dark:text-green-200 font-semibold mb-3 text-center">
|
||||
🎉 Victory by Timeout!
|
||||
</p>
|
||||
<p className="text-sm text-green-600 dark:text-green-300 text-center">
|
||||
Your opponent failed to reveal in time. You claimed victory!
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-center text-slate-600 dark:text-slate-400">
|
||||
No move selected yet
|
||||
</p>
|
||||
<div className="mb-6 p-4 bg-red-50 dark:bg-red-900 border-2 border-red-400 dark:border-red-600 rounded-lg w-full">
|
||||
<p className="text-red-700 dark:text-red-200 font-semibold mb-3 text-center">
|
||||
⏱️ Timeout Loss
|
||||
</p>
|
||||
<p className="text-sm text-red-600 dark:text-red-300 text-center">
|
||||
You failed to reveal in time. Your opponent claimed victory!
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Game Status Section - Hidden when both revealed */}
|
||||
{!bothRevealed && (
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<div
|
||||
className={`p-4 rounded-lg text-center ${
|
||||
selfRevealed
|
||||
? "bg-green-50 dark:bg-green-900"
|
||||
: "bg-slate-100 dark:bg-slate-700"
|
||||
}`}
|
||||
>
|
||||
<p className="text-2xl mb-1">{selfRevealed ? "✅" : "⏳"}</p>
|
||||
<p className="text-xs font-semibold text-slate-600 dark:text-slate-300">
|
||||
Me
|
||||
</p>
|
||||
<p className="text-xs text-slate-500 dark:text-slate-400 mt-1">
|
||||
{selfRevealed ? "Revealed" : "Waiting"}
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
className={`p-4 rounded-lg text-center ${
|
||||
opponentRevealed
|
||||
? "bg-green-50 dark:bg-green-900"
|
||||
: "bg-slate-100 dark:bg-slate-700"
|
||||
}`}
|
||||
>
|
||||
<p className="text-2xl mb-1">{opponentRevealed ? "✅" : "⏳"}</p>
|
||||
<p className="text-xs font-semibold text-slate-600 dark:text-slate-300">
|
||||
Opponent
|
||||
</p>
|
||||
<p className="text-xs text-slate-500 dark:text-slate-400 mt-1">
|
||||
{opponentRevealed ? "Revealed" : "Waiting"}
|
||||
</p>
|
||||
</div>
|
||||
<div className="p-4 rounded-lg text-center bg-blue-50 dark:bg-blue-900">
|
||||
<p className="text-sm font-mono text-slate-600 dark:text-slate-300">
|
||||
⏱️
|
||||
</p>
|
||||
<p className="text-xs font-semibold text-slate-600 dark:text-slate-300">
|
||||
Time Left
|
||||
</p>
|
||||
<p className="text-xs text-slate-500 dark:text-slate-400 mt-1">
|
||||
{0}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{!isGameFinishedByTimeout && (
|
||||
<>
|
||||
{/* Your Move Section - Hidden when both revealed */}
|
||||
{!bothRevealed && !timeoutExpired && (
|
||||
<div className="border p-6 rounded-lg bg-gradient-to-br from-slate-50 to-slate-100 dark:from-slate-700 dark:to-slate-800">
|
||||
<h2 className="font-semibold text-lg mb-4 text-slate-900 dark:text-white">
|
||||
Your Move
|
||||
</h2>
|
||||
{selectedMove ? (
|
||||
<div className="flex items-center justify-center gap-4">
|
||||
<div className="flex flex-col items-center">
|
||||
<span className="text-6xl mb-2">{MOVES[selectedMove].icon}</span>
|
||||
<span className="font-semibold text-lg">
|
||||
{MOVES[selectedMove].name}
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-3xl text-slate-400">→</div>
|
||||
<div className="bg-white dark:bg-slate-600 p-4 rounded-lg">
|
||||
<p className="text-xs text-slate-600 dark:text-slate-300 mb-1">
|
||||
Clear Move:
|
||||
</p>
|
||||
<code className="text-sm font-mono text-slate-700 dark:text-slate-200">
|
||||
{clearMove}
|
||||
</code>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-center text-slate-600 dark:text-slate-400">
|
||||
No move selected yet
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{/* Timeout Warning */}
|
||||
{timeoutExpired && (
|
||||
<div className="mb-6 p-4 bg-red-50 dark:bg-red-900 border-2 border-red-400 dark:border-red-600 rounded-lg">
|
||||
<p className="text-red-700 dark:text-red-200 font-semibold mb-3">
|
||||
⏱️ Reveal phase timeout expired!
|
||||
</p>
|
||||
<p className="text-sm text-red-600 dark:text-red-300 mb-3">
|
||||
{selfRevealed
|
||||
? "The opponent failed to reveal in time. You can claim victory!"
|
||||
: "You failed to reveal in time. The opponent can claim victory!"}
|
||||
</p>
|
||||
{selfRevealed && (
|
||||
<Button
|
||||
onClick={handleResolveTimeout}
|
||||
disabled={loading || !account || !contract}
|
||||
variant="primary"
|
||||
className="w-full py-3 text-lg"
|
||||
>
|
||||
{loading ? "Processing..." : "⚡ Resolve Timeout"}
|
||||
</Button>)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Game Status Section - Hidden when both revealed */}
|
||||
{!bothRevealed && !timeoutExpired && (
|
||||
<>
|
||||
|
||||
{/* Reveal Section - Hidden when both revealed */}
|
||||
{!bothRevealed && (
|
||||
<div className="border-2 border-blue-300 dark:border-blue-600 p-6 rounded-lg bg-blue-50 dark:bg-slate-700">
|
||||
<h2 className="font-semibold text-lg mb-4 text-slate-900 dark:text-white">
|
||||
Reveal Your Move
|
||||
</h2>
|
||||
<p className="text-sm text-slate-600 dark:text-slate-300 mb-4">
|
||||
Submit your clear move and secret to the blockchain. This proves you
|
||||
didn't cheat!
|
||||
</p>
|
||||
<Button
|
||||
onClick={handleReveal}
|
||||
disabled={loading || !account || !contract || !clearMove || selfRevealed}
|
||||
variant="primary"
|
||||
className="w-full py-3 text-lg"
|
||||
>
|
||||
{loading ? "Submitting..." : selfRevealed ? "✅ Revealed" : "Reveal Move"}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Winner Section - Only show if both revealed */}
|
||||
{bothRevealed && (
|
||||
<div className="space-y-4">
|
||||
{/* Moves Comparison */}
|
||||
<div className="border-2 border-slate-300 dark:border-slate-600 p-6 rounded-lg bg-slate-50 dark:bg-slate-800">
|
||||
<h2 className="font-semibold text-lg mb-4 text-slate-900 dark:text-white text-center">
|
||||
Final Moves
|
||||
</h2>
|
||||
<div className="flex items-center justify-center gap-8">
|
||||
{/* Your Move (always on left) */}
|
||||
<div className="flex flex-col items-center">
|
||||
<span className="text-6xl mb-2">
|
||||
{gameDetails && MOVES[String(whoAmI === "player1" ? gameDetails.playerA.move : gameDetails.playerB.move)]?.icon}
|
||||
</span>
|
||||
<span className="font-semibold text-slate-700 dark:text-slate-300">
|
||||
You
|
||||
</span>
|
||||
<span className="text-sm text-slate-500 dark:text-slate-400">
|
||||
{gameDetails && MOVES[String(whoAmI === "player1" ? gameDetails.playerA.move : gameDetails.playerB.move)]?.name}
|
||||
</span>
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<div
|
||||
className={`p-4 rounded-lg text-center ${
|
||||
selfRevealed
|
||||
? "bg-green-50 dark:bg-green-900"
|
||||
: "bg-slate-100 dark:bg-slate-700"
|
||||
}`}
|
||||
>
|
||||
<p className="text-2xl mb-1">{selfRevealed ? "✅" : "⏳"}</p>
|
||||
<p className="text-xs font-semibold text-slate-600 dark:text-slate-300">
|
||||
Me
|
||||
</p>
|
||||
<p className="text-xs text-slate-500 dark:text-slate-400 mt-1">
|
||||
{selfRevealed ? "Revealed" : "Waiting"}
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
className={`p-4 rounded-lg text-center ${
|
||||
opponentRevealed
|
||||
? "bg-green-50 dark:bg-green-900"
|
||||
: "bg-slate-100 dark:bg-slate-700"
|
||||
}`}
|
||||
>
|
||||
<p className="text-2xl mb-1">{opponentRevealed ? "✅" : "⏳"}</p>
|
||||
<p className="text-xs font-semibold text-slate-600 dark:text-slate-300">
|
||||
Opponent
|
||||
</p>
|
||||
<p className="text-xs text-slate-500 dark:text-slate-400 mt-1">
|
||||
{opponentRevealed ? "Revealed" : "Waiting"}
|
||||
</p>
|
||||
</div>
|
||||
<div className="p-4 rounded-lg text-center bg-blue-50 dark:bg-blue-900">
|
||||
<p className="text-sm font-mono text-slate-600 dark:text-slate-300">
|
||||
⏱️
|
||||
</p>
|
||||
<p className="text-xs font-semibold text-slate-600 dark:text-slate-300">
|
||||
Time Left
|
||||
</p>
|
||||
<p className="text-xs text-slate-500 dark:text-slate-400 mt-1">
|
||||
{revealTimeLeft}s
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Reveal Section - Hidden when both revealed */}
|
||||
{!bothRevealed && !timeoutExpired && (
|
||||
<div className="border-2 border-blue-300 dark:border-blue-600 p-6 rounded-lg bg-blue-50 dark:bg-slate-700">
|
||||
<h2 className="font-semibold text-lg mb-4 text-slate-900 dark:text-white">
|
||||
Reveal Your Move
|
||||
</h2>
|
||||
<p className="text-sm text-slate-600 dark:text-slate-300 mb-4">
|
||||
Submit your clear move and secret to the blockchain. This proves you
|
||||
didn't cheat!
|
||||
</p>
|
||||
<Button
|
||||
onClick={handleReveal}
|
||||
disabled={loading || !account || !contract || !clearMove || selfRevealed}
|
||||
variant="primary"
|
||||
className="w-full py-3 text-lg"
|
||||
>
|
||||
{loading ? "Submitting..." : selfRevealed ? "✅ Revealed" : "Reveal Move"}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Winner Section - Only show if both revealed */}
|
||||
{bothRevealed && (
|
||||
<div className="space-y-4">
|
||||
{/* Moves Comparison */}
|
||||
<div className="border-2 border-slate-300 dark:border-slate-600 p-6 rounded-lg bg-slate-50 dark:bg-slate-800">
|
||||
<h2 className="font-semibold text-lg mb-4 text-slate-900 dark:text-white text-center">
|
||||
Final Moves
|
||||
</h2>
|
||||
<div className="flex items-center justify-center gap-8">
|
||||
{/* Your Move (always on left) */}
|
||||
<div className="flex flex-col items-center">
|
||||
<span className="text-6xl mb-2">
|
||||
{gameDetails && MOVES[String(whoAmI === "player1" ? gameDetails.playerA.move : gameDetails.playerB.move)]?.icon}
|
||||
</span>
|
||||
<span className="font-semibold text-slate-700 dark:text-slate-300">
|
||||
You
|
||||
</span>
|
||||
<span className="text-sm text-slate-500 dark:text-slate-400">
|
||||
{gameDetails && MOVES[String(whoAmI === "player1" ? gameDetails.playerA.move : gameDetails.playerB.move)]?.name}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* VS */}
|
||||
<div className="flex flex-col items-center">
|
||||
<span className="text-4xl text-slate-400 dark:text-slate-500">
|
||||
VS
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Opponent Move (always on right) */}
|
||||
<div className="flex flex-col items-center">
|
||||
<span className="text-6xl mb-2">
|
||||
{gameDetails && MOVES[String(whoAmI === "player1" ? gameDetails.playerB.move : gameDetails.playerA.move)]?.icon}
|
||||
</span>
|
||||
<span className="font-semibold text-slate-700 dark:text-slate-300">
|
||||
Opponent
|
||||
</span>
|
||||
<span className="text-sm text-slate-500 dark:text-slate-400">
|
||||
{gameDetails && MOVES[String(whoAmI === "player1" ? gameDetails.playerB.move : gameDetails.playerA.move)]?.name}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* VS */}
|
||||
<div className="flex flex-col items-center">
|
||||
<span className="text-4xl text-slate-400 dark:text-slate-500">
|
||||
VS
|
||||
</span>
|
||||
</div>
|
||||
{/* Outcome Section */}
|
||||
<div
|
||||
className={`border-2 p-6 rounded-lg text-center ${
|
||||
outcomeData.color === "green"
|
||||
? "border-green-400 bg-green-50 dark:bg-green-900 dark:border-green-600"
|
||||
: outcomeData.color === "red"
|
||||
? "border-red-400 bg-red-50 dark:bg-red-900 dark:border-red-600"
|
||||
: outcomeData.color === "yellow"
|
||||
? "border-yellow-400 bg-yellow-50 dark:bg-yellow-900 dark:border-yellow-600"
|
||||
: "border-slate-400 bg-slate-100 dark:bg-slate-700 dark:border-slate-600"
|
||||
}`}
|
||||
>
|
||||
<p
|
||||
className={`text-6xl mb-3 ${
|
||||
outcomeData.color === "green"
|
||||
? "text-green-600 dark:text-green-400"
|
||||
: outcomeData.color === "red"
|
||||
? "text-red-600 dark:text-red-400"
|
||||
: outcomeData.color === "yellow"
|
||||
? "text-yellow-600 dark:text-yellow-400"
|
||||
: "text-slate-600 dark:text-slate-400"
|
||||
}`}
|
||||
>
|
||||
{outcomeData.emoji}
|
||||
</p>
|
||||
<h3
|
||||
className={`text-2xl font-bold ${
|
||||
outcomeData.color === "green"
|
||||
? "text-green-700 dark:text-green-300"
|
||||
: outcomeData.color === "red"
|
||||
? "text-red-700 dark:text-red-300"
|
||||
: outcomeData.color === "yellow"
|
||||
? "text-yellow-700 dark:text-yellow-300"
|
||||
: "text-slate-700 dark:text-slate-300"
|
||||
}`}
|
||||
>
|
||||
{outcomeData.name}
|
||||
</h3>
|
||||
|
||||
{/* Opponent Move (always on right) */}
|
||||
<div className="flex flex-col items-center">
|
||||
<span className="text-6xl mb-2">
|
||||
{gameDetails && MOVES[String(whoAmI === "player1" ? gameDetails.playerB.move : gameDetails.playerA.move)]?.icon}
|
||||
</span>
|
||||
<span className="font-semibold text-slate-700 dark:text-slate-300">
|
||||
Opponent
|
||||
</span>
|
||||
<span className="text-sm text-slate-500 dark:text-slate-400">
|
||||
{gameDetails && MOVES[String(whoAmI === "player1" ? gameDetails.playerB.move : gameDetails.playerA.move)]?.name}
|
||||
</span>
|
||||
{/* Display ETH winnings */}
|
||||
{gameDetails && outcome !== 0 && (
|
||||
<div className="mt-4 pt-4 border-t border-current border-opacity-20">
|
||||
<p className="text-sm text-slate-600 dark:text-slate-300 mb-2">
|
||||
{outcome === 1 ? "You Won" : outcome === 3 ? "Draw" : "You Lost"}
|
||||
</p>
|
||||
<p className="text-3xl font-bold">
|
||||
{outcome === 1 ? (
|
||||
<>
|
||||
+{web3?.utils.fromWei(String(BigInt(gameDetails.initialBet) * BigInt(2)), "ether")} ETH
|
||||
</>
|
||||
) : outcome === 3 ? (
|
||||
<>
|
||||
+{web3?.utils.fromWei(gameDetails.initialBet, "ether")} ETH
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
-{web3?.utils.fromWei(gameDetails.initialBet, "ether")} ETH
|
||||
</>
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Show Claim Coins button only on win or draw and if game is active */}
|
||||
{(outcome === 1 || outcome === 3) && gameDetails?.isActive && (
|
||||
<Button
|
||||
onClick={handleGetOutcome}
|
||||
disabled={loading || !account || !contract}
|
||||
variant="primary"
|
||||
className="mt-4 w-full py-3 text-lg"
|
||||
>
|
||||
{loading ? "Processing..." : "💰 Claim Coins"}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Outcome Section */}
|
||||
<div
|
||||
className={`border-2 p-6 rounded-lg text-center ${
|
||||
outcomeData.color === "green"
|
||||
? "border-green-400 bg-green-50 dark:bg-green-900 dark:border-green-600"
|
||||
: outcomeData.color === "red"
|
||||
? "border-red-400 bg-red-50 dark:bg-red-900 dark:border-red-600"
|
||||
: outcomeData.color === "yellow"
|
||||
? "border-yellow-400 bg-yellow-50 dark:bg-yellow-900 dark:border-yellow-600"
|
||||
: "border-slate-400 bg-slate-100 dark:bg-slate-700 dark:border-slate-600"
|
||||
}`}
|
||||
>
|
||||
<p
|
||||
className={`text-6xl mb-3 ${
|
||||
outcomeData.color === "green"
|
||||
? "text-green-600 dark:text-green-400"
|
||||
: outcomeData.color === "red"
|
||||
? "text-red-600 dark:text-red-400"
|
||||
: outcomeData.color === "yellow"
|
||||
? "text-yellow-600 dark:text-yellow-400"
|
||||
: "text-slate-600 dark:text-slate-400"
|
||||
}`}
|
||||
>
|
||||
{outcomeData.emoji}
|
||||
</p>
|
||||
<h3
|
||||
className={`text-2xl font-bold ${
|
||||
outcomeData.color === "green"
|
||||
? "text-green-700 dark:text-green-300"
|
||||
: outcomeData.color === "red"
|
||||
? "text-red-700 dark:text-red-300"
|
||||
: outcomeData.color === "yellow"
|
||||
? "text-yellow-700 dark:text-yellow-300"
|
||||
: "text-slate-700 dark:text-slate-300"
|
||||
}`}
|
||||
>
|
||||
{outcomeData.name}
|
||||
</h3>
|
||||
|
||||
{/* Display ETH winnings */}
|
||||
{gameDetails && (
|
||||
<div className="mt-4 pt-4 border-t border-current border-opacity-20">
|
||||
<p className="text-sm text-slate-600 dark:text-slate-300 mb-2">
|
||||
{outcome === 1 ? "You Won" : outcome === 3 ? "Draw" : "You Lost"}
|
||||
</p>
|
||||
<p className="text-3xl font-bold">
|
||||
{outcome === 1 ? (
|
||||
<>
|
||||
+{web3?.utils.fromWei(String(BigInt(gameDetails.initialBet) * BigInt(2)), "ether")} ETH
|
||||
</>
|
||||
) : outcome === 3 ? (
|
||||
<>
|
||||
+{web3?.utils.fromWei(gameDetails.initialBet, "ether")} ETH
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
-{web3?.utils.fromWei(gameDetails.initialBet, "ether")} ETH
|
||||
</>
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Show Claim Coins button only on win or draw and if game is active */}
|
||||
{(outcome === 1 || outcome === 3) && gameDetails?.isActive && (
|
||||
<Button
|
||||
onClick={handleGetOutcome}
|
||||
disabled={loading || !account || !contract}
|
||||
variant="primary"
|
||||
className="mt-4 w-full py-3 text-lg"
|
||||
>
|
||||
{loading ? "Processing..." : "💰 Claim Coins"}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user