Building QuizCraft - Smart contract integeration to the frontend

Building QuizCraft: A Technical Deep Dive into AI-Powered Web3 Quiz Gaming

How I built a full-stack decentralized quiz platform on Conflux eSpace with AI-generated content, independent gameplay, and robust state management

Project Overview

QuizCraft is a revolutionary Web3 quiz platform that combines AI-generated content with blockchain-based competitive gaming. Built on Conflux eSpace, it features two distinct modes:

  • Solo Training Mode: Free-to-play AI-generated quizzes with NFT rewards
  • Live Arena Mode: Competitive PvP matches with CFX prize pools

The platform leverages Conflux’s low transaction costs and high throughput to enable seamless micro-transactions and real-time competitive gaming.

Key Features

  • :robot: AI-powered question generation using OpenAI GPT-4
  • :moneybag: CFX-based economy with smart contract escrow
  • :video_game: Independent per-user gameplay with state persistence
  • :trophy: Real-time leaderboards and NFT achievements
  • :zap: Optimized for Conflux eSpace’s low fees and high throughput

Architecture & Tech Stack

Frontend Stack

// Core technologies
- Next.js 15.2.4 with TypeScript
- Tailwind CSS + Shadcn/ui components
- Ethers.js v6 for Web3 integration
- OpenAI GPT-4 API for question generation
- Supabase for leaderboard management

Smart Contract Stack

// Solidity with security best practices
- Solidity ^0.8.0
- OpenZeppelin security libraries
- Hardhat framework with Conflux plugin
- ReentrancyGuard and Ownable patterns

Smart Contract Implementation

The core of QuizCraft is the QuizCraftArena smart contract, which handles lobby management, CFX escrow, and prize distribution.

Contract Structure

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.10;

import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
import "@openzeppelin/contracts/access/Ownable.sol";

contract QuizCraftArena is ReentrancyGuard, Ownable {
    uint256 public constant LOBBY_TIMEOUT = 5 minutes;
    
    enum LobbyStatus { OPEN, STARTED, IN_PROGRESS, COMPLETED, CANCELLED }
    enum DistributionStatus { NOT_DISTRIBUTED, DISTRIBUTED }
    
    struct Lobby {
        uint256 id;
        string name;
        string category;
        uint256 entryFee;
        uint256 playerCount;
        uint256 maxPlayers;
        uint256 prizePool;
        uint256 createdAt;
        LobbyStatus status;
        DistributionStatus distribution;
        address[] players;
        address winner;
        address creator;
    }
    
    mapping(uint256 => Lobby) public lobbies;
    mapping(uint256 => mapping(address => uint256)) public playerScores;
    uint256 public nextLobbyId;
}

Core Functions

1. Lobby Creation

function createLobby(
    string memory _name,
    string memory _category,
    uint256 _entryFee,
    uint256 _maxPlayers
) external returns (uint256) {
    require(bytes(_name).length > 0, "Lobby name cannot be empty");
    require(bytes(_category).length > 0, "Category cannot be empty");
    require(_entryFee > 0, "Entry fee must be greater than 0");
    require(_maxPlayers > 1 && _maxPlayers <= 10, "Invalid max players");

    uint256 lobbyId = nextLobbyId++;
    Lobby storage newLobby = lobbies[lobbyId];

    newLobby.id = lobbyId;
    newLobby.name = _name;
    newLobby.category = _category;
    newLobby.entryFee = _entryFee;
    newLobby.maxPlayers = _maxPlayers;
    newLobby.createdAt = block.timestamp;
    newLobby.status = LobbyStatus.OPEN;
    newLobby.distribution = DistributionStatus.NOT_DISTRIBUTED;
    newLobby.creator = msg.sender;

    emit LobbyCreated(lobbyId, _name, _category, _entryFee, _maxPlayers, msg.sender);
    return lobbyId;
}

2. Player Joining with CFX Escrow

function joinLobby(uint256 _lobbyId) external payable nonReentrant validLobby(_lobbyId) {
    Lobby storage lobby = lobbies[_lobbyId];
    require(lobby.status == LobbyStatus.OPEN || lobby.status == LobbyStatus.STARTED, "Lobby not open");
    require(msg.value == lobby.entryFee, "Incorrect entry fee");
    require(lobby.players.length < lobby.maxPlayers, "Lobby full");
    require(block.timestamp <= lobby.createdAt + LOBBY_TIMEOUT, "Lobby expired");
    require(msg.sender != lobby.creator, "Creator cannot join this lobby");

    // Add player and lock CFX
    lobby.players.push(msg.sender);
    lobby.playerCount++;
    lobby.prizePool += msg.value;

    emit PlayerJoined(_lobbyId, msg.sender);

    // Update lobby status
    if (lobby.players.length == 1) {
        lobby.status = LobbyStatus.STARTED;
    }
    if (lobby.players.length == lobby.maxPlayers) {
        lobby.status = LobbyStatus.IN_PROGRESS;
    }
}

3. Winner Payout with Security

function executeWinnerPayout(uint256 _lobbyId, address _winner)
    external
    nonReentrant
    validLobby(_lobbyId)
    onlyLobbyCreator(_lobbyId)
{
    Lobby storage lobby = lobbies[_lobbyId];
    require(lobby.status == LobbyStatus.IN_PROGRESS, "Lobby not in progress");
    require(isPlayerInLobby(_lobbyId, _winner), "Winner not in this lobby");
    require(lobby.distribution == DistributionStatus.NOT_DISTRIBUTED, "Already distributed");
    require(block.timestamp >= lobby.createdAt + LOBBY_TIMEOUT, "Lobby not expired yet");

    // Mark complete and transfer prize
    lobby.status = LobbyStatus.COMPLETED;
    lobby.winner = _winner;
    lobby.distribution = DistributionStatus.DISTRIBUTED;

    uint256 prize = lobby.prizePool;
    require(prize > 0, "No prize to distribute");
    lobby.prizePool = 0; // Prevent re-entrancy

    (bool success, ) = payable(_winner).call{value: prize}("");
    require(success, "Prize transfer failed");

    emit LobbyCompleted(_lobbyId, _winner, prize);
}

Security Features

  • ReentrancyGuard: Prevents re-entrancy attacks
  • Ownable: Access control for admin functions
  • Input Validation: Comprehensive parameter checking
  • Timeout Protection: Automatic lobby expiration
  • Safe Transfers: Secure CFX transfers with error handling

AI-Powered Question Generation

The question generation system uses OpenAI GPT-4 to create dynamic, unique quiz content for each game.

API Implementation

// /app/api/generate-quiz/route.ts
export async function POST(request: NextRequest) {
  try {
    const {
      category = "Technology",
      difficulty = "medium",
      questionCount = 10,
      timePerQuestion = 10,
      seed,
    } = await request.json()

    // Compose prompt for OpenAI
    const prompt = `Generate exactly ${questionCount} ${difficulty} multiple-choice quiz questions about ${category}.
Return STRICT JSON ONLY with NO prose, NO markdown, NO code fences.
Ensure questions are diverse and non-repetitive. Vary phrasing, subtopics, and difficulty within the band.
Return a JSON array of ${questionCount} objects, each with this schema:
{
  "question": string,
  "options": [string, string, string, string],
  "answer": string,
  "explanation": string
}`

    const callOpenAI = async (model: string) => {
      if (!apiKey) {
        return { ok: false, status: 0, content: "" }
      }
      
      const resp = await fetch("https://api.openai.com/v1/chat/completions", {
        method: "POST",
        headers: {
          "Authorization": `Bearer ${apiKey}`,
          "Content-Type": "application/json",
        },
        body: JSON.stringify({
          model,
          messages: [{ role: "user", content: prompt }],
          max_tokens: 2048,
          temperature: 0.3,
        }),
      })

      if (!resp.ok) {
        console.error("OpenAI API error:", resp.status)
        return { ok: false, status: resp.status, content: "" }
      }
      
      const data = await resp.json()
      const content: string = data.choices?.[0]?.message?.content ?? ""
      return { ok: true, status: 200, content }
    }

    // Try multiple models for reliability
    let p = await callOpenAI("gpt-3.5-turbo")
    if (!p.ok || !p.content) p = await callOpenAI("gpt-4")
    if (!p.ok || !p.content) p = await callOpenAI("gpt-3.5-turbo-16k")

    // Process and validate AI response
    const questions = processAIResponse(p.content, questionCount, seed)
    
    return NextResponse.json({
      success: true,
      quiz: {
        id: crypto.randomUUID(),
        category,
        timePerQuestion: Number(timePerQuestion) || 10,
        questions,
      },
    })
  } catch (error) {
    console.error("Error generating quiz:", error)
    return NextResponse.json({ error: "Failed to generate quiz" }, { status: 500 })
  }
}

Fallback System

// Local fallback questions for reliability
const localGenerate = (topic: string, count: number) => {
  const bank: Array<{
    question: string;
    options: string[];
    correctAnswer: number;
    explanation: string;
  }> = [
    { 
      question: `${topic}: What does CPU stand for?`, 
      options: ["Central Processing Unit", "Computer Personal Unit", "Central Peripheral Unit", "Compute Process Utility"], 
      correctAnswer: 0, 
      explanation: 'CPU stands for "Central Processing Unit".' 
    },
    // ... more questions
  ]
  
  // Deterministic selection with seeded RNG
  const rngFactory = (s?: string) => {
    if (!s) return () => Math.random()
    let h = 2166136261 >>> 0
    for (let i = 0; i < s.length; i++) {
      h ^= s.charCodeAt(i)
      h = Math.imul(h, 16777619)
    }
    let x = h || 123456789
    return () => {
      x ^= x << 13
      x ^= x >>> 17
      x ^= x << 5
      return ((x >>> 0) % 1_000_000) / 1_000_000
    }
  }
  
  // Shuffle options to avoid positional bias
  const shuffle = <T,>(arr: T[]): T[] => {
    for (let i = arr.length - 1; i > 0; i--) {
      const j = Math.floor(rand() * (i + 1))
      const tmp = arr[i]
      arr[i] = arr[j]
      arr[j] = tmp
    }
    return arr
  }
  
  return result
}

Key Features

  • Multiple Model Fallback: Tries GPT-3.5, GPT-4, and GPT-3.5-16k
  • Seeded Randomization: Consistent question order with seed
  • Option Shuffling: Prevents positional bias
  • Deduplication: Removes duplicate questions
  • Local Fallback: Works even without API access

Independent Gameplay System

One of the most innovative features is the independent gameplay system, where each player can play at their own pace without waiting for others.

Core Concept

// IndependentQuizGame.tsx - Main component
export default function IndependentQuizGame({ 
  lobbyId, 
  players, 
  category, 
  onGameEnd, 
  onScoreUpdate, 
  questionDurationSec, 
  seed, 
  currentPlayerAddress 
}: IndependentQuizGameProps) {
  
  // State management with localStorage restoration
  const [gameState, setGameState] = useState<'waiting' | 'countdown' | 'playing' | 'finished'>('waiting');
  const [currentQuestion, setCurrentQuestion] = useState(0);
  const [questions, setQuestions] = useState<QuizQuestion[]>([]);
  const [selectedAnswer, setSelectedAnswer] = useState<number | null>(null);
  const [isAnswered, setIsAnswered] = useState(false);
  const [timeLeft, setTimeLeft] = useState(questionDurationSec);
  
  // Initialize state manager
  const stateManager = new QuizStateManager(lobbyId, currentPlayerAddress);

  // Restore state on component mount
  useEffect(() => {
    const initializeQuiz = async () => {
      const savedState = stateManager.restoreState(questionDurationSec);
      
      if (savedState) {
        console.log('Restoring quiz state from localStorage:', savedState);
        
        // Restore all state variables
        setGameState(savedState.gameState);
        setCurrentQuestion(savedState.currentQuestion);
        setSelectedAnswer(savedState.selectedAnswer);
        setIsAnswered(savedState.isAnswered);
        setTimeLeft(savedState.timeLeft);
        setQuestions(savedState.questions);
        // ... restore other state
        
        console.log('Quiz state restored successfully');
      } else {
        console.log('No saved state found, starting fresh');
        await generateQuiz();
        setGameState('countdown');
        setCountdown(10);
      }
    };

    initializeQuiz();
  }, [lobbyId, currentPlayerAddress, questionDurationSec]);
}

State Persistence

// quizStateManager.ts - State management utility
export class QuizStateManager {
  private localStorageKey: string;

  constructor(lobbyId: string, playerAddress: string) {
    this.localStorageKey = `quizcraft:independent-quiz:${lobbyId}:${playerAddress.toLowerCase()}`;
  }

  // Save state with error handling
  saveState(state: QuizState): boolean {
    try {
      const stateToSave = {
        ...state,
        lastSaved: Date.now()
      };
      localStorage.setItem(this.localStorageKey, JSON.stringify(stateToSave));
      console.log('Quiz state saved successfully');
      return true;
    } catch (error) {
      console.error('Error saving quiz state:', error);
      return false;
    }
  }

  // Restore state with time calculation
  restoreState(questionDurationSec: number): QuizState | null {
    const savedState = this.loadState();
    if (!savedState) return null;

    // Check if state is stale (older than 24 hours)
    if (this.isStateStale()) {
      console.log('State is stale, clearing it');
      this.clearState();
      return null;
    }

    // Calculate current time left for active games
    if (savedState.gameState === 'playing' && savedState.quizStartedAt) {
      const now = Date.now();
      const elapsed = now - savedState.quizStartedAt;
      const questionElapsed = elapsed % (questionDurationSec * 1000);
      const timeRemaining = Math.max(0, questionDurationSec - Math.floor(questionElapsed / 1000));
      
      return {
        ...savedState,
        timeLeft: timeRemaining
      };
    }

    return savedState;
  }

  // Periodic checkpoint saving
  saveCheckpoint(state: QuizState): boolean {
    const checkpointKey = `${this.localStorageKey}:checkpoint`;
    try {
      const checkpointData = {
        ...state,
        checkpointTime: Date.now(),
        checkpointType: 'progress'
      };
      localStorage.setItem(checkpointKey, JSON.stringify(checkpointData));
      return true;
    } catch (error) {
      console.error('Error saving checkpoint:', error);
      return false;
    }
  }
}

Game Flow

// Answer selection with scoring
const handleAnswerSelect = (answerIndex: number) => {
  if (selectedAnswer !== null || isAnswered || gameState !== 'playing') {
    return;
  }
  
  setSelectedAnswer(answerIndex);
  setIsAnswered(true);
  setTimerActive(false);
  setShowExplanation(true);
  
  const question = questions[currentQuestion];
  if (!question) return;
  
  const isCorrect = answerIndex === question.correctAnswer;
  const timeBonus = Math.floor(timeLeft * 2); // 2 points per second remaining
  const points = isCorrect ? 100 + timeBonus : -25;
  
  setPlayerScores(prev => {
    const currentScore = prev[currentPlayerAddress] || 0;
    const newScore = Math.max(0, currentScore + points);
    const newScores = {
      ...prev,
      [currentPlayerAddress]: newScore
    };
    
    if (onScoreUpdate) {
      onScoreUpdate(newScores);
    }
    
    return newScores;
  });
};

// Move to next question or end game
const nextQuestion = () => {
  if (currentQuestion < questions.length - 1) {
    setCurrentQuestion(prev => prev + 1);
    setSelectedAnswer(null);
    setIsAnswered(false);
    setTimeLeft(questionDurationSec);
    setShowExplanation(false);
    setTimerActive(true);
  } else {
    endGame();
  }
};

State Persistence & Management

The state management system ensures users can continue their quiz even after page refreshes or network interruptions.

State Structure

export interface QuizState {
  gameState: 'waiting' | 'countdown' | 'playing' | 'finished';
  currentQuestion: number;
  selectedAnswer: number | null;
  isAnswered: boolean;
  timeLeft: number;
  playerScores: Record<string, number>;
  showExplanation: boolean;
  gameResults: any[];
  questionSource: 'ai' | 'fallback' | null;
  quizStartedAt: number | null;
  quizEndedAt: number | null;
  countdown: number | null;
  questions: any[];
}

Automatic State Saving

// Persist state whenever it changes
useEffect(() => {
  const stateToSave: QuizState = {
    gameState,
    currentQuestion,
    selectedAnswer,
    isAnswered,
    timeLeft,
    playerScores,
    showExplanation,
    gameResults,
    questionSource,
    quizStartedAt,
    quizEndedAt,
    countdown,
    questions
  };
  
  stateManager.saveState(stateToSave);
}, [
  gameState, currentQuestion, selectedAnswer, isAnswered, timeLeft, 
  playerScores, showExplanation, gameResults, questionSource, 
  quizStartedAt, quizEndedAt, countdown, questions
]);

// Periodic checkpoint saving for active games
useEffect(() => {
  if (gameState !== 'playing') return;
  
  const checkpointInterval = setInterval(() => {
    const stateToSave: QuizState = {
      gameState,
      currentQuestion,
      selectedAnswer,
      isAnswered,
      timeLeft,
      playerScores,
      showExplanation,
      gameResults,
      questionSource,
      quizStartedAt,
      quizEndedAt,
      countdown,
      questions
    };
    
    stateManager.saveCheckpoint(stateToSave);
  }, 30000); // Save checkpoint every 30 seconds
  
  return () => clearInterval(checkpointInterval);
}, [/* dependencies */]);

Cleanup and Maintenance

// Clean up old quiz states
export function cleanupOldQuizStates(): void {
  try {
    const keys = Object.keys(localStorage);
    const quizKeys = keys.filter(key => key.startsWith('quizcraft:independent-quiz:'));
    
    let cleanedCount = 0;
    quizKeys.forEach(key => {
      try {
        const data = localStorage.getItem(key);
        if (data) {
          const parsed = JSON.parse(data);
          const age = Date.now() - (parsed.lastSaved || 0);
          const twentyFourHours = 24 * 60 * 60 * 1000;
          
          if (age > twentyFourHours) {
            localStorage.removeItem(key);
            cleanedCount++;
          }
        }
      } catch (error) {
        // Remove corrupted data
        localStorage.removeItem(key);
        cleanedCount++;
      }
    });
    
    if (cleanedCount > 0) {
      console.log(`Cleaned up ${cleanedCount} old quiz states`);
    }
  } catch (error) {
    console.error('Error cleaning up old quiz states:', error);
  }
}

// Initialize cleanup on module load
if (typeof window !== 'undefined') {
  cleanupOldQuizStates();
}

Frontend Implementation

The frontend is built with Next.js 15 and modern React patterns, featuring a responsive design and excellent user experience.

Component Structure

// Main quiz game component
export default function IndependentQuizGame({ 
  lobbyId, 
  players, 
  category, 
  onGameEnd, 
  onScoreUpdate, 
  questionDurationSec, 
  seed, 
  currentPlayerAddress 
}: IndependentQuizGameProps) {
  
  // State management
  const [gameState, setGameState] = useState<'waiting' | 'countdown' | 'playing' | 'finished'>('waiting');
  const [currentQuestion, setCurrentQuestion] = useState(0);
  const [questions, setQuestions] = useState<QuizQuestion[]>([]);
  // ... other state variables
  
  // Initialize state manager
  const stateManager = new QuizStateManager(lobbyId, currentPlayerAddress);
  
  // Component lifecycle
  useEffect(() => {
    // Initialize quiz and restore state
  }, []);
  
  useEffect(() => {
    // Persist state changes
  }, [/* dependencies */]);
  
  useEffect(() => {
    // Handle countdown timer
  }, [gameState, countdown]);
  
  useEffect(() => {
    // Handle question timer
  }, [gameState, timerActive, currentQuestion, questions.length]);
  
  // Render different game states
  if (gameState === 'waiting') {
    return <WaitingState />;
  }
  
  if (gameState === 'countdown') {
    return <CountdownState />;
  }
  
  if (gameState === 'finished') {
    return <FinalLeaderboard />;
  }
  
  if (gameState === 'playing' && questions.length > 0) {
    return <PlayingState />;
  }
  
  return <LoadingState />;
}

UI Components

// Question display with answer selection
const QuestionCard = ({ question, onAnswerSelect, selectedAnswer, isAnswered }) => {
  return (
    <Card>
      <CardHeader>
        <div className="flex items-center justify-between">
          <CardTitle className="text-xl">
            {question.category} - {question.difficulty}
          </CardTitle>
          <Badge variant="outline" className="bg-green-100 text-green-800">
            <Target className="h-3 w-3 mr-1" />
            Question {currentQuestion + 1}
          </Badge>
        </div>
      </CardHeader>
      <CardContent className="space-y-6">
        <p className="text-lg font-medium">{question.question}</p>
        
        <div className="grid gap-3">
          {question.options.map((option, index) => (
            <Button
              key={index}
              variant={selectedAnswer === index ? "default" : "outline"}
              className={`p-4 h-auto text-left justify-start ${
                selectedAnswer !== null && index === question.correctAnswer
                  ? "bg-green-500 hover:bg-green-600 text-white"
                  : selectedAnswer === index && selectedAnswer !== question.correctAnswer
                  ? "bg-red-500 hover:bg-red-600 text-white"
                  : selectedAnswer !== null
                  ? "opacity-50"
                  : "hover:bg-blue-50"
              }`}
              onClick={() => onAnswerSelect(index)}
              disabled={selectedAnswer !== null || isAnswered}
            >
              <span className="font-medium mr-3">{String.fromCharCode(65 + index)}.</span>
              {option}
            </Button>
          ))}
        </div>

        {showExplanation && (
          <div className="p-4 bg-blue-50 rounded-lg border border-blue-200">
            <p className="text-sm text-blue-800">
              <strong>Explanation:</strong> {question.explanation}
            </p>
          </div>
        )}
      </CardContent>
    </Card>
  );
};

Progress Tracking

// QuizProgress component
const QuizProgress = ({ 
  currentQuestion, 
  totalQuestions, 
  playerScore, 
  timeRemaining, 
  questionDuration,
  playersFinished,
  totalPlayers,
  isLastQuestion,
  questionSource 
}) => {
  return (
    <Card className="mb-6">
      <CardContent className="p-6">
        <div className="space-y-4">
          {/* Progress bar */}
          <div className="flex items-center justify-between text-sm text-muted-foreground">
            <span>Question {currentQuestion + 1} of {totalQuestions}</span>
            <span>{Math.round(((currentQuestion + 1) / totalQuestions) * 100)}% Complete</span>
          </div>
          <Progress 
            value={((currentQuestion + 1) / totalQuestions) * 100} 
            className="h-2"
          />
          
          {/* Score and timer */}
          <div className="flex items-center justify-between">
            <div className="flex items-center gap-4">
              <div className="flex items-center gap-2">
                <Trophy className="h-4 w-4 text-yellow-500" />
                <span className="font-medium">{playerScore} points</span>
              </div>
              <div className="flex items-center gap-2">
                <Clock className="h-4 w-4 text-blue-500" />
                <span className="font-medium">{timeRemaining}s</span>
              </div>
            </div>
            
            <div className="flex items-center gap-2 text-sm text-muted-foreground">
              <Users className="h-4 w-4" />
              <span>{playersFinished}/{totalPlayers} finished</span>
            </div>
          </div>
          
          {/* Question source indicator */}
          {questionSource && (
            <div className="flex items-center gap-2 text-xs">
              <Badge variant={questionSource === 'ai' ? 'default' : 'secondary'}>
                {questionSource === 'ai' ? 'AI Generated' : 'Fallback'}
              </Badge>
            </div>
          )}
        </div>
      </CardContent>
    </Card>
  );
};

Challenges & Solutions

Challenge 1: Synchronized vs Independent Gameplay

Problem: Initially designed synchronized gameplay where all players had to wait for each other, creating poor UX.

Solution: Implemented independent per-user gameplay system with robust state persistence.

// Before: Synchronized gameplay
const SynchronizedQuizGame = () => {
  // All players wait for each other
  // Game starts only when all players are ready
  // One player's actions affect everyone
};

// After: Independent gameplay
const IndependentQuizGame = () => {
  // Each player gets their own countdown
  // Players play at their own pace
  // State is persisted locally
  // No interference between players
};

Challenge 2: State Persistence Across Page Refreshes

Problem: Users would lose progress if they refreshed the page during a quiz.

Solution: Implemented comprehensive state management with localStorage.

// State restoration with time calculation
const restoreState = (questionDurationSec: number): QuizState | null => {
  const savedState = this.loadState();
  if (!savedState) return null;

  // Calculate current time left for active games
  if (savedState.gameState === 'playing' && savedState.quizStartedAt) {
    const now = Date.now();
    const elapsed = now - savedState.quizStartedAt;
    const questionElapsed = elapsed % (questionDurationSec * 1000);
    const timeRemaining = Math.max(0, questionDurationSec - Math.floor(questionElapsed / 1000));
    
    return {
      ...savedState,
      timeLeft: timeRemaining
    };
  }

  return savedState;
};

Challenge 3: AI API Reliability

Problem: OpenAI API could fail or be unavailable, breaking the quiz experience.

Solution: Implemented multiple fallback strategies.

// Multiple model fallback
let p = await callOpenAI("gpt-3.5-turbo")
if (!p.ok || !p.content) p = await callOpenAI("gpt-4")
if (!p.ok || !p.content) p = await callOpenAI("gpt-3.5-turbo-16k")

// Local fallback questions
if (needsLocalFallback) {
  questions = localGenerate(category, Number(questionCount) || 10)
}

Challenge 4: Gas Costs for Micro-transactions

Problem: Traditional blockchain networks have high gas fees that make micro-transactions impractical.

Solution: Leveraged Conflux eSpace’s low transaction costs.

// Optimized for low gas costs
function joinLobby(uint256 _lobbyId) external payable nonReentrant {
    // Minimal storage operations
    // Efficient data structures
    // Gas-optimized logic
}

Performance Optimizations

1. State Management Optimization

// Debounced state saving to prevent excessive localStorage writes
const debouncedSaveState = useCallback(
  debounce((state: QuizState) => {
    stateManager.saveState(state);
  }, 500),
  [stateManager]
);

// Only save when state actually changes
useEffect(() => {
  if (hasStateChanged) {
    debouncedSaveState(stateToSave);
  }
}, [/* dependencies */]);

2. Question Generation Caching

// Cache generated questions to avoid repeated API calls
const questionCache = new Map<string, QuizQuestion[]>();

const generateQuiz = async (category: string, difficulty: string, seed: string) => {
  const cacheKey = `${category}-${difficulty}-${seed}`;
  
  if (questionCache.has(cacheKey)) {
    return questionCache.get(cacheKey);
  }
  
  const questions = await generateFromAPI(category, difficulty, seed);
  questionCache.set(cacheKey, questions);
  
  return questions;
};

3. Component Optimization

// Memoized components to prevent unnecessary re-renders
const QuestionCard = memo(({ question, onAnswerSelect, selectedAnswer, isAnswered }) => {
  return (
    <Card>
      {/* Question content */}
    </Card>
  );
});

// Optimized event handlers
const handleAnswerSelect = useCallback((answerIndex: number) => {
  // Answer selection logic
}, [/* dependencies */]);

4. Network Optimization

// Batch API calls and use efficient data structures
const submitScores = async (scores: GameResult[]) => {
  // Batch all score submissions into a single API call
  const response = await fetch('/api/scores/upsert', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      lobbyId: Number(lobbyId),
      results: scores
    })
  });
};

Deployment & Testing

Smart Contract Deployment

// hardhat.config.js
module.exports = {
  solidity: "0.8.10",
  networks: {
    confluxTestnet: {
      url: "https://evmtestnet.confluxrpc.com",
      accounts: [process.env.PRIVATE_KEY],
      chainId: 71,
    },
  },
  plugins: ["@conflux/hardhat-plugin"],
};

// Deploy script
const deploy = async () => {
  const QuizCraftArena = await ethers.getContractFactory("QuizCraftArena");
  const quizCraft = await QuizCraftArena.deploy();
  await quizCraft.deployed();
  
  console.log("QuizCraftArena deployed to:", quizCraft.address);
};

Frontend Deployment

// package.json
{
  "scripts": {
    "build": "next build",
    "start": "next start",
    "dev": "next dev"
  }
}

// Vercel deployment
// Automatic deployment on push to main branch
// Environment variables configured in Vercel dashboard

Testing Strategy

// State persistence testing
const runQuizStateTests = () => {
  const stateManager = new QuizStateManager('test-lobby', '0x123');
  
  // Test basic save/load
  const testState: QuizState = {
    gameState: 'playing',
    currentQuestion: 5,
    selectedAnswer: 2,
    isAnswered: true,
    timeLeft: 30,
    playerScores: { '0x123': 500 },
    showExplanation: false,
    gameResults: [],
    questionSource: 'ai',
    quizStartedAt: Date.now(),
    quizEndedAt: null,
    countdown: null,
    questions: []
  };
  
  stateManager.saveState(testState);
  const loadedState = stateManager.loadState();
  
  console.assert(JSON.stringify(testState) === JSON.stringify(loadedState), 'State save/load failed');
  console.log('State persistence test passed');
};

// Run tests from browser console
if (typeof window !== 'undefined') {
  window.runQuizStateTests = runQuizStateTests;
}

Future Enhancements

Phase 1: Enhanced Features

  • Cross-Space Integration: Enable interactions between Conflux Core Space and eSpace
  • Advanced NFT Collections: Expand achievement system with multiple NFT tiers
  • Mobile App: React Native mobile application
  • Tournament System: Automated tournament brackets

Phase 2: Token Economy

  • $QUIZ Token Launch: Native utility token for platform governance
  • Staking System: Allow users to stake CFX and earn $QUIZ tokens
  • DAO Governance: Community-driven decision making
  • Cross-Chain Integration: Expand to other EVM-compatible networks

Phase 3: Ecosystem Growth

  • Conflux Community Grants: Seek funding from Conflux Foundation
  • Partnership Integrations: Collaborate with other Conflux dApps
  • Enterprise Solutions: B2B quiz platform for corporate training
  • Multi-language Support: Global expansion with localization

Conclusion

QuizCraft represents a successful integration of AI, blockchain technology, and modern web development practices. The project demonstrates how Conflux eSpace’s low fees and high throughput can enable practical Web3 applications with real utility.

Key Achievements

  • :white_check_mark: Full-stack dApp: Complete frontend + smart contract solution
  • :white_check_mark: AI Integration: Dynamic question generation with fallback systems
  • :white_check_mark: Independent Gameplay: Revolutionary approach to multiplayer quiz gaming
  • :white_check_mark: State Persistence: Robust localStorage-based state management
  • :white_check_mark: Security: Audited smart contract patterns with OpenZeppelin
  • :white_check_mark: User Experience: Intuitive, responsive design with modern UI/UX

Technical Highlights

  • Smart Contract: Gas-optimized, secure CFX escrow system
  • AI System: Multi-model fallback with local question generation
  • State Management: Comprehensive persistence with time calculation
  • Frontend: Modern React patterns with excellent UX
  • Deployment: Production-ready on Conflux eSpace

The project showcases the potential of Conflux eSpace for innovative Web3 applications and serves as a foundation for future development in the educational gaming space.


Built with :heart: for the Conflux Hackathon

Showcasing the power of Conflux eSpace for innovative, practical Web3 applications.


Resources