const nouns = [
// Masculin – 50 words
{ word: “arbre”, gender: “m” },
{ word: “banc”, gender: “m” },
{ word: “ciel”, gender: “m” },
{ word: “doigt”, gender: “m” },
{ word: “escalier”, gender: “m” },
{ word: “feu”, gender: “m” },
{ word: “genou”, gender: “m” },
{ word: “hiver”, gender: “m” },
{ word: “jardin”, gender: “m” },
{ word: “lac”, gender: “m” },
{ word: “mur”, gender: “m” },
{ word: “nez”, gender: “m” },
{ word: “oiseau”, gender: “m” },
{ word: “pont”, gender: “m” },
{ word: “rideau”, gender: “m” },
{ word: “sac”, gender: “m” },
{ word: “tableau”, gender: “m” },
{ word: “verre”, gender: “m” },
{ word: “wagon”, gender: “m” },
{ word: “zoo”, gender: “m” },
{ word: “avion”, gender: “m” },
{ word: “ballon”, gender: “m” },
{ word: “camion”, gender: “m” },
{ word: “dessin”, gender: “m” },
{ word: “emploi”, gender: “m” },
{ word: “fruit”, gender: “m” },
{ word: “gâteau”, gender: “m” },
{ word: “hôtel”, gender: “m” },
{ word: “journal”, gender: “m” },
{ word: “lit”, gender: “m” },
{ word: “métier”, gender: “m” },
{ word: “neveu”, gender: “m” },
{ word: “ordinateur”, gender: “m” },
{ word: “pays”, gender: “m” },
{ word: “quartier”, gender: “m” },
{ word: “repas”, gender: “m” },
{ word: “soir”, gender: “m” },
{ word: “toit”, gender: “m” },
{ word: “visage”, gender: “m” },
{ word: “weekend”, gender: “m” },
{ word: “bureau”, gender: “m” },
{ word: “château”, gender: “m” },
{ word: “couteau”, gender: “m” },
{ word: “drapeau”, gender: “m” },
{ word: “cadeau”, gender: “m” },
{ word: “manteau”, gender: “m” },
{ word: “chapeau”, gender: “m” },
{ word: “bateau”, gender: “m” },
{ word: “morceau”, gender: “m” },
{ word: “réseau”, gender: “m” },
// Féminin – 50 words
{ word: “amitié”, gender: “f” },
{ word: “banque”, gender: “f” },
{ word: “chaise”, gender: “f” },
{ word: “danse”, gender: “f” },
{ word: “école”, gender: “f” },
{ word: “fenêtre”, gender: “f” },
{ word: “gare”, gender: “f” },
{ word: “heure”, gender: “f” },
{ word: “île”, gender: “f” },
{ word: “jambe”, gender: “f” },
{ word: “langue”, gender: “f” },
{ word: “montagne”, gender: “f” },
{ word: “nièce”, gender: “f” },
{ word: “oreille”, gender: “f” },
{ word: “porte”, gender: “f” },
{ word: “queue”, gender: “f” },
{ word: “route”, gender: “f” },
{ word: “soeur”, gender: “f” },
{ word: “terre”, gender: “f” },
{ word: “usine”, gender: “f” },
{ word: “valise”, gender: “f” },
{ word: “zone”, gender: “f” },
{ word: “année”, gender: “f” },
{ word: “bouteille”, gender: “f” },
{ word: “cuisine”, gender: “f” },
{ word: “douleur”, gender: “f” },
{ word: “étoile”, gender: “f” },
{ word: “fleur”, gender: “f” },
{ word: “guerre”, gender: “f” },
{ word: “histoire”, gender: “f” },
{ word: “joie”, gender: “f” },
{ word: “lettre”, gender: “f” },
{ word: “musique”, gender: “f” },
{ word: “nature”, gender: “f” },
{ word: “orange”, gender: “f” },
{ word: “peur”, gender: “f” },
{ word: “rivière”, gender: “f” },
{ word: “semaine”, gender: “f” },
{ word: “tasse”, gender: “f” },
{ word: “voix”, gender: “f” },
{ word: “affaire”, gender: “f” },
{ word: “boulangerie”, gender: “f” },
{ word: “chaleur”, gender: “f” },
{ word: “différence”, gender: “f” },
{ word: “entrée”, gender: “f” },
{ word: “forêt”, gender: “f” },
{ word: “glace”, gender: “f” },
{ word: “hauteur”, gender: “f” },
{ word: “idée”, gender: “f” },
{ word: “jeunesse”, gender: “f” }
];
let currentNounIndex = 0;
let correctCount = 0;
let totalAnswered = 0;
let shuffledNouns = [];
let currentSet = [];
let dropOffset = { x: 0, y: 0 };
const SET_SIZE = 50;
// Explanations for common gender patterns
const genderExplanations = {
m: {
‘age’: ‘Les mots en -age sont généralement masculins (exceptions: une page, une plage, une cage, une image, une nage, une rage)’,
‘ège’: ‘Les mots en -ège sont généralement masculins’,
‘é’: ‘Les mots en -é sont souvent masculins’,
‘ment’: ‘Les mots en -ment sont généralement masculins’,
‘eau’: ‘Les mots en -eau sont généralement masculins’,
‘phone’: ‘Les mots en -phone sont généralement masculins’,
‘scope’: ‘Les mots en -scope sont généralement masculins’,
‘isme’: ‘Les mots en -isme sont généralement masculins’,
‘acle’: ‘Les mots en -acle sont masculins’,
‘cle’: ‘Les mots en -cle sont souvent masculins’,
‘ule’: ‘Les mots en -ule sont souvent masculins’
},
f: {
‘tion’: ‘Les mots en -tion sont toujours féminins’,
‘sion’: ‘Les mots en -sion sont toujours féminins’,
‘té’: ‘Les mots en -té sont généralement féminins (abstractions)’,
‘ette’: ‘Les mots en -ette sont généralement féminins’,
‘esse’: ‘Les mots en -esse sont généralement féminins’,
‘ance’: ‘Les mots en -ance sont toujours féminins’,
‘ence’: ‘Les mots en -ence sont toujours féminins’,
‘ure’: ‘Les mots en -ure sont souvent féminins’,
‘ade’: ‘Les mots en -ade sont souvent féminins’,
‘ison’: ‘Les mots en -ison sont généralement féminins’,
‘age’: ‘Exception: page, plage, cage, image, nage, rage sont féminins’,
‘eige’: ‘Les mots en -eige sont féminins (neige)’,
‘orge’: ‘Les mots en -orge sont féminins (forge, gorge)’,
‘oge’: ‘Les mots en -oge sont féminins (horloge, loge)’
}
};
// Specific exceptions that need special explanations
const specificExplanations = {
// Masculine words ending in -eau
‘oiseau’: ‘Les mots en -eau sont masculins’,
‘tableau’: ‘Les mots en -eau sont masculins’,
‘rideau’: ‘Les mots en -eau sont masculins’,
‘gâteau’: ‘Les mots en -eau sont masculins’,
‘bureau’: ‘Les mots en -eau sont masculins’,
‘château’: ‘Les mots en -eau sont masculins’,
‘couteau’: ‘Les mots en -eau sont masculins’,
‘drapeau’: ‘Les mots en -eau sont masculins’,
‘cadeau’: ‘Les mots en -eau sont masculins’,
‘manteau’: ‘Les mots en -eau sont masculins’,
‘chapeau’: ‘Les mots en -eau sont masculins’,
‘bateau’: ‘Les mots en -eau sont masculins’,
‘morceau’: ‘Les mots en -eau sont masculins’,
‘réseau’: ‘Les mots en -eau sont masculins’,
// Masculine -age
‘visage’: ‘Les mots en -age sont généralement masculins’,
// Feminine exceptions
‘orange’: ‘Exception: “orange” est féminin (fruit)’,
‘voix’: ‘”Voix” est féminin (se termine par -oix)’,
‘nièce’: ‘Les noms de famille féminins en -èce sont féminins’,
‘glace’: ‘Les mots en -ace sont souvent féminins’,
// Other specific words
‘hiver’: ‘”Hiver” est masculin (saison)’,
‘escalier’: ‘Les mots en -ier sont généralement masculins’,
‘quartier’: ‘Les mots en -ier sont généralement masculins’,
‘métier’: ‘Les mots en -ier sont généralement masculins’
};
function getExplanation(word, correctGender) {
// Check for specific word explanations first
if (specificExplanations[word]) {
return specificExplanations[word];
}
// Check pattern-based explanations
const patterns = genderExplanations[correctGender];
for (const [ending, explanation] of Object.entries(patterns)) {
if (word.endsWith(ending)) {
return explanation;
}
}
// Default explanations based on gender
if (correctGender === ‘m’) {
return ‘Ce mot est masculin’;
} else {
return ‘Ce mot est féminin’;
}
}
function shuffleArray(array) {
const newArray = […array];
for (let i = newArray.length – 1; i > 0; i–) {
const j = Math.floor(Math.random() * (i + 1));
[newArray[i], newArray[j]] = [newArray[j], newArray[i]];
}
return newArray;
}
function initGame() {
// Shuffle all nouns and take first 50
shuffledNouns = shuffleArray(nouns);
currentSet = shuffledNouns.slice(0, SET_SIZE);
currentNounIndex = 0;
correctCount = 0;
totalAnswered = 0;
document.getElementById(‘masculin-cards’).innerHTML = ”;
document.getElementById(‘feminin-cards’).innerHTML = ”;
updateScore();
showNextNoun();
}
function newSet() {
// Keep 25 random nouns from current set
const shuffledCurrentSet = shuffleArray(currentSet);
const keptNouns = shuffledCurrentSet.slice(0, SET_SIZE / 2);
// Get remaining nouns (those not in current set)
const remainingNouns = nouns.filter(noun => !currentSet.includes(noun));
// Shuffle remaining nouns and take 25
const shuffledRemaining = shuffleArray(remainingNouns);
const newNouns = shuffledRemaining.slice(0, SET_SIZE / 2);
// Combine kept and new nouns, then shuffle the combined set
currentSet = shuffleArray([…keptNouns, …newNouns]);
// Reset game state
currentNounIndex = 0;
correctCount = 0;
totalAnswered = 0;
document.getElementById(‘masculin-cards’).innerHTML = ”;
document.getElementById(‘feminin-cards’).innerHTML = ”;
updateScore();
showNextNoun();
}
function showNextNoun() {
if (currentNounIndex >= currentSet.length) {
document.getElementById(‘current-noun’).innerHTML = ‘
Terminé! 🎉
‘;
document.getElementById(‘current-noun’).draggable = false;
return;
}
const nounCard = document.getElementById(‘current-noun’);
nounCard.textContent = currentSet[currentNounIndex].word;
nounCard.className = ‘noun-card’;
nounCard.draggable = true;
}
function updateScore() {
document.getElementById(‘correct’).textContent = correctCount;
document.getElementById(‘total’).textContent = currentSet.length;
document.getElementById(‘current-index’).textContent = currentNounIndex + 1;
}
function resetGame() {
initGame();
}
// Drag and drop functionality
const currentNoun = document.getElementById(‘current-noun’);
const dropZones = document.querySelectorAll(‘.drop-zone’);
// Prevent default drag behavior on the entire document
document.addEventListener(‘dragover’, (e) => {
e.preventDefault();
e.stopPropagation();
}, false);
// Only allow drops in drop zones – prevent all other drops
document.addEventListener(‘drop’, (e) => {
const isInDropZone = e.target.closest(‘.drop-zone’);
if (!isInDropZone) {
e.preventDefault();
e.stopPropagation();
return false;
}
}, true);
// Prevent scrolling during drag
let isDragging = false;
currentNoun.addEventListener(‘dragstart’, (e) => {
isDragging = true;
document.body.style.overflow = ‘hidden’;
e.dataTransfer.effectAllowed = ‘move’;
e.dataTransfer.setData(‘text/html’, currentNoun.innerHTML);
// Calculate offset between cursor and card center
const rect = currentNoun.getBoundingClientRect();
dropOffset.x = e.clientX – rect.left;
dropOffset.y = e.clientY – rect.top;
// Add dragging class after a small delay to show the card
setTimeout(() => {
currentNoun.classList.add(‘dragging’);
}, 0);
});
currentNoun.addEventListener(‘dragend’, (e) => {
isDragging = false;
document.body.style.overflow = ‘auto’;
currentNoun.classList.remove(‘dragging’);
});
dropZones.forEach(zone => {
zone.addEventListener(‘dragover’, (e) => {
e.preventDefault();
e.dataTransfer.dropEffect = ‘move’;
zone.classList.add(‘drag-over’);
});
zone.addEventListener(‘dragleave’, (e) => {
// Only remove drag-over if we’re actually leaving the zone
if (e.target === zone) {
zone.classList.remove(‘drag-over’);
}
});
zone.addEventListener(‘drop’, (e) => {
e.preventDefault();
e.stopPropagation();
zone.classList.remove(‘drag-over’);
const currentNoun = currentSet[currentNounIndex];
const droppedZone = zone.id;
const isCorrect = (droppedZone === ‘masculin’ && currentNoun.gender === ‘m’) ||
(droppedZone === ‘feminin’ && currentNoun.gender === ‘f’);
// Get the cards container
const cardsContainer = document.getElementById(droppedZone + ‘-cards’);
const containerRect = cardsContainer.getBoundingClientRect();
// Calculate drop position relative to container
let x = e.clientX – containerRect.left – dropOffset.x;
let y = e.clientY – containerRect.top – dropOffset.y;
// Create new card for dropped zone
const newCard = document.createElement(‘div’);
newCard.className = ‘dropped-card ‘ + (isCorrect ? ‘correct’ : ‘incorrect’);
newCard.textContent = currentNoun.word;
newCard.style.position = ‘absolute’;
newCard.style.visibility = ‘hidden’;
// Add to container first so we can measure it
cardsContainer.appendChild(newCard);
// Force a reflow to get accurate dimensions
newCard.offsetHeight;
// Now get its actual size with all padding, borders, and box-shadow
const cardRect = newCard.getBoundingClientRect();
const cardWidth = cardRect.width;
const cardHeight = cardRect.height;
// Get container available space (actual inner dimensions)
const availableWidth = containerRect.width;
const availableHeight = containerRect.height;
// Large margin to keep cards well away from edges (accounting for shadows and borders)
const margin = 25;
// Ensure card stays completely within container
// For X: must be at least margin from left, and at most (containerWidth – cardWidth – margin) from left
const maxX = Math.max(margin, availableWidth – cardWidth – margin);
const minX = margin;
x = Math.max(minX, Math.min(x, maxX));
// For Y: must be at least margin from top, and at most (containerHeight – cardHeight – margin) from top
const maxY = Math.max(margin, availableHeight – cardHeight – margin);
const minY = margin;
y = Math.max(minY, Math.min(y, maxY));
// Set final position and make visible
newCard.style.left = x + ‘px’;
newCard.style.top = y + ‘px’;
newCard.style.visibility = ‘visible’;
// Add tooltip for incorrect answers
if (!isCorrect) {
const tooltip = document.createElement(‘span’);
tooltip.className = ‘tooltip’;
tooltip.textContent = getExplanation(currentNoun.word, currentNoun.gender);
newCard.appendChild(tooltip);
}
// Update score
totalAnswered++;
if (isCorrect) {
correctCount++;
}
updateScore();
// Move to next noun
currentNounIndex++;
showNextNoun();
});
});
// Initialize game on load
initGame();