- Enhanced Story Reader with text-to-story conversion methods - Added support for simple texts and sentences in Story Reader - Removed Text Reader game file (js/games/text-reader.js) - Updated all configuration files to remove Text Reader references - Modified game compatibility system to use Story Reader instead - Updated test fixtures to reflect game changes - Cleaned up debug/test HTML files 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
570 lines
20 KiB
JavaScript
570 lines
20 KiB
JavaScript
// === VÉRIFICATEUR DE COMPATIBILITÉ CONTENU-JEU ===
|
|
|
|
class ContentGameCompatibility {
|
|
constructor() {
|
|
this.compatibilityCache = new Map();
|
|
this.minimumScores = {
|
|
'whack-a-mole': 40,
|
|
'whack-a-mole-hard': 45,
|
|
'memory-match': 50,
|
|
'quiz-game': 30,
|
|
'fill-the-blank': 30,
|
|
'adventure-reader': 50,
|
|
'chinese-study': 35,
|
|
'story-builder': 35,
|
|
'story-reader': 40,
|
|
'word-storm': 15
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Vérifie si un contenu est compatible avec un jeu
|
|
* @param {Object} content - Le contenu à vérifier
|
|
* @param {string} gameType - Le type de jeu
|
|
* @returns {Object} - { compatible: boolean, score: number, reason: string, requirements: string[] }
|
|
*/
|
|
checkCompatibility(content, gameType) {
|
|
// Utiliser le cache si disponible
|
|
const cacheKey = `${content.id || content.name}_${gameType}`;
|
|
if (this.compatibilityCache.has(cacheKey)) {
|
|
return this.compatibilityCache.get(cacheKey);
|
|
}
|
|
|
|
let result;
|
|
|
|
// Si le contenu a déjà une analyse de compatibilité (depuis ContentScanner)
|
|
if (content.gameCompatibility && content.gameCompatibility[gameType]) {
|
|
result = this.enrichCompatibilityInfo(content.gameCompatibility[gameType], gameType);
|
|
} else {
|
|
// Analyser manuellement
|
|
result = this.analyzeCompatibility(content, gameType);
|
|
}
|
|
|
|
// Mettre en cache
|
|
this.compatibilityCache.set(cacheKey, result);
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Enrichit les informations de compatibilité existantes
|
|
*/
|
|
enrichCompatibilityInfo(existingCompat, gameType) {
|
|
const minScore = this.minimumScores[gameType] || 30;
|
|
return {
|
|
compatible: existingCompat.score >= minScore,
|
|
score: existingCompat.score,
|
|
reason: existingCompat.reason || this.getDefaultReason(gameType),
|
|
requirements: this.getGameRequirements(gameType),
|
|
details: this.getDetailedAnalysis(existingCompat, gameType)
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Analyse manuelle de compatibilité si pas déjà calculée
|
|
*/
|
|
analyzeCompatibility(content, gameType) {
|
|
const capabilities = this.analyzeContentCapabilities(content);
|
|
const compatResult = this.calculateGameCompatibilityForType(capabilities, gameType);
|
|
|
|
return {
|
|
compatible: compatResult.compatible,
|
|
score: compatResult.score,
|
|
reason: compatResult.reason,
|
|
requirements: this.getGameRequirements(gameType),
|
|
details: this.getDetailedAnalysis(compatResult, gameType),
|
|
capabilities: capabilities
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Analyse les capacités d'un contenu
|
|
*/
|
|
analyzeContentCapabilities(content) {
|
|
return {
|
|
hasVocabulary: this.hasContent(content, 'vocabulary'),
|
|
hasSentences: this.hasContent(content, 'sentences'),
|
|
hasGrammar: this.hasContent(content, 'grammar'),
|
|
hasAudio: this.hasContent(content, 'audio'),
|
|
hasDialogues: this.hasContent(content, 'dialogues'),
|
|
hasExercises: this.hasExercises(content),
|
|
hasFillInBlanks: this.hasContent(content, 'fillInBlanks'),
|
|
hasCorrections: this.hasContent(content, 'corrections'),
|
|
hasComprehension: this.hasContent(content, 'comprehension'),
|
|
hasMatching: this.hasContent(content, 'matching'),
|
|
|
|
// Compteurs
|
|
vocabularyCount: this.countItems(content, 'vocabulary'),
|
|
sentenceCount: this.countItems(content, 'sentences'),
|
|
dialogueCount: this.countItems(content, 'dialogues'),
|
|
grammarCount: this.countItems(content, 'grammar')
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Calcule la compatibilité pour un type de jeu spécifique
|
|
*/
|
|
calculateGameCompatibilityForType(capabilities, gameType) {
|
|
switch (gameType) {
|
|
case 'whack-a-mole':
|
|
case 'whack-a-mole-hard':
|
|
return this.calculateWhackAMoleCompat(capabilities, gameType === 'whack-a-mole-hard');
|
|
|
|
case 'memory-match':
|
|
return this.calculateMemoryMatchCompat(capabilities);
|
|
|
|
case 'quiz-game':
|
|
return this.calculateQuizGameCompat(capabilities);
|
|
|
|
case 'fill-the-blank':
|
|
return this.calculateFillBlankCompat(capabilities);
|
|
|
|
case 'story-reader':
|
|
return this.calculateTextReaderCompat(capabilities);
|
|
|
|
case 'adventure-reader':
|
|
return this.calculateAdventureCompat(capabilities);
|
|
|
|
case 'chinese-study':
|
|
return this.calculateChineseStudyCompat(capabilities);
|
|
|
|
case 'story-builder':
|
|
return this.calculateStoryBuilderCompat(capabilities);
|
|
|
|
case 'word-storm':
|
|
return this.calculateWordStormCompat(capabilities);
|
|
|
|
default:
|
|
return { compatible: true, score: 50, reason: 'Jeu non spécifiquement analysé' };
|
|
}
|
|
}
|
|
|
|
// === CALCULS DE COMPATIBILITÉ SPÉCIFIQUES PAR JEU ===
|
|
|
|
calculateWhackAMoleCompat(capabilities, isHard = false) {
|
|
let score = 0;
|
|
const reasons = [];
|
|
|
|
if (capabilities.hasVocabulary && capabilities.vocabularyCount >= 5) {
|
|
score += 40;
|
|
reasons.push(`${capabilities.vocabularyCount} mots de vocabulaire`);
|
|
} else if (capabilities.vocabularyCount > 0) {
|
|
score += 20;
|
|
reasons.push(`${capabilities.vocabularyCount} mots (minimum recommandé: 5)`);
|
|
}
|
|
|
|
if (capabilities.hasSentences && capabilities.sentenceCount >= 3) {
|
|
score += 30;
|
|
reasons.push(`${capabilities.sentenceCount} phrases`);
|
|
} else if (capabilities.sentenceCount > 0) {
|
|
score += 15;
|
|
reasons.push(`${capabilities.sentenceCount} phrases (minimum recommandé: 3)`);
|
|
}
|
|
|
|
if (capabilities.hasAudio) {
|
|
score += 20;
|
|
reasons.push('Fichiers audio disponibles');
|
|
}
|
|
|
|
const minScore = isHard ? 45 : 40;
|
|
const compatible = score >= minScore;
|
|
|
|
return {
|
|
compatible,
|
|
score,
|
|
reason: compatible ?
|
|
`Compatible: ${reasons.join(', ')}` :
|
|
`Incompatible (score: ${score}/${minScore}): Nécessite plus de vocabulaire ou phrases`
|
|
};
|
|
}
|
|
|
|
calculateMemoryMatchCompat(capabilities) {
|
|
let score = 0;
|
|
const reasons = [];
|
|
|
|
if (capabilities.hasVocabulary && capabilities.vocabularyCount >= 4) {
|
|
score += 50;
|
|
reasons.push(`${capabilities.vocabularyCount} paires de vocabulaire`);
|
|
} else {
|
|
return { compatible: false, score: 0, reason: 'Nécessite au moins 4 mots de vocabulaire' };
|
|
}
|
|
|
|
if (capabilities.hasAudio) {
|
|
score += 30;
|
|
reasons.push('Audio pour pronunciation');
|
|
}
|
|
|
|
return {
|
|
compatible: score >= 50,
|
|
score,
|
|
reason: `Compatible: ${reasons.join(', ')}`
|
|
};
|
|
}
|
|
|
|
calculateQuizGameCompat(capabilities) {
|
|
let score = 0;
|
|
const reasons = [];
|
|
|
|
// Quiz est très flexible
|
|
if (capabilities.hasVocabulary) {
|
|
score += 30;
|
|
reasons.push('Questions de vocabulaire');
|
|
}
|
|
if (capabilities.hasGrammar) {
|
|
score += 25;
|
|
reasons.push('Questions de grammaire');
|
|
}
|
|
if (capabilities.hasSentences) {
|
|
score += 20;
|
|
reasons.push('Questions sur les phrases');
|
|
}
|
|
if (capabilities.hasExercises) {
|
|
score += 45;
|
|
reasons.push('Exercices intégrés');
|
|
}
|
|
|
|
// Quiz fonctionne avec presque tout
|
|
if (score === 0 && (capabilities.vocabularyCount > 0 || capabilities.sentenceCount > 0)) {
|
|
score = 30;
|
|
reasons.push('Contenu de base disponible');
|
|
}
|
|
|
|
return {
|
|
compatible: score >= 30,
|
|
score,
|
|
reason: `Compatible: ${reasons.join(', ')}`
|
|
};
|
|
}
|
|
|
|
calculateFillBlankCompat(capabilities) {
|
|
let score = 0;
|
|
const reasons = [];
|
|
|
|
if (capabilities.hasFillInBlanks) {
|
|
score += 70;
|
|
reasons.push('Exercices à trous intégrés');
|
|
} else if (capabilities.hasSentences && capabilities.sentenceCount >= 3) {
|
|
score += 30;
|
|
reasons.push('Phrases pouvant être adaptées en exercices à trous');
|
|
} else {
|
|
return { compatible: false, score: 0, reason: 'Nécessite des phrases ou exercices à trous' };
|
|
}
|
|
|
|
return {
|
|
compatible: score >= 30,
|
|
score,
|
|
reason: `Compatible: ${reasons.join(', ')}`
|
|
};
|
|
}
|
|
|
|
calculateTextReaderCompat(capabilities) {
|
|
let score = 0;
|
|
const reasons = [];
|
|
|
|
if (capabilities.hasSentences && capabilities.sentenceCount >= 3) {
|
|
score += 40;
|
|
reasons.push(`${capabilities.sentenceCount} phrases à lire`);
|
|
}
|
|
if (capabilities.hasDialogues && capabilities.dialogueCount > 0) {
|
|
score += 50;
|
|
reasons.push(`${capabilities.dialogueCount} dialogues`);
|
|
}
|
|
if (capabilities.hasAudio) {
|
|
score += 10;
|
|
reasons.push('Audio disponible');
|
|
}
|
|
|
|
return {
|
|
compatible: score >= 40,
|
|
score,
|
|
reason: score >= 40 ? `Compatible: ${reasons.join(', ')}` : 'Nécessite des phrases ou dialogues à lire'
|
|
};
|
|
}
|
|
|
|
calculateAdventureCompat(capabilities) {
|
|
let score = 0;
|
|
const reasons = [];
|
|
|
|
if (capabilities.hasDialogues && capabilities.dialogueCount > 0) {
|
|
score += 60;
|
|
reasons.push('Dialogues pour narration');
|
|
}
|
|
if (capabilities.hasSentences && capabilities.sentenceCount >= 5) {
|
|
score += 30;
|
|
reasons.push('Contenu narratif suffisant');
|
|
}
|
|
if (capabilities.hasVocabulary && capabilities.vocabularyCount >= 10) {
|
|
score += 10;
|
|
reasons.push('Vocabulaire riche');
|
|
}
|
|
|
|
return {
|
|
compatible: score >= 50,
|
|
score,
|
|
reason: score >= 50 ? `Compatible: ${reasons.join(', ')}` : 'Nécessite plus de dialogues et contenu narratif'
|
|
};
|
|
}
|
|
|
|
calculateChineseStudyCompat(capabilities) {
|
|
let score = 0;
|
|
const reasons = [];
|
|
|
|
if (capabilities.hasVocabulary) {
|
|
score += 35;
|
|
reasons.push('Vocabulaire chinois');
|
|
}
|
|
if (capabilities.hasSentences) {
|
|
score += 25;
|
|
reasons.push('Phrases chinoises');
|
|
}
|
|
if (capabilities.hasAudio) {
|
|
score += 40;
|
|
reasons.push('Prononciation audio');
|
|
}
|
|
|
|
return {
|
|
compatible: score >= 35,
|
|
score,
|
|
reason: score >= 35 ? `Compatible: ${reasons.join(', ')}` : 'Optimisé pour contenu chinois'
|
|
};
|
|
}
|
|
|
|
calculateStoryBuilderCompat(capabilities) {
|
|
let score = 0;
|
|
const reasons = [];
|
|
|
|
if (capabilities.hasDialogues) {
|
|
score += 40;
|
|
reasons.push('Dialogues pour construction');
|
|
}
|
|
if (capabilities.hasSentences && capabilities.sentenceCount >= 5) {
|
|
score += 35;
|
|
reasons.push('Phrases pour séquences');
|
|
}
|
|
if (capabilities.hasVocabulary && capabilities.vocabularyCount >= 8) {
|
|
score += 25;
|
|
reasons.push('Vocabulaire varié');
|
|
}
|
|
|
|
return {
|
|
compatible: score >= 35,
|
|
score,
|
|
reason: score >= 35 ? `Compatible: ${reasons.join(', ')}` : 'Nécessite contenu pour construction narrative'
|
|
};
|
|
}
|
|
|
|
calculateWordStormCompat(capabilities) {
|
|
let score = 0;
|
|
const reasons = [];
|
|
|
|
// Word Storm nécessite principalement du vocabulaire
|
|
if (capabilities.hasVocabulary && capabilities.vocabularyCount >= 5) {
|
|
score += 60;
|
|
reasons.push(`${capabilities.vocabularyCount} mots de vocabulaire`);
|
|
} else if (capabilities.vocabularyCount > 0) {
|
|
score += 30;
|
|
reasons.push(`${capabilities.vocabularyCount} mots (peu mais suffisant)`);
|
|
}
|
|
|
|
// Bonus pour plus de vocabulaire
|
|
if (capabilities.vocabularyCount >= 20) {
|
|
score += 15;
|
|
reasons.push('Vocabulaire riche');
|
|
}
|
|
|
|
// Bonus si les mots ont des prononciations
|
|
if (capabilities.hasPronunciation) {
|
|
score += 10;
|
|
reasons.push('Prononciations disponibles');
|
|
}
|
|
|
|
// Word Storm peut fonctionner même avec peu de contenu
|
|
if (score === 0 && (capabilities.hasSentences || capabilities.hasDialogues)) {
|
|
score = 25;
|
|
reasons.push('Peut extraire vocabulaire des phrases/dialogues');
|
|
}
|
|
|
|
return {
|
|
compatible: score >= 15,
|
|
score,
|
|
reason: score >= 15 ? `Compatible: ${reasons.join(', ')}` : 'Nécessite au moins quelques mots de vocabulaire'
|
|
};
|
|
}
|
|
|
|
// === UTILITAIRES ===
|
|
|
|
hasContent(content, type) {
|
|
// Vérification standard
|
|
const data = content[type] || content.rawContent?.[type] || content.adaptedContent?.[type];
|
|
if (data) {
|
|
if (Array.isArray(data)) return data.length > 0;
|
|
if (typeof data === 'object') return Object.keys(data).length > 0;
|
|
return !!data;
|
|
}
|
|
|
|
// Support pour formats spéciaux
|
|
if (type === 'sentences' && content.story?.chapters) {
|
|
// Format story avec chapitres (comme Dragon's Pearl)
|
|
return content.story.chapters.some(chapter =>
|
|
chapter.sentences && chapter.sentences.length > 0
|
|
);
|
|
}
|
|
|
|
if (type === 'dialogues' && content.story?.chapters) {
|
|
// Vérifier s'il y a du contenu narratif riche dans les stories
|
|
return content.story.chapters.length > 1; // Multiple chapitres = contenu narratif
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
hasExercises(content) {
|
|
return this.hasContent(content, 'exercises') ||
|
|
this.hasContent(content, 'fillInBlanks') ||
|
|
this.hasContent(content, 'corrections') ||
|
|
this.hasContent(content, 'comprehension') ||
|
|
this.hasContent(content, 'matching');
|
|
}
|
|
|
|
countItems(content, type) {
|
|
// Vérification standard
|
|
const data = content[type] || content.rawContent?.[type] || content.adaptedContent?.[type];
|
|
if (data) {
|
|
if (Array.isArray(data)) return data.length;
|
|
if (typeof data === 'object') return Object.keys(data).length;
|
|
}
|
|
|
|
// Support pour formats spéciaux
|
|
if (type === 'sentences' && content.story?.chapters) {
|
|
// Compter toutes les phrases dans tous les chapitres
|
|
return content.story.chapters.reduce((total, chapter) =>
|
|
total + (chapter.sentences ? chapter.sentences.length : 0), 0
|
|
);
|
|
}
|
|
|
|
if (type === 'dialogues' && content.story?.chapters) {
|
|
// Considérer chaque chapitre comme un "dialogue" narratif
|
|
return content.story.chapters.length;
|
|
}
|
|
|
|
if (type === 'vocabulary') {
|
|
// Vérifier d'abord le format standard
|
|
const vocab = content.vocabulary || content.rawContent?.vocabulary || content.adaptedContent?.vocabulary;
|
|
if (vocab) {
|
|
if (Array.isArray(vocab)) return vocab.length;
|
|
if (typeof vocab === 'object') return Object.keys(vocab).length;
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
getGameRequirements(gameType) {
|
|
const requirements = {
|
|
'whack-a-mole': ['5+ mots de vocabulaire OU 3+ phrases', 'Contenu simple et répétitif'],
|
|
'whack-a-mole-hard': ['5+ mots de vocabulaire ET 3+ phrases', 'Contenu varié'],
|
|
'memory-match': ['4+ paires de vocabulaire', 'Idéalement avec images/audio'],
|
|
'quiz-game': ['Vocabulaire OU phrases OU exercices', 'Très flexible'],
|
|
'fill-the-blank': ['Phrases avec exercices à trous OU phrases simples', 'Contenu éducatif'],
|
|
'adventure-reader': ['Dialogues + contenu narratif riche', 'Histoire cohérente'],
|
|
'chinese-study': ['Vocabulaire et phrases chinoises', 'Audio recommandé'],
|
|
'story-builder': ['Dialogues OU 5+ phrases', 'Vocabulaire varié'],
|
|
'story-reader': ['Textes à lire, dialogues recommandés', 'Contenu narratif'],
|
|
'word-storm': ['3+ mots de vocabulaire', 'Prononciations recommandées']
|
|
};
|
|
|
|
return requirements[gameType] || ['Contenu de base'];
|
|
}
|
|
|
|
getDefaultReason(gameType) {
|
|
const reasons = {
|
|
'whack-a-mole': 'Jeu de rapidité nécessitant vocabulaire ou phrases',
|
|
'memory-match': 'Jeu de mémoire optimisé pour paires vocabulaire-traduction',
|
|
'quiz-game': 'Jeu polyvalent compatible avec la plupart des contenus',
|
|
'fill-the-blank': 'Exercices à trous nécessitant phrases structurées',
|
|
'adventure-reader': 'Aventure narrative nécessitant contenu riche',
|
|
'chinese-study': 'Optimisé pour apprentissage du chinois',
|
|
'story-builder': 'Construction narrative nécessitant éléments variés',
|
|
'story-reader': 'Lecture d\'histoires nécessitant contenu narratif'
|
|
};
|
|
|
|
return reasons[gameType] || 'Compatibilité non évaluée spécifiquement';
|
|
}
|
|
|
|
getDetailedAnalysis(compatResult, gameType) {
|
|
return {
|
|
minimumScore: this.minimumScores[gameType] || 30,
|
|
actualScore: compatResult.score,
|
|
recommendation: compatResult.score >= (this.minimumScores[gameType] || 30) ?
|
|
'Fortement recommandé' :
|
|
compatResult.score >= (this.minimumScores[gameType] || 30) * 0.7 ?
|
|
'Compatible avec limitations' :
|
|
'Non recommandé'
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Filtre une liste de contenus pour un jeu spécifique
|
|
* @param {Array} contentList - Liste des contenus
|
|
* @param {string} gameType - Type de jeu
|
|
* @returns {Array} - Contenus compatibles triés par score
|
|
*/
|
|
filterCompatibleContent(contentList, gameType) {
|
|
return contentList
|
|
.map(content => ({
|
|
...content,
|
|
compatibility: this.checkCompatibility(content, gameType)
|
|
}))
|
|
.filter(content => content.compatibility.compatible)
|
|
.sort((a, b) => b.compatibility.score - a.compatibility.score);
|
|
}
|
|
|
|
/**
|
|
* Obtient des suggestions d'amélioration pour rendre un contenu compatible
|
|
* @param {Object} content - Le contenu à analyser
|
|
* @param {string} gameType - Le type de jeu
|
|
* @returns {Array} - Liste de suggestions
|
|
*/
|
|
getImprovementSuggestions(content, gameType) {
|
|
const compatibility = this.checkCompatibility(content, gameType);
|
|
if (compatibility.compatible) return [];
|
|
|
|
const suggestions = [];
|
|
const capabilities = compatibility.capabilities || this.analyzeContentCapabilities(content);
|
|
|
|
switch (gameType) {
|
|
case 'whack-a-mole':
|
|
case 'whack-a-mole-hard':
|
|
if (capabilities.vocabularyCount < 5) {
|
|
suggestions.push(`Ajouter ${5 - capabilities.vocabularyCount} mots de vocabulaire supplémentaires`);
|
|
}
|
|
if (capabilities.sentenceCount < 3) {
|
|
suggestions.push(`Ajouter ${3 - capabilities.sentenceCount} phrases supplémentaires`);
|
|
}
|
|
break;
|
|
|
|
case 'memory-match':
|
|
if (capabilities.vocabularyCount < 4) {
|
|
suggestions.push(`Ajouter ${4 - capabilities.vocabularyCount} paires de vocabulaire supplémentaires`);
|
|
}
|
|
break;
|
|
|
|
// Autres cas...
|
|
}
|
|
|
|
if (suggestions.length === 0) {
|
|
suggestions.push('Enrichir le contenu général du module');
|
|
}
|
|
|
|
return suggestions;
|
|
}
|
|
|
|
/**
|
|
* Vide le cache de compatibilité
|
|
*/
|
|
clearCache() {
|
|
this.compatibilityCache.clear();
|
|
}
|
|
}
|
|
|
|
// Export global
|
|
window.ContentGameCompatibility = ContentGameCompatibility; |