mirror of
https://github.com/averel10/crypto_clash.git
synced 2026-03-12 10:58:11 +01:00
start fixing frontend
This commit is contained in:
@@ -18,6 +18,16 @@ interface CommitProps {
|
||||
gameDetails: GameDetails | null;
|
||||
setSecret: (secret: string) => void;
|
||||
savePlayMove: (playMove: string) => void;
|
||||
// MinusOne mode props
|
||||
selectedMove1?: string | null;
|
||||
setSelectedMove1?: (move: string | null) => void;
|
||||
selectedMove2?: string | null;
|
||||
setSelectedMove2?: (move: string | null) => void;
|
||||
secret1?: string;
|
||||
setSecret1?: (secret: string) => void;
|
||||
secret2?: string;
|
||||
setSecret2?: (secret: string) => void;
|
||||
isWithdrawPhase?: boolean;
|
||||
}
|
||||
|
||||
type MoveName = "Rock" | "Paper" | "Scissors";
|
||||
@@ -40,10 +50,21 @@ export default function Commit({
|
||||
setSecret,
|
||||
savePlayMove,
|
||||
whoAmI,
|
||||
gameDetails
|
||||
gameDetails,
|
||||
selectedMove1,
|
||||
setSelectedMove1,
|
||||
selectedMove2,
|
||||
setSelectedMove2,
|
||||
secret1,
|
||||
setSecret1,
|
||||
secret2,
|
||||
setSecret2,
|
||||
isWithdrawPhase = false,
|
||||
}: Readonly<CommitProps>) {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [playMove, setPlayMove] = useState<string>("");
|
||||
const [playMove1, setPlayMove1] = useState<string>("");
|
||||
const [playMove2, setPlayMove2] = useState<string>("");
|
||||
const [selfPlayed, setSelfPlayed] = useState<string>("");
|
||||
const [opponentPlayed, setOpponentPlayed] = useState<string>("");
|
||||
const [bothPlayed, setBothPlayed] = useState<string>("");
|
||||
@@ -52,17 +73,32 @@ export default function Commit({
|
||||
const [commitTimeLeft, setCommitTimeLeft] = useState<number>(0);
|
||||
const [timeoutExpired, setTimeoutExpired] = useState(false);
|
||||
|
||||
const isMinusOne = gameDetails?.gameMode === "minusone";
|
||||
|
||||
// Update encrypted move when move or secret changes
|
||||
useEffect(() => {
|
||||
if (selectedMove && secret) {
|
||||
if (isMinusOne && !isWithdrawPhase) {
|
||||
// MinusOne initial commit: two moves
|
||||
if (selectedMove1 && secret1) {
|
||||
const clearMove1 = `${selectedMove1}-${secret1}`;
|
||||
const hash1 = Web3.utils.keccak256(clearMove1);
|
||||
setPlayMove1(hash1);
|
||||
}
|
||||
if (selectedMove2 && secret2) {
|
||||
const clearMove2 = `${selectedMove2}-${secret2}`;
|
||||
const hash2 = Web3.utils.keccak256(clearMove2);
|
||||
setPlayMove2(hash2);
|
||||
}
|
||||
} else if (selectedMove && secret) {
|
||||
// Classic mode or withdrawal phase: single move/choice
|
||||
const clearMove = `${selectedMove}-${secret}`;
|
||||
// Use keccak256 (Ethereum's standard hash function)
|
||||
const hash = Web3.utils.keccak256(clearMove);
|
||||
setPlayMove(hash);
|
||||
// Persist to sessionStorage through parent
|
||||
savePlayMove(hash);
|
||||
if (!isWithdrawPhase) {
|
||||
savePlayMove(hash);
|
||||
}
|
||||
}
|
||||
}, [selectedMove, secret, savePlayMove]);
|
||||
}, [selectedMove, secret, selectedMove1, secret1, selectedMove2, secret2, isMinusOne, isWithdrawPhase, savePlayMove]);
|
||||
|
||||
// Auto-check if both players have committed and trigger callback
|
||||
useEffect(() => {
|
||||
@@ -75,9 +111,18 @@ export default function Commit({
|
||||
|
||||
const checkSelfPlayed = async () => {
|
||||
try {
|
||||
const encrMove = gameDetails[whoAmI === "player1" ? "playerA" : "playerB"].encrMove;
|
||||
|
||||
setSelfPlayed(Number(encrMove) !== 0 ? "true" : "false");
|
||||
const player = gameDetails[whoAmI === "player1" ? "playerA" : "playerB"];
|
||||
|
||||
if (isMinusOne && !isWithdrawPhase) {
|
||||
// Check hash1 for initial commit phase
|
||||
setSelfPlayed(player.hash1 && Number(player.hash1) !== 0 ? "true" : "false");
|
||||
} else if (isMinusOne && isWithdrawPhase) {
|
||||
// Check wHash for withdrawal commit phase
|
||||
setSelfPlayed(player.wHash && Number(player.wHash) !== 0 ? "true" : "false");
|
||||
} else {
|
||||
// Classic mode: check encrMove
|
||||
setSelfPlayed(player.encrMove && Number(player.encrMove) !== 0 ? "true" : "false");
|
||||
}
|
||||
} catch (err: any) {
|
||||
console.error("Auto-check self played failed:", err.message);
|
||||
}
|
||||
@@ -88,8 +133,15 @@ export default function Commit({
|
||||
const checkOpponentPlayed = async () => {
|
||||
try {
|
||||
const opponentKey = whoAmI === "player1" ? "playerB" : "playerA";
|
||||
const encrMove = gameDetails[opponentKey].encrMove;
|
||||
setOpponentPlayed(Number(encrMove) !== 0 ? "true" : "false");
|
||||
const opponent = gameDetails[opponentKey];
|
||||
|
||||
if (isMinusOne && !isWithdrawPhase) {
|
||||
setOpponentPlayed(opponent.hash1 && Number(opponent.hash1) !== 0 ? "true" : "false");
|
||||
} else if (isMinusOne && isWithdrawPhase) {
|
||||
setOpponentPlayed(opponent.wHash && Number(opponent.wHash) !== 0 ? "true" : "false");
|
||||
} else {
|
||||
setOpponentPlayed(opponent.encrMove && Number(opponent.encrMove) !== 0 ? "true" : "false");
|
||||
}
|
||||
} catch (err: any) {
|
||||
console.error("Auto-check opponent played failed:", err.message);
|
||||
}
|
||||
@@ -100,10 +152,22 @@ export default function Commit({
|
||||
// Check immediately on mount or when dependencies change
|
||||
const checkBothPlayed = async () => {
|
||||
try {
|
||||
const playerAEncrMove = gameDetails.playerA.encrMove;
|
||||
const playerBEncrMove = gameDetails.playerB.encrMove;
|
||||
|
||||
const res = Number(playerAEncrMove) !== 0 && Number(playerBEncrMove) !== 0;
|
||||
let res: boolean = false;
|
||||
|
||||
if (isMinusOne && !isWithdrawPhase) {
|
||||
const playerAHash = gameDetails.playerA.hash1;
|
||||
const playerBHash = gameDetails.playerB.hash1;
|
||||
res = !!(playerAHash && playerBHash && Number(playerAHash) !== 0 && Number(playerBHash) !== 0);
|
||||
} else if (isMinusOne && isWithdrawPhase) {
|
||||
const playerAWHash = gameDetails.playerA.wHash;
|
||||
const playerBWHash = gameDetails.playerB.wHash;
|
||||
res = !!(playerAWHash && playerBWHash && Number(playerAWHash) !== 0 && Number(playerBWHash) !== 0);
|
||||
} else {
|
||||
const playerAEncrMove = gameDetails.playerA.encrMove;
|
||||
const playerBEncrMove = gameDetails.playerB.encrMove;
|
||||
res = !!(playerAEncrMove && playerBEncrMove && Number(playerAEncrMove) !== 0 && Number(playerBEncrMove) !== 0);
|
||||
}
|
||||
|
||||
console.log("Both played check:", res);
|
||||
if (res) {
|
||||
setBothPlayed("true");
|
||||
@@ -118,7 +182,14 @@ export default function Commit({
|
||||
// Check commit timeout
|
||||
const checkCommitTimeout = async () => {
|
||||
try {
|
||||
const timeLeft = await contract.methods.commitTimeLeft(gameDetails.returnGameId).call();
|
||||
let timeLeft;
|
||||
if (isMinusOne) {
|
||||
// MinusOne uses getTimeLeft() for all phases
|
||||
timeLeft = await contract.methods.getTimeLeft(gameDetails.returnGameId).call();
|
||||
} else {
|
||||
// Classic uses commitTimeLeft()
|
||||
timeLeft = await contract.methods.commitTimeLeft(gameDetails.returnGameId).call();
|
||||
}
|
||||
console.log("Commit time left:", timeLeft);
|
||||
setCommitTimeLeft(Number(timeLeft));
|
||||
if (Number(timeLeft) <= 0) {
|
||||
@@ -145,11 +216,35 @@ export default function Commit({
|
||||
|
||||
// Commit phase read-only handlers
|
||||
const handlePlay = async () => {
|
||||
if (!contract || !web3 || !account || !playMove) return;
|
||||
if (!contract || !web3 || !account) return;
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
// playMove should be a hex string (bytes32)
|
||||
const tx = contract.methods.play(gameDetails?.returnGameId, playMove);
|
||||
let tx;
|
||||
|
||||
if (isMinusOne && !isWithdrawPhase) {
|
||||
// MinusOne initial commit: commitInitialMoves(gameId, hash1, hash2)
|
||||
if (!playMove1 || !playMove2) {
|
||||
showToast("Please select both moves and enter secrets", "error");
|
||||
return;
|
||||
}
|
||||
tx = contract.methods.commitInitialMoves(gameDetails?.returnGameId, playMove1, playMove2);
|
||||
} else if (isMinusOne && isWithdrawPhase) {
|
||||
// MinusOne withdrawal commit: commitWithdraw(gameId, wHash)
|
||||
if (!playMove) {
|
||||
showToast("Please select withdrawal choice and enter secret", "error");
|
||||
return;
|
||||
}
|
||||
tx = contract.methods.commitWithdraw(gameDetails?.returnGameId, playMove);
|
||||
} else {
|
||||
// Classic mode: play(gameId, hash)
|
||||
if (!playMove) {
|
||||
showToast("Please select a move and enter secret", "error");
|
||||
return;
|
||||
}
|
||||
tx = contract.methods.play(gameDetails?.returnGameId, playMove);
|
||||
}
|
||||
|
||||
const gas = await tx.estimateGas({ from: account });
|
||||
const result = await (globalThis as any).ethereum.request({
|
||||
method: "eth_sendTransaction",
|
||||
@@ -163,10 +258,10 @@ export default function Commit({
|
||||
},
|
||||
],
|
||||
});
|
||||
showToast("Play tx sent: " + result, "success");
|
||||
showToast("Commit tx sent: " + result, "success");
|
||||
setMoveSubmitted(true);
|
||||
} catch (err: any) {
|
||||
showToast("Play failed: " + err.message, "error");
|
||||
showToast("Commit failed: " + err.message, "error");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
@@ -174,16 +269,43 @@ export default function Commit({
|
||||
|
||||
const regenerateSecret = () => {
|
||||
const randomHex = Math.random().toString(16).slice(2, 18);
|
||||
setSecret(randomHex);
|
||||
if (isMinusOne && !isWithdrawPhase) {
|
||||
// For MinusOne, we might want separate secrets
|
||||
// For now, let's keep them simple
|
||||
setSecret1?.(randomHex);
|
||||
} else {
|
||||
setSecret(randomHex);
|
||||
}
|
||||
};
|
||||
|
||||
const regenerateSecret2 = () => {
|
||||
const randomHex = Math.random().toString(16).slice(2, 18);
|
||||
setSecret2?.(randomHex);
|
||||
};
|
||||
|
||||
const handleSecretChange = (value: string) => {
|
||||
setSecret(value);
|
||||
};
|
||||
|
||||
const handleSecret1Change = (value: string) => {
|
||||
setSecret1?.(value);
|
||||
};
|
||||
|
||||
const handleSecret2Change = (value: string) => {
|
||||
setSecret2?.(value);
|
||||
};
|
||||
|
||||
const handleMoveSelect = (move: string) => {
|
||||
setSelectedMove(move);
|
||||
};
|
||||
|
||||
const handleMove1Select = (move: string) => {
|
||||
setSelectedMove1?.(move);
|
||||
};
|
||||
|
||||
const handleMove2Select = (move: string) => {
|
||||
setSelectedMove2?.(move);
|
||||
};
|
||||
|
||||
const handleResolveTimeout = async () => {
|
||||
if (!contract || !web3 || !account) return;
|
||||
@@ -331,76 +453,264 @@ export default function Commit({
|
||||
</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>
|
||||
{/* Move Selection - Different UI for MinusOne vs Classic */}
|
||||
{isMinusOne && !isWithdrawPhase ? (
|
||||
// MinusOne Mode: Select TWO different moves
|
||||
<>
|
||||
<div className="mb-6">
|
||||
<p className="text-sm text-slate-600 dark:text-slate-300 mb-4 font-medium">
|
||||
Choose your FIRST move:
|
||||
</p>
|
||||
<div className="flex gap-4 justify-center">
|
||||
{(["1", "2", "3"] as const).map((move) => (
|
||||
<button
|
||||
key={move}
|
||||
onClick={() => handleMove1Select(move)}
|
||||
disabled={selectedMove2 === move}
|
||||
className={`flex flex-col items-center justify-center p-6 rounded-lg transition-all transform ${
|
||||
selectedMove1 === move
|
||||
? "bg-blue-500 text-white shadow-lg scale-110"
|
||||
: selectedMove2 === move
|
||||
? "bg-slate-300 dark:bg-slate-600 text-slate-400 dark:text-slate-500 cursor-not-allowed"
|
||||
: "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>
|
||||
|
||||
{/* 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>
|
||||
<div className="mb-6 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 for first move:
|
||||
</label>
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
type="text"
|
||||
value={secret1 || ""}
|
||||
onChange={(e) => handleSecret1Change(e.target.value)}
|
||||
placeholder="Your secret passphrase"
|
||||
className="flex-1"
|
||||
/>
|
||||
<Button
|
||||
onClick={regenerateSecret}
|
||||
variant="secondary"
|
||||
disabled={loading}
|
||||
>
|
||||
🔄 New
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mb-6">
|
||||
<p className="text-sm text-slate-600 dark:text-slate-300 mb-4 font-medium">
|
||||
Choose your SECOND move (must be different):
|
||||
</p>
|
||||
<div className="flex gap-4 justify-center">
|
||||
{(["1", "2", "3"] as const).map((move) => (
|
||||
<button
|
||||
key={move}
|
||||
onClick={() => handleMove2Select(move)}
|
||||
disabled={selectedMove1 === move}
|
||||
className={`flex flex-col items-center justify-center p-6 rounded-lg transition-all transform ${
|
||||
selectedMove2 === move
|
||||
? "bg-green-500 text-white shadow-lg scale-110"
|
||||
: selectedMove1 === move
|
||||
? "bg-slate-300 dark:bg-slate-600 text-slate-400 dark:text-slate-500 cursor-not-allowed"
|
||||
: "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>
|
||||
|
||||
<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 for second move:
|
||||
</label>
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
type="text"
|
||||
value={secret2 || ""}
|
||||
onChange={(e) => handleSecret2Change(e.target.value)}
|
||||
placeholder="Your secret passphrase"
|
||||
className="flex-1"
|
||||
/>
|
||||
<Button
|
||||
onClick={regenerateSecret2}
|
||||
variant="secondary"
|
||||
disabled={loading}
|
||||
>
|
||||
🔄 New
|
||||
</Button>
|
||||
</div>
|
||||
<p className="text-xs text-slate-500 dark:text-slate-400 mt-2">
|
||||
Keep both secrets safe! You'll need them to reveal your moves later.
|
||||
</p>
|
||||
</div>
|
||||
</>
|
||||
) : isWithdrawPhase ? (
|
||||
// Withdrawal Phase: Choose which move to withdraw (1 or 2)
|
||||
<>
|
||||
<div className="mb-6 p-4 bg-yellow-50 dark:bg-yellow-900 border-2 border-yellow-300 dark:border-yellow-700 rounded-lg">
|
||||
<p className="text-yellow-800 dark:text-yellow-200 text-sm mb-2 font-medium">
|
||||
🎯 Withdrawal Phase
|
||||
</p>
|
||||
<p className="text-yellow-700 dark:text-yellow-300 text-sm">
|
||||
Choose which move to WITHDRAW (1 or 2). The remaining move will be used in the final battle!
|
||||
</p>
|
||||
</div>
|
||||
<div className="mb-6">
|
||||
<p className="text-sm text-slate-600 dark:text-slate-300 mb-4 font-medium">
|
||||
Which move do you want to withdraw?
|
||||
</p>
|
||||
<div className="flex gap-4 justify-center">
|
||||
{(["1", "2"] as const).map((choice) => (
|
||||
<button
|
||||
key={choice}
|
||||
onClick={() => handleMoveSelect(choice)}
|
||||
className={`flex flex-col items-center justify-center p-8 rounded-lg transition-all transform ${
|
||||
selectedMove === choice
|
||||
? "bg-red-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-6xl mb-2">{choice === "1" ? "1️⃣" : "2️⃣"}</span>
|
||||
<span className="font-semibold">Withdraw Move {choice}</span>
|
||||
<span className="text-xs mt-1 opacity-75">
|
||||
{choice === "1" ? "Keep move 2" : "Keep move 1"}
|
||||
</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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 for withdrawal:
|
||||
</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">
|
||||
This secret is for your withdrawal choice. Keep it safe!
|
||||
</p>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
// Classic Mode: Select ONE move
|
||||
<>
|
||||
<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>
|
||||
|
||||
{/* 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):
|
||||
{isMinusOne && !isWithdrawPhase ? "Encrypted Moves (to be sent):" : "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>
|
||||
{isMinusOne && !isWithdrawPhase ? (
|
||||
<>
|
||||
<div className="bg-white dark:bg-slate-700 p-3 rounded border border-blue-200 dark:border-blue-700 overflow-x-auto mb-2">
|
||||
<p className="text-xs text-slate-500 dark:text-slate-400 mb-1">Move 1:</p>
|
||||
<code className="text-xs text-slate-600 dark:text-slate-300 font-mono break-all">
|
||||
{playMove1 || "Select first move and enter secret"}
|
||||
</code>
|
||||
</div>
|
||||
<div className="bg-white dark:bg-slate-700 p-3 rounded border border-green-200 dark:border-green-700 overflow-x-auto">
|
||||
<p className="text-xs text-slate-500 dark:text-slate-400 mb-1">Move 2:</p>
|
||||
<code className="text-xs text-slate-600 dark:text-slate-300 font-mono break-all">
|
||||
{playMove2 || "Select second move and enter secret"}
|
||||
</code>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<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/choice 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}
|
||||
disabled={
|
||||
loading ||
|
||||
!account ||
|
||||
!contract ||
|
||||
(isMinusOne && !isWithdrawPhase ? (!selectedMove1 || !selectedMove2 || !secret1 || !secret2) : (!selectedMove || !secret))
|
||||
}
|
||||
variant="primary"
|
||||
className="w-full py-3 text-lg"
|
||||
>
|
||||
{loading ? "Submitting..." : "Submit Move"}
|
||||
{loading ? "Submitting..." : isWithdrawPhase ? "Submit Withdrawal Choice" : "Submit Move"}
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
|
||||
@@ -181,17 +181,44 @@ export default function GameList({
|
||||
};
|
||||
|
||||
const getGamePhase = (game: GameDetails) => {
|
||||
const playerARevealed = Number(game.playerA.move) !== 0;
|
||||
const playerBRevealed = Number(game.playerB.move) !== 0;
|
||||
const playerACommitted = Number(game.playerA.encrMove) !== 0;
|
||||
const playerBCommitted = Number(game.playerB.encrMove) !== 0;
|
||||
|
||||
if (playerARevealed && playerBRevealed) {
|
||||
return { phase: "Outcome", color: "bg-purple-100 dark:bg-purple-900 text-purple-800 dark:text-purple-200" };
|
||||
} else if (playerACommitted && playerBCommitted) {
|
||||
return { phase: "Reveal", color: "bg-blue-100 dark:bg-blue-900 text-blue-800 dark:text-blue-200" };
|
||||
const isMinusOne = game.gameMode === "minusone";
|
||||
|
||||
if (isMinusOne) {
|
||||
// MinusOne game phases
|
||||
const playerARevealed1 = Number(game.playerA.move1) !== 0;
|
||||
const playerBRevealed1 = Number(game.playerB.move1) !== 0;
|
||||
const playerAWithdrawn = Number(game.playerA.withdrawn) !== 0;
|
||||
const playerBWithdrawn = Number(game.playerB.withdrawn) !== 0;
|
||||
const playerACommitted1 = Number(game.playerA.hash1) !== 0;
|
||||
const playerBCommitted1 = Number(game.playerB.hash1) !== 0;
|
||||
const playerACommittedW = Number(game.playerA.wHash) !== 0;
|
||||
const playerBCommittedW = Number(game.playerB.wHash) !== 0;
|
||||
|
||||
if (playerAWithdrawn && playerBWithdrawn) {
|
||||
return { phase: "Done", color: "bg-purple-100 dark:bg-purple-900 text-purple-800 dark:text-purple-200" };
|
||||
} else if (playerACommittedW && playerBCommittedW) {
|
||||
return { phase: "Withdraw Reveal", color: "bg-orange-100 dark:bg-orange-900 text-orange-800 dark:text-orange-200" };
|
||||
} else if (playerARevealed1 && playerBRevealed1) {
|
||||
return { phase: "Withdraw Commit", color: "bg-amber-100 dark:bg-amber-900 text-amber-800 dark:text-amber-200" };
|
||||
} else if (playerACommitted1 && playerBCommitted1) {
|
||||
return { phase: "Initial Reveal", color: "bg-blue-100 dark:bg-blue-900 text-blue-800 dark:text-blue-200" };
|
||||
} else {
|
||||
return { phase: "Initial Commit", color: "bg-yellow-100 dark:bg-yellow-900 text-yellow-800 dark:text-yellow-200" };
|
||||
}
|
||||
} else {
|
||||
return { phase: "Commit", color: "bg-yellow-100 dark:bg-yellow-900 text-yellow-800 dark:text-yellow-200" };
|
||||
// Classic game phases
|
||||
const playerARevealed = Number(game.playerA.move) !== 0;
|
||||
const playerBRevealed = Number(game.playerB.move) !== 0;
|
||||
const playerACommitted = Number(game.playerA.encrMove) !== 0;
|
||||
const playerBCommitted = Number(game.playerB.encrMove) !== 0;
|
||||
|
||||
if (playerARevealed && playerBRevealed) {
|
||||
return { phase: "Outcome", color: "bg-purple-100 dark:bg-purple-900 text-purple-800 dark:text-purple-200" };
|
||||
} else if (playerACommitted && playerBCommitted) {
|
||||
return { phase: "Reveal", color: "bg-blue-100 dark:bg-blue-900 text-blue-800 dark:text-blue-200" };
|
||||
} else {
|
||||
return { phase: "Commit", color: "bg-yellow-100 dark:bg-yellow-900 text-yellow-800 dark:text-yellow-200" };
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -9,8 +9,16 @@ import { showErrorToast } from "@/app/lib/toast";
|
||||
export type Player = {
|
||||
addr: string;
|
||||
bet: string;
|
||||
encrMove: string;
|
||||
move: number;
|
||||
// Classic mode fields
|
||||
encrMove?: string;
|
||||
move?: number;
|
||||
// MinusOne mode fields
|
||||
hash1?: string;
|
||||
hash2?: string;
|
||||
move1?: number;
|
||||
move2?: number;
|
||||
wHash?: string;
|
||||
withdrawn?: number;
|
||||
nickname: string;
|
||||
};
|
||||
|
||||
@@ -22,6 +30,7 @@ export type GameDetails = {
|
||||
isActive: boolean;
|
||||
returnGameId: number;
|
||||
gameMode?: string; // "classic" or "minusone"
|
||||
phase?: number; // GamePhase for minusone mode
|
||||
};
|
||||
|
||||
interface GameModalProps {
|
||||
@@ -45,20 +54,38 @@ export default function GameModal({
|
||||
web3,
|
||||
setStatus,
|
||||
}: Readonly<GameModalProps>) {
|
||||
const [phase, setPhase] = useState<"commit" | "reveal">("commit");
|
||||
const [phase, setPhase] = useState<"commit" | "reveal" | "withdrawCommit" | "withdrawReveal">("commit");
|
||||
const [whoAmI, setWhoAmI] = useState<"player1" | "player2" | "">("");
|
||||
const [gameDetails, setGameDetails] = useState<GameDetails | null>(null);
|
||||
|
||||
// Classic mode state
|
||||
const [selectedMove, setSelectedMove] = useState<string | null>(null);
|
||||
const [secret, setSecret] = useState<string>("");
|
||||
|
||||
// MinusOne mode state
|
||||
const [selectedMove1, setSelectedMove1] = useState<string | null>(null);
|
||||
const [selectedMove2, setSelectedMove2] = useState<string | null>(null);
|
||||
const [secret1, setSecret1] = useState<string>("");
|
||||
const [secret2, setSecret2] = useState<string>("");
|
||||
const [withdrawChoice, setWithdrawChoice] = useState<string | null>(null);
|
||||
const [withdrawSecret, setWithdrawSecret] = useState<string>("");
|
||||
|
||||
// Helper function to generate game-specific storage key
|
||||
const getGameStorageKey = () => `game_${gameDetails?.returnGameId}`;
|
||||
|
||||
// Game storage object structure
|
||||
type GameStorage = {
|
||||
secret: string;
|
||||
selectedMove: string | null;
|
||||
playMove: string;
|
||||
// Classic mode
|
||||
secret?: string;
|
||||
selectedMove?: string | null;
|
||||
playMove?: string;
|
||||
// MinusOne mode
|
||||
secret1?: string;
|
||||
secret2?: string;
|
||||
selectedMove1?: string | null;
|
||||
selectedMove2?: string | null;
|
||||
withdrawChoice?: string | null;
|
||||
withdrawSecret?: string;
|
||||
timestamp?: number;
|
||||
};
|
||||
|
||||
@@ -101,8 +128,16 @@ export default function GameModal({
|
||||
if (storedData) {
|
||||
try {
|
||||
const parsed: GameStorage = JSON.parse(storedData);
|
||||
// Classic mode
|
||||
if (parsed.secret) setSecret(parsed.secret);
|
||||
if (parsed.selectedMove) setSelectedMove(parsed.selectedMove);
|
||||
// MinusOne mode
|
||||
if (parsed.secret1) setSecret1(parsed.secret1);
|
||||
if (parsed.secret2) setSecret2(parsed.secret2);
|
||||
if (parsed.selectedMove1) setSelectedMove1(parsed.selectedMove1);
|
||||
if (parsed.selectedMove2) setSelectedMove2(parsed.selectedMove2);
|
||||
if (parsed.withdrawChoice) setWithdrawChoice(parsed.withdrawChoice);
|
||||
if (parsed.withdrawSecret) setWithdrawSecret(parsed.withdrawSecret);
|
||||
} catch (err) {
|
||||
console.error("Failed to parse stored game data:", err);
|
||||
}
|
||||
@@ -111,7 +146,7 @@ export default function GameModal({
|
||||
|
||||
const saveGameData = (updates: Partial<GameStorage>) => {
|
||||
const storedData = sessionStorage.getItem(getGameStorageKey());
|
||||
let currentData: GameStorage = { secret: "", selectedMove: null, playMove: "", timestamp: Date.now() };
|
||||
let currentData: GameStorage = { timestamp: Date.now() };
|
||||
|
||||
if (storedData) {
|
||||
try {
|
||||
@@ -125,6 +160,7 @@ export default function GameModal({
|
||||
sessionStorage.setItem(getGameStorageKey(), JSON.stringify(updatedData));
|
||||
};
|
||||
|
||||
// Classic mode save functions
|
||||
const saveSecret = (value: string) => {
|
||||
setSecret(value);
|
||||
saveGameData({ secret: value });
|
||||
@@ -141,6 +177,43 @@ export default function GameModal({
|
||||
saveGameData({ playMove });
|
||||
};
|
||||
|
||||
// MinusOne mode save functions
|
||||
const saveSecret1 = (value: string) => {
|
||||
setSecret1(value);
|
||||
saveGameData({ secret1: value });
|
||||
};
|
||||
|
||||
const saveSecret2 = (value: string) => {
|
||||
setSecret2(value);
|
||||
saveGameData({ secret2: value });
|
||||
};
|
||||
|
||||
const saveMoveSelection1 = (move: string | null) => {
|
||||
setSelectedMove1(move);
|
||||
if (move !== null) {
|
||||
saveGameData({ selectedMove1: move });
|
||||
}
|
||||
};
|
||||
|
||||
const saveMoveSelection2 = (move: string | null) => {
|
||||
setSelectedMove2(move);
|
||||
if (move !== null) {
|
||||
saveGameData({ selectedMove2: move });
|
||||
}
|
||||
};
|
||||
|
||||
const saveWithdrawChoice = (choice: string | null) => {
|
||||
setWithdrawChoice(choice);
|
||||
if (choice !== null) {
|
||||
saveGameData({ withdrawChoice: choice });
|
||||
}
|
||||
};
|
||||
|
||||
const saveWithdrawSecret = (value: string) => {
|
||||
setWithdrawSecret(value);
|
||||
saveGameData({ withdrawSecret: value });
|
||||
};
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
const fetchPlayerInfo = async () => {
|
||||
@@ -163,23 +236,44 @@ export default function GameModal({
|
||||
console.log("Game details:", details);
|
||||
setGameDetails(details);
|
||||
|
||||
// Determine the correct phase based on game state
|
||||
const playerAHasMove = Number(details.playerA.encrMove) !== 0;
|
||||
const playerBHasMove = Number(details.playerB.encrMove) !== 0;
|
||||
const playerARevealed = Number(details.playerA.move) !== 0;
|
||||
const playerBRevealed = Number(details.playerB.move) !== 0;
|
||||
// Determine game mode
|
||||
const isMinusOne = details.gameMode === "minusone";
|
||||
|
||||
// If both players have revealed their moves, show reveal phase (with results)
|
||||
if (playerARevealed && playerBRevealed) {
|
||||
setPhase("reveal");
|
||||
}
|
||||
// If both players have committed but not revealed, show reveal phase
|
||||
else if (playerAHasMove && playerBHasMove) {
|
||||
setPhase("reveal");
|
||||
}
|
||||
// Otherwise, show commit phase
|
||||
else {
|
||||
setPhase("commit");
|
||||
if (isMinusOne) {
|
||||
// MinusOne mode: use phase enum
|
||||
// GamePhase enum: Reg=0, InitC=1, FirstR=2, WithdC=3, WithdR=4, Done=5
|
||||
const gamePhase = Number(details.phase);
|
||||
|
||||
if (gamePhase === 5) { // Done
|
||||
setPhase("reveal"); // Show final results
|
||||
} else if (gamePhase === 4) { // WithdR
|
||||
setPhase("withdrawReveal");
|
||||
} else if (gamePhase === 3) { // WithdC
|
||||
setPhase("withdrawCommit");
|
||||
} else if (gamePhase === 2) { // FirstR
|
||||
setPhase("reveal");
|
||||
} else { // InitC or Reg
|
||||
setPhase("commit");
|
||||
}
|
||||
} else {
|
||||
// Classic mode: check encrMove and move fields
|
||||
const playerAHasMove = details.playerA.encrMove && Number(details.playerA.encrMove) !== 0;
|
||||
const playerBHasMove = details.playerB.encrMove && Number(details.playerB.encrMove) !== 0;
|
||||
const playerARevealed = details.playerA.move && Number(details.playerA.move) !== 0;
|
||||
const playerBRevealed = details.playerB.move && Number(details.playerB.move) !== 0;
|
||||
|
||||
// If both players have revealed their moves, show reveal phase (with results)
|
||||
if (playerARevealed && playerBRevealed) {
|
||||
setPhase("reveal");
|
||||
}
|
||||
// If both players have committed but not revealed, show reveal phase
|
||||
else if (playerAHasMove && playerBHasMove) {
|
||||
setPhase("reveal");
|
||||
}
|
||||
// Otherwise, show commit phase
|
||||
else {
|
||||
setPhase("commit");
|
||||
}
|
||||
}
|
||||
} catch (err: any) {
|
||||
showErrorToast("Error fetching game details: " + err.message);
|
||||
@@ -218,6 +312,12 @@ export default function GameModal({
|
||||
setPhase("commit");
|
||||
setSelectedMove(null);
|
||||
setSecret("");
|
||||
setSelectedMove1(null);
|
||||
setSelectedMove2(null);
|
||||
setSecret1("");
|
||||
setSecret2("");
|
||||
setWithdrawChoice(null);
|
||||
setWithdrawSecret("");
|
||||
onClose();
|
||||
};
|
||||
|
||||
@@ -234,9 +334,10 @@ export default function GameModal({
|
||||
{gameId ? `Game #${gameId}` : "Game"}
|
||||
</h2>
|
||||
<p className="text-sm text-slate-600 dark:text-slate-400 mt-1">
|
||||
{phase === "commit"
|
||||
? "Commit your move"
|
||||
: "Reveal your move"}
|
||||
{phase === "commit" && "Commit your move"}
|
||||
{phase === "reveal" && "Reveal your move"}
|
||||
{phase === "withdrawCommit" && "Choose which move to withdraw"}
|
||||
{phase === "withdrawReveal" && "Reveal your withdrawal choice"}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
@@ -266,6 +367,15 @@ export default function GameModal({
|
||||
secret={secret}
|
||||
setSecret={saveSecret}
|
||||
savePlayMove={savePlayMove}
|
||||
// MinusOne mode props
|
||||
selectedMove1={selectedMove1}
|
||||
setSelectedMove1={saveMoveSelection1}
|
||||
selectedMove2={selectedMove2}
|
||||
setSelectedMove2={saveMoveSelection2}
|
||||
secret1={secret1}
|
||||
setSecret1={saveSecret1}
|
||||
secret2={secret2}
|
||||
setSecret2={saveSecret2}
|
||||
/>
|
||||
)}
|
||||
{phase === "reveal" && (
|
||||
@@ -279,6 +389,42 @@ export default function GameModal({
|
||||
secret={secret}
|
||||
gameDetails={gameDetails}
|
||||
whoAmI={whoAmI}
|
||||
// MinusOne mode props
|
||||
selectedMove1={selectedMove1}
|
||||
selectedMove2={selectedMove2}
|
||||
secret1={secret1}
|
||||
secret2={secret2}
|
||||
/>
|
||||
)}
|
||||
{phase === "withdrawCommit" && (
|
||||
<Commit
|
||||
account={account}
|
||||
contract={contract}
|
||||
config={config}
|
||||
web3={web3}
|
||||
whoAmI={whoAmI}
|
||||
gameDetails={gameDetails}
|
||||
setStatus={setStatus}
|
||||
selectedMove={withdrawChoice}
|
||||
setSelectedMove={saveWithdrawChoice}
|
||||
secret={withdrawSecret}
|
||||
setSecret={saveWithdrawSecret}
|
||||
savePlayMove={() => {}}
|
||||
isWithdrawPhase={true}
|
||||
/>
|
||||
)}
|
||||
{phase === "withdrawReveal" && (
|
||||
<Reveal
|
||||
account={account}
|
||||
contract={contract}
|
||||
config={config}
|
||||
web3={web3}
|
||||
setStatus={setStatus}
|
||||
selectedMove={withdrawChoice}
|
||||
secret={withdrawSecret}
|
||||
gameDetails={gameDetails}
|
||||
whoAmI={whoAmI}
|
||||
isWithdrawPhase={true}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -14,6 +14,12 @@ interface RevealProps {
|
||||
secret: string;
|
||||
gameDetails: GameDetails | null;
|
||||
whoAmI: "player1" | "player2" | "";
|
||||
// MinusOne mode props
|
||||
selectedMove1?: string | null;
|
||||
selectedMove2?: string | null;
|
||||
secret1?: string;
|
||||
secret2?: string;
|
||||
isWithdrawPhase?: boolean;
|
||||
}
|
||||
|
||||
type MoveName = "Rock" | "Paper" | "Scissors";
|
||||
@@ -42,6 +48,11 @@ export default function Reveal({
|
||||
secret,
|
||||
gameDetails,
|
||||
whoAmI,
|
||||
selectedMove1,
|
||||
selectedMove2,
|
||||
secret1,
|
||||
secret2,
|
||||
isWithdrawPhase = false,
|
||||
}: Readonly<RevealProps>) {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [selfRevealed, setSelfRevealed] = useState(false);
|
||||
@@ -51,30 +62,76 @@ export default function Reveal({
|
||||
const [revealTimeLeft, setRevealTimeLeft] = useState<number>(0);
|
||||
const [timeoutExpired, setTimeoutExpired] = useState(false);
|
||||
|
||||
const isMinusOne = gameDetails?.gameMode === "minusone";
|
||||
|
||||
// Generate clear text for reveal
|
||||
const clearMove = selectedMove && secret ? `${selectedMove}-${secret}` : "";
|
||||
const clearMove1 = selectedMove1 && secret1 ? `${selectedMove1}-${secret1}` : "";
|
||||
const clearMove2 = selectedMove2 && secret2 ? `${selectedMove2}-${secret2}` : "";
|
||||
|
||||
// Check game status on mount
|
||||
useEffect(() => {
|
||||
const setStateFromGameDetails = () => {
|
||||
if (!gameDetails) return;
|
||||
const playerARevealed = Number(gameDetails.playerA.move) !== 0;
|
||||
const playerBRevealed = Number(gameDetails.playerB.move) !== 0;
|
||||
|
||||
if (isMinusOne && !isWithdrawPhase) {
|
||||
// MinusOne initial reveal: check move1
|
||||
const playerARevealed = !!(gameDetails.playerA.move1 && Number(gameDetails.playerA.move1) !== 0);
|
||||
const playerBRevealed = !!(gameDetails.playerB.move1 && Number(gameDetails.playerB.move1) !== 0);
|
||||
|
||||
setSelfRevealed(
|
||||
(whoAmI === "player1" && playerARevealed) ||
|
||||
(whoAmI === "player2" && playerBRevealed)
|
||||
);
|
||||
setOpponentRevealed(
|
||||
(whoAmI === "player1" && playerBRevealed) ||
|
||||
(whoAmI === "player2" && playerARevealed)
|
||||
);
|
||||
setBothRevealed(playerARevealed && playerBRevealed);
|
||||
if(bothRevealed){
|
||||
if(Number(gameDetails.outcome) === 1 && whoAmI === "player1") setOutcome(1);
|
||||
else if(Number(gameDetails.outcome) === 2 && whoAmI === "player2") setOutcome(1);
|
||||
else if(Number(gameDetails.outcome) === 1 && whoAmI === "player2") setOutcome(2);
|
||||
else if(Number(gameDetails.outcome) === 2 && whoAmI === "player1") setOutcome(2);
|
||||
else setOutcome(3);
|
||||
setSelfRevealed(
|
||||
(whoAmI === "player1" && playerARevealed) ||
|
||||
(whoAmI === "player2" && playerBRevealed)
|
||||
);
|
||||
setOpponentRevealed(
|
||||
(whoAmI === "player1" && playerBRevealed) ||
|
||||
(whoAmI === "player2" && playerARevealed)
|
||||
);
|
||||
setBothRevealed(playerARevealed && playerBRevealed);
|
||||
} else if (isMinusOne && isWithdrawPhase) {
|
||||
// MinusOne withdrawal reveal: check withdrawn field
|
||||
const playerARevealed = !!(gameDetails.playerA.withdrawn && Number(gameDetails.playerA.withdrawn) !== 0);
|
||||
const playerBRevealed = !!(gameDetails.playerB.withdrawn && Number(gameDetails.playerB.withdrawn) !== 0);
|
||||
|
||||
setSelfRevealed(
|
||||
(whoAmI === "player1" && playerARevealed) ||
|
||||
(whoAmI === "player2" && playerBRevealed)
|
||||
);
|
||||
setOpponentRevealed(
|
||||
(whoAmI === "player1" && playerBRevealed) ||
|
||||
(whoAmI === "player2" && playerARevealed)
|
||||
);
|
||||
setBothRevealed(playerARevealed && playerBRevealed);
|
||||
|
||||
// Set outcome when both revealed withdrawal
|
||||
if(bothRevealed){
|
||||
if(Number(gameDetails.outcome) === 1 && whoAmI === "player1") setOutcome(1);
|
||||
else if(Number(gameDetails.outcome) === 2 && whoAmI === "player2") setOutcome(1);
|
||||
else if(Number(gameDetails.outcome) === 1 && whoAmI === "player2") setOutcome(2);
|
||||
else if(Number(gameDetails.outcome) === 2 && whoAmI === "player1") setOutcome(2);
|
||||
else if(Number(gameDetails.outcome) === 3) setOutcome(3);
|
||||
}
|
||||
} else {
|
||||
// Classic mode: check move field
|
||||
const playerARevealed = !!(gameDetails.playerA.move && Number(gameDetails.playerA.move) !== 0);
|
||||
const playerBRevealed = !!(gameDetails.playerB.move && Number(gameDetails.playerB.move) !== 0);
|
||||
|
||||
setSelfRevealed(
|
||||
(whoAmI === "player1" && playerARevealed) ||
|
||||
(whoAmI === "player2" && playerBRevealed)
|
||||
);
|
||||
setOpponentRevealed(
|
||||
(whoAmI === "player1" && playerBRevealed) ||
|
||||
(whoAmI === "player2" && playerARevealed)
|
||||
);
|
||||
setBothRevealed(playerARevealed && playerBRevealed);
|
||||
if(bothRevealed){
|
||||
if(Number(gameDetails.outcome) === 1 && whoAmI === "player1") setOutcome(1);
|
||||
else if(Number(gameDetails.outcome) === 2 && whoAmI === "player2") setOutcome(1);
|
||||
else if(Number(gameDetails.outcome) === 1 && whoAmI === "player2") setOutcome(2);
|
||||
else if(Number(gameDetails.outcome) === 2 && whoAmI === "player1") setOutcome(2);
|
||||
else setOutcome(3);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -84,7 +141,14 @@ export default function Reveal({
|
||||
const checkRevealTimeout = async () => {
|
||||
if (!contract || !gameDetails) return;
|
||||
try {
|
||||
const timeLeft = await contract.methods.revealTimeLeft(gameDetails.returnGameId).call();
|
||||
let timeLeft;
|
||||
if (isMinusOne) {
|
||||
// MinusOne uses getTimeLeft() for all phases
|
||||
timeLeft = await contract.methods.getTimeLeft(gameDetails.returnGameId).call();
|
||||
} else {
|
||||
// Classic uses revealTimeLeft()
|
||||
timeLeft = await contract.methods.revealTimeLeft(gameDetails.returnGameId).call();
|
||||
}
|
||||
setRevealTimeLeft(Number(timeLeft));
|
||||
if (Number(timeLeft) <= 0) {
|
||||
setTimeoutExpired(true);
|
||||
@@ -101,10 +165,38 @@ export default function Reveal({
|
||||
}, [gameDetails, contract, account, whoAmI]);
|
||||
|
||||
const handleReveal = async () => {
|
||||
if (!contract || !web3 || !account || !clearMove) return;
|
||||
if (!contract || !web3 || !account) return;
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
const tx = contract.methods.reveal(gameDetails?.returnGameId, clearMove);
|
||||
let tx;
|
||||
|
||||
if (isMinusOne && !isWithdrawPhase) {
|
||||
// MinusOne initial reveal: revealInitialMoves(gameId, clear1, clear2)
|
||||
if (!clearMove1 || !clearMove2) {
|
||||
showErrorToast("Please provide both cleartext moves");
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
tx = contract.methods.revealInitialMoves(gameDetails?.returnGameId, clearMove1, clearMove2);
|
||||
} else if (isMinusOne && isWithdrawPhase) {
|
||||
// MinusOne withdrawal reveal: withdrawMove(gameId, clear)
|
||||
if (!clearMove) {
|
||||
showErrorToast("Please provide cleartext withdrawal choice");
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
tx = contract.methods.withdrawMove(gameDetails?.returnGameId, clearMove);
|
||||
} else {
|
||||
// Classic mode: reveal(gameId, clear)
|
||||
if (!clearMove) {
|
||||
showErrorToast("Please provide cleartext move");
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
tx = contract.methods.reveal(gameDetails?.returnGameId, clearMove);
|
||||
}
|
||||
|
||||
const gas = await tx.estimateGas({ from: account });
|
||||
const result = await (globalThis as any).ethereum.request({
|
||||
method: "eth_sendTransaction",
|
||||
@@ -244,29 +336,66 @@ export default function Reveal({
|
||||
{!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
|
||||
{isWithdrawPhase ? "Your Withdrawal Choice" : isMinusOne ? "Your Moves" : "Your Move"}
|
||||
</h2>
|
||||
{selectedMove ? (
|
||||
{isMinusOne && !isWithdrawPhase && selectedMove1 && selectedMove2 ? (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-center gap-4 p-3 bg-blue-50 dark:bg-blue-900 rounded-lg">
|
||||
<div className="flex flex-col items-center">
|
||||
<span className="text-xs text-slate-500 dark:text-slate-400 mb-1">Move 1</span>
|
||||
<span className="text-5xl mb-2">{MOVES[selectedMove1].icon}</span>
|
||||
<span className="font-semibold text-sm">{MOVES[selectedMove1].name}</span>
|
||||
</div>
|
||||
<div className="text-2xl text-slate-400">→</div>
|
||||
<div className="bg-white dark:bg-slate-600 p-3 rounded-lg flex-1">
|
||||
<p className="text-xs text-slate-600 dark:text-slate-300 mb-1">Clear Text 1:</p>
|
||||
<code className="text-xs font-mono text-slate-700 dark:text-slate-200 break-all">
|
||||
{clearMove1}
|
||||
</code>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center justify-center gap-4 p-3 bg-green-50 dark:bg-green-900 rounded-lg">
|
||||
<div className="flex flex-col items-center">
|
||||
<span className="text-xs text-slate-500 dark:text-slate-400 mb-1">Move 2</span>
|
||||
<span className="text-5xl mb-2">{MOVES[selectedMove2].icon}</span>
|
||||
<span className="font-semibold text-sm">{MOVES[selectedMove2].name}</span>
|
||||
</div>
|
||||
<div className="text-2xl text-slate-400">→</div>
|
||||
<div className="bg-white dark:bg-slate-600 p-3 rounded-lg flex-1">
|
||||
<p className="text-xs text-slate-600 dark:text-slate-300 mb-1">Clear Text 2:</p>
|
||||
<code className="text-xs font-mono text-slate-700 dark:text-slate-200 break-all">
|
||||
{clearMove2}
|
||||
</code>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : isWithdrawPhase && 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>
|
||||
<span className="text-6xl mb-2">{selectedMove === "1" ? "1️⃣" : "2️⃣"}</span>
|
||||
<span className="font-semibold text-lg">Withdraw Move {selectedMove}</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>
|
||||
<p className="text-xs text-slate-600 dark:text-slate-300 mb-1">Clear Text:</p>
|
||||
<code className="text-sm font-mono text-slate-700 dark:text-slate-200">{clearMove}</code>
|
||||
</div>
|
||||
</div>
|
||||
) : 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
|
||||
No {isWithdrawPhase ? "withdrawal choice" : "move"} selected yet
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
@@ -349,19 +478,22 @@ export default function Reveal({
|
||||
{!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
|
||||
{isWithdrawPhase ? "Reveal Your Withdrawal" : isMinusOne ? "Reveal Your Moves" : "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!
|
||||
{isWithdrawPhase
|
||||
? "Submit your clear withdrawal choice and secret to the blockchain."
|
||||
: isMinusOne
|
||||
? "Submit your clear moves and secrets to the blockchain. This proves you didn't cheat!"
|
||||
: "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}
|
||||
disabled={loading || !account || !contract || (!clearMove && (!clearMove1 || !clearMove2)) || selfRevealed}
|
||||
variant="primary"
|
||||
className="w-full py-3 text-lg"
|
||||
>
|
||||
{loading ? "Submitting..." : selfRevealed ? "✅ Revealed" : "Reveal Move"}
|
||||
{loading ? "Submitting..." : selfRevealed ? "✅ Revealed" : isWithdrawPhase ? "Reveal Withdrawal" : "Reveal Move"}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
@@ -372,42 +504,133 @@ export default function Reveal({
|
||||
{/* 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
|
||||
{isMinusOne ? "Final Moves (After Withdrawal)" : "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>
|
||||
|
||||
{isMinusOne && gameDetails ? (
|
||||
<div className="space-y-6">
|
||||
{/* Show all moves including withdrawn ones for MinusOne */}
|
||||
<div className="flex items-center justify-center gap-8">
|
||||
{/* Your Moves */}
|
||||
<div className="flex flex-col items-center space-y-2">
|
||||
<span className="font-semibold text-slate-700 dark:text-slate-300 mb-2">You</span>
|
||||
<div className="flex gap-2">
|
||||
<div className={`flex flex-col items-center p-2 rounded ${
|
||||
(whoAmI === "player1" ? gameDetails.playerA.withdrawn : gameDetails.playerB.withdrawn) === 1
|
||||
? "opacity-40 bg-red-100 dark:bg-red-900"
|
||||
: "bg-green-100 dark:bg-green-900"
|
||||
}`}>
|
||||
<span className="text-xs text-slate-500 dark:text-slate-400">Move 1</span>
|
||||
<span className="text-3xl">
|
||||
{MOVES[String(whoAmI === "player1" ? gameDetails.playerA.move1 : gameDetails.playerB.move1)]?.icon}
|
||||
</span>
|
||||
<span className="text-xs">
|
||||
{MOVES[String(whoAmI === "player1" ? gameDetails.playerA.move1 : gameDetails.playerB.move1)]?.name}
|
||||
</span>
|
||||
{(whoAmI === "player1" ? gameDetails.playerA.withdrawn : gameDetails.playerB.withdrawn) === 1 && (
|
||||
<span className="text-xs text-red-600 dark:text-red-400">Withdrawn</span>
|
||||
)}
|
||||
</div>
|
||||
<div className={`flex flex-col items-center p-2 rounded ${
|
||||
(whoAmI === "player1" ? gameDetails.playerA.withdrawn : gameDetails.playerB.withdrawn) === 2
|
||||
? "opacity-40 bg-red-100 dark:bg-red-900"
|
||||
: "bg-green-100 dark:bg-green-900"
|
||||
}`}>
|
||||
<span className="text-xs text-slate-500 dark:text-slate-400">Move 2</span>
|
||||
<span className="text-3xl">
|
||||
{MOVES[String(whoAmI === "player1" ? gameDetails.playerA.move2 : gameDetails.playerB.move2)]?.icon}
|
||||
</span>
|
||||
<span className="text-xs">
|
||||
{MOVES[String(whoAmI === "player1" ? gameDetails.playerA.move2 : gameDetails.playerB.move2)]?.name}
|
||||
</span>
|
||||
{(whoAmI === "player1" ? gameDetails.playerA.withdrawn : gameDetails.playerB.withdrawn) === 2 && (
|
||||
<span className="text-xs text-red-600 dark:text-red-400">Withdrawn</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>
|
||||
{/* 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>
|
||||
{/* Opponent Moves */}
|
||||
<div className="flex flex-col items-center space-y-2">
|
||||
<span className="font-semibold text-slate-700 dark:text-slate-300 mb-2">Opponent</span>
|
||||
<div className="flex gap-2">
|
||||
<div className={`flex flex-col items-center p-2 rounded ${
|
||||
(whoAmI === "player1" ? gameDetails.playerB.withdrawn : gameDetails.playerA.withdrawn) === 1
|
||||
? "opacity-40 bg-red-100 dark:bg-red-900"
|
||||
: "bg-green-100 dark:bg-green-900"
|
||||
}`}>
|
||||
<span className="text-xs text-slate-500 dark:text-slate-400">Move 1</span>
|
||||
<span className="text-3xl">
|
||||
{MOVES[String(whoAmI === "player1" ? gameDetails.playerB.move1 : gameDetails.playerA.move1)]?.icon}
|
||||
</span>
|
||||
<span className="text-xs">
|
||||
{MOVES[String(whoAmI === "player1" ? gameDetails.playerB.move1 : gameDetails.playerA.move1)]?.name}
|
||||
</span>
|
||||
{(whoAmI === "player1" ? gameDetails.playerB.withdrawn : gameDetails.playerA.withdrawn) === 1 && (
|
||||
<span className="text-xs text-red-600 dark:text-red-400">Withdrawn</span>
|
||||
)}
|
||||
</div>
|
||||
<div className={`flex flex-col items-center p-2 rounded ${
|
||||
(whoAmI === "player1" ? gameDetails.playerB.withdrawn : gameDetails.playerA.withdrawn) === 2
|
||||
? "opacity-40 bg-red-100 dark:bg-red-900"
|
||||
: "bg-green-100 dark:bg-green-900"
|
||||
}`}>
|
||||
<span className="text-xs text-slate-500 dark:text-slate-400">Move 2</span>
|
||||
<span className="text-3xl">
|
||||
{MOVES[String(whoAmI === "player1" ? gameDetails.playerB.move2 : gameDetails.playerA.move2)]?.icon}
|
||||
</span>
|
||||
<span className="text-xs">
|
||||
{MOVES[String(whoAmI === "player1" ? gameDetails.playerB.move2 : gameDetails.playerA.move2)]?.name}
|
||||
</span>
|
||||
{(whoAmI === "player1" ? gameDetails.playerB.withdrawn : gameDetails.playerA.withdrawn) === 2 && (
|
||||
<span className="text-xs text-red-600 dark:text-red-400">Withdrawn</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center justify-center gap-8">
|
||||
{/* Classic game - 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>
|
||||
|
||||
{/* Classic game - 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>
|
||||
|
||||
{/* Outcome Section */}
|
||||
|
||||
267
docs/DUAL_MODE_IMPLEMENTATION.md
Normal file
267
docs/DUAL_MODE_IMPLEMENTATION.md
Normal file
@@ -0,0 +1,267 @@
|
||||
# Dual-Mode Frontend Implementation
|
||||
|
||||
This document describes the frontend implementation that supports both Classic and Minus One game modes.
|
||||
|
||||
## Overview
|
||||
|
||||
The frontend has been updated to seamlessly work with two different smart contracts:
|
||||
- **Game.sol** (Classic Mode): Standard Rock-Paper-Scissors with 2 phases
|
||||
- **GameMinusOne.sol** (Minus One Mode): Squid Game-inspired variant with 6 phases
|
||||
|
||||
## Game Modes Comparison
|
||||
|
||||
### Classic Mode
|
||||
**Phases:**
|
||||
1. Commit Phase (both players submit encrypted moves)
|
||||
2. Reveal Phase (both players reveal their moves)
|
||||
|
||||
**Functions:**
|
||||
- `register(gameId, nickname)` - Join/create game
|
||||
- `play(gameId, encrMove)` - Submit encrypted move
|
||||
- `reveal(gameId, clearMove)` - Reveal move
|
||||
- `commitTimeLeft()` / `revealTimeLeft()` - Check timeouts
|
||||
- `getOutcome(gameId)` - Get game result
|
||||
|
||||
**Data Structure:**
|
||||
```typescript
|
||||
Player {
|
||||
addr: string;
|
||||
nickname: string;
|
||||
encrMove?: string; // Classic only
|
||||
move?: string; // Classic only
|
||||
}
|
||||
```
|
||||
|
||||
### Minus One Mode
|
||||
**Phases:**
|
||||
1. Registration
|
||||
2. Initial Commit (both players commit 2 moves)
|
||||
3. First Reveal (reveal both moves)
|
||||
4. Withdraw Commit (commit which move to withdraw)
|
||||
5. Withdraw Reveal (reveal withdrawal choice)
|
||||
6. Done
|
||||
|
||||
**Functions:**
|
||||
- `register(gameId, nickname)` - Join/create game
|
||||
- `commitInitialMoves(gameId, hash1, hash2)` - Submit 2 encrypted moves
|
||||
- `revealInitialMoves(gameId, clear1, clear2)` - Reveal both moves
|
||||
- `commitWithdraw(gameId, wHash)` - Submit withdrawal choice
|
||||
- `withdrawMove(gameId, clear)` - Reveal withdrawal
|
||||
- `getTimeLeft(gameId)` - Universal timeout checker
|
||||
- `getOutcome(gameId)` - Get game result
|
||||
|
||||
**Data Structure:**
|
||||
```typescript
|
||||
Player {
|
||||
addr: string;
|
||||
nickname: string;
|
||||
hash1?: string; // MinusOne only
|
||||
hash2?: string; // MinusOne only
|
||||
move1?: string; // MinusOne only
|
||||
move2?: string; // MinusOne only
|
||||
wHash?: string; // MinusOne only
|
||||
withdrawn?: string; // MinusOne only (1 or 2)
|
||||
}
|
||||
```
|
||||
|
||||
## Implementation Details
|
||||
|
||||
### Type System
|
||||
The frontend uses optional fields to support both modes:
|
||||
|
||||
```typescript
|
||||
interface Player {
|
||||
addr: string;
|
||||
nickname: string;
|
||||
// Classic fields
|
||||
encrMove?: string;
|
||||
move?: string;
|
||||
// MinusOne fields
|
||||
hash1?: string;
|
||||
hash2?: string;
|
||||
move1?: string;
|
||||
move2?: string;
|
||||
wHash?: string;
|
||||
withdrawn?: string;
|
||||
}
|
||||
|
||||
interface GameDetails {
|
||||
playerA: Player;
|
||||
playerB: Player;
|
||||
initialBet: string;
|
||||
outcome: number;
|
||||
isActive: boolean;
|
||||
returnGameId: number;
|
||||
gameMode?: string; // "classic" or "minusone"
|
||||
}
|
||||
```
|
||||
|
||||
### Component Updates
|
||||
|
||||
#### GameModal.tsx
|
||||
- **Purpose:** Manages game state and routes to correct phase component
|
||||
- **Changes:**
|
||||
- Added `gameMode` field to GameDetails
|
||||
- Phase detection logic checks `gameMode` and appropriate fields
|
||||
- Storage structure expanded for MinusOne state (move1/2, secret1/2, withdrawChoice)
|
||||
|
||||
#### Commit.tsx
|
||||
- **Purpose:** Handles all commit phases (move commits and withdrawal commits)
|
||||
- **Changes:**
|
||||
- Added MinusOne-specific props: `selectedMove1`, `selectedMove2`, `secret1`, `secret2`, `withdrawChoice`
|
||||
- Hash generation adapts based on mode:
|
||||
- Classic: `keccak256(move-password)`
|
||||
- MinusOne initial: Two hashes `keccak256(move1-password1)`, `keccak256(move2-password2)`
|
||||
- MinusOne withdrawal: `keccak256(1-password)` or `keccak256(2-password)`
|
||||
- UI conditionally renders:
|
||||
- Classic: Single move selector
|
||||
- MinusOne initial: Two move selectors with different colored borders
|
||||
- MinusOne withdrawal: Choice between withdrawing move 1 or 2
|
||||
- Contract calls:
|
||||
- Classic: `play(gameId, encrMove)`
|
||||
- MinusOne initial: `commitInitialMoves(gameId, hash1, hash2)`
|
||||
- MinusOne withdrawal: `commitWithdraw(gameId, wHash)`
|
||||
|
||||
#### Reveal.tsx
|
||||
- **Purpose:** Handles all reveal phases (move reveals and withdrawal reveals)
|
||||
- **Changes:**
|
||||
- Added MinusOne-specific props: `selectedMove1`, `selectedMove2`, `secret1`, `secret2`, `isWithdrawPhase`
|
||||
- State checks adapted:
|
||||
- Classic: Check `move` field
|
||||
- MinusOne initial: Check `move1` field
|
||||
- MinusOne withdrawal: Check `withdrawn` field
|
||||
- Reveal function calls:
|
||||
- Classic: `reveal(gameId, clearMove)`
|
||||
- MinusOne initial: `revealInitialMoves(gameId, clear1, clear2)`
|
||||
- MinusOne withdrawal: `withdrawMove(gameId, clear)`
|
||||
- UI conditionally displays:
|
||||
- Classic: Single move display
|
||||
- MinusOne initial: Both moves displayed side-by-side
|
||||
- MinusOne withdrawal: Withdrawal choice (1️⃣ or 2️⃣)
|
||||
- Final outcome shows:
|
||||
- Classic: Final move from each player
|
||||
- MinusOne: All 4 moves with withdrawn ones marked (grayed out with "Withdrawn" label)
|
||||
|
||||
#### GameList.tsx
|
||||
- **Purpose:** Lists available games and shows their status
|
||||
- **Changes:**
|
||||
- `getGamePhase()` function checks `gameMode` and appropriate fields:
|
||||
- Classic: Returns "Commit", "Reveal", or "Outcome"
|
||||
- MinusOne: Returns "Initial Commit", "Initial Reveal", "Withdraw Commit", "Withdraw Reveal", or "Done"
|
||||
- Game mode badge displays "Classic" or "Minus One"
|
||||
- Phase colors:
|
||||
- Yellow: Commit phases
|
||||
- Blue: Initial reveal
|
||||
- Amber: Withdraw commit
|
||||
- Orange: Withdraw reveal
|
||||
- Purple: Complete/Done
|
||||
|
||||
### Hash Generation
|
||||
|
||||
All hashing uses Keccak256 with Web3.js:
|
||||
|
||||
```typescript
|
||||
// Classic mode
|
||||
const hash = web3.utils.keccak256(web3.utils.utf8ToHex(`${move}-${secret}`));
|
||||
|
||||
// MinusOne initial moves
|
||||
const hash1 = web3.utils.keccak256(web3.utils.utf8ToHex(`${move1}-${secret1}`));
|
||||
const hash2 = web3.utils.keccak256(web3.utils.utf8ToHex(`${move2}-${secret2}`));
|
||||
|
||||
// MinusOne withdrawal
|
||||
const wHash = web3.utils.keccak256(web3.utils.utf8ToHex(`${withdrawChoice}-${secret}`));
|
||||
```
|
||||
|
||||
### Session Storage
|
||||
|
||||
Game state persists across page refreshes using sessionStorage:
|
||||
|
||||
**Classic Mode:**
|
||||
```typescript
|
||||
{
|
||||
selectedMove: string,
|
||||
secret: string,
|
||||
expiresAt: number
|
||||
}
|
||||
```
|
||||
|
||||
**MinusOne Mode:**
|
||||
```typescript
|
||||
{
|
||||
selectedMove1: string,
|
||||
selectedMove2: string,
|
||||
secret1: string,
|
||||
secret2: string,
|
||||
withdrawChoice: string,
|
||||
expiresAt: number
|
||||
}
|
||||
```
|
||||
|
||||
Storage expires after 1 hour to prevent stale data.
|
||||
|
||||
## Testing Checklist
|
||||
|
||||
### Classic Mode
|
||||
- [ ] Create new game with "Classic Mode"
|
||||
- [ ] Join game as Player B
|
||||
- [ ] Both players commit moves
|
||||
- [ ] Both players reveal moves
|
||||
- [ ] Verify correct outcome (win/lose/draw)
|
||||
- [ ] Check ETH payouts
|
||||
|
||||
### Minus One Mode
|
||||
- [ ] Create new game with "Minus One Mode"
|
||||
- [ ] Join game as Player B
|
||||
- [ ] Both players commit 2 initial moves
|
||||
- [ ] Both players reveal initial moves
|
||||
- [ ] Verify both moves displayed correctly
|
||||
- [ ] Both players commit withdrawal choice
|
||||
- [ ] Both players reveal withdrawal
|
||||
- [ ] Verify final moves (1 active, 1 withdrawn per player)
|
||||
- [ ] Check correct outcome based on remaining moves
|
||||
- [ ] Check ETH payouts
|
||||
|
||||
### Edge Cases
|
||||
- [ ] Timeout handling in Classic mode
|
||||
- [ ] Timeout handling in MinusOne mode (different phases)
|
||||
- [ ] Page refresh preserves state (both modes)
|
||||
- [ ] GameList shows correct phase for both modes
|
||||
- [ ] Switching between active games (mixed modes)
|
||||
|
||||
## Deployment Steps
|
||||
|
||||
1. **Deploy GameMinusOne contract:**
|
||||
```bash
|
||||
cd crypto_clash_contract
|
||||
npx hardhat run scripts/deploy.ts --network <network>
|
||||
```
|
||||
|
||||
2. **Update config.json:**
|
||||
```json
|
||||
{
|
||||
"GAME_CONTRACT_ADDRESS": "0x...",
|
||||
"GAME_MINUSONE_CONTRACT_ADDRESS": "0x...",
|
||||
"NETWORK_NAME": "...",
|
||||
"CHAIN_ID": ...
|
||||
}
|
||||
```
|
||||
|
||||
3. **Copy config to frontend:**
|
||||
```bash
|
||||
cd crypto_clash_frontend
|
||||
node copy_config.js
|
||||
```
|
||||
|
||||
4. **Build and deploy frontend:**
|
||||
```bash
|
||||
npm run build
|
||||
npm run start
|
||||
```
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
- Contract mode detection could be automatic (read contract bytecode or interface)
|
||||
- Support switching contracts without page reload
|
||||
- Statistics tracking for each mode separately
|
||||
- Leaderboard for each mode
|
||||
- Tournament mode supporting both game types
|
||||
Reference in New Issue
Block a user