start fixing frontend

This commit is contained in:
SamKry
2025-12-16 15:58:40 +01:00
parent 2711b6ab87
commit 7192f82add
5 changed files with 1156 additions and 183 deletions

View File

@@ -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>
</>