Chinese
Learn 50 common Chinese radicals by quick pinyin recall, vocabulary review, and character-building examples.
/* Chinese radical learning game */
const CHINESE_RADICAL_STORAGE_KEY = 'ppt-chinese-radical-selection-v1';
const CHINESE_NOTES_STORAGE_KEY = 'ppt-chinese-topic-notes-v1';
const CHINESE_HANDWRITING_FONT = '"Ma Shan Zheng", "Kaiti SC", STKaiti, KaiTi, "PingFang SC", cursive';
const CHINESE_RADICALS = [
{ id: 'ren', radical: '人', alt: '亻', pinyin: 'rén', tone: 'ren2', meaning: 'person', vi: 'người', hint: 'Liên quan đến người và hành động của con người.', examples: ['你', '他', '休'] },
{ id: 'kou', radical: '口', alt: '', pinyin: 'kǒu', tone: 'kou3', meaning: 'mouth', vi: 'miệng', hint: 'Liên quan đến nói, ăn, miệng hoặc âm thanh.', examples: ['吗', '叫', '吃'] },
{ id: 'nu', radical: '女', alt: '', pinyin: 'nǚ', tone: 'nu3', meaning: 'woman', vi: 'nữ', hint: 'Thường gặp trong chữ về phụ nữ, gia đình, quan hệ.', examples: ['好', '妈', '姐'] },
{ id: 'zi', radical: '子', alt: '', pinyin: 'zǐ', tone: 'zi3', meaning: 'child', vi: 'con', hint: 'Gợi ý trẻ em, con cái, người nhỏ tuổi.', examples: ['好', '字', '孩'] },
{ id: 'xin', radical: '心', alt: '忄', pinyin: 'xīn', tone: 'xin1', meaning: 'heart', vi: 'tâm', hint: 'Cảm xúc, suy nghĩ, trạng thái tinh thần.', examples: ['想', '快', '忙'] },
{ id: 'shou', radical: '手', alt: '扌', pinyin: 'shǒu', tone: 'shou3', meaning: 'hand', vi: 'tay', hint: 'Hành động làm bằng tay.', examples: ['打', '找', '拿'] },
{ id: 'shui', radical: '水', alt: '氵', pinyin: 'shuǐ', tone: 'shui3', meaning: 'water', vi: 'nước', hint: 'Nước, chất lỏng, sông suối, trạng thái ẩm ướt.', examples: ['河', '海', '洗'] },
{ id: 'huo', radical: '火', alt: '灬', pinyin: 'huǒ', tone: 'huo3', meaning: 'fire', vi: 'lửa', hint: 'Lửa, nhiệt, nấu, cháy.', examples: ['热', '烧', '灯'] },
{ id: 'mu_tree', radical: '木', alt: '', pinyin: 'mù', tone: 'mu4', meaning: 'wood/tree', vi: 'mộc', hint: 'Cây, gỗ, vật liệu gỗ.', examples: ['林', '机', '桌'] },
{ id: 'ri', radical: '日', alt: '', pinyin: 'rì', tone: 'ri4', meaning: 'sun/day', vi: 'nhật', hint: 'Ngày, mặt trời, thời gian.', examples: ['明', '时', '晚'] },
{ id: 'yue', radical: '月', alt: '', pinyin: 'yuè', tone: 'yue4', meaning: 'moon/month', vi: 'nguyệt', hint: 'Trăng, tháng; trong nhiều chữ cũng gợi ý bộ phận cơ thể.', examples: ['明', '期', '腿'] },
{ id: 'mu_eye', radical: '目', alt: '', pinyin: 'mù', tone: 'mu4', meaning: 'eye', vi: 'mục', hint: 'Mắt, nhìn, quan sát.', examples: ['看', '眼', '睛'] },
{ id: 'yan', radical: '言', alt: '讠', pinyin: 'yán', tone: 'yan2', meaning: 'speech', vi: 'ngôn', hint: 'Nói, ngôn ngữ, lời hứa.', examples: ['说', '语', '请'] },
{ id: 'jin', radical: '金', alt: '钅', pinyin: 'jīn', tone: 'jin1', meaning: 'metal/gold', vi: 'kim', hint: 'Kim loại, tiền, vật dụng bằng kim loại.', examples: ['钱', '铁', '银'] },
{ id: 'tu', radical: '土', alt: '', pinyin: 'tǔ', tone: 'tu3', meaning: 'earth/soil', vi: 'thổ', hint: 'Đất, vùng đất, nền móng.', examples: ['地', '场', '城'] },
{ id: 'yu', radical: '玉', alt: '王', pinyin: 'yù', tone: 'yu4', meaning: 'jade', vi: 'ngọc', hint: 'Ngọc, đá quý, vật đẹp hoặc quý.', examples: ['球', '玩', '现'] },
{ id: 'shan', radical: '山', alt: '', pinyin: 'shān', tone: 'shan1', meaning: 'mountain', vi: 'sơn', hint: 'Núi, địa hình cao.', examples: ['岛', '峰', '岁'] },
{ id: 'tian', radical: '田', alt: '', pinyin: 'tián', tone: 'tian2', meaning: 'field', vi: 'điền', hint: 'Ruộng đồng, ô đất, canh tác.', examples: ['男', '界', '留'] },
{ id: 'mi_thread', radical: '糸', alt: '纟', pinyin: 'mì', tone: 'mi4', meaning: 'silk/thread', vi: 'mịch', hint: 'Sợi chỉ, dệt, kết nối.', examples: ['线', '给', '红'] },
{ id: 'cao', radical: '艹', alt: '', pinyin: 'cǎo', tone: 'cao3', meaning: 'grass', vi: 'thảo', hint: 'Cây cỏ, thực vật, hoa lá.', examples: ['花', '茶', '草'] },
{ id: 'chong', radical: '虫', alt: '', pinyin: 'chóng', tone: 'chong2', meaning: 'insect', vi: 'trùng', hint: 'Côn trùng, động vật nhỏ.', examples: ['蚊', '蛇', '蛋'] },
{ id: 'zu', radical: '足', alt: '', pinyin: 'zú', tone: 'zu2', meaning: 'foot', vi: 'túc', hint: 'Chân, đi lại, động tác bằng chân.', examples: ['跑', '跳', '跟'] },
{ id: 'che', radical: '车', alt: '', pinyin: 'chē', tone: 'che1', meaning: 'vehicle', vi: 'xa', hint: 'Xe, bánh xe, di chuyển.', examples: ['辆', '转', '轮'] },
{ id: 'ma', radical: '马', alt: '', pinyin: 'mǎ', tone: 'ma3', meaning: 'horse', vi: 'mã', hint: 'Ngựa; trong nhiều chữ dùng làm thành phần âm.', examples: ['妈', '吗', '码'] },
{ id: 'men', radical: '门', alt: '', pinyin: 'mén', tone: 'men2', meaning: 'door', vi: 'môn', hint: 'Cửa, cổng, không gian bên trong hoặc bên ngoài.', examples: ['问', '间', '闻'] },
{ id: 'shi_food', radical: '食', alt: '饣', pinyin: 'shí', tone: 'shi2', meaning: 'food', vi: 'thực', hint: 'Ăn uống, thực phẩm.', examples: ['饭', '饮', '饱'] },
{ id: 'yi', radical: '衣', alt: '衤', pinyin: 'yī', tone: 'yi1', meaning: 'clothing', vi: 'y', hint: 'Quần áo, vải, mặc.', examples: ['被', '裙', '补'] },
{ id: 'bei', radical: '贝', alt: '', pinyin: 'bèi', tone: 'bei4', meaning: 'shell/money', vi: 'bối', hint: 'Tiền, tài sản, mua bán.', examples: ['财', '货', '贵'] },
{ id: 'li', radical: '力', alt: '', pinyin: 'lì', tone: 'li4', meaning: 'power', vi: 'lực', hint: 'Sức mạnh, cố gắng, lao động.', examples: ['加', '动', '办'] },
{ id: 'dao', radical: '刀', alt: '刂', pinyin: 'dāo', tone: 'dao1', meaning: 'knife', vi: 'đao', hint: 'Dao, cắt, tách.', examples: ['到', '别', '分'] },
{ id: 'quan', radical: '犬', alt: '犭', pinyin: 'quǎn', tone: 'quan3', meaning: 'dog/animal', vi: 'khuyển', hint: 'Chó, thú vật, hành vi của động vật.', examples: ['狗', '猫', '狼'] },
{ id: 'niao', radical: '鸟', alt: '', pinyin: 'niǎo', tone: 'niao3', meaning: 'bird', vi: 'điểu', hint: 'Chim, loài bay.', examples: ['鸡', '鸭', '鹅'] },
{ id: 'yu_fish', radical: '鱼', alt: '', pinyin: 'yú', tone: 'yu2', meaning: 'fish', vi: 'ngư', hint: 'Cá, thủy sản.', examples: ['鲜', '鲤', '鲸'] },
{ id: 'shi_stone', radical: '石', alt: '', pinyin: 'shí', tone: 'shi2', meaning: 'stone', vi: 'thạch', hint: 'Đá, khoáng vật, vật cứng.', examples: ['码', '矿', '破'] },
{ id: 'mi_rice', radical: '米', alt: '', pinyin: 'mǐ', tone: 'mi3', meaning: 'rice', vi: 'mễ', hint: 'Gạo, hạt, lương thực.', examples: ['精', '粉', '粮'] },
{ id: 'zhu', radical: '竹', alt: '⺮', pinyin: 'zhú', tone: 'zhu2', meaning: 'bamboo', vi: 'trúc', hint: 'Tre, trúc, đồ làm từ tre.', examples: ['笑', '笔', '第'] },
{ id: 'er', radical: '耳', alt: '', pinyin: 'ěr', tone: 'er3', meaning: 'ear', vi: 'nhĩ', hint: 'Tai, nghe.', examples: ['听', '职', '聪'] },
{ id: 'mian', radical: '宀', alt: '', pinyin: 'mián', tone: 'mian2', meaning: 'roof', vi: 'miên', hint: 'Mái nhà, nơi ở, bên trong nhà.', examples: ['家', '字', '安'] },
{ id: 'chuo', radical: '辶', alt: '', pinyin: 'chuò', tone: 'chuo4', meaning: 'walk/go', vi: 'sước', hint: 'Di chuyển, đường đi, quá trình.', examples: ['这', '进', '远'] },
{ id: 'fu', radical: '阝', alt: '', pinyin: 'fù', tone: 'fu4', meaning: 'mound/city side', vi: 'phụ / ấp', hint: 'Bên trái thường liên quan đồi núi; bên phải thường liên quan thành ấp.', examples: ['院', '都', '阳'] },
{ id: 'guang', radical: '广', alt: '', pinyin: 'guǎng', tone: 'guang3', meaning: 'shelter', vi: 'nghiễm', hint: 'Mái che, công trình, không gian rộng.', examples: ['店', '座', '床'] },
{ id: 'ne', radical: '疒', alt: '', pinyin: 'nè', tone: 'ne4', meaning: 'sickness', vi: 'nạch', hint: 'Bệnh, đau, trạng thái cơ thể.', examples: ['病', '痛', '痒'] },
{ id: 'pu', radical: '攵', alt: '', pinyin: 'pū', tone: 'pu1', meaning: 'tap/strike', vi: 'phộc', hint: 'Đánh, tác động, hành động thay đổi.', examples: ['教', '放', '改'] },
{ id: 'qian', radical: '欠', alt: '', pinyin: 'qiàn', tone: 'qian4', meaning: 'lack/yawn', vi: 'khiếm', hint: 'Thiếu, nợ, hoặc liên quan đến hơi thở.', examples: ['欢', '歌', '欲'] },
{ id: 'ye', radical: '页', alt: '', pinyin: 'yè', tone: 'ye4', meaning: 'head/page', vi: 'hiệt', hint: 'Đầu, mặt, trang giấy trong chữ giản thể.', examples: ['题', '顾', '颜'] },
{ id: 'da', radical: '大', alt: '', pinyin: 'dà', tone: 'da4', meaning: 'big', vi: 'đại', hint: 'Lớn, to, mở rộng.', examples: ['天', '太', '奇'] },
{ id: 'xiao', radical: '小', alt: '⺌', pinyin: 'xiǎo', tone: 'xiao3', meaning: 'small', vi: 'tiểu', hint: 'Nhỏ, ít, nhỏ bé.', examples: ['少', '尖', '尘'] },
{ id: 'shi_show', radical: '示', alt: '礻', pinyin: 'shì', tone: 'shi4', meaning: 'show/altar', vi: 'thị', hint: 'Lễ nghi, tinh thần, chúc phúc.', examples: ['神', '礼', '祝'] },
{ id: 'he', radical: '禾', alt: '', pinyin: 'hé', tone: 'he2', meaning: 'grain', vi: 'hòa', hint: 'Lúa, cây lương thực, nông nghiệp.', examples: ['种', '秋', '秒'] },
{ id: 'yu_rain', radical: '雨', alt: '', pinyin: 'yǔ', tone: 'yu3', meaning: 'rain', vi: 'vũ', hint: 'Mưa, thời tiết, hiện tượng trời.', examples: ['雪', '雷', '露'] },
];
const CHINESE_TOPIC_GROUPS = [
{
id: 'people-body',
label: 'People & Body',
vi: 'Người và cơ thể',
ids: ['ren', 'nu', 'zi', 'xin', 'shou', 'mu_eye', 'er', 'da', 'xiao'],
},
{
id: 'speech-learning',
label: 'Speech & Learning',
vi: 'Ngôn ngữ và học tập',
ids: ['kou', 'yan', 'shi_show', 'pu'],
},
{
id: 'nature-time',
label: 'Nature & Time',
vi: 'Tự nhiên và thời gian',
ids: ['shui', 'huo', 'mu_tree', 'ri', 'yue', 'tu', 'shan', 'tian', 'cao', 'yu_rain', 'he'],
},
{
id: 'animals',
label: 'Animals',
vi: 'Động vật',
ids: ['chong', 'ma', 'quan', 'niao', 'yu_fish'],
},
{
id: 'home-place',
label: 'Home & Places',
vi: 'Nhà cửa và nơi chốn',
ids: ['men', 'mian', 'guang', 'fu', 'chuo', 'che'],
},
{
id: 'food-materials',
label: 'Food & Materials',
vi: 'Đồ ăn và vật liệu',
ids: ['shi_food', 'yi', 'bei', 'jin', 'yu', 'mi_thread', 'shi_stone', 'mi_rice', 'zhu'],
},
{
id: 'actions-state',
label: 'Actions & State',
vi: 'Hành động và trạng thái',
ids: ['zu', 'li', 'dao', 'ne', 'qian', 'ye'],
},
];
const CHINESE_COMPOSITIONS = [
{ topic: 'People & Family', parts: ['女', '子'], result: '好', pinyin: 'hǎo', meaning: 'good', note: 'Ghép nữ + con, học để nhớ chữ "hǎo".' },
{ topic: 'Nature', parts: ['日', '月'], result: '明', pinyin: 'míng', meaning: 'bright', note: 'Mặt trời + mặt trăng gợi ý sáng rõ.' },
{ topic: 'Actions', parts: ['亻', '木'], result: '休', pinyin: 'xiū', meaning: 'rest', note: 'Người dựa vào cây là nghỉ ngơi.' },
{ topic: 'Nature', parts: ['木', '木'], result: '林', pinyin: 'lín', meaning: 'woods', note: 'Hai cây thành rừng nhỏ.' },
{ topic: 'Nature', parts: ['氵', '青'], result: '清', pinyin: 'qīng', meaning: 'clear', note: 'Nước + xanh/trong gợi ý trong sạch.' },
{ topic: 'Speech', parts: ['口', '马'], result: '吗', pinyin: 'ma', meaning: 'question particle', note: 'Khẩu là nói; mã làm thành phần âm.' },
{ topic: 'Speech', parts: ['讠', '青'], result: '请', pinyin: 'qǐng', meaning: 'please/invite', note: 'Ngôn ngữ + thành phần âm qīng.' },
{ topic: 'Actions', parts: ['扌', '丁'], result: '打', pinyin: 'dǎ', meaning: 'hit/call', note: 'Tay + thành phần âm, thường là hành động bằng tay.' },
{ topic: 'Nature', parts: ['艹', '化'], result: '花', pinyin: 'huā', meaning: 'flower', note: 'Cỏ/cây + hóa, liên quan thực vật.' },
{ topic: 'Home', parts: ['宀', '女'], result: '安', pinyin: 'ān', meaning: 'peace', note: 'Dưới mái nhà gợi ý yên ổn.' },
{ topic: 'Money', parts: ['贝', '才'], result: '财', pinyin: 'cái', meaning: 'wealth', note: 'Bối gợi ý tiền/tài sản.' },
{ topic: 'Health', parts: ['疒', '丙'], result: '病', pinyin: 'bìng', meaning: 'illness', note: 'Nạch là bộ bệnh, gặp trong nhiều chữ về sức khỏe.' },
];
const CHINESE_SENTENCES = [
{ topic: 'Greeting', zh: '你好,我叫安。', en: 'Hello, my name is An.' },
{ topic: 'Home & Family', zh: '他在家休息。', en: 'He rests at home.' },
{ topic: 'Food & Drink', zh: '妈妈喝水。', en: 'Mom drinks water.' },
{ topic: 'Food & Drink', zh: '我喜欢喝茶。', en: 'I like drinking tea.' },
{ topic: 'Time & Weather', zh: '今天太阳很大。', en: 'The sun is bright today.' },
{ topic: 'School', zh: '明天我们去学校。', en: 'Tomorrow we go to school.' },
{ topic: 'Travel', zh: '请问,车站在哪里?', en: 'Excuse me, where is the station?' },
{ topic: 'School', zh: '她买了一个新书包。', en: 'She bought a new school bag.' },
{ topic: 'Nature', zh: '这条河很清。', en: 'This river is very clear.' },
{ topic: 'Home & Family', zh: '小猫在门口睡觉。', en: 'The kitten sleeps by the door.' },
{ topic: 'School', zh: '我用手写汉字。', en: 'I write Chinese characters by hand.' },
{ topic: 'Home & Family', zh: '桌子上有一本书。', en: 'There is a book on the table.' },
{ topic: 'Nature', zh: '秋天的山很漂亮。', en: 'The mountains in autumn are beautiful.' },
{ topic: 'Actions', zh: '他跑得很快。', en: 'He runs very fast.' },
{ topic: 'Food & Drink', zh: '我们一起吃饭。', en: 'We eat together.' },
{ topic: 'Health', zh: '医生说他没事。', en: 'The doctor says he is fine.' },
{ topic: 'Nature', zh: '她喜欢红色的花。', en: 'She likes red flowers.' },
{ topic: 'Time & Weather', zh: '雨下得很大。', en: 'It is raining heavily.' },
];
const CHINESE_DEFAULT_IDS = CHINESE_RADICALS.slice(0, 20).map(item => item.id);
const CHINESE_TOPIC_BY_RADICAL_ID = CHINESE_TOPIC_GROUPS.reduce((lookup, topic) => {
topic.ids.forEach(id => {
lookup[id] = topic;
});
return lookup;
}, {});
const CHINESE_NOTE_TOPICS = [
{ id: 'general', label: 'General', vi: 'Ghi chú chung', ids: [] },
...CHINESE_TOPIC_GROUPS,
{ id: 'sentences', label: 'Sentences', vi: 'Câu mẫu', ids: [] },
];
function getMarkedPinyin(text, fallback = '') {
const pinyin = window.pinyinPro && window.pinyinPro.pinyin;
if (!pinyin || !text) return fallback;
try {
const result = pinyin(text, {
toneType: 'symbol',
type: 'array',
nonZh: 'removed',
});
return Array.isArray(result) && result.length ? result.join(' ') : fallback;
} catch (err) {
return fallback;
}
}
function radicalPinyin(item) {
return getMarkedPinyin(item.radical, item.pinyin) || item.pinyin;
}
function radicalTopic(item) {
return CHINESE_TOPIC_BY_RADICAL_ID[item.id] || { id: 'other', label: 'Other', vi: 'Khác', ids: [] };
}
function groupRadicalsByTopic(radicals) {
return CHINESE_TOPIC_GROUPS.map(topic => ({
topic,
items: radicals.filter(item => topic.ids.includes(item.id)),
})).filter(group => group.items.length);
}
function groupItemsByTopic(items) {
return items.reduce((groups, item) => {
const key = item.topic || 'Other';
if (!groups[key]) groups[key] = [];
groups[key].push(item);
return groups;
}, {});
}
function sameSet(left, right) {
return left.length === right.length && left.every(item => right.includes(item));
}
function speakChinese(text) {
if (!text || !window.speechSynthesis || !window.SpeechSynthesisUtterance) return false;
const utterance = new SpeechSynthesisUtterance(text);
const voices = window.speechSynthesis.getVoices ? window.speechSynthesis.getVoices() : [];
const preferredVoice = voices.find(voice => voice.lang && voice.lang.toLowerCase().startsWith('zh-cn'))
|| voices.find(voice => voice.lang && voice.lang.toLowerCase().startsWith('zh'));
utterance.lang = preferredVoice ? preferredVoice.lang : 'zh-CN';
utterance.rate = 0.82;
utterance.pitch = 1;
if (preferredVoice) utterance.voice = preferredVoice;
window.speechSynthesis.cancel();
window.speechSynthesis.speak(utterance);
return true;
}
function normalizeSearchText(value) {
return String(value || '')
.normalize('NFD')
.replace(/[\u0300-\u036f]/g, '')
.toLowerCase();
}
function sampleArray(items, count) {
const pool = [...items];
for (let i = pool.length - 1; i > 0; i -= 1) {
const j = Math.floor(Math.random() * (i + 1));
[pool[i], pool[j]] = [pool[j], pool[i]];
}
return pool.slice(0, count);
}
function buildChineseChoices(current) {
const used = new Set([current.pinyin]);
const distractors = sampleArray(
CHINESE_RADICALS.filter(item => item.pinyin !== current.pinyin),
CHINESE_RADICALS.length
).filter(item => {
if (used.has(item.pinyin)) return false;
used.add(item.pinyin);
return true;
}).slice(0, 3);
return sampleArray([current, ...distractors], 4);
}
function ChineseLearningPage({ tweaks, onBackHome }) {
const primary = tweaks.primaryColor;
const [activeTab, setActiveTab] = React.useState('game');
const [selectedIds, setSelectedIds] = React.useState(() => {
try {
const saved = JSON.parse(localStorage.getItem(CHINESE_RADICAL_STORAGE_KEY) || 'null');
return Array.isArray(saved) && saved.length ? saved : CHINESE_DEFAULT_IDS;
} catch (err) {
return CHINESE_DEFAULT_IDS;
}
});
const [duration, setDuration] = React.useState(60);
const [timeLeft, setTimeLeft] = React.useState(60);
const [running, setRunning] = React.useState(false);
const [current, setCurrent] = React.useState(null);
const [choices, setChoices] = React.useState([]);
const [score, setScore] = React.useState({ correct: 0, total: 0, streak: 0 });
const [feedback, setFeedback] = React.useState(null);
const [query, setQuery] = React.useState('');
const [sentenceIndex, setSentenceIndex] = React.useState(0);
const [showSentenceEnglish, setShowSentenceEnglish] = React.useState(false);
const [activeNoteTopic, setActiveNoteTopic] = React.useState('general');
const [notes, setNotes] = React.useState(() => {
try {
const saved = JSON.parse(localStorage.getItem(CHINESE_NOTES_STORAGE_KEY) || '{}');
return saved && typeof saved === 'object' && !Array.isArray(saved) ? saved : {};
} catch (err) {
return {};
}
});
React.useEffect(() => {
try {
localStorage.setItem(CHINESE_RADICAL_STORAGE_KEY, JSON.stringify(selectedIds));
} catch (err) {}
}, [selectedIds]);
React.useEffect(() => {
try {
localStorage.setItem(CHINESE_NOTES_STORAGE_KEY, JSON.stringify(notes));
} catch (err) {}
}, [notes]);
React.useEffect(() => {
if (!running || duration === 0) return undefined;
const timer = window.setInterval(() => {
setTimeLeft(value => {
if (value <= 1) {
window.clearInterval(timer);
setRunning(false);
setFeedback({ type: 'done', text: 'Time is up. Review the words below, then play again.' });
return 0;
}
return value - 1;
});
}, 1000);
return () => window.clearInterval(timer);
}, [running, duration]);
const selectedRadicals = CHINESE_RADICALS.filter(item => selectedIds.includes(item.id));
const filteredRadicals = CHINESE_RADICALS.filter(item => {
const topic = radicalTopic(item);
const text = normalizeSearchText(`${item.radical} ${item.alt} ${item.pinyin} ${item.tone} ${item.meaning} ${item.vi} ${item.hint} ${topic.label} ${topic.vi} ${item.examples.join(' ')}`);
return text.includes(normalizeSearchText(query.trim()));
});
const nextQuestion = React.useCallback((pool = selectedRadicals) => {
if (!pool.length) {
setCurrent(null);
setChoices([]);
return;
}
const next = pool[Math.floor(Math.random() * pool.length)];
setCurrent(next);
setChoices(buildChineseChoices(next));
}, [selectedRadicals]);
const startGame = () => {
const pool = selectedRadicals.length ? selectedRadicals : CHINESE_RADICALS.slice(0, 10);
setScore({ correct: 0, total: 0, streak: 0 });
setFeedback(null);
setTimeLeft(duration || 0);
setRunning(true);
nextQuestion(pool);
};
const answer = (choice) => {
if (!running || !current) return;
const ok = choice.pinyin === current.pinyin;
setScore(prev => ({
correct: prev.correct + (ok ? 1 : 0),
total: prev.total + 1,
streak: ok ? prev.streak + 1 : 0,
}));
setFeedback({
type: ok ? 'correct' : 'wrong',
text: ok
? `${current.radical}${current.alt ? ` / ${current.alt}` : ''} = ${radicalPinyin(current)} (${current.tone})`
: `${current.radical}${current.alt ? ` / ${current.alt}` : ''} is ${radicalPinyin(current)} (${current.tone}), not ${radicalPinyin(choice)}.`,
});
window.setTimeout(() => nextQuestion(), 420);
};
const toggleRadical = (id) => {
setSelectedIds(currentIds => (
currentIds.includes(id)
? currentIds.filter(item => item !== id)
: [...currentIds, id]
));
};
const updateNote = (topicId, value) => {
setNotes(currentNotes => ({
...currentNotes,
[topicId]: value,
}));
};
const accuracy = score.total ? Math.round((score.correct / score.total) * 100) : 0;
return (
Learn 50 common Chinese radicals by quick pinyin recall, vocabulary review, and character-building examples.
Chinese
Look at the radical and choose its pinyin as fast as possible.
Select at least 4 radicals, choose a timer, then start.
Review the exact words used in the game, grouped by learning topic.
Radicals often give a meaning hint. The other part may give sound, meaning, or both.
{item.note}
Practice with Chinese sentences, pinyin, and English meaning.
Notes are saved locally in this browser for quick review.