import React, { useState, useEffect, useCallback } from 'react'; import { initializeApp } from 'firebase/app'; import { getAuth, signInAnonymously, signInWithCustomToken, onAuthStateChanged } from 'firebase/auth'; import { getFirestore, collection, query, where, onSnapshot, runTransaction, doc, setDoc, writeBatch } from 'firebase/firestore'; import { LuSword, LuUserCheck, LuClock, LuDollarSign, LuLoader2, LuAlertTriangle, LuArrowLeft } from 'lucide-react'; // Variáveis globais de ambiente (MANDATÓRIO) const appId = typeof __app_id !== 'undefined' ? __app_id : 'default-app-id'; const firebaseConfig = typeof __firebase_config !== 'undefined' ? JSON.parse(__firebase_config) : {}; const initialAuthToken = typeof __initial_auth_token !== 'undefined' ? __initial_auth_token : null; // Inicializa o Firebase const app = initializeApp(firebaseConfig); const db = getFirestore(app); const auth = getAuth(app); // Adiciona logging do Firestore // import { setLogLevel } from 'firebase/firestore'; // setLogLevel('debug'); /** * Funções utilitárias para conversão de Áudio (mantidas como padrão de código Gemini, mas não usadas na lógica principal do jogo). * Estas funções são para converter Base64 PCM para Blob WAV (Necessária para futuras integrações de Áudio TTS/Voz). */ function base64ToArrayBuffer(base64) { const binaryString = atob(base64); const len = binaryString.length; const bytes = new Uint8Array(len); for (let i = 0; i < len; i++) { bytes[i] = binaryString.charCodeAt(i); } return bytes.buffer; } function pcmToWav(pcm16, sampleRate) { const numChannels = 1; const bytesPerSample = 2; const blockAlign = numChannels * bytesPerSample; const byteRate = sampleRate * blockAlign; const pcmDataLength = pcm16.length * bytesPerSample; const wavDataLength = pcmDataLength + 44; // 44 bytes for the WAV header const buffer = new ArrayBuffer(wavDataLength); const view = new DataView(buffer); // RIFF header writeString(view, 0, 'RIFF'); view.setUint32(4, wavDataLength - 8, true); // ChunkSize writeString(view, 8, 'WAVE'); // fmt sub-chunk writeString(view, 12, 'fmt '); // Subchunk1ID view.setUint32(16, 16, true); // Subchunk1Size (16 for PCM) view.setUint16(20, 1, true); // AudioFormat (1 for PCM) view.setUint16(22, numChannels, true); // NumChannels view.setUint32(24, sampleRate, true); // SampleRate view.setUint32(28, byteRate, true); // ByteRate view.setUint16(32, blockAlign, true); // BlockAlign view.setUint16(34, 16, true); // BitsPerSample // data sub-chunk writeString(view, 36, 'data'); // Subchunk2ID view.setUint32(40, pcmDataLength, true); // Subchunk2Size // Write PCM data let offset = 44; for (let i = 0; i < pcm16.length; i++) { view.setInt16(offset, pcm16[i], true); offset += 2; } return new Blob([buffer], { type: 'audio/wav' }); } function writeString(view, offset, string) { for (let i = 0; i < string.length; i++) { view.setUint8(offset + i, string.charCodeAt(i)); } } // Fim das funções utilitárias de Áudio // --- Configuração do Tabuleiro (35 tiles) --- // Dados do tabuleiro como no sistema original do usuário, mas formatado para inserção em lote. const createGameBoardData = (gameId, p1Name, p2Name) => [ { game: gameId, id2: 1, peca: '', player: 0, tipo: 1, n: '0', l: '2', s: '8', o: '0' }, { game: gameId, id2: 2, peca: '1', player: p1Name, tipo: 1, n: '0', l: '3', s: '9', o: '1' }, { game: gameId, id2: 3, peca: '2', player: p1Name, tipo: 1, n: '0', l: '4', s: '10', o: '2' }, { game: gameId, id2: 4, peca: '3', player: p1Name, tipo: 1, n: '0', l: '5', s: '11', o: '3' }, { game: gameId, id2: 5, peca: '4', player: p1Name, tipo: 1, n: '0', l: '6', s: '12', o: '4' }, { game: gameId, id2: 6, peca: '5', player: p1Name, tipo: 1, n: '0', l: '7', s: '13', o: '5' }, { game: gameId, id2: 7, peca: '', player: 0, tipo: 1, n: '0', l: '0', s: '14', o: '6' }, { game: gameId, id2: 8, peca: '', player: 0, tipo: 1, n: '1', l: '9', s: '15', o: '0' }, { game: gameId, id2: 9, peca: '', player: 0, tipo: 1, n: '2', l: '10', s: '0', o: '8' }, { game: gameId, id2: 10, peca: '', player: 0, tipo: 1, n: '3', l: '11', s: '17', o: '9' }, { game: gameId, id2: 11, peca: '', player: 0, tipo: 1, n: '4', l: '12', s: '0', o: '10' }, { game: gameId, id2: 12, peca: '', player: 0, tipo: 1, n: '5', l: '13', s: '19', o: '11' }, { game: gameId, id2: 13, peca: '', player: 0, tipo: 1, n: '6', l: '14', s: '0', o: '12' }, { game: gameId, id2: 14, peca: '', player: 0, tipo: 1, n: '7', l: '0', s: '21', o: '13' }, { game: gameId, id2: 15, peca: '', player: 0, tipo: 1, n: '8', l: '23', s: '22', o: '0' }, { game: gameId, id2: 16, peca: 'x', player: 0, tipo: 0, n: '9', l: '17', s: '23', o: '15' }, { game: gameId, id2: 17, peca: '', player: 0, tipo: 1, n: '10', l: '0', s: '24', o: '0' }, { game: gameId, id2: 18, peca: 'x', player: 0, tipo: 0, n: '11', l: '19', s: '25', o: '17' }, { game: gameId, id2: 19, peca: '', player: 0, tipo: 1, n: '12', l: '0', s: '26', o: '0' }, { game: gameId, id2: 20, peca: 'x', player: 0, tipo: 0, n: '13', l: '21', s: '27', o: '19' }, { game: gameId, id2: 21, peca: '', player: 0, tipo: 1, n: '14', l: '0', s: '28', o: '0' }, { game: gameId, id2: 22, peca: '', player: 0, tipo: 1, n: '15', l: '23', s: '29', o: '0' }, { game: gameId, id2: 23, peca: '', player: 0, tipo: 1, n: '0', l: '24', s: '30', o: '22' }, { game: gameId, id2: 24, peca: '', player: 0, tipo: 1, n: '17', l: '25', s: '31', o: '23' }, { game: gameId, id2: 25, peca: '', player: 0, tipo: 1, n: '0', l: '26', s: '32', o: '24' }, { game: gameId, id2: 26, peca: '', player: 0, tipo: 1, n: '19', l: '27', s: '33', o: '25' }, { game: gameId, id2: 27, peca: '', player: 0, tipo: 1, n: '0', l: '28', s: '34', o: '26' }, { game: gameId, id2: 28, peca: '', player: 0, tipo: 1, n: '21', l: '0', s: '35', o: '27' }, { game: gameId, id2: 29, peca: '', player: 0, tipo: 1, n: '22', l: '30', s: '0', o: '0' }, { game: gameId, id2: 30, peca: '5', player: p2Name, tipo: 1, n: '23', l: '31', s: '0', o: '29' }, { game: gameId, id2: 31, peca: '4', player: p2Name, tipo: 1, n: '24', l: '32', s: '0', o: '30' }, { game: gameId, id2: 32, peca: '3', player: p2Name, tipo: 1, n: '25', l: '33', s: '0', o: '31' }, { game: gameId, id2: 33, peca: '2', player: p2Name, tipo: 1, n: '26', l: '34', s: '0', o: '32' }, { game: gameId, id2: 34, peca: '1', player: p2Name, tipo: 1, n: '27', l: '35', s: '0', o: '33' }, { game: gameId, id2: 35, peca: '', player: 0, tipo: 1, n: '28', l: '0', s: '0', o: '34' } ]; // Função principal do Batch Write para o Tabuleiro (Executada após a Transação) // Cria os 35 documentos do tabuleiro de forma eficiente e atômica para o jogo. const createGameBoardBatch = async (gameId, p1Name, p2Name) => { const batch = writeBatch(db); const boardData = createGameBoardData(gameId, p1Name, p2Name); // O collection 'tabuleiro' é público para que o jogo funcione para ambos os players const tabuleiroCollectionRef = collection(db, 'artifacts', appId, 'public', 'data', 'tabuleiro'); for (const tile of boardData) { // Usa uma combinação de gameId e id2 como ID do documento para unicidade (Ex: "123_tile_5") const tileDocRef = doc(tabuleiroCollectionRef, `${gameId}_tile_${tile.id2}`); batch.set(tile, { merge: false }); // merge: false garante que o documento seja totalmente sobrescrito } await batch.commit(); console.log(`Tabuleiro com ${boardData.length} tiles criado para o jogo ${gameId}.`); }; const App = () => { const [userId, setUserId] = useState(null); const [userName, setUserName] = useState(null); const [isAuthReady, setIsAuthReady] = useState(false); const [availableGames, setAvailableGames] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [saldoLivre, setSaldoLivre] = useState(0); const [joinStatus, setJoinStatus] = useState({}); // {gameId: 'loading' | 'success' | 'error'} // 1. Configuração de Autenticação e Usuário useEffect(() => { const unsubscribe = onAuthStateChanged(auth, async (user) => { if (user) { // Usuário autenticado (anônimo ou custom token) setUserId(user.uid); // Nome de Usuário simulado (em um sistema real, viria de uma tabela de perfil) setUserName(`User-${user.uid.substring(0, 8)}`); } else { // Tentativa de login inicial try { if (initialAuthToken) { const userCredential = await signInWithCustomToken(auth, initialAuthToken); setUserId(userCredential.user.uid); setUserName(`User-${userCredential.user.uid.substring(0, 8)}`); } else { // Se não houver token, login anônimo const userCredential = await signInAnonymously(auth); setUserId(userCredential.user.uid); setUserName(`Anon-${userCredential.user.uid.substring(0, 8)}`); } } catch (e) { console.error("Erro na autenticação:", e); setError("Falha na autenticação. Recarregue a página."); } } setIsAuthReady(true); }); return () => unsubscribe(); }, []); // 2. Cálculo do Saldo Livre (Baseado no Mempool) useEffect(() => { // Não prossegue sem autenticação if (!isAuthReady || !userId) return; // O 'mempool' é uma coleção privada do usuário para segurança (somente o usuário pode ler/escrever o seu próprio mempool) const mempoolRef = collection(db, 'artifacts', appId, 'users', userId, 'mempool'); // Consulta para ouvir mudanças no mempool const q = query(mempoolRef); const unsubscribe = onSnapshot(q, (snapshot) => { let totalDeposited = 0; let totalWithdrawn = 0; snapshot.forEach((doc) => { const data = doc.data(); const value = parseFloat(data.amount) || 0; // Simulação de cálculo de saldo livre a partir do mempool if (data.type === 'deposit') { // Valor colocado em Escrow (sai do saldo livre) totalDeposited += value; } else if (data.type === 'withdrawal') { // Valor retornado (volta para o saldo livre) totalWithdrawn += value; } }); // Simulação de Saldo Inicial (Você deve ajustar '1000' para o saldo inicial real do usuário) const initialBalance = 1000; const newSaldoLivre = initialBalance - totalDeposited + totalWithdrawn; setSaldoLivre(Math.max(0, newSaldoLivre)); }, (err) => { console.error("Erro ao carregar Mempool:", err); setError("Erro ao carregar dados financeiros."); }); return () => unsubscribe(); }, [isAuthReady, userId]); // 3. Carregamento dos Jogos Disponíveis useEffect(() => { if (!isAuthReady || !userId) return; // A coleção 'games' é pública para que todos vejam const gamesRef = collection(db, 'artifacts', appId, 'public', 'data', 'games'); // Filtra: p2 vazio E p1 não é o próprio usuário const q = query( gamesRef, where('p2', '==', ''), where('p1', '!=', userName) ); const unsubscribe = onSnapshot(q, (snapshot) => { const gamesList = snapshot.docs.map(doc => ({ id: doc.id, ...doc.data() })); setAvailableGames(gamesList); setLoading(false); }, (err) => { console.error("Erro ao carregar jogos disponíveis:", err); setError("Erro ao carregar a lista de jogos."); setLoading(false); }); return () => unsubscribe(); }, [isAuthReady, userId, userName]); // 4. Lógica Segura para Entrar no Jogo (usando Firestore Transaction) // ESSA FUNÇÃO GARANTE A ATOMICIDADE E SEGURANÇA NA APOSTA/BLOQUEIO DA VAGA. const handleJoinGame = useCallback(async (game) => { setJoinStatus(prev => ({ ...prev, [game.id]: 'loading' })); setError(null); const gameDocRef = doc(db, 'artifacts', appId, 'public', 'data', 'games', game.id); const mempoolCollectionRef = collection(db, 'artifacts', appId, 'users', userId, 'mempool'); const betAmount = parseFloat(game.bet); try { // --- INÍCIO DA TRANSAÇÃO ATÔMICA (Atomicidade Garantida) --- await runTransaction(db, async (transaction) => { // 1. LER o estado atual do Jogo DENTRO da Transação const gameDoc = await transaction.get(gameDocRef); if (!gameDoc.exists()) { throw new Error("Game-nao-encontrado"); } const gameData = gameDoc.data(); // 2. CONCORRÊNCIA CHECK: Verificar se P2 ainda está vazio if (gameData.p2 !== '') { throw new Error("Jogo-lotado"); } // 3. FINANCEIRO CHECK: Verificar Saldo if (saldoLivre < betAmount) { throw new Error("Saldo-insuficiente"); } // 4. ATUALIZAR o documento 'games' (Bloqueia a vaga) transaction.update(gameDocRef, { p2: userName, turn: gameData.p1, // O primeiro jogador a jogar é o P1. (Ajuste lógico do código original que usava p2) played: new Date().toISOString(), }); // 5. INSERIR Transação no Mempool do Usuário (Escrow/Garantia da Aposta) const newMempoolDocRef = doc(mempoolCollectionRef); transaction.set(newMempoolDocRef, { type: 'deposit', // Retira do saldo livre (Escrow) amount: betAmount, gameId: game.id, gameName: game.gameName, dataehora: new Date().toISOString(), destinatario: game.id, // Game ID como destinatário (Escrow) remetente: userId, status: 'escrowed' }); }); // --- FIM DA TRANSAÇÃO ATÔMICA (COMMIT) --- // 6. Criar Tabuleiro (Batch Write, Rápido e Eficiente) // Feito fora da transação principal para evitar limite de 500 operações. await createGameBoardBatch(game.id, game.p1, userName); // 7. Sucesso setJoinStatus(prev => ({ ...prev, [game.id]: 'success' })); // Mensagem de sucesso e Redirecionamento simulado setTimeout(() => { alert(`Sucesso! Você entrou no jogo ${game.gameName} e o tabuleiro foi criado. O jogo irá começar!`); // Em um app real, você faria window.location.href = `/game/${game.id}`; setJoinStatus(prev => ({ ...prev, [game.id]: undefined })); }, 1000); } catch (e) { console.error("Erro ao entrar no jogo:", e); setJoinStatus(prev => ({ ...prev, [game.id]: 'error' })); let userMessage = "Ocorreu um erro desconhecido ao entrar no jogo."; if (e.message.includes("Game-nao-encontrado")) { userMessage = "O jogo não foi encontrado ou foi excluído."; } else if (e.message.includes("Jogo-lotado")) { userMessage = "Este jogo acabou de ser preenchido por outro jogador (Corrida de Concorrência)."; } else if (e.message.includes("Saldo-insuficiente")) { userMessage = `Você não tem saldo suficiente. Aposta necessária: R$ ${betAmount.toFixed(2)}, Saldo: R$ ${saldoLivre.toFixed(2)}.`; } else if (e.message.includes('transaction failed')) { userMessage = "Falha na transação. Tente novamente, o jogo pode ter sido preenchido por outro usuário."; } setError(userMessage); setTimeout(() => setJoinStatus(prev => ({ ...prev, [game.id]: undefined })), 5000); } }, [userId, userName, saldoLivre]); // --- Renderização da UI --- const renderActionButton = (game) => { const status = joinStatus[game.id]; if (status === 'loading') { return ( ); } if (status === 'success') { return ( ); } return ( ); }; if (!isAuthReady || loading) { return (
Carregando dados do sistema...
Aguarde a autenticação e o cálculo do saldo.
Acesse os combates criados por outros jogadores.
ID de Usuário: {userName || 'N/A'}
{error}
|
|
|
|
|
Ação |
|---|---|---|---|---|
| {game.gameName || `Jogo #${game.id.substring(0, 4)}`} | R$ {parseFloat(game.bet).toFixed(2)} | {game.p1} | {game.time} | {renderActionButton(game)} |