diff --git "a/index.html" "b/index.html" --- "a/index.html" +++ "b/index.html" @@ -1,605 +1,1274 @@ -import React, { useState, useEffect, useRef } from 'react'; -import { RefreshCw, Check, HelpCircle, ArrowRight, Sparkles, Coins, X } from 'lucide-react'; - -// 定義題目結構 -interface Problem { - id: number; - category: string; - question: string; // LaTeX 格式 - answers: string[]; // 所有可接受的答案格式 - hint: string; -} - -// 題庫資料 (從 PDF 2-6 頁摘錄) -const PROBLEMS: Problem[] = [ - // Page 2: 提公因式 - { - id: 1, - category: "基礎提公因式 (Page 2)", - question: "3b^2 + 2b", - answers: ["b(3b+2)", "(3b+2)b", "b*(3b+2)"], - hint: "觀察兩項都有什麼共同的英文字母?" - }, - { - id: 2, - category: "基礎提公因式 (Page 2)", - question: "5x^2 + 2x", - answers: ["x(5x+2)", "(5x+2)x"], - hint: "提出公因式 x" - }, - { - id: 3, - category: "基礎提公因式 (Page 2)", - question: "6y^2 - 2y", - answers: ["2y(3y-1)", "(3y-1)2y"], - hint: "數字 6 和 2 的公因數是?變數 y 也可以提出來。" - }, - - // Page 3: 分組/括號提公因式 - { - id: 4, - category: "分組提公因式 (Page 3)", - question: "2x(x-2) + 5(x-2)", - answers: ["(x-2)(2x+5)", "(2x+5)(x-2)"], - hint: "把 (x-2) 當作一個整體提出來" - }, - { - id: 5, - category: "分組提公因式 (Page 3)", - question: "x(3x-7) + 4(3x-7)", - answers: ["(3x-7)(x+4)", "(x+4)(3x-7)"], - hint: "大家都有 (3x-7)" - }, - { - id: 6, - category: "分組提公因式 (Page 3)", - question: "3x(2x-5) + (2x-5)(x+8)", - answers: ["(2x-5)(4x+8)", "4(2x-5)(x+2)", "(4x+8)(2x-5)", "4(x+2)(2x-5)"], - hint: "提出 (2x-5) 後,剩下的要合併:3x + (x+8)" - }, - - // Page 4: 變號提公因式 - { - id: 7, - category: "變號提公因式 (Page 4)", - question: "x(2x-1) + (1-2x)", - answers: ["(2x-1)(x-1)", "(x-1)(2x-1)"], - hint: "注意 (1-2x) 可以寫成 -(2x-1)" - }, - { - id: 8, - category: "變號提公因式 (Page 4)", - question: "x(2x-3) - 3(3-2x)", - answers: ["(2x-3)(x+3)", "(x+3)(2x-3)"], - hint: "-(3-2x) 其實就是 +(2x-3)" - }, - - // Page 5: 平方差公式 a^2 - b^2 = (a+b)(a-b) - { - id: 9, - category: "平方差公式 (Page 5)", - question: "x^2 - 49", - answers: ["(x+7)(x-7)", "(x-7)(x+7)"], - hint: "49 是 7 的平方" - }, - { - id: 10, - category: "平方差公式 (Page 5)", - question: "25x^2 - 36", - answers: ["(5x+6)(5x-6)", "(5x-6)(5x+6)"], - hint: "25x^2 是 (5x) 的平方,36 是 6 的平方" - }, - { - id: 11, - category: "平方差公式 (Page 5)", - question: "49 - (y-1)^2", - answers: ["(8-y)(y+6)", "(y+6)(8-y)", "-(y-8)(y+6)", "(7-(y-1))(7+(y-1))"], // 包含未化簡與化簡後的答案 - hint: "A=7, B=(y-1),套用 (A+B)(A-B)" - }, - - // Page 6: 完全平方公式 - { - id: 12, - category: "完全平方公式 (Page 6)", - question: "x^2 + 8x + 16", - answers: ["(x+4)^2", "(x+4)(x+4)"], - hint: "頭平方,尾平方,2倍頭尾在中央。16 是 4 的平方。" - }, - { - id: 13, - category: "完全平方公式 (Page 6)", - question: "x^2 - 10x + 25", - answers: ["(x-5)^2", "(x-5)(x-5)"], - hint: "中間是負號,所以是差的平方" - }, - { - id: 14, - category: "完全平方公式 (Page 6)", - question: "16x^2 - 24x + 9", - answers: ["(4x-3)^2", "(4x-3)(4x-3)"], - hint: "16x^2 是 (4x)^2,9 是 3^2" - }, - { - id: 15, - category: "綜合挑戰", - question: "(x+1)(x-3) - (x-3)", - answers: ["x(x-3)", "(x-3)x"], - hint: "後面的 -(x-3) 可以看作 -1(x-3)" - } -]; - -// 工具:標準化輸入字串 (移除空格,統一格式) -const normalizeInput = (input: string): string => { - return input.replace(/\s/g, "") - .replace(/\^2/g, "^2") // 確保次方符號一致 - .replace(/(/g, "(") // 處理全形括號 - .replace(/)/g, ")"); -}; - -// 取得當前題目需要的變數 -const getProblemVariables = (question: string): string[] => { - const vars = new Set(); - if (question.includes('x')) vars.add('x'); - if (question.includes('y')) vars.add('y'); - if (question.includes('a')) vars.add('a'); - if (question.includes('b')) vars.add('b'); - // 如果題目完全沒有變數(純數字題),預設給 x - if (vars.size === 0) return ['x']; - return Array.from(vars).sort(); -}; - -// 特效組件:漂浮的數字/符號 -const FloatingNumber = ({ x, y, value }: { x: number, y: number, value: string }) => ( -
- {value} -
-); - -// 金幣噴發特效 -const CoinBurst = () => { - const coins = Array.from({ length: 25 }).map((_, i) => ({ - id: i, - tx: (Math.random() - 0.5) * 300, - ty: (Math.random() - 1.5) * 250, - rot: Math.random() * 720, - scale: 0.5 + Math.random() * 0.5, - delay: Math.random() * 0.2 - })); - - return ( -
- {coins.map((c) => ( -
- $ -
- ))} -
- ); -}; - -// SVG 寶箱組件 -const TreasureChest = ({ - isOpen, - shake, - children, - questionText -}: { - isOpen: boolean, - shake: boolean, - children: React.ReactNode, - questionText: React.ReactNode -}) => { - return ( -
- - {/* 寶箱本體 SVG */} - - - - - - - - - - - - - - - - - - - - - - - - - - - - {isOpen && ( - - - - - - )} - - -
-
- {questionText} -
-
- -
-
- {children} -
-
SECRET CODE
-
- - {isOpen && ( - <> -
- -
- - - )} -
- ); -}; - -export default function FactoringGame() { - const [currentLevel, setCurrentLevel] = useState(0); - const [inputAnswer, setInputAnswer] = useState(""); - const [gameStatus, setGameStatus] = useState<"playing" | "success" | "error" | "completed">("playing"); - const [shake, setShake] = useState(false); - const [showHint, setShowHint] = useState(false); - const [score, setScore] = useState(0); - const inputRef = useRef(null); - - // 嚴格模式下的音效處理 (Web Audio API) - const playSuccessSound = () => { - try { - const AudioContext = window.AudioContext || (window as any).webkitAudioContext; - if (!AudioContext) return; - - const ctx = new AudioContext(); - const oscillator = ctx.createOscillator(); - const gainNode = ctx.createGain(); - - oscillator.connect(gainNode); - gainNode.connect(ctx.destination); - - oscillator.type = 'sine'; - oscillator.frequency.setValueAtTime(523.25, ctx.currentTime); - oscillator.frequency.exponentialRampToValueAtTime(1046.5, ctx.currentTime + 0.1); - - gainNode.gain.setValueAtTime(0.3, ctx.currentTime); - gainNode.gain.exponentialRampToValueAtTime(0.01, ctx.currentTime + 0.5); - - oscillator.start(); - oscillator.stop(ctx.currentTime + 0.5); - } catch (e) { - console.error("Audio play failed", e); - } - }; - - // 處理背景動畫 - const [particles, setParticles] = useState<{x:number, y:number, val:string}[]>([]); - - useEffect(() => { - const symbols = ["x²", "y", "+", "-", "( )", "a", "b"]; - const interval = setInterval(() => { - setParticles(prev => { - const newP = [...prev, { - x: Math.random() * 100, - y: Math.random() * 100, - val: symbols[Math.floor(Math.random() * symbols.length)] - }]; - if (newP.length > 20) newP.shift(); - return newP; - }); - }, 2000); - return () => clearInterval(interval); - }, []); - - const currentProblem = PROBLEMS[currentLevel]; - - const handleCheckAnswer = () => { - if (!inputAnswer) return; - - const normalizedUser = normalizeInput(inputAnswer); - const normalizedAnswers = currentProblem.answers.map(normalizeInput); - - if (normalizedAnswers.includes(normalizedUser)) { - setGameStatus("success"); - playSuccessSound(); - - setTimeout(() => { - if (currentLevel < PROBLEMS.length - 1) { - setCurrentLevel(prev => prev + 1); - setInputAnswer(""); - setGameStatus("playing"); - setShowHint(false); - setScore(prev => prev + 100); - } else { - setGameStatus("completed"); - } - }, 2000); - } else { - setGameStatus("error"); - setShake(true); - setTimeout(() => setShake(false), 500); - if (navigator.vibrate) navigator.vibrate(200); - } - }; - - const handleKeyDown = (e: React.KeyboardEvent) => { - if (e.key === 'Enter') { - handleCheckAnswer(); - } - }; - - const insertSymbol = (symbol: string) => { - setInputAnswer(prev => prev + symbol); - inputRef.current?.focus(); - }; - - const renderMath = (text: string) => { - const parts = text.split(/(\^[0-9]+)/g); - return ( - - {parts.map((part, i) => { - if (part.startsWith("^")) { - return {part.substring(1)}; - } - return {part}; - })} - - ); - }; - - const getKeyboardButtons = () => { - const variableButtons = getProblemVariables(currentProblem.question); - const operatorButtons = ['+', '-', '^2', '(', ')']; - const numberButtons = ['1', '2', '3', '4', '5', '6', '7', '8', '9', '0']; + + + + + + 代數遺跡:公因式的寶藏 - return { - topRow: Array.from(new Set([...variableButtons, ...operatorButtons])), - numberRow: numberButtons - }; - }; - - const keyboard = getKeyboardButtons(); - - if (gameStatus === "completed") { - return ( -
-
- {particles.map((p, i) => )} -
-
- -

大富翁誕生!

-

你已成功解開所有謎題,獲得了傳說中的代數寶藏!

-
- 總獎金: - ${score + 100} -
- -
-
- ); - } - - return ( -
- {/* 背景粒子 */} -
- {particles.map((p, i) => ( -
- {p.val} -
- ))} -
- - {/* 遊戲主介面 */} -
- - {/* 頂部資訊欄 */} -
-
- - Level {currentLevel + 1} - -
-
- - ${score} -
-
- - {/* 寶箱與題目區域 */} -
- - {/* 嵌入寶箱的輸入框 */} -
- { - setInputAnswer(e.target.value); - setGameStatus("playing"); - }} - onKeyDown={handleKeyDown} - placeholder="密碼" - className={`w-full bg-transparent text-center text-xl font-mono font-bold tracking-widest focus:outline-none transition-colors placeholder-amber-900/50 ${ - gameStatus === "error" ? "text-red-400" : "text-amber-400" - }`} - autoComplete="off" - disabled={gameStatus === "success"} - readOnly // 防止手機虛擬鍵盤彈出,強制使用遊戲內鍵盤 - /> - -
-
-
- - {/* 操作區域 (提示與鍵盤) */} -
- - {/* 提示按鈕與內容 */} -
- -
- - {showHint && ( -
- - 💡 提示:{currentProblem.hint} -
- )} - - {/* 狀態訊息 */} -
- {gameStatus === "error" && ( -

- 密碼錯誤! -

- )} -
- - {/* 動態虛擬鍵盤 - 優化觸控區 */} -
- {/* 第一層:變數與運算符 */} -
- {keyboard.topRow.map((symbol) => ( - - ))} - -
- - {/* 第二層:數字鍵盤 */} -
- {keyboard.numberRow.map((num) => ( - - ))} -
-
-
- -
- 代數遺跡:公因式的寶藏 -
-
+ + + + + + + + + - + + +
+ + + + + + + \ No newline at end of file