import React, { useEffect, useMemo, useState } from "react"; import { motion, AnimatePresence } from "framer-motion"; import { Button } from "@/components/ui/button"; import { Card, CardContent } from "@/components/ui/card"; const STORAGE_KEY = "circusChineseMatchingWordBank"; const defaultWords = `山,shān 水,shuǐ 人,rén 月,yuè 火,huǒ 木,mù 口,kǒu 日,rì`; function normalizeComma(line) { return line.replace(/,/g, ","); } function parseWords(text) { return text .split("\n") .map((line) => normalizeComma(line).trim()) .filter(Boolean) .slice(0, 8) .map((line, index) => { const [hanzi, pinyin] = line.split(",").map((item) => item?.trim()); if (!hanzi || !pinyin) return null; return { id: index, hanzi, pinyin }; }) .filter(Boolean); } function shuffle(array) { const result = [...array]; for (let i = result.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [result[i], result[j]] = [result[j], result[i]]; } return result; } function makeDeck(words) { const cards = words.flatMap((word) => [ { uid: `${word.id}-hanzi`, pairId: word.id, type: "hanzi", text: word.hanzi, }, { uid: `${word.id}-pinyin`, pairId: word.id, type: "pinyin", text: word.pinyin, }, ]); return shuffle(cards); } export default function CircusChineseMatchingGame() { const [screen, setScreen] = useState("setup"); const [playerNames, setPlayerNames] = useState(["Player 1", "Player 2"]); const [wordText, setWordText] = useState(defaultWords); const [deck, setDeck] = useState([]); const [flipped, setFlipped] = useState([]); const [matched, setMatched] = useState([]); const [failed, setFailed] = useState([]); const [currentPlayer, setCurrentPlayer] = useState(0); const [scores, setScores] = useState([0, 0]); const [locked, setLocked] = useState(false); useEffect(() => { const saved = localStorage.getItem(STORAGE_KEY); if (saved) setWordText(saved); }, []); const words = useMemo(() => parseWords(wordText), [wordText]); const winnerIndex = scores[0] === scores[1] ? -1 : scores[0] > scores[1] ? 0 : 1; function saveWordBank() { localStorage.setItem(STORAGE_KEY, wordText); } function startGame() { if (words.length < 1) return; saveWordBank(); setDeck(makeDeck(words)); setFlipped([]); setMatched([]); setFailed([]); setCurrentPlayer(0); setScores([0, 0]); setLocked(false); setScreen("game"); } function resetToSetup() { setScreen("setup"); setDeck([]); setFlipped([]); setMatched([]); setFailed([]); setCurrentPlayer(0); setScores([0, 0]); setLocked(false); } function handleCardClick(card) { if (locked) return; if (matched.includes(card.uid)) return; if (flipped.some((item) => item.uid === card.uid)) return; if (flipped.length >= 2) return; const nextFlipped = [...flipped, card]; setFlipped(nextFlipped); if (nextFlipped.length === 2) { setLocked(true); const [first, second] = nextFlipped; const isMatch = first.pairId === second.pairId && first.type !== second.type; if (isMatch) { setTimeout(() => { setMatched((prev) => [...prev, first.uid, second.uid]); setScores((prev) => { const copy = [...prev]; copy[currentPlayer] += 1; return copy; }); setFlipped([]); setLocked(false); }, 600); } else { setFailed([first.uid, second.uid]); setTimeout(() => { setFailed([]); setFlipped([]); setCurrentPlayer((prev) => (prev === 0 ? 1 : 0)); setLocked(false); }, 900); } } } useEffect(() => { if (deck.length > 0 && matched.length === deck.length) { setTimeout(() => setScreen("result"), 800); } }, [matched, deck]); return (
Circus Chinese Match

🎪 汉字 & Pinyin Matching Game 🎪

{screen === "setup" && (

Game Rules

1. There are 16 face-down cards: purple Hanzi cards and green Pinyin cards.

2. Two players take turns flipping two cards each round.

3. If one Hanzi card matches its correct Pinyin card, the player gets 1 point and plays again.

4. If the two cards do not match, they turn red briefly, flip back, and the turn changes.

5. When all cards are matched, the player with the higher score wins.

Teacher Setup

setPlayerNames([e.target.value, playerNames[1]])} placeholder="Player 1 name" /> setPlayerNames([playerNames[0], e.target.value])} placeholder="Player 2 name" />
{words.length}/8 words