Class_generator/js/games/chinese-study.js
StillHammer 24362165ab Implement dynamic percentage compatibility system across all games
Major architectural update to replace fixed 50%/100% scoring with true dynamic percentages based on content volume:

• Replace old interpolation system with Math.min(100, (count/optimal)*100) formula
• Add embedded compatibility methods to all 14 game modules with static requirements
• Remove compatibility cache system for real-time calculation
• Fix content loading to pass complete modules with vocabulary (not just metadata)
• Clean up duplicate syntax errors in adventure-reader and grammar-discovery
• Update navigation.js module mapping to match actual exported class names

Examples of new dynamic scoring:
- 15 words / 20 optimal = 75% (was 87.5% with old interpolation)
- 5 words / 10 minimum = 50% (was 25% with old linear system)
- 30 words / 20 optimal = 100% (unchanged)

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-20 17:00:52 +08:00

1742 lines
58 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// === CHINESE STUDY MODE ===
class ChineseStudyGame {
constructor(options) {
this.container = options.container;
this.content = options.content;
this.onScoreUpdate = options.onScoreUpdate || (() => {});
this.onGameEnd = options.onGameEnd || (() => {});
// Game state
this.vocabulary = [];
this.currentMode = null;
this.currentIndex = 0;
this.score = 0;
this.correctAnswers = 0;
this.isRunning = false;
this.studyState = 'menu'; // 'menu', 'playing', 'review'
// Extract vocabulary
this.vocabulary = this.extractVocabulary(this.content);
this.init();
}
init() {
// Check if we have enough vocabulary
if (!this.vocabulary || this.vocabulary.length === 0) {
logSh('No Chinese vocabulary found for Chinese Study Game', 'ERROR');
this.showInitError();
return;
}
this.createGameInterface();
}
showInitError() {
this.container.innerHTML = `
<div class="game-error">
<h3>❌ Error loading</h3>
<p>This content doesn't have Chinese vocabulary for the Chinese Study Game.</p>
<p>The game needs vocabulary with Chinese characters, translations, and optional pinyin.</p>
<button onclick="AppNavigation.navigateTo('games')" class="back-btn">← Back</button>
</div>
`;
this.addStyles();
}
extractVocabulary(content) {
let vocabulary = [];
logSh('🔍 Extracting Chinese vocabulary from:', content?.name || 'content', 'INFO');
// Priority 1: Use raw module content (simple format)
if (content.rawContent) {
logSh('📦 Using raw module content', 'INFO');
return this.extractVocabularyFromRaw(content.rawContent);
}
// Priority 2: Ultra-modular format (vocabulary object) - ONLY format supported
if (content.vocabulary && typeof content.vocabulary === 'object' && !Array.isArray(content.vocabulary)) {
logSh('✨ Ultra-modular format detected (vocabulary object)', 'INFO');
vocabulary = Object.entries(content.vocabulary).map(([word, data]) => {
// Support ultra-modular format ONLY
if (typeof data === 'object' && data.user_language) {
return {
chinese: word, // Clé = caractère chinois
translation: data.user_language.split('')[0], // First translation
fullTranslation: data.user_language, // Complete translation
pronunciation: data.pronunciation || '', // Pinyin
type: data.type || 'general',
hskLevel: data.hskLevel || null,
examples: data.examples || [],
strokeOrder: data.strokeOrder || []
};
}
// Legacy fallback - simple string (temporary, will be removed)
else if (typeof data === 'string') {
return {
chinese: word,
translation: data.split('')[0],
fullTranslation: data,
pronunciation: '',
type: 'general',
hskLevel: null
};
}
return null;
}).filter(Boolean);
}
// No other formats supported - ultra-modular only
return this.finalizeVocabulary(vocabulary);
}
extractVocabularyFromRaw(rawContent) {
logSh('🔧 Extracting from raw content:', rawContent.name || 'Module', 'INFO');
let vocabulary = [];
// Extract from vocabulary object in raw content
if (rawContent.vocabulary && typeof rawContent.vocabulary === 'object') {
vocabulary = Object.entries(rawContent.vocabulary).map(([word, data]) => {
if (typeof data === 'object' && data.user_language) {
return {
chinese: word,
translation: data.user_language.split('')[0],
fullTranslation: data.user_language,
pronunciation: data.pronunciation || '',
type: data.type || 'general',
hskLevel: data.hskLevel || null,
examples: data.examples || [],
strokeOrder: data.strokeOrder || []
};
} else if (typeof data === 'string') {
return {
chinese: word,
translation: data.split('')[0],
fullTranslation: data,
pronunciation: '',
type: 'general',
hskLevel: null
};
}
return null;
}).filter(Boolean);
}
return vocabulary;
}
finalizeVocabulary(vocabulary) {
if (vocabulary.length === 0) {
logSh('⚠️ No valid vocabulary found', 'WARNING');
return [];
}
// Shuffle vocabulary
vocabulary = vocabulary.sort(() => Math.random() - 0.5);
logSh(`✅ Vocabulary extraction complete: ${vocabulary.length} items`, 'INFO');
return vocabulary;
}
createGameInterface() {
if (this.studyState === 'menu') {
this.createModeSelection();
} else if (this.studyState === 'playing') {
this.createStudyMode();
}
this.addStyles();
}
createModeSelection() {
const hasPinyin = this.vocabulary.some(item => item.pronunciation);
const hasHSK = this.vocabulary.some(item => item.hskLevel);
this.container.innerHTML = `
<div class="chinese-study-container">
<div class="game-header">
<h2>🇨🇳 Chinese Study Mode</h2>
<div class="game-stats">
<div class="score-display">Score: <span id="score">${this.score}</span></div>
<div class="vocab-count">${this.vocabulary.length} characters available</div>
${hasHSK ? '<div class="hsk-indicator">📊 HSK levels included</div>' : ''}
${hasPinyin ? '<div class="pinyin-indicator">🗣️ Pinyin available</div>' : ''}
</div>
</div>
<div class="study-modes">
<div class="mode-card" data-mode="flashcards">
<div class="mode-icon">📚</div>
<h3>Flashcards</h3>
<p>Study characters with flip cards</p>
<button class="mode-btn">Start Learning</button>
</div>
<div class="mode-card" data-mode="recognition" ${!hasPinyin ? 'data-disabled="true"' : ''}>
<div class="mode-icon">🧠</div>
<h3>Character Recognition</h3>
<p>Match characters to their meanings</p>
${!hasPinyin ? '<div class="mode-requirement">Requires pinyin data</div>' : ''}
<button class="mode-btn" ${!hasPinyin ? 'disabled' : ''}>Start Learning</button>
</div>
<div class="mode-card" data-mode="pinyin" ${!hasPinyin ? 'data-disabled="true"' : ''}>
<div class="mode-icon">🗣️</div>
<h3>Pinyin Practice</h3>
<p>Learn pronunciation with pinyin</p>
${!hasPinyin ? '<div class="mode-requirement">Requires pinyin data</div>' : ''}
<button class="mode-btn" ${!hasPinyin ? 'disabled' : ''}>Start Learning</button>
</div>
<div class="mode-card" data-mode="hsk" ${!hasHSK ? 'data-disabled="true"' : ''}>
<div class="mode-icon">📊</div>
<h3>HSK Review</h3>
<p>Study by HSK difficulty levels</p>
${!hasHSK ? '<div class="mode-requirement">Requires HSK level data</div>' : ''}
<button class="mode-btn" ${!hasHSK ? 'disabled' : ''}>Start Learning</button>
</div>
</div>
<div class="vocabulary-preview">
<h4>📖 Vocabulary Preview</h4>
<div class="preview-items">
${this.vocabulary.slice(0, 6).map(item => `
<div class="preview-item">
<span class="chinese">${item.chinese}</span>
<span class="translation">${item.translation}</span>
${item.pronunciation ? `<span class="pinyin">${item.pronunciation}</span>` : ''}
${item.hskLevel ? `<span class="hsk-badge">${item.hskLevel}</span>` : ''}
</div>
`).join('')}
${this.vocabulary.length > 6 ? `<div class="more-items">... and ${this.vocabulary.length - 6} more</div>` : ''}
</div>
</div>
<div class="game-controls">
<button onclick="AppNavigation.navigateTo('games')" class="back-btn">← Back to Games</button>
</div>
</div>
`;
this.setupModeListeners();
}
createStudyMode() {
const currentItem = this.vocabulary[this.currentIndex];
const progress = Math.round(((this.currentIndex + 1) / this.vocabulary.length) * 100);
let modeContent = '';
switch (this.currentMode) {
case 'flashcards':
modeContent = this.createFlashcardMode(currentItem);
break;
case 'recognition':
modeContent = this.createRecognitionMode(currentItem);
break;
case 'pinyin':
modeContent = this.createPinyinMode(currentItem);
break;
case 'hsk':
modeContent = this.createHSKMode(currentItem);
break;
}
this.container.innerHTML = `
<div class="chinese-study-container study-mode-active">
<div class="study-header">
<div class="mode-title">
<h2>${this.getModeTitle()}</h2>
<button class="back-to-menu-btn" onclick="this.backToMenu()">← Menu</button>
</div>
<div class="progress-section">
<div class="progress-bar">
<div class="progress-fill" style="width: ${progress}%"></div>
</div>
<div class="progress-text">${this.currentIndex + 1} / ${this.vocabulary.length}</div>
<div class="score-display">Score: <span id="score">${this.score}</span></div>
</div>
</div>
<div class="study-content">
${modeContent}
</div>
<div class="study-controls">
<button class="prev-btn" onclick="this.previousItem()" ${this.currentIndex === 0 ? 'disabled' : ''}>
← Previous
</button>
<button class="next-btn" onclick="this.nextItem()" ${this.currentIndex === this.vocabulary.length - 1 ? 'disabled' : ''}>
Next →
</button>
</div>
</div>
`;
this.setupStudyListeners();
}
setupModeListeners() {
const modeCards = this.container.querySelectorAll('.mode-card:not([data-disabled])');
modeCards.forEach(card => {
card.addEventListener('click', (e) => {
const mode = card.dataset.mode;
this.startMode(mode);
});
});
}
setupStudyListeners() {
// Bind this context to methods for onclick handlers
window.chineseStudyInstance = this;
// Override global onclick handlers
this.container.querySelector('.back-to-menu-btn').onclick = () => this.backToMenu();
this.container.querySelector('.prev-btn').onclick = () => this.previousItem();
this.container.querySelector('.next-btn').onclick = () => this.nextItem();
// Setup mode-specific listeners
this.setupModeSpecificListeners();
}
setupModeSpecificListeners() {
if (this.currentMode === 'flashcards') {
const flashcard = this.container.querySelector('.flashcard');
if (flashcard) {
flashcard.addEventListener('click', () => this.flipCard());
}
} else if (this.currentMode === 'recognition') {
const options = this.container.querySelectorAll('.option-btn');
options.forEach(option => {
option.addEventListener('click', (e) => this.selectOption(e.target.dataset.translation));
});
} else if (this.currentMode === 'pinyin') {
const pinyinInput = this.container.querySelector('.pinyin-input');
if (pinyinInput) {
pinyinInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter') this.checkPinyinAnswer();
});
}
const checkBtn = this.container.querySelector('.check-pinyin-btn');
if (checkBtn) {
checkBtn.onclick = () => this.checkPinyinAnswer();
}
}
}
startMode(mode) {
this.currentMode = mode;
this.studyState = 'playing';
this.currentIndex = 0;
this.correctAnswers = 0;
this.createGameInterface();
}
backToMenu() {
this.studyState = 'menu';
this.currentMode = null;
this.createGameInterface();
}
getModeTitle() {
const titles = {
flashcards: '📚 Flashcards',
recognition: '🧠 Character Recognition',
pinyin: '🗣️ Pinyin Practice',
hsk: '📊 HSK Review'
};
return titles[this.currentMode] || 'Chinese Study';
}
createFlashcardMode(item) {
return `
<div class="flashcard-container">
<div class="flashcard" data-flipped="false">
<div class="flashcard-front">
<div class="chinese-character">${item.chinese}</div>
<div class="card-hint">Click to reveal translation</div>
</div>
<div class="flashcard-back">
<div class="translation">${item.translation}</div>
${item.pronunciation ? `<div class="pronunciation">${item.pronunciation}</div>` : ''}
${item.type ? `<div class="word-type">${item.type}</div>` : ''}
${item.hskLevel ? `<div class="hsk-level">${item.hskLevel}</div>` : ''}
</div>
</div>
<div class="flashcard-actions">
<button class="know-btn" onclick="chineseStudyInstance.markAsKnown(true)">✅ I know this</button>
<button class="dont-know-btn" onclick="chineseStudyInstance.markAsKnown(false)">❌ Need practice</button>
</div>
</div>
`;
}
createRecognitionMode(item) {
// Create wrong options
const wrongOptions = this.vocabulary
.filter(v => v.chinese !== item.chinese)
.sort(() => Math.random() - 0.5)
.slice(0, 3)
.map(v => v.translation);
const allOptions = [...wrongOptions, item.translation].sort(() => Math.random() - 0.5);
return `
<div class="recognition-container">
<div class="question-section">
<div class="chinese-character">${item.chinese}</div>
${item.pronunciation ? `<div class="pronunciation-hint">${item.pronunciation}</div>` : ''}
<div class="question-text">What does this character mean?</div>
</div>
<div class="options-grid">
${allOptions.map(option => `
<button class="option-btn" data-translation="${option}">
${option}
</button>
`).join('')}
</div>
<div class="result-feedback" style="display: none;"></div>
</div>
`;
}
createPinyinMode(item) {
return `
<div class="pinyin-container">
<div class="character-section">
<div class="chinese-character">${item.chinese}</div>
<div class="translation-hint">${item.translation}</div>
</div>
<div class="pinyin-exercise">
<label class="pinyin-label">Enter the pinyin pronunciation:</label>
<input type="text" class="pinyin-input" placeholder="Type pinyin here..." />
<button class="check-pinyin-btn">Check Answer</button>
</div>
<div class="pinyin-feedback" style="display: none;"></div>
${item.pronunciation ? `<div class="correct-answer" style="display: none;">Correct: ${item.pronunciation}</div>` : ''}
</div>
`;
}
createHSKMode(item) {
const hskInfo = item.hskLevel || 'No HSK level';
return `
<div class="hsk-container">
<div class="hsk-header">
<div class="hsk-level-badge">${hskInfo}</div>
<div class="character-difficulty">Chinese Character Study</div>
</div>
<div class="character-study">
<div class="chinese-character">${item.chinese}</div>
<div class="character-details">
<div class="translation">${item.translation}</div>
${item.pronunciation ? `<div class="pronunciation">${item.pronunciation}</div>` : ''}
${item.type ? `<div class="word-type">Type: ${item.type}</div>` : ''}
${item.examples && item.examples.length > 0 ? `
<div class="examples">
<h5>Examples:</h5>
${item.examples.slice(0, 2).map(ex => `<div class="example">${ex}</div>`).join('')}
</div>
` : ''}
</div>
</div>
<div class="hsk-actions">
<button class="difficulty-btn easy" onclick="chineseStudyInstance.markDifficulty('easy')">😊 Easy</button>
<button class="difficulty-btn medium" onclick="chineseStudyInstance.markDifficulty('medium')">🤔 Medium</button>
<button class="difficulty-btn hard" onclick="chineseStudyInstance.markDifficulty('hard')">😅 Hard</button>
</div>
</div>
`;
}
// Navigation methods
nextItem() {
if (this.currentIndex < this.vocabulary.length - 1) {
this.currentIndex++;
this.createStudyMode();
}
}
previousItem() {
if (this.currentIndex > 0) {
this.currentIndex--;
this.createStudyMode();
}
}
// Flashcard methods
flipCard() {
const flashcard = this.container.querySelector('.flashcard');
const isFlipped = flashcard.dataset.flipped === 'true';
flashcard.dataset.flipped = (!isFlipped).toString();
}
markAsKnown(known) {
const points = known ? 10 : 5;
this.score += points;
this.correctAnswers += known ? 1 : 0;
this.onScoreUpdate(this.score);
this.updateScoreDisplay();
// Auto-advance after a short delay
setTimeout(() => {
if (this.currentIndex < this.vocabulary.length - 1) {
this.nextItem();
} else {
this.endStudySession();
}
}, 1000);
}
// Recognition mode methods
selectOption(selectedTranslation) {
const currentItem = this.vocabulary[this.currentIndex];
const isCorrect = selectedTranslation === currentItem.translation;
const feedback = this.container.querySelector('.result-feedback');
if (isCorrect) {
this.score += 15;
this.correctAnswers++;
feedback.innerHTML = '✅ Correct! Well done!';
feedback.className = 'result-feedback correct';
} else {
this.score = Math.max(0, this.score - 5);
feedback.innerHTML = `❌ Incorrect. The correct answer is: ${currentItem.translation}`;
feedback.className = 'result-feedback incorrect';
}
feedback.style.display = 'block';
this.onScoreUpdate(this.score);
this.updateScoreDisplay();
// Disable all option buttons
const options = this.container.querySelectorAll('.option-btn');
options.forEach(btn => btn.disabled = true);
// Auto-advance after a delay
setTimeout(() => {
if (this.currentIndex < this.vocabulary.length - 1) {
this.nextItem();
} else {
this.endStudySession();
}
}, 2000);
}
// Pinyin mode methods
checkPinyinAnswer() {
const input = this.container.querySelector('.pinyin-input');
const userAnswer = input.value.trim().toLowerCase();
const currentItem = this.vocabulary[this.currentIndex];
const correctPinyin = currentItem.pronunciation ? currentItem.pronunciation.toLowerCase() : '';
const feedback = this.container.querySelector('.pinyin-feedback');
const correctAnswer = this.container.querySelector('.correct-answer');
if (correctPinyin && this.normalizePinyin(userAnswer) === this.normalizePinyin(correctPinyin)) {
this.score += 20;
this.correctAnswers++;
feedback.innerHTML = '🎉 Excellent pronunciation!';
feedback.className = 'pinyin-feedback correct';
} else {
this.score = Math.max(0, this.score - 3);
feedback.innerHTML = '🤔 Not quite right. Try again or see the correct answer below.';
feedback.className = 'pinyin-feedback incorrect';
if (correctAnswer) correctAnswer.style.display = 'block';
}
feedback.style.display = 'block';
input.disabled = true;
this.container.querySelector('.check-pinyin-btn').disabled = true;
this.onScoreUpdate(this.score);
this.updateScoreDisplay();
// Auto-advance after a delay
setTimeout(() => {
if (this.currentIndex < this.vocabulary.length - 1) {
this.nextItem();
} else {
this.endStudySession();
}
}, 3000);
}
normalizePinyin(pinyin) {
// Remove tone marks and accents for easier comparison
return pinyin.replace(/[āáǎàēéěèīíǐìōóǒòūúǔùüǖǘǚǜ]/g, (match) => {
const toneMap = {
'ā': 'a', 'á': 'a', 'ǎ': 'a', 'à': 'a',
'ē': 'e', 'é': 'e', 'ě': 'e', 'è': 'e',
'ī': 'i', 'í': 'i', 'ǐ': 'i', 'ì': 'i',
'ō': 'o', 'ó': 'o', 'ǒ': 'o', 'ò': 'o',
'ū': 'u', 'ú': 'u', 'ǔ': 'u', 'ù': 'u',
'ü': 'u', 'ǖ': 'u', 'ǘ': 'u', 'ǚ': 'u', 'ǜ': 'u'
};
return toneMap[match] || match;
}).replace(/\s+/g, '');
}
// HSK mode methods
markDifficulty(difficulty) {
const points = {
'easy': 5,
'medium': 8,
'hard': 12
};
this.score += points[difficulty];
this.correctAnswers++;
this.onScoreUpdate(this.score);
this.updateScoreDisplay();
// Visual feedback
const buttons = this.container.querySelectorAll('.difficulty-btn');
buttons.forEach(btn => btn.disabled = true);
const selectedBtn = this.container.querySelector(`.difficulty-btn.${difficulty}`);
selectedBtn.style.backgroundColor = '#10b981';
selectedBtn.style.color = 'white';
// Auto-advance after a delay
setTimeout(() => {
if (this.currentIndex < this.vocabulary.length - 1) {
this.nextItem();
} else {
this.endStudySession();
}
}, 1500);
}
updateScoreDisplay() {
const scoreElement = this.container.querySelector('#score');
if (scoreElement) {
scoreElement.textContent = this.score;
}
}
endStudySession() {
const accuracy = Math.round((this.correctAnswers / this.vocabulary.length) * 100);
this.container.innerHTML = `
<div class="chinese-study-container">
<div class="study-complete">
<h2>🎓 Study Session Complete!</h2>
<div class="final-stats">
<div class="stat-item">
<div class="stat-value">${this.score}</div>
<div class="stat-label">Final Score</div>
</div>
<div class="stat-item">
<div class="stat-value">${this.correctAnswers}/${this.vocabulary.length}</div>
<div class="stat-label">Correct</div>
</div>
<div class="stat-item">
<div class="stat-value">${accuracy}%</div>
<div class="stat-label">Accuracy</div>
</div>
</div>
<div class="completion-message">
${accuracy >= 80 ? '🌟 Excellent work! You\'ve mastered these characters!' :
accuracy >= 60 ? '👍 Good job! Keep practicing to improve further.' :
'💪 Nice effort! More practice will help you improve.'}
</div>
<div class="final-actions">
<button onclick="chineseStudyInstance.restart()" class="restart-btn">🔄 Study Again</button>
<button onclick="chineseStudyInstance.backToMenu()" class="menu-btn">📚 Back to Modes</button>
<button onclick="AppNavigation.navigateTo('games')" class="back-btn">← Back to Games</button>
</div>
</div>
</div>
`;
this.addStyles();
// Trigger game end callback
this.onGameEnd({
score: this.score,
accuracy: accuracy,
mode: this.currentMode,
totalItems: this.vocabulary.length,
correctAnswers: this.correctAnswers
});
}
addStyles() {
const style = document.createElement('style');
style.textContent = `
.chinese-study-container {
max-width: 1000px;
margin: 0 auto;
padding: 20px;
font-family: 'Arial', sans-serif;
}
.game-header {
text-align: center;
margin-bottom: 30px;
border-bottom: 2px solid #e5e7eb;
padding-bottom: 20px;
}
.game-header h2 {
color: #dc2626;
font-size: 2.2em;
margin-bottom: 15px;
}
.game-stats {
display: flex;
justify-content: center;
gap: 20px;
flex-wrap: wrap;
margin-top: 10px;
}
.score-display, .vocab-count, .hsk-indicator, .pinyin-indicator {
font-size: 1em;
padding: 5px 12px;
border-radius: 20px;
font-weight: bold;
}
.score-display {
background: #10b981;
color: white;
}
.vocab-count {
background: #3b82f6;
color: white;
}
.hsk-indicator, .pinyin-indicator {
background: #f59e0b;
color: white;
font-size: 0.9em;
}
.study-modes {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 20px;
margin-bottom: 30px;
}
.mode-card {
background: linear-gradient(135deg, #fff 0%, #f8fafc 100%);
border: 2px solid #e5e7eb;
border-radius: 16px;
padding: 24px;
text-align: center;
cursor: pointer;
transition: all 0.3s ease;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
position: relative;
}
.mode-card:not([data-disabled]):hover {
border-color: #dc2626;
transform: translateY(-4px);
box-shadow: 0 8px 20px rgba(220, 38, 38, 0.15);
}
.mode-card[data-disabled="true"] {
opacity: 0.6;
cursor: not-allowed;
background: #f9fafb;
}
.mode-requirement {
background: #fee2e2;
color: #dc2626;
padding: 4px 8px;
border-radius: 12px;
font-size: 0.8em;
margin-bottom: 10px;
}
.mode-icon {
font-size: 3em;
margin-bottom: 12px;
}
.mode-card h3 {
color: #374151;
margin-bottom: 8px;
font-size: 1.3em;
}
.mode-card p {
color: #6b7280;
margin-bottom: 16px;
line-height: 1.5;
}
.mode-btn {
background: #dc2626;
color: white;
border: none;
padding: 10px 20px;
border-radius: 8px;
cursor: pointer;
font-weight: bold;
transition: background 0.3s ease;
}
.mode-btn:hover:not(:disabled) {
background: #b91c1c;
}
.mode-btn:disabled {
background: #9ca3af;
cursor: not-allowed;
}
.vocabulary-preview {
background: #f8fafc;
border: 1px solid #e5e7eb;
border-radius: 12px;
padding: 20px;
margin-bottom: 30px;
}
.vocabulary-preview h4 {
color: #374151;
margin-bottom: 15px;
text-align: center;
}
.preview-items {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 10px;
}
.preview-item {
background: white;
border: 1px solid #e5e7eb;
border-radius: 8px;
padding: 10px;
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
gap: 4px;
}
.preview-item .chinese {
font-size: 1.4em;
font-weight: bold;
color: #dc2626;
}
.preview-item .translation {
color: #374151;
font-size: 0.9em;
}
.preview-item .pinyin {
color: #6b7280;
font-size: 0.8em;
font-style: italic;
}
.preview-item .hsk-badge {
background: #f59e0b;
color: white;
padding: 2px 6px;
border-radius: 10px;
font-size: 0.7em;
font-weight: bold;
}
.more-items {
grid-column: 1/-1;
text-align: center;
color: #6b7280;
font-style: italic;
padding: 10px;
}
/* Study Mode Styles */
.study-mode-active {
max-width: 800px;
}
.study-header {
background: linear-gradient(135deg, #dc2626 0%, #b91c1c 100%);
color: white;
border-radius: 12px;
padding: 20px;
margin-bottom: 30px;
}
.mode-title {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
}
.mode-title h2 {
margin: 0;
font-size: 1.8em;
}
.back-to-menu-btn {
background: rgba(255,255,255,0.2);
color: white;
border: 1px solid rgba(255,255,255,0.3);
padding: 8px 16px;
border-radius: 6px;
cursor: pointer;
transition: background 0.3s ease;
}
.back-to-menu-btn:hover {
background: rgba(255,255,255,0.3);
}
.progress-section {
display: flex;
align-items: center;
gap: 15px;
flex-wrap: wrap;
}
.progress-bar {
flex: 1;
min-width: 200px;
height: 8px;
background: rgba(255,255,255,0.3);
border-radius: 4px;
overflow: hidden;
}
.progress-fill {
height: 100%;
background: white;
transition: width 0.3s ease;
}
.progress-text {
font-weight: bold;
min-width: 60px;
}
.study-content {
background: white;
border: 1px solid #e5e7eb;
border-radius: 12px;
padding: 30px;
margin-bottom: 20px;
min-height: 300px;
display: flex;
align-items: center;
justify-content: center;
}
/* Flashcard Styles */
.flashcard-container {
width: 100%;
max-width: 400px;
text-align: center;
}
.flashcard {
width: 100%;
height: 250px;
position: relative;
transform-style: preserve-3d;
transition: transform 0.6s;
cursor: pointer;
margin-bottom: 20px;
}
.flashcard[data-flipped="true"] {
transform: rotateY(180deg);
}
.flashcard-front, .flashcard-back {
position: absolute;
width: 100%;
height: 100%;
backface-visibility: hidden;
border: 2px solid #e5e7eb;
border-radius: 12px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
padding: 20px;
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
}
.flashcard-back {
transform: rotateY(180deg);
background: #f8fafc;
}
.chinese-character {
font-size: 4em;
font-weight: bold;
color: #dc2626;
margin-bottom: 10px;
}
.card-hint {
color: #6b7280;
font-style: italic;
}
.translation {
font-size: 1.5em;
color: #374151;
margin-bottom: 10px;
}
.pronunciation {
color: #6b7280;
font-style: italic;
margin-bottom: 8px;
}
.word-type, .hsk-level {
background: #e5e7eb;
color: #374151;
padding: 4px 8px;
border-radius: 12px;
font-size: 0.8em;
margin-bottom: 4px;
}
.hsk-level {
background: #f59e0b;
color: white;
}
.flashcard-actions {
display: flex;
gap: 15px;
justify-content: center;
}
.know-btn, .dont-know-btn {
padding: 12px 24px;
border: none;
border-radius: 8px;
font-weight: bold;
cursor: pointer;
transition: all 0.3s ease;
}
.know-btn {
background: #10b981;
color: white;
}
.know-btn:hover {
background: #059669;
}
.dont-know-btn {
background: #f59e0b;
color: white;
}
.dont-know-btn:hover {
background: #d97706;
}
/* Recognition Mode Styles */
.recognition-container {
width: 100%;
text-align: center;
}
.question-section {
margin-bottom: 30px;
}
.pronunciation-hint {
color: #6b7280;
font-style: italic;
margin-bottom: 15px;
}
.question-text {
font-size: 1.2em;
color: #374151;
margin-bottom: 20px;
}
.options-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 15px;
margin-bottom: 20px;
}
.option-btn {
background: #f8fafc;
border: 2px solid #e5e7eb;
border-radius: 8px;
padding: 15px;
cursor: pointer;
font-size: 1em;
transition: all 0.3s ease;
}
.option-btn:hover:not(:disabled) {
border-color: #dc2626;
background: #fef2f2;
}
.option-btn:disabled {
cursor: not-allowed;
opacity: 0.6;
}
.result-feedback {
padding: 15px;
border-radius: 8px;
font-weight: bold;
margin-bottom: 15px;
}
.result-feedback.correct {
background: #d1fae5;
color: #065f46;
border: 1px solid #10b981;
}
.result-feedback.incorrect {
background: #fee2e2;
color: #991b1b;
border: 1px solid #ef4444;
}
/* Pinyin Mode Styles */
.pinyin-container {
width: 100%;
text-align: center;
}
.character-section {
margin-bottom: 30px;
}
.translation-hint {
font-size: 1.2em;
color: #6b7280;
margin-top: 10px;
}
.pinyin-exercise {
margin-bottom: 20px;
}
.pinyin-label {
display: block;
font-weight: bold;
color: #374151;
margin-bottom: 10px;
}
.pinyin-input {
padding: 12px;
border: 2px solid #e5e7eb;
border-radius: 8px;
font-size: 1.1em;
width: 250px;
max-width: 100%;
margin-bottom: 15px;
margin-right: 10px;
}
.pinyin-input:focus {
outline: none;
border-color: #dc2626;
}
.check-pinyin-btn {
background: #dc2626;
color: white;
border: none;
padding: 12px 20px;
border-radius: 8px;
cursor: pointer;
font-weight: bold;
}
.check-pinyin-btn:hover:not(:disabled) {
background: #b91c1c;
}
.check-pinyin-btn:disabled {
background: #9ca3af;
cursor: not-allowed;
}
.pinyin-feedback {
padding: 15px;
border-radius: 8px;
font-weight: bold;
margin-bottom: 15px;
}
.pinyin-feedback.correct {
background: #d1fae5;
color: #065f46;
border: 1px solid #10b981;
}
.pinyin-feedback.incorrect {
background: #fee2e2;
color: #991b1b;
border: 1px solid #ef4444;
}
.correct-answer {
background: #f0f9ff;
color: #0c4a6e;
border: 1px solid #3b82f6;
padding: 10px;
border-radius: 8px;
font-weight: bold;
}
/* HSK Mode Styles */
.hsk-container {
width: 100%;
text-align: center;
}
.hsk-header {
margin-bottom: 30px;
}
.hsk-level-badge {
background: #f59e0b;
color: white;
padding: 8px 16px;
border-radius: 20px;
font-weight: bold;
display: inline-block;
margin-bottom: 10px;
}
.character-difficulty {
color: #6b7280;
font-size: 1em;
}
.character-study {
margin-bottom: 30px;
}
.character-details {
background: #f8fafc;
border-radius: 8px;
padding: 20px;
margin-top: 15px;
}
.character-details .translation {
font-size: 1.3em;
margin-bottom: 10px;
}
.character-details .word-type {
background: #e5e7eb;
color: #374151;
padding: 4px 12px;
border-radius: 12px;
font-size: 0.9em;
display: inline-block;
margin-bottom: 15px;
}
.examples {
text-align: left;
margin-top: 15px;
}
.examples h5 {
color: #374151;
margin-bottom: 8px;
text-align: center;
}
.example {
background: white;
border: 1px solid #e5e7eb;
border-radius: 6px;
padding: 8px;
margin-bottom: 5px;
color: #6b7280;
}
.hsk-actions {
display: flex;
gap: 15px;
justify-content: center;
flex-wrap: wrap;
}
.difficulty-btn {
padding: 12px 20px;
border: 2px solid #e5e7eb;
border-radius: 8px;
background: white;
cursor: pointer;
font-weight: bold;
transition: all 0.3s ease;
min-width: 100px;
}
.difficulty-btn.easy {
border-color: #10b981;
color: #10b981;
}
.difficulty-btn.easy:hover:not(:disabled) {
background: #10b981;
color: white;
}
.difficulty-btn.medium {
border-color: #f59e0b;
color: #f59e0b;
}
.difficulty-btn.medium:hover:not(:disabled) {
background: #f59e0b;
color: white;
}
.difficulty-btn.hard {
border-color: #ef4444;
color: #ef4444;
}
.difficulty-btn.hard:hover:not(:disabled) {
background: #ef4444;
color: white;
}
.difficulty-btn:disabled {
cursor: not-allowed;
opacity: 0.7;
}
/* Study Controls */
.study-controls {
display: flex;
justify-content: space-between;
gap: 15px;
}
.prev-btn, .next-btn {
background: #6b7280;
color: white;
border: none;
padding: 12px 24px;
border-radius: 8px;
cursor: pointer;
font-weight: bold;
transition: background 0.3s ease;
flex: 1;
max-width: 150px;
}
.prev-btn:hover:not(:disabled), .next-btn:hover:not(:disabled) {
background: #4b5563;
}
.prev-btn:disabled, .next-btn:disabled {
background: #9ca3af;
cursor: not-allowed;
}
/* Study Complete Styles */
.study-complete {
text-align: center;
padding: 40px 20px;
}
.study-complete h2 {
color: #dc2626;
font-size: 2.5em;
margin-bottom: 30px;
}
.final-stats {
display: flex;
justify-content: center;
gap: 30px;
margin-bottom: 30px;
flex-wrap: wrap;
}
.stat-item {
background: #f8fafc;
border: 1px solid #e5e7eb;
border-radius: 12px;
padding: 20px;
min-width: 120px;
}
.stat-value {
font-size: 2em;
font-weight: bold;
color: #dc2626;
margin-bottom: 5px;
}
.stat-label {
color: #6b7280;
font-size: 0.9em;
}
.completion-message {
background: #f0f9ff;
border: 1px solid #3b82f6;
border-radius: 12px;
padding: 20px;
color: #1e40af;
font-size: 1.1em;
font-weight: bold;
margin-bottom: 30px;
}
.final-actions {
display: flex;
gap: 15px;
justify-content: center;
flex-wrap: wrap;
}
.restart-btn, .menu-btn, .back-btn {
padding: 12px 24px;
border: none;
border-radius: 8px;
cursor: pointer;
font-weight: bold;
transition: background 0.3s ease;
font-size: 1em;
}
.restart-btn {
background: #10b981;
color: white;
}
.restart-btn:hover {
background: #059669;
}
.menu-btn {
background: #3b82f6;
color: white;
}
.menu-btn:hover {
background: #2563eb;
}
.back-btn {
background: #6b7280;
color: white;
}
.back-btn:hover {
background: #4b5563;
}
.game-controls {
text-align: center;
}
.game-error {
text-align: center;
padding: 40px 20px;
background: #fee2e2;
border: 1px solid #ef4444;
border-radius: 12px;
}
.game-error h3 {
color: #991b1b;
margin-bottom: 15px;
}
.game-error p {
color: #7f1d1d;
margin-bottom: 10px;
}
@media (max-width: 768px) {
.chinese-study-container {
padding: 15px;
}
.game-header h2 {
font-size: 1.8em;
}
.study-modes {
grid-template-columns: 1fr;
}
.preview-items {
grid-template-columns: 1fr;
}
.game-stats {
flex-direction: column;
align-items: center;
gap: 10px;
}
.chinese-character {
font-size: 3em;
}
.flashcard {
height: 200px;
}
.options-grid {
grid-template-columns: 1fr;
}
.final-stats {
flex-direction: column;
align-items: center;
gap: 15px;
}
.final-actions {
flex-direction: column;
align-items: center;
}
.hsk-actions {
flex-direction: column;
align-items: center;
}
.study-controls {
flex-direction: column;
}
.prev-btn, .next-btn {
max-width: none;
}
.mode-title {
flex-direction: column;
gap: 10px;
text-align: center;
}
.progress-section {
flex-direction: column;
gap: 10px;
}
}
`;
document.head.appendChild(style);
}
start() {
this.isRunning = true;
logSh('Chinese Study Mode initialized with ultra-modular format', 'INFO');
}
destroy() {
this.isRunning = false;
// Clean up global references
if (window.chineseStudyInstance === this) {
delete window.chineseStudyInstance;
}
logSh('Chinese Study Mode destroyed', 'INFO');
}
restart() {
this.score = 0;
this.correctAnswers = 0;
this.currentIndex = 0;
this.studyState = 'menu';
this.currentMode = null;
this.onScoreUpdate(this.score);
// Re-shuffle vocabulary
this.vocabulary = this.vocabulary.sort(() => Math.random() - 0.5);
this.createGameInterface();
logSh('Chinese Study Mode restarted', 'INFO');
}
// === COMPATIBILITY SYSTEM ===
static getCompatibilityRequirements() {
return {
minimum: {
vocabulary: 10
},
optimal: {
vocabulary: 20
},
name: "Chinese Study",
description: "Chinese character and vocabulary learning with multiple study modes"
};
}
static checkContentCompatibility(content) {
const requirements = ChineseStudyGame.getCompatibilityRequirements();
// Extract vocabulary using same method as instance
const vocabulary = ChineseStudyGame.extractVocabularyStatic(content);
const vocabCount = vocabulary.length;
// Dynamic percentage based on optimal volume (10 min → 20 optimal)
// 0 words = 0%, 10 words = 50%, 20 words = 100%
const score = Math.min(100, (vocabCount / requirements.optimal.vocabulary) * 100);
const recommendations = [];
if (vocabCount < requirements.optimal.vocabulary) {
recommendations.push(`Add ${requirements.optimal.vocabulary - vocabCount} more vocabulary words for optimal experience`);
}
// Count Chinese-specific features for bonus recommendations
const hasChineseCharacters = vocabulary.some(word => word.chinese || /[\u4e00-\u9fff]/.test(word.original));
const hasPronunciation = vocabulary.some(word => word.pronunciation || word.pinyin);
if (!hasChineseCharacters) {
recommendations.push("Add Chinese characters for authentic Chinese study experience");
}
if (!hasPronunciation) {
recommendations.push("Add pronunciation (pinyin) for pronunciation practice");
}
return {
score: Math.round(score),
details: {
vocabulary: {
found: vocabCount,
minimum: requirements.minimum.vocabulary,
optimal: requirements.optimal.vocabulary,
status: vocabCount >= requirements.minimum.vocabulary ? 'sufficient' : 'insufficient'
},
chineseFeatures: {
characters: hasChineseCharacters ? 'available' : 'missing',
pronunciation: hasPronunciation ? 'available' : 'missing'
}
},
recommendations: recommendations
};
}
static extractVocabularyStatic(content) {
let vocabulary = [];
// Priority 1: Use raw module content (simple format)
if (content.rawContent) {
return ChineseStudyGame.extractVocabularyFromRawStatic(content.rawContent);
}
// Priority 2: Ultra-modular format (vocabulary object) - ONLY format supported
if (content.vocabulary && typeof content.vocabulary === 'object' && !Array.isArray(content.vocabulary)) {
vocabulary = Object.entries(content.vocabulary).map(([word, data]) => {
// Support ultra-modular format ONLY
if (typeof data === 'object' && data.user_language) {
return {
original: word,
chinese: word, // Use original as Chinese if it contains Chinese characters
translation: data.user_language.split('')[0],
fullTranslation: data.user_language,
type: data.type || 'general',
audio: data.audio,
image: data.image,
examples: data.examples,
pronunciation: data.pronunciation,
pinyin: data.pronunciation, // Use pronunciation as pinyin
category: data.type || 'general'
};
}
// Legacy fallback - simple string (temporary, will be removed)
else if (typeof data === 'string') {
return {
original: word,
chinese: word,
translation: data.split('')[0],
fullTranslation: data,
type: 'general',
category: 'general'
};
}
return null;
}).filter(Boolean);
}
return ChineseStudyGame.finalizeVocabularyStatic(vocabulary);
}
static extractVocabularyFromRawStatic(rawContent) {
let vocabulary = [];
// Ultra-modular format (vocabulary object) - ONLY format supported
if (rawContent.vocabulary && typeof rawContent.vocabulary === 'object' && !Array.isArray(rawContent.vocabulary)) {
vocabulary = Object.entries(rawContent.vocabulary).map(([word, data]) => {
// Support ultra-modular format ONLY
if (typeof data === 'object' && data.user_language) {
return {
original: word,
chinese: word,
translation: data.user_language.split('')[0],
fullTranslation: data.user_language,
type: data.type || 'general',
audio: data.audio,
image: data.image,
examples: data.examples,
pronunciation: data.pronunciation,
pinyin: data.pronunciation,
category: data.type || 'general'
};
}
// Legacy fallback - simple string (temporary, will be removed)
else if (typeof data === 'string') {
return {
original: word,
chinese: word,
translation: data.split('')[0],
fullTranslation: data,
type: 'general',
category: 'general'
};
}
return null;
}).filter(Boolean);
}
return ChineseStudyGame.finalizeVocabularyStatic(vocabulary);
}
static finalizeVocabularyStatic(vocabulary) {
// Validation and cleanup for ultra-modular format
vocabulary = vocabulary.filter(word =>
word &&
typeof word.original === 'string' &&
typeof word.translation === 'string' &&
word.original.trim() !== '' &&
word.translation.trim() !== ''
);
return vocabulary;
}
}
// Export to global scope
window.GameModules = window.GameModules || {};
window.GameModules.ChineseStudy = ChineseStudyGame;