"use client"; import { useEffect, useState } from "react"; import Web3 from "web3"; import Commit from "./Commit"; import Reveal from "./Reveal"; import { showErrorToast } from "@/app/lib/toast"; export type Player = { addr: string; bet: string; // Classic mode fields encrMove?: string; move?: number; // MinusOne mode fields hash1?: string; hash2?: string; move1?: number; move2?: number; wHash?: string; withdrawn?: number; nickname: string; }; export type GameDetails = { playerA: Player; playerB: Player; initialBet: string; outcome: number; isActive: boolean; returnGameId: number; gameMode?: string; // "classic" or "minusone" phase?: number; // GamePhase for minusone mode }; interface GameModalProps { gameId?: number; isOpen: boolean; onClose: () => void; account: string; contract: any; config: Config | null; web3: Web3 | null; setStatus: (status: string) => void; } export default function GameModal({ gameId, isOpen, onClose, account, contract, config, web3, setStatus, }: Readonly) { const [phase, setPhase] = useState<"commit" | "reveal" | "withdrawCommit" | "withdrawReveal">("commit"); const [whoAmI, setWhoAmI] = useState<"player1" | "player2" | "">(""); const [gameDetails, setGameDetails] = useState(null); // Classic mode state const [selectedMove, setSelectedMove] = useState(null); const [secret, setSecret] = useState(""); // MinusOne mode state const [selectedMove1, setSelectedMove1] = useState(null); const [selectedMove2, setSelectedMove2] = useState(null); const [secret1, setSecret1] = useState(""); const [secret2, setSecret2] = useState(""); const [withdrawChoice, setWithdrawChoice] = useState(null); const [withdrawSecret, setWithdrawSecret] = useState(""); // Helper function to generate game-specific storage key const getGameStorageKey = () => `game_${gameDetails?.returnGameId}`; // Game storage object structure type GameStorage = { // Classic mode secret?: string; selectedMove?: string | null; playMove?: string; // MinusOne mode secret1?: string; secret2?: string; selectedMove1?: string | null; selectedMove2?: string | null; withdrawChoice?: string | null; withdrawSecret?: string; timestamp?: number; }; // Constants for expiration const STORAGE_EXPIRATION_TIME = 60 * 60 * 1000; // 1 hour in milliseconds // Function to check and clean expired storage entries const cleanExpiredStorage = () => { const now = Date.now(); const keysToDelete: string[] = []; for (let i = 0; i < sessionStorage.length; i++) { const key = sessionStorage.key(i); if (key && key.startsWith("game_")) { try { const storedData = sessionStorage.getItem(key); if (storedData) { const parsed: GameStorage = JSON.parse(storedData); if (parsed.timestamp && now - parsed.timestamp > STORAGE_EXPIRATION_TIME) { keysToDelete.push(key); } } } catch (err) { console.error(`Failed to parse or clean storage key ${key}:`, err); } } } // Delete expired entries keysToDelete.forEach(key => { sessionStorage.removeItem(key); console.log(`Cleared expired session storage: ${key}`); }); }; // Storage helper functions const loadFromStorage = () => { if (!gameDetails) return; const storedData = sessionStorage.getItem(getGameStorageKey()); if (storedData) { try { const parsed: GameStorage = JSON.parse(storedData); // Classic mode if (parsed.secret) setSecret(parsed.secret); if (parsed.selectedMove) setSelectedMove(parsed.selectedMove); // MinusOne mode if (parsed.secret1) setSecret1(parsed.secret1); if (parsed.secret2) setSecret2(parsed.secret2); if (parsed.selectedMove1) setSelectedMove1(parsed.selectedMove1); if (parsed.selectedMove2) setSelectedMove2(parsed.selectedMove2); if (parsed.withdrawChoice) setWithdrawChoice(parsed.withdrawChoice); if (parsed.withdrawSecret) setWithdrawSecret(parsed.withdrawSecret); } catch (err) { console.error("Failed to parse stored game data:", err); } } }; const saveGameData = (updates: Partial) => { const storedData = sessionStorage.getItem(getGameStorageKey()); let currentData: GameStorage = { timestamp: Date.now() }; if (storedData) { try { currentData = JSON.parse(storedData); } catch (err) { console.error("Failed to parse stored game data:", err); } } const updatedData = { ...currentData, ...updates, timestamp: Date.now() }; sessionStorage.setItem(getGameStorageKey(), JSON.stringify(updatedData)); }; // Classic mode save functions const saveSecret = (value: string) => { setSecret(value); saveGameData({ secret: value }); }; const saveMoveSelection = (move: string | null) => { setSelectedMove(move); if (move !== null) { saveGameData({ selectedMove: move }); } }; const savePlayMove = (playMove: string) => { saveGameData({ playMove }); }; // MinusOne mode save functions const saveSecret1 = (value: string) => { setSecret1(value); saveGameData({ secret1: value }); }; const saveSecret2 = (value: string) => { setSecret2(value); saveGameData({ secret2: value }); }; const saveMoveSelection1 = (move: string | null) => { setSelectedMove1(move); if (move !== null) { saveGameData({ selectedMove1: move }); } }; const saveMoveSelection2 = (move: string | null) => { setSelectedMove2(move); if (move !== null) { saveGameData({ selectedMove2: move }); } }; const saveWithdrawChoice = (choice: string | null) => { setWithdrawChoice(choice); if (choice !== null) { saveGameData({ withdrawChoice: choice }); } }; const saveWithdrawSecret = (value: string) => { setWithdrawSecret(value); saveGameData({ withdrawSecret: value }); }; useEffect(() => { const fetchPlayerInfo = async () => { if (contract && account && gameId !== undefined) { try { let player = await contract.methods.whoAmI(gameId).call({ from: account }); if(player == 1) player = "player1"; else if(player == 2) player = "player2"; else player = ""; setWhoAmI(player); } catch (err: any) { showErrorToast("Error fetching player info: " + err.message); } } } const fetchGameDetails = async () => { if (contract && gameId !== undefined) { try { const details = await contract.methods.getGameDetails(gameId).call(); console.log("Game details:", details); setGameDetails(details); // Determine game mode const isMinusOne = details.gameMode === "minusone"; if (isMinusOne) { // MinusOne mode: use phase enum // GamePhase enum: Reg=0, InitC=1, FirstR=2, WithdC=3, WithdR=4, Done=5 const gamePhase = Number(details.phase); if (gamePhase === 5) { // Done setPhase("reveal"); // Show final results } else if (gamePhase === 4) { // WithdR setPhase("withdrawReveal"); } else if (gamePhase === 3) { // WithdC setPhase("withdrawCommit"); } else if (gamePhase === 2) { // FirstR setPhase("reveal"); } else { // InitC or Reg setPhase("commit"); } } else { // Classic mode: check encrMove and move fields const playerAHasMove = details.playerA.encrMove && Number(details.playerA.encrMove) !== 0; const playerBHasMove = details.playerB.encrMove && Number(details.playerB.encrMove) !== 0; const playerARevealed = details.playerA.move && Number(details.playerA.move) !== 0; const playerBRevealed = details.playerB.move && Number(details.playerB.move) !== 0; // If both players have revealed their moves, show reveal phase (with results) if (playerARevealed && playerBRevealed) { setPhase("reveal"); } // If both players have committed but not revealed, show reveal phase else if (playerAHasMove && playerBHasMove) { setPhase("reveal"); } // Otherwise, show commit phase else { setPhase("commit"); } } } catch (err: any) { showErrorToast("Error fetching game details: " + err.message); } } }; // Only reset state when game ID actually changes (not on first render) if (gameDetails) { setSelectedMove(null); setSecret(""); } fetchGameDetails(); fetchPlayerInfo(); // Refetch game details periodically every 2 seconds const intervalId = setInterval(fetchGameDetails, 2000); return () => clearInterval(intervalId); }, [contract, account, gameId]); // Load from storage after game details are fetched useEffect(() => { loadFromStorage(); }, [gameDetails]); // Set up interval to clean expired storage entries every 5 minutes useEffect(() => { const cleanupIntervalId = setInterval(cleanExpiredStorage, 5 * 60 * 1000); return () => clearInterval(cleanupIntervalId); }, []); const handleClose = () => { // Reset state when closing setPhase("commit"); setSelectedMove(null); setSecret(""); setSelectedMove1(null); setSelectedMove2(null); setSecret1(""); setSecret2(""); setWithdrawChoice(null); setWithdrawSecret(""); onClose(); }; if (!isOpen) return null; return (
{/* Header */}

{gameId ? `Game #${gameId}` : "Game"}

{phase === "commit" && "Commit your move"} {phase === "reveal" && "Reveal your move"} {phase === "withdrawCommit" && "Choose which move to withdraw"} {phase === "withdrawReveal" && "Reveal your withdrawal choice"}

{/* Content */}
{/* Phase Content */}
{phase === "commit" && ( )} {phase === "reveal" && ( )} {phase === "withdrawCommit" && ( {}} isWithdrawPhase={true} /> )} {phase === "withdrawReveal" && ( )}
); }