import { useCollection, useIdentity } from '@deplixo/sdk'; import RoomEntry from './components/RoomEntry.jsx'; import WordSetup from './components/WordSetup.jsx'; import GameBoard from './components/GameBoard.jsx'; import Results from './components/Results.jsx'; function App() { const { user, loading: idLoading } = useIdentity(); const [roomId, setRoomId] = useState(null); if (idLoading || !user) { return (

Loading…

); } return (

Duel Hangman

{user.name}
); } function GameRouter({ user, roomId, setRoomId }) { if (!roomId) { return ; } return setRoomId(null)} />; } function RoomGame({ user, roomId, onLeave }) { const { items: players, loading: playersLoading, add: addPlayer, update: updatePlayer, remove: removePlayer } = useCollection('players', { room: roomId }); const { items: guesses, add: addGuess, remove: removeGuess } = useCollection('guesses', { room: roomId }); // Register self as a player on mount (idempotent) useEffect(() => { if (playersLoading) return; const me = (players || []).find((p) => p.author?.id === user.id); if (!me) { // Only allow joining if room has < 2 players const distinctAuthors = new Set((players || []).map((p) => p.author?.id)); if (distinctAuthors.size < 2 || distinctAuthors.has(user.id)) { addPlayer({ name: user.name, word: null, ready: false }); } } // eslint-disable-next-line react-hooks/exhaustive-deps }, [playersLoading]); if (playersLoading) { return (

Joining room…

); } // Deduplicate by author.id (keep the latest) const playerMap = new Map(); for (const p of players || []) { if (!p.author?.id) continue; const existing = playerMap.get(p.author.id); if (!existing || (p.createdAt || 0) > (existing.createdAt || 0)) { playerMap.set(p.author.id, p); } } const uniquePlayers = Array.from(playerMap.values()); const me = uniquePlayers.find((p) => p.author?.id === user.id); const opponent = uniquePlayers.find((p) => p.author?.id !== user.id); // Determine game state const bothJoined = uniquePlayers.length >= 2; const bothWordsSet = bothJoined && me?.value?.word && opponent?.value?.word; const myGuesses = (guesses || []).filter((g) => g.author?.id === user.id); const opponentGuesses = (guesses || []).filter((g) => g.author?.id === opponent?.author?.id); // Compute winner const myWordToGuess = opponent?.value?.word?.toLowerCase() || ''; const opponentWordToGuess = me?.value?.word?.toLowerCase() || ''; const myCorrectLetters = new Set( myGuesses.filter((g) => myWordToGuess.includes(g.value.letter)).map((g) => g.value.letter) ); const opponentCorrectLetters = new Set( opponentGuesses.filter((g) => opponentWordToGuess.includes(g.value.letter)).map((g) => g.value.letter) ); const iWon = bothWordsSet && myWordToGuess.length > 0 && [...myWordToGuess].every((ch) => ch === ' ' || myCorrectLetters.has(ch)); const opponentWon = bothWordsSet && opponentWordToGuess.length > 0 && [...opponentWordToGuess].every((ch) => ch === ' ' || opponentCorrectLetters.has(ch)); // Out of guesses (6 wrong allowed) const MAX_WRONG = 6; const myWrongCount = myGuesses.filter((g) => !myWordToGuess.includes(g.value.letter)).length; const opponentWrongCount = opponentGuesses.filter((g) => !opponentWordToGuess.includes(g.value.letter)).length; const myEliminated = myWrongCount >= MAX_WRONG; const opponentEliminated = opponentWrongCount >= MAX_WRONG; // Winner logic: first to fully reveal opponent's word wins. // If both eliminated and neither solved -> draw. let winner = null; // 'me' | 'opponent' | 'draw' | null if (iWon && !opponentWon) winner = 'me'; else if (opponentWon && !iWon) winner = 'opponent'; else if (iWon && opponentWon) winner = 'me'; // tiebreak: shouldn't really happen since first guess wins else if (myEliminated && opponentEliminated) winner = 'draw'; const handleLeave = async () => { if (me) await removePlayer(me.id); onLeave(); }; const handleSetWord = async (word) => { if (!me) return; await updatePlayer(me.id, { ...me.value, word: word.toLowerCase(), ready: true }); }; const handleGuess = async (letter) => { const lower = letter.toLowerCase(); // Prevent duplicate guesses if (myGuesses.some((g) => g.value.letter === lower)) return; await addGuess({ letter: lower }); }; const handleRematch = async () => { // Clear all guesses and reset both players' words for (const g of guesses || []) { await removeGuess(g.id); } if (me) await updatePlayer(me.id, { ...me.value, word: null, ready: false }); // Note: opponent will reset their own word client-side }; // ----- Render the right view ----- if (winner) { return ( ); } if (!bothJoined) { return ( ); } if (!bothWordsSet) { return ( ); } return ( ); } function WaitingRoom({ roomId, me, playerCount, onLeave }) { return (

Waiting for opponent…

Room: {roomId}

Share the room name {roomId} with a friend. The match starts as soon as they join.

{me?.value?.name || 'You'} (you)
Waiting for player 2…
); } ReactDOM.createRoot(document.getElementById('root')).render();