<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Jeu de Mesure - Unités Non Conventionnelles</title>
<script src="https://cdn.tailwindcss.com"></script>
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
<style>
@import url('https://fonts.googleapis.com/css2?family=Fredoka:wght@400;600&display=swap');
body {
font-family: 'Fredoka', sans-serif;
background-color: #f0f9ff;
user-select: none;
}
.game-container {
max-width: 800px;
margin: 0 auto;
}
.pop-in {
animation: popIn 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275);
}
@keyframes popIn {
0% { transform: scale(0); opacity: 0; }
100% { transform: scale(1); opacity: 1; }
}
.measurement-zone {
background-image: repeating-linear-gradient(90deg, #e5e7eb 0px, #e5e7eb 1px, transparent 1px, transparent 60px);
background-size: 60px 100%;
border-bottom: 4px solid #94a3b8;
}
.unit-item {
width: 60px;
height: 60px;
display: flex;
align-items: center;
justify-content: center;
font-size: 2rem;
position: relative;
z-index: 10;
transition: all 0.2s;
border-radius: 8px;
}
/* --- STYLES DE VALIDATION --- */
.counting-highlight {
transform: scale(1.15) !important;
background-color: #fef08a !important;
border: 3px solid #eab308 !important;
z-index: 50 !important;
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
}
.excess-highlight {
transform: scale(1.15) !important;
background-color: #fecaca !important;
border: 3px solid #ef4444 !important;
z-index: 50 !important;
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
}
.missing-ghost {
width: 60px;
height: 60px;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.5rem;
color: #94a3b8;
border: 3px dashed #cbd5e1;
background-color: rgba(255, 255, 255, 0.5);
border-radius: 8px;
margin: 0;
animation: fadeIn 0.5s ease;
}
@keyframes fadeIn {
from { opacity: 0; transform: scale(0.8); }
to { opacity: 1; transform: scale(1); }
}
.count-badge {
position: absolute;
top: -15px;
right: -10px;
background-color: #22c55e;
color: white;
width: 28px;
height: 28px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 1rem;
font-weight: bold;
border: 2px solid white;
animation: badgePop 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275) forwards;
box-shadow: 0 2px 4px rgba(0,0,0,0.2);
}
.count-badge.excess {
background-color: #ef4444;
}
@keyframes badgePop {
0% { transform: scale(0); }
100% { transform: scale(1); }
}
/* --- OBJET CIBLE (Modifié pour ressembler à l'objet physique) --- */
.target-object {
height: 80px; /* Un peu moins haut pour faire plus "objet" */
display: flex;
align-items: center;
justify-content: center;
font-size: 2.5rem;
position: relative;
transition: width 0.5s ease-in-out;
/* Plus de bordure dashed par défaut */
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.2);
color: rgba(255,255,255,0.9);
text-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
/* Styles spécifiques aux formes */
.shape-pencil {
border-radius: 8px 40px 40px 8px; /* Pointe à droite */
border-right: 15px solid #fcd34d; /* Effet mine taillée */
}
.shape-book {
border-radius: 4px;
border-left: 10px solid rgba(0,0,0,0.1); /* Reliure */
background-image: linear-gradient(90deg, transparent 95%, rgba(255,255,255,0.3) 95%);
background-size: 20px 100%; /* Effet pages si on veut, ou juste reliure */
}
.shape-worm {
border-radius: 40px; /* Forme pilule */
border: 2px solid rgba(255,255,255,0.2);
}
.shape-truck {
border-radius: 8px;
/* On pourrait ajouter des roues en CSS mais restons simple */
}
.confetti {
position: absolute;
width: 10px;
height: 10px;
background-color: #f00;
animation: fall linear forwards;
}
@keyframes fall {
to { transform: translateY(100vh) rotate(720deg); }
}
</style>
</head>
<body class="min-h-screen flex flex-col p-4">
<!-- En-tête -->
<header class="text-center mb-6">
<h1 class="text-3xl md:text-4xl font-bold text-sky-600 drop-shadow-sm">
<i class="fas fa-ruler-horizontal mr-2"></i>J'apprends à mesurer !
</h1>
<p class="text-slate-500 mt-2 text-lg">Mesure la longueur de l'objet coloré.</p>
</header>
<!-- Zone de Jeu -->
<main id="app" class="game-container flex-grow flex flex-col items-center w-full">
<!-- Barre de progression -->
<div class="w-full bg-white rounded-full h-4 mb-6 shadow-inner overflow-hidden border border-slate-200">
<div id="progress-bar" class="bg-green-400 h-4 rounded-full transition-all duration-500" style="width: 0%"></div>
</div>
<!-- Zone Scène -->
<div class="bg-white p-6 rounded-3xl shadow-xl w-full border-4 border-sky-100 relative overflow-hidden">
<!-- Question -->
<div class="text-center mb-8">
<span class="bg-yellow-100 text-yellow-700 px-4 py-2 rounded-full font-bold text-xl inline-block shadow-sm">
Niveau <span id="level-indicator">1</span>
</span>
<p id="instruction-text" class="text-2xl mt-4 text-slate-700 font-bold">
Combien de <span class="text-sky-600">trombones</span> mesure le crayon ?
</p>
</div>
<!-- Espace de mesure (Objet + Unités) -->
<div class="relative w-full overflow-x-auto pb-4 custom-scrollbar">
<div class="min-w-max px-4">
<!-- Objet à mesurer -->
<div class="flex items-end mb-1">
<!-- L'objet est maintenant représenté par cette div elle-même -->
<div id="target-object" class="target-object mb-2">
<span id="target-icon" class="drop-shadow-md">✏️</span>
</div>
</div>
<!-- Ligne de mesure (Zone de drop/clic) -->
<div id="measurement-track" class="measurement-zone h-20 flex items-center rounded-lg relative bg-slate-50 w-full min-w-[600px]">
<!-- Les unités ajoutées apparaitront ici -->
</div>
</div>
</div>
<!-- Contrôles -->
<div class="mt-8 flex flex-col md:flex-row justify-between items-center gap-6 bg-slate-50 p-4 rounded-2xl border border-slate-200">
<!-- Outils -->
<div class="flex gap-4">
<button id="add-btn" onclick="game.addUnit()" class="group relative bg-white border-2 border-b-4 border-sky-200 hover:border-sky-400 active:border-b-2 active:translate-y-0.5 text-sky-600 rounded-xl p-4 flex flex-col items-center transition-all w-28 shadow-sm">
<span id="unit-preview" class="text-3xl mb-1 group-hover:scale-110 transition-transform">📎</span>
<span class="font-bold text-sm">Ajouter</span>
<div class="absolute -top-3 -right-3 bg-green-500 text-white rounded-full w-8 h-8 flex items-center justify-center font-bold shadow-md transform group-hover:scale-110 transition-transform">+</div>
</button>
<button id="remove-btn" onclick="game.removeUnit()" class="bg-white border-2 border-b-4 border-red-100 hover:border-red-300 active:border-b-2 active:translate-y-0.5 text-red-500 rounded-xl p-4 flex flex-col items-center transition-all w-24 shadow-sm">
<span class="text-3xl mb-1 text-red-400"><i class="fas fa-undo"></i></span>
<span class="font-bold text-sm">Enlever</span>
</button>
</div>
<!-- Réponse -->
<div class="flex items-center gap-3 bg-blue-50 px-6 py-4 rounded-xl border border-blue-100">
<div class="text-right">
<p class="text-sm text-blue-600 font-bold uppercase tracking-wider">Ma réponse</p>
<p class="text-xs text-blue-400">Compte les objets !</p>
</div>
<input type="number" id="user-answer" class="w-20 h-16 text-center text-3xl font-bold border-2 border-blue-200 rounded-lg focus:outline-none focus:border-blue-500 text-slate-700 bg-white" placeholder="?">
<button id="validate-btn" onclick="game.checkAnswer()" class="bg-green-500 hover:bg-green-600 text-white text-xl font-bold py-4 px-6 rounded-xl shadow-lg border-b-4 border-green-700 active:border-b-0 active:translate-y-1 transition-all ml-2">
Valider <i class="fas fa-check ml-2"></i>
</button>
</div>
</div>
</div>
</main>
<!-- Modal de Feedback -->
<div id="feedback-modal" class="fixed inset-0 bg-black bg-opacity-50 hidden flex items-center justify-center z-50 backdrop-blur-sm">
<div class="bg-white rounded-3xl p-8 max-w-sm w-full text-center shadow-2xl transform scale-90 transition-transform duration-300" id="modal-content">
<div id="feedback-icon" class="text-6xl mb-4">🎉</div>
<h2 id="feedback-title" class="text-3xl font-bold mb-2 text-slate-800">Bravo !</h2>
<p id="feedback-message" class="text-lg text-slate-600 mb-6">Tu as trouvé la bonne mesure.</p>
<button onclick="game.nextLevel()" id="next-btn" class="w-full bg-sky-500 text-white font-bold py-3 px-6 rounded-xl shadow-md hover:bg-sky-600 transition-colors text-xl">
Suivant <i class="fas fa-arrow-right ml-2"></i>
</button>
<button onclick="game.closeModal()" id="retry-btn" class="hidden w-full bg-orange-400 text-white font-bold py-3 px-6 rounded-xl shadow-md hover:bg-orange-500 transition-colors text-xl">
Réessayer <i class="fas fa-redo ml-2"></i>
</button>
</div>
</div>
<script>
const levels = [
{
targetName: "Le Crayon",
targetIcon: "", // On enlève l'icone pour le crayon car la forme suffit, ou on laisse vide si on veut juste la forme
targetIconDisplay: "✏️",
// Style de l'objet "Crayon" (Jaune, forme crayon)
objectClass: "bg-yellow-400 shape-pencil",
unitName: "trombones",
unitSingular: "trombone",
unitIcon: "📎",
correctCount: 5,
unitWidth: 60
},
{
targetName: "Le Livre",
targetIconDisplay: "📘",
// Style de l'objet "Livre" (Bleu, rectangulaire)
objectClass: "bg-blue-500 shape-book",
unitName: "pièces",
unitSingular: "pièce",
unitIcon: "🟡",
correctCount: 4,
unitWidth: 60
},
{
targetName: "La Chenille",
targetIconDisplay: "🐛",
// Style de l'objet "Chenille" (Vert, arrondi)
objectClass: "bg-green-400 shape-worm",
unitName: "feuilles",
unitSingular: "feuille",
unitIcon: "🍃",
correctCount: 7,
unitWidth: 60
},
{
targetName: "Le Serpent",
targetIconDisplay: "🐍",
// Style de l'objet "Serpent" (Emeraude, arrondi)
objectClass: "bg-emerald-500 shape-worm",
unitName: "baskets",
unitSingular: "basket",
unitIcon: "👟",
correctCount: 6,
unitWidth: 60
},
{
targetName: "Le Camion",
targetIconDisplay: "🚚",
// Style de l'objet "Camion" (Rouge, rectangle)
objectClass: "bg-red-500 shape-truck",
unitName: "blocs",
unitSingular: "bloc",
unitIcon: "🟥",
correctCount: 8,
unitWidth: 60
}
];
class MeasuringGame {
constructor() {
this.currentLevelIndex = 0;
this.placedUnitsCount = 0;
this.isAnimating = false;
this.elements = {
levelIndicator: document.getElementById('level-indicator'),
progressBar: document.getElementById('progress-bar'),
instructionText: document.getElementById('instruction-text'),
targetObject: document.getElementById('target-object'),
targetIcon: document.getElementById('target-icon'),
measurementTrack: document.getElementById('measurement-track'),
unitPreview: document.getElementById('unit-preview'),
userAnswer: document.getElementById('user-answer'),
modal: document.getElementById('feedback-modal'),
modalTitle: document.getElementById('feedback-title'),
modalMessage: document.getElementById('feedback-message'),
modalIcon: document.getElementById('feedback-icon'),
nextBtn: document.getElementById('next-btn'),
retryBtn: document.getElementById('retry-btn'),
btns: {
add: document.getElementById('add-btn'),
remove: document.getElementById('remove-btn'),
validate: document.getElementById('validate-btn')
}
};
this.initLevel();
}
getCurrentLevel() {
return levels[this.currentLevelIndex];
}
initLevel() {
const level = this.getCurrentLevel();
this.placedUnitsCount = 0;
this.isAnimating = false;
this.toggleControls(true);
this.elements.userAnswer.value = '';
this.elements.measurementTrack.innerHTML = '';
this.elements.levelIndicator.textContent = this.currentLevelIndex + 1;
this.elements.progressBar.style.width = `${(this.currentLevelIndex / levels.length) * 100}%`;
this.elements.instructionText.innerHTML = `Combien de <span class="text-sky-600 font-bold">${level.unitName}</span> mesure ${level.targetName.toLowerCase()} ?`;
// --- CONFIGURATION DE L'OBJET VISUEL ---
// La largeur correspond au nombre d'unités
this.elements.targetObject.style.width = `${level.correctCount * level.unitWidth}px`;
// On applique les classes de forme et couleur, on enlève les anciennes classes dynamiques
this.elements.targetObject.className = `target-object mb-2 pop-in ${level.objectClass}`;
// On met l'icône, mais elle est maintenant décorative à l'intérieur de la forme
this.elements.targetIcon.textContent = level.targetIconDisplay;
this.elements.unitPreview.textContent = level.unitIcon;
}
toggleControls(enable) {
Object.values(this.elements.btns).forEach(btn => btn.disabled = !enable);
this.elements.userAnswer.disabled = !enable;
if(enable) {
Object.values(this.elements.btns).forEach(btn => btn.classList.remove('opacity-50', 'cursor-not-allowed'));
} else {
Object.values(this.elements.btns).forEach(btn => btn.classList.add('opacity-50', 'cursor-not-allowed'));
}
}
addUnit() {
if (this.isAnimating) return;
const level = this.getCurrentLevel();
if (this.placedUnitsCount >= 15) return;
const unitDiv = document.createElement('div');
unitDiv.className = 'unit-item pop-in bg-white border border-slate-200 shadow-sm mx-[0px]';
unitDiv.textContent = level.unitIcon;
unitDiv.style.animation = 'popIn 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275)';
this.elements.measurementTrack.appendChild(unitDiv);
this.placedUnitsCount++;
this.elements.measurementTrack.parentElement.scrollLeft = this.elements.measurementTrack.parentElement.scrollWidth;
}
removeUnit() {
if (this.isAnimating) return;
if (this.placedUnitsCount > 0) {
this.elements.measurementTrack.removeChild(this.elements.measurementTrack.lastChild);
this.placedUnitsCount--;
}
}
async checkAnswer() {
if (this.isAnimating) return;
const level = this.getCurrentLevel();
const userVal = parseInt(this.elements.userAnswer.value);
if (isNaN(userVal)) {
this.showFeedback("Oups !", "Tu dois écrire un nombre dans la case bleue.", "🤔", false);
return;
}
this.isAnimating = true;
this.toggleControls(false);
const units = this.elements.measurementTrack.children;
const countLoop = Math.max(units.length, level.correctCount);
for (let i = 0; i < countLoop; i++) {
await new Promise(r => setTimeout(r, 600)); // Tempo
if (i < units.length) {
const unit = units[i];
if (i < level.correctCount) {
unit.classList.add('counting-highlight');
this.createBadge(unit, i + 1, false);
} else {
unit.classList.add('excess-highlight');
this.createBadge(unit, i + 1, true);
}
} else {
const ghost = document.createElement('div');
ghost.className = 'missing-ghost';
ghost.textContent = '?';
this.elements.measurementTrack.appendChild(ghost);
this.elements.measurementTrack.parentElement.scrollLeft = this.elements.measurementTrack.parentElement.scrollWidth;
}
}
await new Promise(r => setTimeout(r, 800));
const isNumberCorrect = userVal === level.correctCount;
if (isNumberCorrect) {
if (this.placedUnitsCount === level.correctCount) {
this.createConfetti();
this.showFeedback("Bravo !", `Parfait ! C'est exactement ça.`, "🌟", true);
} else {
this.showFeedback("Bien vu !", `Tu as écrit ${userVal} (c'est juste), mais vérifie bien si la longueur correspond.`, "👍", true);
}
} else {
let message = "";
if (this.placedUnitsCount > level.correctCount) {
message = `Regarde les objets rouges, il y en a trop !`;
} else if (this.placedUnitsCount < level.correctCount) {
message = `Regarde les cases vides, il en manque !`;
} else {
message = `Tu as bien mis les objets, mais recomptes-les bien.`;
}
this.showFeedback("Essaie encore", message, "👀", false);
}
this.isAnimating = false;
}
createBadge(element, number, isExcess) {
const badge = document.createElement('div');
badge.className = isExcess ? 'count-badge excess' : 'count-badge';
badge.textContent = number;
element.appendChild(badge);
}
showFeedback(title, message, icon, isSuccess) {
this.elements.modalTitle.textContent = title;
this.elements.modalMessage.textContent = message;
this.elements.modalIcon.textContent = icon;
this.elements.modal.classList.remove('hidden');
if (isSuccess) {
this.elements.modalTitle.className = "text-3xl font-bold mb-2 text-green-600";
this.elements.nextBtn.classList.remove('hidden');
this.elements.retryBtn.classList.add('hidden');
} else {
this.elements.modalTitle.className = "text-3xl font-bold mb-2 text-orange-500";
this.elements.nextBtn.classList.add('hidden');
this.elements.retryBtn.classList.remove('hidden');
}
}
closeModal() {
this.elements.modal.classList.add('hidden');
if (this.elements.nextBtn.classList.contains('hidden')) {
this.toggleControls(true);
const units = this.elements.measurementTrack.children;
Array.from(units).forEach(u => {
if (u.classList.contains('missing-ghost')) {
u.remove();
} else {
u.classList.remove('counting-highlight', 'excess-highlight');
const badge = u.querySelector('.count-badge');
if(badge) badge.remove();
}
});
}
}
nextLevel() {
this.closeModal();
this.currentLevelIndex++;
if (this.currentLevelIndex >= levels.length) {
this.endGame();
} else {
this.initLevel();
}
}
endGame() {
this.elements.instructionText.textContent = "Tu as terminé tous les niveaux !";
this.elements.measurementTrack.innerHTML = '<div class="w-full text-center text-green-500 font-bold text-2xl pt-4">Champion de la mesure ! 🏆</div>';
this.elements.targetObject.style.display = 'none';
this.elements.progressBar.style.width = '100%';
document.querySelectorAll('button').forEach(b => b.disabled = true);
this.createConfetti();
}
createConfetti() {
for(let i=0; i<50; i++) {
const confetti = document.createElement('div');
confetti.classList.add('confetti');
confetti.style.left = Math.random() * 100 + 'vw';
confetti.style.animationDuration = (Math.random() * 3 + 2) + 's';
confetti.style.backgroundColor = ['#ff0', '#f00', '#0f0', '#00f', '#f0f'][Math.floor(Math.random() * 5)];
document.body.appendChild(confetti);
setTimeout(() => confetti.remove(), 5000);
}
}
}
const game = new MeasuringGame();
</script>
</body>
</html>