From 06176e74f2037c4d7f09a82c00590a9fa339914f Mon Sep 17 00:00:00 2001 From: averel10 Date: Fri, 21 Nov 2025 18:18:59 +0100 Subject: [PATCH] update game logic and UI components for move selection and reveal phases; remove Lobby component --- config.json | 6 +- crypto_clash_contract/contracts/Game.sol | 4 +- crypto_clash_frontend/app/clash/Commit.tsx | 161 ++++++-- crypto_clash_frontend/app/clash/GameList.tsx | 3 +- crypto_clash_frontend/app/clash/Lobby.tsx | 238 ------------ crypto_clash_frontend/app/clash/Reveal.tsx | 381 ++++++++++++------- crypto_clash_frontend/app/clash/page.tsx | 8 + crypto_clash_frontend/public/config.json | 6 +- 8 files changed, 391 insertions(+), 416 deletions(-) delete mode 100644 crypto_clash_frontend/app/clash/Lobby.tsx diff --git a/config.json b/config.json index ee85ce6..0b3e280 100644 --- a/config.json +++ b/config.json @@ -1,6 +1,6 @@ { - "API_URL": "https://rpc.hasrv.averel10.app/", - "CONTRACT_ADDRESS": "0x8B7746B922a80d9B29cA939fD90577932527112C", + "API_URL": "http://185.48.228.49:8545", + "CONTRACT_ADDRESS": "0x8aF379ED8C452612676ACB856E26955237358944", "ABI": [ { "inputs": [ @@ -59,7 +59,7 @@ "type": "function" } ], - "GAME_CONTRACT_ADDRESS": "0x9230eBBB05c3e953a5F135F032b352c6F0B6E164", + "GAME_CONTRACT_ADDRESS": "0x267d1b8bf8C13ea60D39F90cC68AEc4b8389bb64", "GAME_ABI": [ { "inputs": [], diff --git a/crypto_clash_contract/contracts/Game.sol b/crypto_clash_contract/contracts/Game.sol index cea6735..0627196 100644 --- a/crypto_clash_contract/contracts/Game.sol +++ b/crypto_clash_contract/contracts/Game.sol @@ -204,7 +204,7 @@ contract Game { uint gameId = playerToActiveGame[msg.sender]; GameState storage game = games[gameId]; - bytes32 encrMove = sha256(abi.encodePacked(clearMove)); // Hash of clear input (= "move-password") + bytes32 encrMove = keccak256(abi.encodePacked(clearMove)); // Hash of clear input (= "move-password") Moves move = Moves(getFirstChar(clearMove)); // Actual move (Rock / Paper / Scissors) // If move invalid, exit @@ -381,7 +381,7 @@ contract Game { if (gameId == 0) return false; GameState storage game = games[gameId]; - return (game.playerA.encrMove != 0x0 && game.playerB.encrMove != 0x0); + return (game.playerA.encrMove != bytes32(0) && game.playerB.encrMove != bytes32(0)); } // Return 'true' if both players have revealed their move, 'false' otherwise. diff --git a/crypto_clash_frontend/app/clash/Commit.tsx b/crypto_clash_frontend/app/clash/Commit.tsx index 4097900..65590af 100644 --- a/crypto_clash_frontend/app/clash/Commit.tsx +++ b/crypto_clash_frontend/app/clash/Commit.tsx @@ -1,4 +1,4 @@ -import { useState } from "react"; +import { useState, useEffect } from "react"; import Web3 from "web3"; import { Button } from "./Button"; import { Input } from "./Input"; @@ -9,25 +9,60 @@ interface CommitProps { config: Config | null; web3: Web3 | null; setStatus: (status: string) => void; + selectedMove: string | null; + setSelectedMove: (move: string | null) => void; + secret: string; + setSecret: (secret: string) => void; } +type Move = "1" | "2" | "3" | null; +type MoveName = "Rock" | "Paper" | "Scissors"; + +const MOVES: Record = { + "1": { name: "Rock", icon: "✊" }, + "2": { name: "Paper", icon: "✋" }, + "3": { name: "Scissors", icon: "✌️" }, +}; + export default function Commit({ account, contract, config, web3, setStatus, + selectedMove, + setSelectedMove, + secret, + setSecret, }: Readonly) { const [loading, setLoading] = useState(false); const [playMove, setPlayMove] = useState(""); const [bothPlayed, setBothPlayed] = useState(""); + // Generate random secret on mount if not already set + useEffect(() => { + if (!secret) { + const randomHex = Math.random().toString(16).slice(2, 18); + setSecret(randomHex); + } + }, []); + + // Update encrypted move when move or secret changes + useEffect(() => { + if (selectedMove && secret) { + const clearMove = `${selectedMove}-${secret}`; + // Use keccak256 (Ethereum's standard hash function) + const hash = Web3.utils.keccak256(clearMove); + setPlayMove(hash); + } + }, [selectedMove, secret]); + // Commit phase read-only handlers const handleBothPlayed = async () => { if (!contract) return; setLoading(true); try { - const res = await contract.methods.bothPlayed().call(); + const res = await contract.methods.bothPlayed().call({from : account}); setBothPlayed(res ? "true" : "false"); } catch (err: any) { setStatus("Failed to fetch bothPlayed: " + err.message); @@ -37,7 +72,7 @@ export default function Commit({ }; const handlePlay = async () => { - if (!contract || !web3 || !account) return; + if (!contract || !web3 || !account || !playMove) return; setLoading(true); setStatus(""); try { @@ -63,33 +98,111 @@ export default function Commit({ setLoading(false); } }; - return ( -
-

play(bytes32 encrMove)

- setPlayMove(e.target.value)} - className="mr-2" - /> - -
+ const regenerateSecret = () => { + const randomHex = Math.random().toString(16).slice(2, 18); + setSecret(randomHex); + }; + + return ( +
+

+ Select Your Move +

+ + {/* Move Selection */} +
+

+ Choose your move: +

+
+ {(["1", "2", "3"] as const).map((move) => ( + + ))} +
+
+ + {/* Secret Input */} +
+ +
+ setSecret(e.target.value)} + placeholder="Your secret passphrase" + className="flex-1" + /> + +
+

+ Keep this secret safe! It's needed to reveal your move later. +

+
+ + {/* Encrypted Move Display */} +
+ +
+ + {playMove || "Select a move and enter a secret"} + +
+
+ + {/* Action Buttons */} +
+ + - {bothPlayed} -
+ {bothPlayed && ( + + {bothPlayed === "true" + ? "✓ Both players have committed!" + : "Waiting for opponent..."} + + )}
); diff --git a/crypto_clash_frontend/app/clash/GameList.tsx b/crypto_clash_frontend/app/clash/GameList.tsx index 4620fd8..0e52edd 100644 --- a/crypto_clash_frontend/app/clash/GameList.tsx +++ b/crypto_clash_frontend/app/clash/GameList.tsx @@ -55,7 +55,6 @@ export default function GameList({ } setGames(gameDetails); - // Check which games the user is participating in if (account) { const userGames = new Set(); @@ -85,7 +84,7 @@ export default function GameList({ return () => { if (interval) clearInterval(interval); }; - }, [contract, web3]); + }, [contract, web3, account]); // Join an existing game const handleJoinGame = async (gameId: number, bet: string) => { diff --git a/crypto_clash_frontend/app/clash/Lobby.tsx b/crypto_clash_frontend/app/clash/Lobby.tsx deleted file mode 100644 index 21b598b..0000000 --- a/crypto_clash_frontend/app/clash/Lobby.tsx +++ /dev/null @@ -1,238 +0,0 @@ -import { useState } from "react"; -import Web3 from "web3"; -import { Button } from "./Button"; -import { Input } from "./Input"; - -interface LobbyProps { - account: string; - contract: any; - config: Config | null; - web3: Web3 | null; - setStatus: (status: string) => void; -} - -export default function Lobby({ - account, - contract, - config, - web3, - setStatus, -}: Readonly) { - const [loading, setLoading] = useState(false); - const [registerGameId, setRegisterGameId] = useState("0"); - const [registerBet, setRegisterBet] = useState(""); - const [betMin, setBetMin] = useState(""); - const [activeGameIds, setActiveGameIds] = useState(""); - const [contractBalance, setContractBalance] = useState(""); - const [gameDetailsId, setGameDetailsId] = useState(""); - const [gameDetails, setGameDetails] = useState(null); - const [myActiveGameId, setMyActiveGameId] = useState(""); - const [pastGameIndex, setPastGameIndex] = useState(""); - const [pastGame, setPastGame] = useState(null); - const [pastGamesCount, setPastGamesCount] = useState(""); - const [whoAmI, setWhoAmI] = useState(""); - - // Read-only contract function handlers - const handleGetBetMin = async () => { - if (!contract || !web3) return; - setLoading(true); - try { - const res = await contract.methods.BET_MIN().call(); - setBetMin(web3.utils.fromWei(res, "ether") + " ETH"); - } catch (err: any) { - setStatus("Failed to fetch BET_MIN: " + err.message); - } finally { - setLoading(false); - } - }; - - const handleGetActiveGameIds = async () => { - if (!contract) return; - setLoading(true); - try { - const res = await contract.methods.getActiveGameIds().call(); - setActiveGameIds(res.join(", ")); - } catch (err: any) { - setStatus("Failed to fetch active game IDs: " + err.message); - } finally { - setLoading(false); - } - }; - - const handleGetContractBalance = async () => { - if (!contract || !web3) return; - setLoading(true); - try { - const res = await contract.methods.getContractBalance().call(); - setContractBalance(web3.utils.fromWei(res, "ether") + " ETH"); - } catch (err: any) { - setStatus("Failed to fetch contract balance: " + err.message); - } finally { - setLoading(false); - } - }; - - const handleGetGameDetails = async () => { - if (!contract) return; - setLoading(true); - try { - const res = await contract.methods - .getGameDetails(Number(gameDetailsId)) - .call(); - setGameDetails(res); - } catch (err: any) { - setStatus("Failed to fetch game details: " + err.message); - } finally { - setLoading(false); - } - }; - - const handleGetMyActiveGameId = async () => { - if (!contract || !account) return; - setLoading(true); - try { - const res = await contract.methods - .getMyActiveGameId() - .call({ from: account }); - setMyActiveGameId(res.toString()); - } catch (err: any) { - setStatus("Failed to fetch my active game ID: " + err.message); - } finally { - setLoading(false); - } - }; - - const handleGetPastGame = async () => { - if (!contract) return; - setLoading(true); - try { - const res = await contract.methods - .getPastGame(Number(pastGameIndex)) - .call(); - setPastGame(res); - } catch (err: any) { - setStatus("Failed to fetch past game: " + err.message); - } finally { - setLoading(false); - } - }; - - const handleGetPastGamesCount = async () => { - if (!contract) return; - setLoading(true); - try { - const res = await contract.methods.getPastGamesCount().call(); - setPastGamesCount(res.toString()); - } catch (err: any) { - setStatus("Failed to fetch past games count: " + err.message); - } finally { - setLoading(false); - } - }; - - const handleWhoAmI = async () => { - if (!contract || !account) return; - setLoading(true); - try { - const res = await contract.methods.whoAmI().call({ from: account }); - setWhoAmI(res.toString()); - } catch (err: any) { - setStatus("Failed to fetch whoAmI: " + err.message); - } finally { - setLoading(false); - } - }; - - const handleRegister = async () => { - if (!contract || !web3 || !account) return; - setLoading(true); - setStatus(""); - try { - console.log(registerBet); - const bet = web3.utils.toWei(registerBet || "0.01", "ether"); - console.log(bet); - console.log(web3.utils.toHex(bet)); - const tx = contract.methods.register(Number(registerGameId || 0)); - const gas = await tx.estimateGas({ from: account, value: bet }); - const result = await (globalThis as any).ethereum.request({ - method: "eth_sendTransaction", - params: [ - { - from: account, - to: config?.GAME_CONTRACT_ADDRESS, - data: tx.encodeABI(), - value: web3.utils.numberToHex(bet), - gas: web3.utils.toHex(gas), - chainId: web3.utils.toHex(await web3.eth.net.getId()), - }, - ], - }); - setStatus("Register tx sent: " + result); - } catch (err: any) { - setStatus("Register failed: " + err.message); - console.error(err); - } finally { - setLoading(false); - } - }; - return ( -
-

register(uint gameId) (payable)

- setRegisterGameId(e.target.value)} - className="mr-2" - /> - setRegisterBet(e.target.value)} - className="mr-2" - /> -
- Enter amount in ETH (e.g., 0.01 for 0.01 ETH). Entering 1 means 1 full - ETH. -
- - -
- - {betMin} -
- - {activeGameIds} -
- - {contractBalance} -
- setGameDetailsId(e.target.value)} className="mr-2" /> - - {gameDetails &&
{JSON.stringify(gameDetails, (_, v) => typeof v === 'bigint' ? v.toString() : v, 2)}
}
-
- - {myActiveGameId} -
- setPastGameIndex(e.target.value)} className="mr-2" /> - - {pastGame &&
{JSON.stringify(pastGame, (_, v) => typeof v === 'bigint' ? v.toString() : v, 2)}
}
-
- - {pastGamesCount} -
- - {whoAmI} -
-
- ); -} diff --git a/crypto_clash_frontend/app/clash/Reveal.tsx b/crypto_clash_frontend/app/clash/Reveal.tsx index e039797..a0a7e01 100644 --- a/crypto_clash_frontend/app/clash/Reveal.tsx +++ b/crypto_clash_frontend/app/clash/Reveal.tsx @@ -1,7 +1,6 @@ -import { useState } from "react"; +import { useState, useEffect } from "react"; import Web3 from "web3"; import { Button } from "./Button"; -import { Input } from "./Input"; interface RevealProps { account: string; @@ -9,23 +8,104 @@ interface RevealProps { config: Config | null; web3: Web3 | null; setStatus: (status: string) => void; + selectedMove: string | null; + secret: string; } +type MoveName = "Rock" | "Paper" | "Scissors"; + +const MOVES: Record = { + "1": { name: "Rock", icon: "✊" }, + "2": { name: "Paper", icon: "✋" }, + "3": { name: "Scissors", icon: "✌️" }, +}; + +const OUTCOMES: Record = + { + 0: { name: "None", emoji: "❓", color: "gray" }, + 1: { name: "You Won!", emoji: "🏆", color: "green" }, + 2: { name: "You Lost", emoji: "😢", color: "red" }, + 3: { name: "Draw", emoji: "🤝", color: "yellow" }, + }; + export default function Reveal({ account, contract, config, web3, setStatus, + selectedMove, + secret, }: Readonly) { const [loading, setLoading] = useState(false); - const [revealMove, setRevealMove] = useState(""); - const [bothRevealed, setBothRevealed] = useState(""); - const [playerARevealed, setPlayerARevealed] = useState(""); - const [playerBRevealed, setPlayerBRevealed] = useState(""); - const [revealTimeLeft, setRevealTimeLeft] = useState(""); - const [outcome, setOutcome] = useState(""); - // getOutcome handler + const [bothRevealed, setBothRevealed] = useState(false); + const [playerARevealed, setPlayerARevealed] = useState(false); + const [playerBRevealed, setPlayerBRevealed] = useState(false); + const [revealTimeLeft, setRevealTimeLeft] = useState(0); + const [outcome, setOutcome] = useState(0); + const [revealed, setRevealed] = useState(false); + + const clearMove = selectedMove && secret ? `${selectedMove}-${secret}` : ""; + + // Check game status on mount + useEffect(() => { + const checkStatus = async () => { + if (!contract) return; + try { + const [br, par, pbr, rtl, out] = await Promise.all([ + await contract.methods.bothRevealed().call({ from : account}), + await contract.methods.playerARevealed().call({ from : account}), + await contract.methods.playerBRevealed().call({ from : account}), + await contract.methods.revealTimeLeft().call({ from : account}), + await contract.methods.getLastWinner().call({ from : account}), + ]); + + console.log("Status:", { + br, par, pbr, rtl, out + }); + setBothRevealed(br); + setPlayerARevealed(par); + setPlayerBRevealed(pbr); + setRevealTimeLeft(Number(rtl)); + setOutcome(Number(out)); + } catch (err: any) { + console.error("Failed to check status:", err); + } + }; + + const interval = setInterval(checkStatus, 3000); + checkStatus(); + return () => clearInterval(interval); + }, [contract, account]); + + const handleReveal = async () => { + if (!contract || !web3 || !account || !clearMove) return; + setLoading(true); + setStatus(""); + try { + const tx = contract.methods.reveal(clearMove); + const gas = await tx.estimateGas({ from: account }); + const result = await (globalThis as any).ethereum.request({ + method: "eth_sendTransaction", + params: [ + { + from: account, + to: config?.GAME_CONTRACT_ADDRESS, + data: tx.encodeABI(), + gas: web3.utils.toHex(gas), + chainId: web3.utils.toHex(await web3.eth.net.getId()), + }, + ], + }); + setStatus("✅ Reveal tx sent: " + result); + setRevealed(true); + } catch (err: any) { + setStatus("❌ Reveal failed: " + err.message); + } finally { + setLoading(false); + } + }; + const handleGetOutcome = async () => { if (!contract || !web3 || !account) return; setLoading(true); @@ -45,157 +125,170 @@ export default function Reveal({ }, ], }); - setStatus("getOutcome tx sent: " + result); - setOutcome("Transaction sent: " + result); + setStatus("✅ Claim tx sent: " + result); } catch (err: any) { - setStatus("getOutcome failed: " + err.message); - setOutcome(""); + setStatus("❌ Claim failed: " + err.message); } finally { setLoading(false); } }; - // Reveal phase read-only handlers - const handleBothRevealed = async () => { - if (!contract) return; - setLoading(true); - try { - const res = await contract.methods.bothRevealed().call(); - setBothRevealed(res ? "true" : "false"); - } catch (err: any) { - setStatus("Failed to fetch bothRevealed: " + err.message); - } finally { - setLoading(false); - } - }; + const outcomeData = OUTCOMES[outcome] || OUTCOMES[0]; - const handlePlayerARevealed = async () => { - if (!contract) return; - setLoading(true); - try { - const res = await contract.methods.playerARevealed().call(); - setPlayerARevealed(res ? "true" : "false"); - } catch (err: any) { - setStatus("Failed to fetch playerARevealed: " + err.message); - } finally { - setLoading(false); - } - }; - - const handlePlayerBRevealed = async () => { - if (!contract) return; - setLoading(true); - try { - const res = await contract.methods.playerBRevealed().call(); - setPlayerBRevealed(res ? "true" : "false"); - } catch (err: any) { - setStatus("Failed to fetch playerBRevealed: " + err.message); - } finally { - setLoading(false); - } - }; - - const handleRevealTimeLeft = async () => { - if (!contract) return; - setLoading(true); - try { - const res = await contract.methods.revealTimeLeft().call(); - setRevealTimeLeft(res.toString()); - } catch (err: any) { - setStatus("Failed to fetch revealTimeLeft: " + err.message); - } finally { - setLoading(false); - } - }; - - const handleReveal = async () => { - if (!contract || !web3 || !account) return; - setLoading(true); - setStatus(""); - try { - const tx = contract.methods.reveal(revealMove); - const gas = await tx.estimateGas({ from: account }); - const result = await (globalThis as any).ethereum.request({ - method: "eth_sendTransaction", - params: [ - { - from: account, - to: config?.GAME_CONTRACT_ADDRESS, - data: tx.encodeABI(), - gas: web3.utils.toHex(gas), - chainId: web3.utils.toHex(await web3.eth.net.getId()), - }, - ], - }); - setStatus("Reveal tx sent: " + result); - } catch (err: any) { - setStatus("Reveal failed: " + err.message); - } finally { - setLoading(false); - } - }; return ( -
-

reveal(string clearMove)

- setRevealMove(e.target.value)} - className="mr-2" - /> - +
+ {/* Your Move Section */} +
+

+ Your Move +

+ {selectedMove ? ( +
+
+ {MOVES[selectedMove].icon} + + {MOVES[selectedMove].name} + +
+
+
+

+ Clear Move: +

+ + {clearMove} + +
+
+ ) : ( +

+ No move selected yet +

+ )} +
-
+ {/* Game Status Section */} +
+
+

{playerARevealed ? "✅" : "⏳"}

+

+ Player A +

+

+ {playerARevealed ? "Revealed" : "Waiting"} +

+
+
+

{playerBRevealed ? "✅" : "⏳"}

+

+ Player B +

+

+ {playerBRevealed ? "Revealed" : "Waiting"} +

+
+
+

+ ⏱️ +

+

+ Time Left +

+

+ {revealTimeLeft > 0 ? `${revealTimeLeft}s` : "Expired"} +

+
+
+ + {/* Reveal Section */} +
+

+ Reveal Your Move +

+

+ Submit your clear move and secret to the blockchain. This proves you + didn't cheat! +

- {outcome}
-
- - {bothRevealed} -
- - {playerARevealed} -
- - {playerBRevealed} -
- - {revealTimeLeft} -
+

+ {outcomeData.emoji} +

+

+ {outcomeData.name} +

+ +
+ )} + + {/* Status Messages */} + {!bothRevealed && !revealed && ( +
+

+ ⏳ Waiting for both players to reveal... +

+
+ )}
); } diff --git a/crypto_clash_frontend/app/clash/page.tsx b/crypto_clash_frontend/app/clash/page.tsx index daad444..30d8747 100644 --- a/crypto_clash_frontend/app/clash/page.tsx +++ b/crypto_clash_frontend/app/clash/page.tsx @@ -15,6 +15,8 @@ export default function Clash() { // Inputs for contract functions const [phase, setPhase] = useState<"games" | "commit" | "reveal">("games"); + const [selectedMove, setSelectedMove] = useState(null); + const [secret, setSecret] = useState(""); const handlePlayClick = (gameId: number) => { setPhase("commit"); @@ -138,6 +140,10 @@ export default function Clash() { config={config} web3={web3} setStatus={setStatus} + selectedMove={selectedMove} + setSelectedMove={setSelectedMove} + secret={secret} + setSecret={setSecret} /> )} {phase === "reveal" && ( @@ -147,6 +153,8 @@ export default function Clash() { config={config} web3={web3} setStatus={setStatus} + selectedMove={selectedMove} + secret={secret} /> )}
diff --git a/crypto_clash_frontend/public/config.json b/crypto_clash_frontend/public/config.json index ee85ce6..0b3e280 100644 --- a/crypto_clash_frontend/public/config.json +++ b/crypto_clash_frontend/public/config.json @@ -1,6 +1,6 @@ { - "API_URL": "https://rpc.hasrv.averel10.app/", - "CONTRACT_ADDRESS": "0x8B7746B922a80d9B29cA939fD90577932527112C", + "API_URL": "http://185.48.228.49:8545", + "CONTRACT_ADDRESS": "0x8aF379ED8C452612676ACB856E26955237358944", "ABI": [ { "inputs": [ @@ -59,7 +59,7 @@ "type": "function" } ], - "GAME_CONTRACT_ADDRESS": "0x9230eBBB05c3e953a5F135F032b352c6F0B6E164", + "GAME_CONTRACT_ADDRESS": "0x267d1b8bf8C13ea60D39F90cC68AEc4b8389bb64", "GAME_ABI": [ { "inputs": [],