<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Globe Quiz</title>
<script src="https://cdn.tailwindcss.com"></script>
<style>
body {
margin: 0;
overflow: hidden;
font-family: 'Inter', sans-serif;
background-color: #111827; /* bg-gray-900 */
}
#ui-container {
position: absolute;
top: 20px;
left: 20px;
background-color: rgba(17, 24, 39, 0.9); /* bg-gray-900 with opacity */
color: #f3f4f6; /* text-gray-100 */
padding: 1rem;
border-radius: 0.5rem;
border: 1px solid #374151; /* border-gray-700 */
width: 300px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
z-index: 10;
}
#popup {
position: absolute;
display: none;
padding: 0.75rem 1.5rem;
border-radius: 0.5rem;
font-weight: 700;
font-size: 1.25rem;
color: white;
z-index: 20;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
text-align: center;
}
.correct {
background-color: #22c55e; /* green-500 */
}
.incorrect {
background-color: #ef4444; /* red-500 */
}
canvas {
display: block;
}
button {
width: 100%;
padding: 0.75rem;
background-color: #3b82f6; /* blue-500 */
color: white;
border: none;
border-radius: 0.375rem;
font-weight: 600;
cursor: pointer;
transition: background-color 0.2s;
}
button:hover {
background-color: #2563eb; /* blue-600 */
}
</style>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap" rel="stylesheet">
</head>
<body>
<div id="container"></div>
<div id="popup"></div>
<div id="ui-container">
<h1 class="text-xl font-bold text-white mb-2">Globe Quiz</h1>
<div id="start-screen">
<p class="text-sm text-gray-300 mb-4">Find 10 countries on the globe as fast as you can. Your total time is your score!</p>
<button id="start-button">Start Quiz</button>
</div>
<div id="quiz-screen" class="hidden">
<p class="text-sm text-gray-400">Question <span id="question-count">1</span> of 10</p>
<p class="text-lg text-gray-200 mb-2">Find this country:</p>
<p id="target-country" class="text-2xl font-bold text-white mb-4"></p>
<p class="text-sm text-gray-400">Current Time: <span id="timer">0.0s</span></p>
</div>
<div id="end-screen" class="hidden">
<h2 class="text-lg font-bold text-white mb-2">Quiz Complete!</h2>
<div class="grid grid-cols-2 gap-4 text-center">
<div>
<p class="text-gray-400 text-sm">Total Time</p>
<p id="final-time" class="text-2xl font-bold text-white"></p>
</div>
<div>
<p class="text-gray-400 text-sm">Final Score</p>
<p id="final-score" class="text-2xl font-bold text-white"></p>
</div>
</div>
<button id="play-again-button" class="mt-6">Play Again</button>
</div>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<script src="https://d3js.org/d3.v7.min.js"></script>
<script src="https://d3js.org/d3-geo.v3.min.js"></script>
<script src="https://unpkg.com/topojson-client@3"></script>
<script>
// --- UI Elements ---
const container = document.getElementById('container');
const popup = document.getElementById('popup');
const startScreen = document.getElementById('start-screen');
const quizScreen = document.getElementById('quiz-screen');
const endScreen = document.getElementById('end-screen');
const startButton = document.getElementById('start-button');
const playAgainButton = document.getElementById('play-again-button');
const questionCountEl = document.getElementById('question-count');
const targetCountryEl = document.getElementById('target-country');
const timerEl = document.getElementById('timer');
const finalTimeEl = document.getElementById('final-time');
const finalScoreEl = document.getElementById('final-score');
// --- 3D Setup ---
let scene, camera, renderer, globe, raycaster, mouse;
let isMouseDown = false;
let previousMousePosition = { x: 0, y: 0 };
let countryFeatures;
// --- Game State ---
let gameActive = false;
let questionCount = 0;
let totalTime = 0;
let timerInterval;
let targetCountry;
let askedCountries = [ ];
let hasDragged = false; // Flag to differentiate click from drag
// --- Initialization ---
function init() {
scene = new THREE.Scene();
scene.background = new THREE.Color(0x111827);
camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.z = 15;
renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
container.appendChild(renderer.domElement);
const ambientLight = new THREE.AmbientLight(0xcccccc, 0.8);
scene.add(ambientLight);
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.5);
directionalLight.position.set(1, 1, 1).normalize();
scene.add(directionalLight);
raycaster = new THREE.Raycaster();
mouse = new THREE.Vector2();
const globeRadius = 10;
const globeGeometry = new THREE.SphereGeometry(globeRadius, 64, 64);
const globeMaterial = new THREE.MeshPhongMaterial({ shininess: 5 });
globe = new THREE.Mesh(globeGeometry, globeMaterial);
scene.add(globe);
loadAndDrawMap(globeMaterial);
// --- Event Listeners ---
startButton.addEventListener('click', startGame);
playAgainButton.addEventListener('click', startGame);
renderer.domElement.addEventListener('mousedown', onPointerDown, false);
renderer.domElement.addEventListener('mousemove', onPointerMove, false);
renderer.domElement.addEventListener('mouseup', onPointerUp, false);
renderer.domElement.addEventListener('touchstart', onPointerDown, false);
renderer.domElement.addEventListener('touchmove', onPointerMove, false);
renderer.domElement.addEventListener('touchend', onPointerUp, false);
renderer.domElement.addEventListener('click', onGlobeClick, false);
window.addEventListener('resize', onWindowResize, false);
}
function loadAndDrawMap(material) {
const canvas = document.createElement('canvas');
const context = canvas.getContext('2d');
canvas.width = 2048;
canvas.height = 1024;
context.fillStyle = '#3b82f6';
context.fillRect(0, 0, canvas.width, canvas.height);
fetch('https://cdn.jsdelivr.net/npm/world-atlas@2/countries-110m.json')
.then(res => res.json())
.then(worldData => {
countryFeatures = topojson.feature(worldData, worldData.objects.countries).features.filter(f => f.properties.name !== "Antarctica");
const projection = d3.geoEquirectangular().fitSize([canvas.width, canvas.height], { type: "Sphere" });
const pathGenerator = d3.geoPath().projection(projection).context(context);
context.strokeStyle = '#111827';
context.lineWidth = 0.5;
countryFeatures.forEach(feature => {
const randomColor = '#' + Math.floor(Math.random()*16777215).toString(16).padStart(6, '0');
context.fillStyle = randomColor;
context.beginPath();
pathGenerator(feature);
context.fill();
context.stroke();
});
const texture = new THREE.CanvasTexture(canvas);
texture.needsUpdate = true;
material.map = texture;
material.needsUpdate = true;
});
}
// --- Game Logic ---
function startGame() {
gameActive = true;
questionCount = 0;
totalTime = 0;
askedCountries = [ ];
startScreen.classList.add('hidden');
endScreen.classList.add('hidden');
quizScreen.classList.remove('hidden');
askNextQuestion();
}
function askNextQuestion() {
if (questionCount >= 10) {
endGame();
return;
}
questionCount++;
let availableCountries = countryFeatures.filter(f => !askedCountries.includes(f.properties.name));
targetCountry = availableCountries[Math.floor(Math.random() * availableCountries.length)];
askedCountries.push(targetCountry.properties.name);
questionCountEl.textContent = questionCount;
targetCountryEl.textContent = targetCountry.properties.name;
startTimer();
}
function startTimer() {
clearInterval(timerInterval);
let startTime = Date.now();
timerInterval = setInterval(() => {
const elapsedTime = ((Date.now() - startTime) / 1000).toFixed(1);
timerEl.textContent = `${elapsedTime}s`;
}, 100);
}
function stopTimer() {
clearInterval(timerInterval);
const finalTime = parseFloat(timerEl.textContent);
totalTime += finalTime;
}
function onGlobeClick(event) {
// If the user dragged the mouse, don't register it as a click.
if (hasDragged) {
return;
}
if (!gameActive) return;
mouse.x = (event.clientX / renderer.domElement.clientWidth) * 2 - 1;
mouse.y = -(event.clientY / renderer.domElement.clientHeight) * 2 + 1;
raycaster.setFromCamera(mouse, camera);
const intersects = raycaster.intersectObject(globe);
if (intersects.length > 0) {
const uv = intersects[0].uv;
const lon = uv.x * 360 - 180;
const lat = uv.y * 180 - 90;
const clickedCountry = countryFeatures.find(f => d3.geoContains(f, [lon, lat]));
if (clickedCountry) {
stopTimer();
if (clickedCountry.properties.name === targetCountry.properties.name) {
showFeedback(true);
} else {
showFeedback(false, clickedCountry.properties.name);
}
setTimeout(askNextQuestion, 1500);
}
}
}
function showFeedback(isCorrect, clickedName = '') {
if (isCorrect) {
popup.textContent = "Correct!";
popup.className = "popup correct";
} else {
popup.innerHTML = `Incorrect!<br><span style="font-size: 0.9rem;">That was ${clickedName}</span>`;
popup.className = "popup incorrect";
}
popup.style.display = 'block';
setTimeout(() => { popup.style.display = 'none'; }, 1400);
}
function endGame() {
gameActive = false;
quizScreen.classList.add('hidden');
endScreen.classList.remove('hidden');
// Calculate score based on time. Lower time = higher score.
// A "perfect" score is achieved at 5 seconds per question on average.
const perfectTime = 50; // 10 questions * 5s
let score = Math.max(0, Math.round(10000 * (perfectTime / totalTime)));
finalTimeEl.textContent = `${totalTime.toFixed(1)}s`;
finalScoreEl.textContent = score.toLocaleString();
}
// --- Pointer Handlers ---
function onPointerDown(event) {
hasDragged = false; // Reset drag flag on a new mousedown/touchstart
isMouseDown = true;
const clientX = event.touches ? event.touches[0].clientX : event.clientX;
const clientY = event.touches ? event.touches[0].clientY : event.clientY;
previousMousePosition = { x: clientX, y: clientY };
}
function onPointerMove(event) {
if (!isMouseDown) return;
hasDragged = true; // If the pointer moves while down, it's a drag
if (event.touches) event.preventDefault();
const clientX = event.touches ? event.touches[0].clientX : event.clientX;
const clientY = event.touches ? event.touches[0].clientY : event.clientY;
const deltaMove = { x: clientX - previousMousePosition.x, y: clientY - previousMousePosition.y };
const rotationQuaternion = new THREE.Quaternion().setFromEuler(new THREE.Euler(toRadians(deltaMove.y * 0.5), toRadians(deltaMove.x * 0.5), 0, 'XYZ'));
globe.quaternion.multiplyQuaternions(rotationQuaternion, globe.quaternion);
previousMousePosition = { x: clientX, y: clientY };
}
function onPointerUp() { isMouseDown = false; }
function onWindowResize() {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
}
function toRadians(angle) { return angle * (Math.PI / 180); }
// --- Animation Loop ---
function animate() {
requestAnimationFrame(animate);
renderer.render(scene, camera);
}
// --- Start ---
init();
animate();
</script>
</body>
</html>