2025

Spyfall Online – Multiplayer Social Deduction Game

A real-time web-based implementation of the popular Spyfall party game built with React, TypeScript, and Firebase. Features multiplayer gameplay for up to 12 players, 30+ unique locations, configurable game settings, and responsive design for seamless cross-device play.

Spyfall Online – Multiplayer Social Deduction Game

Project Overview

Spyfall Online is a modern web-based implementation of the popular social deduction party game, built with React, TypeScript, and Firebase. The application enables real-time multiplayer gameplay for up to 12 players with zero setup friction—no registration required, just share a game code and start playing. The architecture prioritizes real-time synchronization, responsive design, and an immersive spy-themed UI that enhances the social deduction experience.

Core Features:

  • Real-time multiplayer with Firebase Realtime Database
  • 30+ unique locations with role-based gameplay
  • Configurable spy count and time limits
  • Mobile-responsive design with Tailwind CSS
  • Leader-based game management with kick functionality
  • Turn-based questioning system with voting mechanics

Design Constraints:

  • Framework: React 18 with TypeScript for type safety
  • Styling: Tailwind CSS for rapid UI development
  • Backend: Firebase Realtime Database for real-time sync
  • Build: Vite for fast development and optimized builds
  • State Management: React hooks with Firebase listeners

Problem & Motivation

Traditional party games like Spyfall require physical presence, manual setup, and careful role management—barriers that prevent spontaneous gameplay sessions.

Pain PointEffect
Physical card managementManual setup time, lost cards, role confusion
Limited player countHard to accommodate larger groups
Geographic constraintsCan’t play with remote friends
Time-consuming setupReduces spontaneous gameplay opportunities
Role assignment errorsGame-breaking mistakes that ruin experience
No persistent stateCan’t pause and resume games

System Architecture

The application follows a client-server architecture with Firebase Realtime Database as the central state store. The flow operates as:

Landing → Lobby → Game → End State

Key modules and their responsibilities:

  • App.tsx: Main orchestrator managing game state transitions and Firebase listeners
  • LandingPage: Game creation and joining interface
  • LobbyScreen: Player management and game configuration
  • MissionScreen: Core gameplay with role display and player interaction
  • Firebase Integration: Real-time state synchronization and persistence
  • Game Timer: Countdown management with visual feedback
  • Player Management: Role assignment, voting, and kick functionality

Design Choices

Real-time Architecture

  • Firebase Realtime Database: Chosen over WebSockets for simplicity and built-in persistence
  • Single source of truth: All game state lives in Firebase, clients sync via listeners
  • Optimistic updates: Local state updates for immediate UI feedback

State Management

  • React hooks: Custom state management without external libraries
  • Firebase listeners: Automatic synchronization across all clients
  • Immutable updates: Prevent race conditions and state corruption

UI/UX Decisions

  • Spy-themed design: Dark interface with red/amber accents for immersion
  • Responsive grid: Adapts from mobile to desktop seamlessly
  • Real-time feedback: Visual indicators for turns, votes, and game status
  • Accessibility: Keyboard navigation and screen reader support

Game Logic

  • Random role assignment: Cryptographic random selection for fairness
  • Leader-based control: Single player manages game flow to prevent conflicts
  • Waiting room system: Players can join mid-game and wait for next round

Technical Deep Dive

Real-time State Synchronization

The core of the application relies on Firebase Realtime Database listeners to maintain game state across all connected clients. The main App component establishes a listener that triggers on every database change.

// src/App.tsx
useEffect(() => {
  if (!gameState.id) return;
  const gameRef = ref(db, `games/${gameState.id}`);
  const unsubscribe = onValue(gameRef, snapshot => {
    const data = snapshot.val();
    if (!data) return;

    // Handle player kick detection
    if (currentPlayer && ![...(data.players || []), ...(data.waitingPlayers || [])].some(
      (p: Player) => p.id === currentPlayer.id
    )) {
      setShowKickDialog(true);
      return;
    }

    // Update local player info including role and spy status
    if (currentPlayer) {
      const me = data.players?.find((p: Player) => p.id === currentPlayer.id);
      if (me && (me.isLeader !== currentPlayer.isLeader || me.isSpy !== currentPlayer.isSpy || me.role !== currentPlayer.role)) {
        setCurrentPlayer(me);
      }
    }

    setGameState({
      ...initialGameState,
      ...data,
      players: data.players || [],
      waitingPlayers: data.waitingPlayers || [],
      votes: data.votes || {},
      currentTurn: data.currentTurn || null
    });
  });
  return () => unsubscribe();
}, [gameState.id, currentPlayer]);

Game State Management

The application uses a comprehensive type system to ensure type safety across the entire game state. The GameState interface defines all possible game configurations and player states.

// src/types.ts
export interface GameState {
  id: string;
  isPlaying: boolean;
  timeRemaining: number;
  location?: string | null;
  players: Player[];
  waitingPlayers?: Player[];
  currentTurn?: string | null;
  votingFor?: string;
  votes: Record<string, boolean>;
  config: GameConfig;
}

export interface Player {
  id: string;
  name: string;
  isSpy?: boolean;
  isLeader?: boolean;
  role?: string;
  score: number;
}

Role Assignment and Game Initialization

The game initialization process randomly assigns roles and spies while ensuring fair distribution. The algorithm uses cryptographic random selection to prevent predictability.

// src/App.tsx
const startGame = async () => {
  const allPlayers = [...gameState.players, ...(gameState.waitingPlayers || [])];
  const locationData = LOCATIONS[Math.floor(Math.random() * LOCATIONS.length)];
  const location = locationData.location;

  const spyIndices = new Set<number>();
  while (spyIndices.size < gameState.config.numSpies) {
    spyIndices.add(Math.floor(Math.random() * allPlayers.length));
  }

  const availableRoles = [...locationData.roles];
  const updatedPlayers = allPlayers.map((p, i) => {
    const isSpy = spyIndices.has(i);
    let role = '';
    if (!isSpy && availableRoles.length > 0) {
      const roleIndex = Math.floor(Math.random() * availableRoles.length);
      role = availableRoles.splice(roleIndex, 1)[0];
    }
    return { ...p, isSpy, role };
  });
  
  const firstTurn = updatedPlayers.find(p => p.isLeader)?.id || updatedPlayers[0]?.id || null;

  const newState = {
    ...gameState,
    isPlaying: true,
    location,
    timeRemaining: gameState.config.timeLimit,
    players: updatedPlayers,
    waitingPlayers: [],
    currentTurn: firstTurn
  };
  await set(ref(db, `games/${gameState.id}`), newState);
};

Timer System with Visual Feedback

The game timer provides real-time countdown with visual feedback that intensifies as time runs low, creating urgency and enhancing gameplay tension.

// src/components/GameTimer.tsx
export function GameTimer({ timeRemaining }: GameTimerProps) {
  const minutes = Math.floor(timeRemaining / 60);
  const seconds = timeRemaining % 60;
  const isLowTime = timeRemaining <= 60; // Last minute

  return (
    <div className={`
      flex items-center gap-3 px-4 py-2 border font-mono
      ${isLowTime ? 'bg-red-900/50 border-red-600 animate-pulse shadow-red-900/50' : 'bg-black/50 border-gray-700'}
      backdrop-blur-sm shadow-lg transition-all duration-300
    `}>
      <Timer className={`w-4 h-4 ${isLowTime ? 'text-red-400' : 'text-amber-400'}`} />
      <span className={`
        text-lg font-mono font-bold tracking-wider
        ${isLowTime ? 'text-red-400' : 'text-gray-200'}
      `}>
        {minutes.toString().padStart(2, '0')}:{seconds.toString().padStart(2, '0')}
      </span>
      <div className="text-xs text-gray-500 font-mono">
        {isLowTime ? 'CRITICAL' : 'REMAINING'}
      </div>
    </div>
  );
}

Role Information Display

The role information component provides a secure way to display sensitive game information while allowing players to hide their roles from others viewing their screen.

// src/components/RoleInfo.tsx
export function RoleInfo({ isSpy, location, role }: RoleInfoProps) {
  const [isHidden, setIsHidden] = useState(false);

  return (
    <div 
      className="bg-gray-900/90 backdrop-blur-sm border-2 border-red-900/50 p-6 shadow-2xl shadow-red-900/20 mb-8 cursor-pointer transition-all duration-300 hover:border-red-700/70 relative"
      onClick={() => setIsHidden(!isHidden)}
    >
      <div className="space-y-3">
        <div className="flex items-center gap-3 border-l-2 border-blue-700 pl-3 bg-blue-900/10">
          <User className="w-4 h-4 text-blue-400" />
          <span className="text-sm text-gray-300 font-mono">
            ROLE: {isHidden ? (
              <span className="text-gray-400 font-medium">CLASSIFIED</span>
            ) : (
              isSpy ? (
                <span className="text-red-400 font-medium">SPY</span>
              ) : (
                <span className="text-blue-400 font-medium">{role}</span>
              )
            )}
          </span>
        </div>

        <div className="flex items-center gap-3 border-l-2 border-amber-700 pl-3 bg-amber-900/10">
          <MapPin className="w-4 h-4 text-amber-400" />
          <span className="text-sm text-gray-300 font-mono">
            LOCATION: {isHidden ? (
              <span className="text-gray-400 font-medium">CLASSIFIED</span>
            ) : (
              isSpy ? (
                <span className="text-red-400 font-medium">UNKNOWN</span>
              ) : (
                <span className="text-green-400 font-medium">{location}</span>
              )
            )}
          </span>
        </div>
      </div>
    </div>
  );
}

Performance

Real-time Performance

  • Firebase Realtime Database provides sub-100ms update latency
  • Optimistic updates prevent UI lag during network operations
  • Efficient re-renders through React’s reconciliation

Scalability Considerations

  • Firebase handles concurrent connections automatically
  • Game state is lightweight (JSON objects under 10KB)
  • No server-side computation required

Caching Strategy

  • Firebase client SDK handles connection pooling
  • Local state caching prevents unnecessary re-renders
  • Offline support through Firebase’s built-in capabilities

Generation Flow The game uses a deterministic but random generation system for fair gameplay:

// Game code generation
const gameId = Math.random().toString(36).substring(2, 8).toUpperCase();

// Player ID generation
const leader: Player = { 
  id: crypto.randomUUID(), 
  name: creatorName, 
  isLeader: true, 
  score: 0, 
  isSpy: false 
};

What’s Next

Immediate Roadmap

  • Voice chat integration for enhanced social interaction
  • Custom location and role creation system
  • Advanced voting mechanics with discussion timers
  • Game replay and statistics tracking

Technical Improvements

  • WebRTC peer-to-peer for reduced latency
  • Progressive Web App (PWA) for offline capabilities
  • Advanced analytics and game balance insights
  • Multi-language support for global accessibility

Performance Enhancements

  • Service Worker for offline game state caching
  • WebSocket fallback for improved real-time performance
  • Optimized bundle splitting for faster initial loads
  • Advanced error handling and recovery mechanisms

The Spyfall Online project demonstrates how modern web technologies can transform traditional board games into engaging digital experiences while maintaining the core social dynamics that make them compelling.

Last updated on August 24, 2025 at 12:16 PM EST. See Changelog

Explore more projects