mirror of
https://github.com/averel10/crypto_clash.git
synced 2026-03-12 19:08:11 +01:00
start fixing frontend
This commit is contained in:
@@ -18,6 +18,16 @@ interface CommitProps {
|
|||||||
gameDetails: GameDetails | null;
|
gameDetails: GameDetails | null;
|
||||||
setSecret: (secret: string) => void;
|
setSecret: (secret: string) => void;
|
||||||
savePlayMove: (playMove: 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";
|
type MoveName = "Rock" | "Paper" | "Scissors";
|
||||||
@@ -40,10 +50,21 @@ export default function Commit({
|
|||||||
setSecret,
|
setSecret,
|
||||||
savePlayMove,
|
savePlayMove,
|
||||||
whoAmI,
|
whoAmI,
|
||||||
gameDetails
|
gameDetails,
|
||||||
|
selectedMove1,
|
||||||
|
setSelectedMove1,
|
||||||
|
selectedMove2,
|
||||||
|
setSelectedMove2,
|
||||||
|
secret1,
|
||||||
|
setSecret1,
|
||||||
|
secret2,
|
||||||
|
setSecret2,
|
||||||
|
isWithdrawPhase = false,
|
||||||
}: Readonly<CommitProps>) {
|
}: Readonly<CommitProps>) {
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [playMove, setPlayMove] = useState<string>("");
|
const [playMove, setPlayMove] = useState<string>("");
|
||||||
|
const [playMove1, setPlayMove1] = useState<string>("");
|
||||||
|
const [playMove2, setPlayMove2] = useState<string>("");
|
||||||
const [selfPlayed, setSelfPlayed] = useState<string>("");
|
const [selfPlayed, setSelfPlayed] = useState<string>("");
|
||||||
const [opponentPlayed, setOpponentPlayed] = useState<string>("");
|
const [opponentPlayed, setOpponentPlayed] = useState<string>("");
|
||||||
const [bothPlayed, setBothPlayed] = useState<string>("");
|
const [bothPlayed, setBothPlayed] = useState<string>("");
|
||||||
@@ -52,17 +73,32 @@ export default function Commit({
|
|||||||
const [commitTimeLeft, setCommitTimeLeft] = useState<number>(0);
|
const [commitTimeLeft, setCommitTimeLeft] = useState<number>(0);
|
||||||
const [timeoutExpired, setTimeoutExpired] = useState(false);
|
const [timeoutExpired, setTimeoutExpired] = useState(false);
|
||||||
|
|
||||||
|
const isMinusOne = gameDetails?.gameMode === "minusone";
|
||||||
|
|
||||||
// Update encrypted move when move or secret changes
|
// Update encrypted move when move or secret changes
|
||||||
useEffect(() => {
|
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}`;
|
const clearMove = `${selectedMove}-${secret}`;
|
||||||
// Use keccak256 (Ethereum's standard hash function)
|
|
||||||
const hash = Web3.utils.keccak256(clearMove);
|
const hash = Web3.utils.keccak256(clearMove);
|
||||||
setPlayMove(hash);
|
setPlayMove(hash);
|
||||||
// Persist to sessionStorage through parent
|
if (!isWithdrawPhase) {
|
||||||
savePlayMove(hash);
|
savePlayMove(hash);
|
||||||
}
|
}
|
||||||
}, [selectedMove, secret, savePlayMove]);
|
}
|
||||||
|
}, [selectedMove, secret, selectedMove1, secret1, selectedMove2, secret2, isMinusOne, isWithdrawPhase, savePlayMove]);
|
||||||
|
|
||||||
// Auto-check if both players have committed and trigger callback
|
// Auto-check if both players have committed and trigger callback
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -75,9 +111,18 @@ export default function Commit({
|
|||||||
|
|
||||||
const checkSelfPlayed = async () => {
|
const checkSelfPlayed = async () => {
|
||||||
try {
|
try {
|
||||||
const encrMove = gameDetails[whoAmI === "player1" ? "playerA" : "playerB"].encrMove;
|
const player = gameDetails[whoAmI === "player1" ? "playerA" : "playerB"];
|
||||||
|
|
||||||
setSelfPlayed(Number(encrMove) !== 0 ? "true" : "false");
|
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) {
|
} catch (err: any) {
|
||||||
console.error("Auto-check self played failed:", err.message);
|
console.error("Auto-check self played failed:", err.message);
|
||||||
}
|
}
|
||||||
@@ -88,8 +133,15 @@ export default function Commit({
|
|||||||
const checkOpponentPlayed = async () => {
|
const checkOpponentPlayed = async () => {
|
||||||
try {
|
try {
|
||||||
const opponentKey = whoAmI === "player1" ? "playerB" : "playerA";
|
const opponentKey = whoAmI === "player1" ? "playerB" : "playerA";
|
||||||
const encrMove = gameDetails[opponentKey].encrMove;
|
const opponent = gameDetails[opponentKey];
|
||||||
setOpponentPlayed(Number(encrMove) !== 0 ? "true" : "false");
|
|
||||||
|
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) {
|
} catch (err: any) {
|
||||||
console.error("Auto-check opponent played failed:", err.message);
|
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
|
// Check immediately on mount or when dependencies change
|
||||||
const checkBothPlayed = async () => {
|
const checkBothPlayed = async () => {
|
||||||
try {
|
try {
|
||||||
|
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 playerAEncrMove = gameDetails.playerA.encrMove;
|
||||||
const playerBEncrMove = gameDetails.playerB.encrMove;
|
const playerBEncrMove = gameDetails.playerB.encrMove;
|
||||||
|
res = !!(playerAEncrMove && playerBEncrMove && Number(playerAEncrMove) !== 0 && Number(playerBEncrMove) !== 0);
|
||||||
|
}
|
||||||
|
|
||||||
const res = Number(playerAEncrMove) !== 0 && Number(playerBEncrMove) !== 0;
|
|
||||||
console.log("Both played check:", res);
|
console.log("Both played check:", res);
|
||||||
if (res) {
|
if (res) {
|
||||||
setBothPlayed("true");
|
setBothPlayed("true");
|
||||||
@@ -118,7 +182,14 @@ export default function Commit({
|
|||||||
// Check commit timeout
|
// Check commit timeout
|
||||||
const checkCommitTimeout = async () => {
|
const checkCommitTimeout = async () => {
|
||||||
try {
|
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);
|
console.log("Commit time left:", timeLeft);
|
||||||
setCommitTimeLeft(Number(timeLeft));
|
setCommitTimeLeft(Number(timeLeft));
|
||||||
if (Number(timeLeft) <= 0) {
|
if (Number(timeLeft) <= 0) {
|
||||||
@@ -145,11 +216,35 @@ export default function Commit({
|
|||||||
|
|
||||||
// Commit phase read-only handlers
|
// Commit phase read-only handlers
|
||||||
const handlePlay = async () => {
|
const handlePlay = async () => {
|
||||||
if (!contract || !web3 || !account || !playMove) return;
|
if (!contract || !web3 || !account) return;
|
||||||
|
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
// playMove should be a hex string (bytes32)
|
let tx;
|
||||||
const tx = contract.methods.play(gameDetails?.returnGameId, playMove);
|
|
||||||
|
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 gas = await tx.estimateGas({ from: account });
|
||||||
const result = await (globalThis as any).ethereum.request({
|
const result = await (globalThis as any).ethereum.request({
|
||||||
method: "eth_sendTransaction",
|
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);
|
setMoveSubmitted(true);
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
showToast("Play failed: " + err.message, "error");
|
showToast("Commit failed: " + err.message, "error");
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
@@ -174,17 +269,44 @@ export default function Commit({
|
|||||||
|
|
||||||
const regenerateSecret = () => {
|
const regenerateSecret = () => {
|
||||||
const randomHex = Math.random().toString(16).slice(2, 18);
|
const randomHex = Math.random().toString(16).slice(2, 18);
|
||||||
|
if (isMinusOne && !isWithdrawPhase) {
|
||||||
|
// For MinusOne, we might want separate secrets
|
||||||
|
// For now, let's keep them simple
|
||||||
|
setSecret1?.(randomHex);
|
||||||
|
} else {
|
||||||
setSecret(randomHex);
|
setSecret(randomHex);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const regenerateSecret2 = () => {
|
||||||
|
const randomHex = Math.random().toString(16).slice(2, 18);
|
||||||
|
setSecret2?.(randomHex);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSecretChange = (value: string) => {
|
const handleSecretChange = (value: string) => {
|
||||||
setSecret(value);
|
setSecret(value);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleSecret1Change = (value: string) => {
|
||||||
|
setSecret1?.(value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSecret2Change = (value: string) => {
|
||||||
|
setSecret2?.(value);
|
||||||
|
};
|
||||||
|
|
||||||
const handleMoveSelect = (move: string) => {
|
const handleMoveSelect = (move: string) => {
|
||||||
setSelectedMove(move);
|
setSelectedMove(move);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleMove1Select = (move: string) => {
|
||||||
|
setSelectedMove1?.(move);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMove2Select = (move: string) => {
|
||||||
|
setSelectedMove2?.(move);
|
||||||
|
};
|
||||||
|
|
||||||
const handleResolveTimeout = async () => {
|
const handleResolveTimeout = async () => {
|
||||||
if (!contract || !web3 || !account) return;
|
if (!contract || !web3 || !account) return;
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
@@ -331,7 +453,171 @@ export default function Commit({
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{/* Move Selection */}
|
{/* 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>
|
||||||
|
|
||||||
|
<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">
|
<div className="mb-8">
|
||||||
<p className="text-sm text-slate-600 dark:text-slate-300 mb-4 font-medium">
|
<p className="text-sm text-slate-600 dark:text-slate-300 mb-4 font-medium">
|
||||||
Choose your move:
|
Choose your move:
|
||||||
@@ -379,28 +665,52 @@ export default function Commit({
|
|||||||
Keep this secret safe! It's needed to reveal your move later.
|
Keep this secret safe! It's needed to reveal your move later.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Encrypted Move Display */}
|
{/* Encrypted Move Display */}
|
||||||
<div className="mb-8 bg-blue-50 dark:bg-blue-900 p-4 rounded-lg">
|
<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">
|
<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>
|
</label>
|
||||||
<div className="bg-white dark:bg-slate-700 p-3 rounded border border-blue-200 dark:border-blue-700 overflow-x-auto">
|
{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">
|
<code className="text-xs text-slate-600 dark:text-slate-300 font-mono break-all">
|
||||||
{playMove || "Select a move and enter a secret"}
|
{playMove1 || "Select first move and enter secret"}
|
||||||
</code>
|
</code>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
|
|
||||||
{/* Action Buttons */}
|
{/* Action Buttons */}
|
||||||
<div className="flex flex-col gap-3">
|
<div className="flex flex-col gap-3">
|
||||||
<Button
|
<Button
|
||||||
onClick={handlePlay}
|
onClick={handlePlay}
|
||||||
disabled={loading || !account || !contract || !selectedMove || !secret}
|
disabled={
|
||||||
|
loading ||
|
||||||
|
!account ||
|
||||||
|
!contract ||
|
||||||
|
(isMinusOne && !isWithdrawPhase ? (!selectedMove1 || !selectedMove2 || !secret1 || !secret2) : (!selectedMove || !secret))
|
||||||
|
}
|
||||||
variant="primary"
|
variant="primary"
|
||||||
className="w-full py-3 text-lg"
|
className="w-full py-3 text-lg"
|
||||||
>
|
>
|
||||||
{loading ? "Submitting..." : "Submit Move"}
|
{loading ? "Submitting..." : isWithdrawPhase ? "Submit Withdrawal Choice" : "Submit Move"}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -181,6 +181,32 @@ export default function GameList({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const getGamePhase = (game: GameDetails) => {
|
const getGamePhase = (game: GameDetails) => {
|
||||||
|
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 {
|
||||||
|
// Classic game phases
|
||||||
const playerARevealed = Number(game.playerA.move) !== 0;
|
const playerARevealed = Number(game.playerA.move) !== 0;
|
||||||
const playerBRevealed = Number(game.playerB.move) !== 0;
|
const playerBRevealed = Number(game.playerB.move) !== 0;
|
||||||
const playerACommitted = Number(game.playerA.encrMove) !== 0;
|
const playerACommitted = Number(game.playerA.encrMove) !== 0;
|
||||||
@@ -193,6 +219,7 @@ export default function GameList({
|
|||||||
} else {
|
} else {
|
||||||
return { phase: "Commit", color: "bg-yellow-100 dark:bg-yellow-900 text-yellow-800 dark:text-yellow-200" };
|
return { phase: "Commit", color: "bg-yellow-100 dark:bg-yellow-900 text-yellow-800 dark:text-yellow-200" };
|
||||||
}
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -9,8 +9,16 @@ import { showErrorToast } from "@/app/lib/toast";
|
|||||||
export type Player = {
|
export type Player = {
|
||||||
addr: string;
|
addr: string;
|
||||||
bet: string;
|
bet: string;
|
||||||
encrMove: string;
|
// Classic mode fields
|
||||||
move: number;
|
encrMove?: string;
|
||||||
|
move?: number;
|
||||||
|
// MinusOne mode fields
|
||||||
|
hash1?: string;
|
||||||
|
hash2?: string;
|
||||||
|
move1?: number;
|
||||||
|
move2?: number;
|
||||||
|
wHash?: string;
|
||||||
|
withdrawn?: number;
|
||||||
nickname: string;
|
nickname: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -22,6 +30,7 @@ export type GameDetails = {
|
|||||||
isActive: boolean;
|
isActive: boolean;
|
||||||
returnGameId: number;
|
returnGameId: number;
|
||||||
gameMode?: string; // "classic" or "minusone"
|
gameMode?: string; // "classic" or "minusone"
|
||||||
|
phase?: number; // GamePhase for minusone mode
|
||||||
};
|
};
|
||||||
|
|
||||||
interface GameModalProps {
|
interface GameModalProps {
|
||||||
@@ -45,20 +54,38 @@ export default function GameModal({
|
|||||||
web3,
|
web3,
|
||||||
setStatus,
|
setStatus,
|
||||||
}: Readonly<GameModalProps>) {
|
}: 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 [whoAmI, setWhoAmI] = useState<"player1" | "player2" | "">("");
|
||||||
const [gameDetails, setGameDetails] = useState<GameDetails | null>(null);
|
const [gameDetails, setGameDetails] = useState<GameDetails | null>(null);
|
||||||
|
|
||||||
|
// Classic mode state
|
||||||
const [selectedMove, setSelectedMove] = useState<string | null>(null);
|
const [selectedMove, setSelectedMove] = useState<string | null>(null);
|
||||||
const [secret, setSecret] = useState<string>("");
|
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
|
// Helper function to generate game-specific storage key
|
||||||
const getGameStorageKey = () => `game_${gameDetails?.returnGameId}`;
|
const getGameStorageKey = () => `game_${gameDetails?.returnGameId}`;
|
||||||
|
|
||||||
// Game storage object structure
|
// Game storage object structure
|
||||||
type GameStorage = {
|
type GameStorage = {
|
||||||
secret: string;
|
// Classic mode
|
||||||
selectedMove: string | null;
|
secret?: string;
|
||||||
playMove: 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;
|
timestamp?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -101,8 +128,16 @@ export default function GameModal({
|
|||||||
if (storedData) {
|
if (storedData) {
|
||||||
try {
|
try {
|
||||||
const parsed: GameStorage = JSON.parse(storedData);
|
const parsed: GameStorage = JSON.parse(storedData);
|
||||||
|
// Classic mode
|
||||||
if (parsed.secret) setSecret(parsed.secret);
|
if (parsed.secret) setSecret(parsed.secret);
|
||||||
if (parsed.selectedMove) setSelectedMove(parsed.selectedMove);
|
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) {
|
} catch (err) {
|
||||||
console.error("Failed to parse stored game data:", err);
|
console.error("Failed to parse stored game data:", err);
|
||||||
}
|
}
|
||||||
@@ -111,7 +146,7 @@ export default function GameModal({
|
|||||||
|
|
||||||
const saveGameData = (updates: Partial<GameStorage>) => {
|
const saveGameData = (updates: Partial<GameStorage>) => {
|
||||||
const storedData = sessionStorage.getItem(getGameStorageKey());
|
const storedData = sessionStorage.getItem(getGameStorageKey());
|
||||||
let currentData: GameStorage = { secret: "", selectedMove: null, playMove: "", timestamp: Date.now() };
|
let currentData: GameStorage = { timestamp: Date.now() };
|
||||||
|
|
||||||
if (storedData) {
|
if (storedData) {
|
||||||
try {
|
try {
|
||||||
@@ -125,6 +160,7 @@ export default function GameModal({
|
|||||||
sessionStorage.setItem(getGameStorageKey(), JSON.stringify(updatedData));
|
sessionStorage.setItem(getGameStorageKey(), JSON.stringify(updatedData));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Classic mode save functions
|
||||||
const saveSecret = (value: string) => {
|
const saveSecret = (value: string) => {
|
||||||
setSecret(value);
|
setSecret(value);
|
||||||
saveGameData({ secret: value });
|
saveGameData({ secret: value });
|
||||||
@@ -141,6 +177,43 @@ export default function GameModal({
|
|||||||
saveGameData({ playMove });
|
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(() => {
|
useEffect(() => {
|
||||||
const fetchPlayerInfo = async () => {
|
const fetchPlayerInfo = async () => {
|
||||||
@@ -163,11 +236,31 @@ export default function GameModal({
|
|||||||
console.log("Game details:", details);
|
console.log("Game details:", details);
|
||||||
setGameDetails(details);
|
setGameDetails(details);
|
||||||
|
|
||||||
// Determine the correct phase based on game state
|
// Determine game mode
|
||||||
const playerAHasMove = Number(details.playerA.encrMove) !== 0;
|
const isMinusOne = details.gameMode === "minusone";
|
||||||
const playerBHasMove = Number(details.playerB.encrMove) !== 0;
|
|
||||||
const playerARevealed = Number(details.playerA.move) !== 0;
|
if (isMinusOne) {
|
||||||
const playerBRevealed = Number(details.playerB.move) !== 0;
|
// 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 both players have revealed their moves, show reveal phase (with results)
|
||||||
if (playerARevealed && playerBRevealed) {
|
if (playerARevealed && playerBRevealed) {
|
||||||
@@ -181,6 +274,7 @@ export default function GameModal({
|
|||||||
else {
|
else {
|
||||||
setPhase("commit");
|
setPhase("commit");
|
||||||
}
|
}
|
||||||
|
}
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
showErrorToast("Error fetching game details: " + err.message);
|
showErrorToast("Error fetching game details: " + err.message);
|
||||||
}
|
}
|
||||||
@@ -218,6 +312,12 @@ export default function GameModal({
|
|||||||
setPhase("commit");
|
setPhase("commit");
|
||||||
setSelectedMove(null);
|
setSelectedMove(null);
|
||||||
setSecret("");
|
setSecret("");
|
||||||
|
setSelectedMove1(null);
|
||||||
|
setSelectedMove2(null);
|
||||||
|
setSecret1("");
|
||||||
|
setSecret2("");
|
||||||
|
setWithdrawChoice(null);
|
||||||
|
setWithdrawSecret("");
|
||||||
onClose();
|
onClose();
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -234,9 +334,10 @@ export default function GameModal({
|
|||||||
{gameId ? `Game #${gameId}` : "Game"}
|
{gameId ? `Game #${gameId}` : "Game"}
|
||||||
</h2>
|
</h2>
|
||||||
<p className="text-sm text-slate-600 dark:text-slate-400 mt-1">
|
<p className="text-sm text-slate-600 dark:text-slate-400 mt-1">
|
||||||
{phase === "commit"
|
{phase === "commit" && "Commit your move"}
|
||||||
? "Commit your move"
|
{phase === "reveal" && "Reveal your move"}
|
||||||
: "Reveal your move"}
|
{phase === "withdrawCommit" && "Choose which move to withdraw"}
|
||||||
|
{phase === "withdrawReveal" && "Reveal your withdrawal choice"}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
@@ -266,6 +367,15 @@ export default function GameModal({
|
|||||||
secret={secret}
|
secret={secret}
|
||||||
setSecret={saveSecret}
|
setSecret={saveSecret}
|
||||||
savePlayMove={savePlayMove}
|
savePlayMove={savePlayMove}
|
||||||
|
// MinusOne mode props
|
||||||
|
selectedMove1={selectedMove1}
|
||||||
|
setSelectedMove1={saveMoveSelection1}
|
||||||
|
selectedMove2={selectedMove2}
|
||||||
|
setSelectedMove2={saveMoveSelection2}
|
||||||
|
secret1={secret1}
|
||||||
|
setSecret1={saveSecret1}
|
||||||
|
secret2={secret2}
|
||||||
|
setSecret2={saveSecret2}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{phase === "reveal" && (
|
{phase === "reveal" && (
|
||||||
@@ -279,6 +389,42 @@ export default function GameModal({
|
|||||||
secret={secret}
|
secret={secret}
|
||||||
gameDetails={gameDetails}
|
gameDetails={gameDetails}
|
||||||
whoAmI={whoAmI}
|
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>
|
</div>
|
||||||
|
|||||||
@@ -14,6 +14,12 @@ interface RevealProps {
|
|||||||
secret: string;
|
secret: string;
|
||||||
gameDetails: GameDetails | null;
|
gameDetails: GameDetails | null;
|
||||||
whoAmI: "player1" | "player2" | "";
|
whoAmI: "player1" | "player2" | "";
|
||||||
|
// MinusOne mode props
|
||||||
|
selectedMove1?: string | null;
|
||||||
|
selectedMove2?: string | null;
|
||||||
|
secret1?: string;
|
||||||
|
secret2?: string;
|
||||||
|
isWithdrawPhase?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
type MoveName = "Rock" | "Paper" | "Scissors";
|
type MoveName = "Rock" | "Paper" | "Scissors";
|
||||||
@@ -42,6 +48,11 @@ export default function Reveal({
|
|||||||
secret,
|
secret,
|
||||||
gameDetails,
|
gameDetails,
|
||||||
whoAmI,
|
whoAmI,
|
||||||
|
selectedMove1,
|
||||||
|
selectedMove2,
|
||||||
|
secret1,
|
||||||
|
secret2,
|
||||||
|
isWithdrawPhase = false,
|
||||||
}: Readonly<RevealProps>) {
|
}: Readonly<RevealProps>) {
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [selfRevealed, setSelfRevealed] = useState(false);
|
const [selfRevealed, setSelfRevealed] = useState(false);
|
||||||
@@ -51,14 +62,59 @@ export default function Reveal({
|
|||||||
const [revealTimeLeft, setRevealTimeLeft] = useState<number>(0);
|
const [revealTimeLeft, setRevealTimeLeft] = useState<number>(0);
|
||||||
const [timeoutExpired, setTimeoutExpired] = useState(false);
|
const [timeoutExpired, setTimeoutExpired] = useState(false);
|
||||||
|
|
||||||
|
const isMinusOne = gameDetails?.gameMode === "minusone";
|
||||||
|
|
||||||
|
// Generate clear text for reveal
|
||||||
const clearMove = selectedMove && secret ? `${selectedMove}-${secret}` : "";
|
const clearMove = selectedMove && secret ? `${selectedMove}-${secret}` : "";
|
||||||
|
const clearMove1 = selectedMove1 && secret1 ? `${selectedMove1}-${secret1}` : "";
|
||||||
|
const clearMove2 = selectedMove2 && secret2 ? `${selectedMove2}-${secret2}` : "";
|
||||||
|
|
||||||
// Check game status on mount
|
// Check game status on mount
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const setStateFromGameDetails = () => {
|
const setStateFromGameDetails = () => {
|
||||||
if (!gameDetails) return;
|
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);
|
||||||
|
} 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(
|
setSelfRevealed(
|
||||||
(whoAmI === "player1" && playerARevealed) ||
|
(whoAmI === "player1" && playerARevealed) ||
|
||||||
@@ -76,6 +132,7 @@ export default function Reveal({
|
|||||||
else if(Number(gameDetails.outcome) === 2 && whoAmI === "player1") setOutcome(2);
|
else if(Number(gameDetails.outcome) === 2 && whoAmI === "player1") setOutcome(2);
|
||||||
else setOutcome(3);
|
else setOutcome(3);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
setStateFromGameDetails();
|
setStateFromGameDetails();
|
||||||
@@ -84,7 +141,14 @@ export default function Reveal({
|
|||||||
const checkRevealTimeout = async () => {
|
const checkRevealTimeout = async () => {
|
||||||
if (!contract || !gameDetails) return;
|
if (!contract || !gameDetails) return;
|
||||||
try {
|
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));
|
setRevealTimeLeft(Number(timeLeft));
|
||||||
if (Number(timeLeft) <= 0) {
|
if (Number(timeLeft) <= 0) {
|
||||||
setTimeoutExpired(true);
|
setTimeoutExpired(true);
|
||||||
@@ -101,10 +165,38 @@ export default function Reveal({
|
|||||||
}, [gameDetails, contract, account, whoAmI]);
|
}, [gameDetails, contract, account, whoAmI]);
|
||||||
|
|
||||||
const handleReveal = async () => {
|
const handleReveal = async () => {
|
||||||
if (!contract || !web3 || !account || !clearMove) return;
|
if (!contract || !web3 || !account) return;
|
||||||
|
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
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 gas = await tx.estimateGas({ from: account });
|
||||||
const result = await (globalThis as any).ethereum.request({
|
const result = await (globalThis as any).ethereum.request({
|
||||||
method: "eth_sendTransaction",
|
method: "eth_sendTransaction",
|
||||||
@@ -244,29 +336,66 @@ export default function Reveal({
|
|||||||
{!bothRevealed && !timeoutExpired && (
|
{!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">
|
<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">
|
<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>
|
</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 items-center justify-center gap-4">
|
||||||
<div className="flex flex-col items-center">
|
<div className="flex flex-col items-center">
|
||||||
<span className="text-6xl mb-2">{MOVES[selectedMove].icon}</span>
|
<span className="text-6xl mb-2">{selectedMove === "1" ? "1️⃣" : "2️⃣"}</span>
|
||||||
<span className="font-semibold text-lg">
|
<span className="font-semibold text-lg">Withdraw Move {selectedMove}</span>
|
||||||
{MOVES[selectedMove].name}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="text-3xl text-slate-400">→</div>
|
<div className="text-3xl text-slate-400">→</div>
|
||||||
<div className="bg-white dark:bg-slate-600 p-4 rounded-lg">
|
<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">
|
<p className="text-xs text-slate-600 dark:text-slate-300 mb-1">Clear Text:</p>
|
||||||
Clear Move:
|
<code className="text-sm font-mono text-slate-700 dark:text-slate-200">{clearMove}</code>
|
||||||
</p>
|
</div>
|
||||||
<code className="text-sm font-mono text-slate-700 dark:text-slate-200">
|
</div>
|
||||||
{clearMove}
|
) : selectedMove ? (
|
||||||
</code>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<p className="text-center text-slate-600 dark:text-slate-400">
|
<p className="text-center text-slate-600 dark:text-slate-400">
|
||||||
No move selected yet
|
No {isWithdrawPhase ? "withdrawal choice" : "move"} selected yet
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -349,19 +478,22 @@ export default function Reveal({
|
|||||||
{!bothRevealed && !timeoutExpired && (
|
{!bothRevealed && !timeoutExpired && (
|
||||||
<div className="border-2 border-blue-300 dark:border-blue-600 p-6 rounded-lg bg-blue-50 dark:bg-slate-700">
|
<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">
|
<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>
|
</h2>
|
||||||
<p className="text-sm text-slate-600 dark:text-slate-300 mb-4">
|
<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
|
{isWithdrawPhase
|
||||||
didn't cheat!
|
? "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>
|
</p>
|
||||||
<Button
|
<Button
|
||||||
onClick={handleReveal}
|
onClick={handleReveal}
|
||||||
disabled={loading || !account || !contract || !clearMove || selfRevealed}
|
disabled={loading || !account || !contract || (!clearMove && (!clearMove1 || !clearMove2)) || selfRevealed}
|
||||||
variant="primary"
|
variant="primary"
|
||||||
className="w-full py-3 text-lg"
|
className="w-full py-3 text-lg"
|
||||||
>
|
>
|
||||||
{loading ? "Submitting..." : selfRevealed ? "✅ Revealed" : "Reveal Move"}
|
{loading ? "Submitting..." : selfRevealed ? "✅ Revealed" : isWithdrawPhase ? "Reveal Withdrawal" : "Reveal Move"}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -372,10 +504,100 @@ export default function Reveal({
|
|||||||
{/* Moves Comparison */}
|
{/* Moves Comparison */}
|
||||||
<div className="border-2 border-slate-300 dark:border-slate-600 p-6 rounded-lg bg-slate-50 dark:bg-slate-800">
|
<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">
|
<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>
|
</h2>
|
||||||
|
|
||||||
|
{isMinusOne && gameDetails ? (
|
||||||
|
<div className="space-y-6">
|
||||||
|
{/* Show all moves including withdrawn ones for MinusOne */}
|
||||||
<div className="flex items-center justify-center gap-8">
|
<div className="flex items-center justify-center gap-8">
|
||||||
{/* Your Move (always on left) */}
|
{/* 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>
|
||||||
|
|
||||||
|
{/* 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 className="flex items-center justify-center gap-8">
|
||||||
|
{/* Classic game - Your Move (always on left) */}
|
||||||
<div className="flex flex-col items-center">
|
<div className="flex flex-col items-center">
|
||||||
<span className="text-6xl mb-2">
|
<span className="text-6xl mb-2">
|
||||||
{gameDetails && MOVES[String(whoAmI === "player1" ? gameDetails.playerA.move : gameDetails.playerB.move)]?.icon}
|
{gameDetails && MOVES[String(whoAmI === "player1" ? gameDetails.playerA.move : gameDetails.playerB.move)]?.icon}
|
||||||
@@ -395,7 +617,7 @@ export default function Reveal({
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Opponent Move (always on right) */}
|
{/* Classic game - Opponent Move (always on right) */}
|
||||||
<div className="flex flex-col items-center">
|
<div className="flex flex-col items-center">
|
||||||
<span className="text-6xl mb-2">
|
<span className="text-6xl mb-2">
|
||||||
{gameDetails && MOVES[String(whoAmI === "player1" ? gameDetails.playerB.move : gameDetails.playerA.move)]?.icon}
|
{gameDetails && MOVES[String(whoAmI === "player1" ? gameDetails.playerB.move : gameDetails.playerA.move)]?.icon}
|
||||||
@@ -408,6 +630,7 @@ export default function Reveal({
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Outcome Section */}
|
{/* 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