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
-
AI-powered question generation using OpenAI GPT-4
-
CFX-based economy with smart contract escrow
-
Independent per-user gameplay with state persistence
-
Real-time leaderboards and NFT achievements
-
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
-
Full-stack dApp: Complete frontend + smart contract solution
-
AI Integration: Dynamic question generation with fallback systems
-
Independent Gameplay: Revolutionary approach to multiplayer quiz gaming
-
State Persistence: Robust localStorage-based state management
-
Security: Audited smart contract patterns with OpenZeppelin
-
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 for the Conflux Hackathon
Showcasing the power of Conflux eSpace for innovative, practical Web3 applications.
Resources
- Live Demo: https://quiz-craft-vjl5.vercel.app/
- GitHub Repository: https://github.com/Vikash-8090-Yadav/QuizCraft
- Demo Video: VIDEO