import React, { useState, useEffect, useCallback, useRef } from 'react'; import { ArrowUp, ArrowRight, ArrowDown, ArrowLeft, Eye, Power, AlertTriangle, Zap, Timer, RotateCcw, Check, X, Trophy, Volume2, Music } from 'lucide-react'; // --- Types & Constants --- type Direction = 'Up' | 'Right' | 'Down' | 'Left'; type ColorType = 'Black' | 'Blue' | 'Red' | 'Yellow'; interface GameConfig { useRed: boolean; useBlue: boolean; useYellow: boolean; } interface LandoltState { id: number; direction: Direction; color: ColorType; x: number; y: number; spawnTime: number; hitsRemaining: number; } interface Feedback { id: number; text: string; type: 'success' | 'error' | 'info'; x: number; y: number; } const GAME_DURATION = 30; // seconds const DIRECTIONS: Direction[] = ['Up', 'Right', 'Down', 'Left']; const RED_LIMIT_MS = 1500; // 赤色は1.5秒以内 // --- Audio Utility --- // 簡易的なシンセサイザー音を生成 const playSound = (type: 'switch' | 'start' | 'gameover' | 'back') => { try { const AudioContext = window.AudioContext || (window as any).webkitAudioContext; if (!AudioContext) return; const ctx = new AudioContext(); const osc = ctx.createOscillator(); const gain = ctx.createGain(); osc.connect(gain); gain.connect(ctx.destination); const now = ctx.currentTime; if (type === 'switch') { // カチッという短い音 osc.type = 'sine'; osc.frequency.setValueAtTime(800, now); osc.frequency.exponentialRampToValueAtTime(1200, now + 0.05); gain.gain.setValueAtTime(0.1, now); gain.gain.linearRampToValueAtTime(0, now + 0.05); osc.start(now); osc.stop(now + 0.05); } else if (type === 'start') { // ピコーン!という上昇音 osc.type = 'triangle'; osc.frequency.setValueAtTime(440, now); osc.frequency.linearRampToValueAtTime(880, now + 0.1); gain.gain.setValueAtTime(0.1, now); gain.gain.linearRampToValueAtTime(0, now + 0.3); osc.start(now); osc.stop(now + 0.3); } else if (type === 'gameover') { // ジャジャーン osc.type = 'sawtooth'; osc.frequency.setValueAtTime(300, now); osc.frequency.exponentialRampToValueAtTime(100, now + 0.8); gain.gain.setValueAtTime(0.1, now); gain.gain.linearRampToValueAtTime(0, now + 0.8); osc.start(now); osc.stop(now + 0.8); } else if (type === 'back') { // ポンッ osc.type = 'sine'; osc.frequency.setValueAtTime(400, now); gain.gain.setValueAtTime(0.1, now); gain.gain.exponentialRampToValueAtTime(0.01, now + 0.1); osc.start(now); osc.stop(now + 0.1); } // クリーンアップ setTimeout(() => { if(ctx.state !== 'closed') ctx.close(); }, 1000); } catch (e) { console.error('Audio play failed', e); } }; // --- Helper Functions --- const getRandomDirection = (): Direction => DIRECTIONS[Math.floor(Math.random() * DIRECTIONS.length)]; const getOppositeDirection = (dir: Direction): Direction => { const index = DIRECTIONS.indexOf(dir); return DIRECTIONS[(index + 2) % 4]; }; const getRandomPosition = () => ({ x: 20 + Math.random() * 60, // 横: 20-80% y: 35 + Math.random() * 30, // 縦: 中央エリアに見やすく配置 }); export default function App() { // Game State const [gameState, setGameState] = useState<'title' | 'playing' | 'gameover'>('title'); const [config, setConfig] = useState({ useRed: true, useBlue: true, useYellow: true }); const [score, setScore] = useState(0.1); const [level, setLevel] = useState(1); const [timeLeft, setTimeLeft] = useState(GAME_DURATION); const [landolt, setLandolt] = useState(null); const [feedbacks, setFeedbacks] = useState([]); const [levelAlert, setLevelAlert] = useState(null); const [highScore, setHighScore] = useState(() => { try { const saved = localStorage.getItem('ultraVisionKidsHighScoreV3'); return saved ? parseFloat(saved) : 0.0; } catch { return 0.0; } }); const timerRef = useRef(null); const alertTimerRef = useRef(null); const redTimeoutRef = useRef(null); // --- Level Logic Helper --- // 現在の設定に基づいて、有効な特殊色を難易度順にリストアップする // 難易度順: Red(簡単) -> Yellow(普通) -> Blue(難しい) と仮定してソート const getActiveSpecialColors = useCallback(() => { const list: ColorType[] = []; if (config.useRed) list.push('Red'); if (config.useYellow) list.push('Yellow'); if (config.useBlue) list.push('Blue'); return list; }, [config]); // --- Game Logic --- const handleRedTimeout = useCallback(() => { setLandolt(current => { if (current && current.color === 'Red') { addFeedback('おそい!', 'error', current.x, current.y); setScore(prev => Math.max(0.1, parseFloat((prev - 0.2).toFixed(1)))); return null; } return current; }); }, []); const spawnLandolt = useCallback(() => { let nextColor: ColorType = 'Black'; let pos = { x: 50, y: 50 }; if (level >= 2) pos = getRandomPosition(); // アクティブな特殊色リストを取得 const activeSpecials = getActiveSpecialColors(); const availableColors: ColorType[] = ['Black']; // レベルに応じて解禁する色を決定 (前から順番に詰める) if (level >= 3 && activeSpecials.length > 0) availableColors.push(activeSpecials[0]); if (level >= 4 && activeSpecials.length > 1) availableColors.push(activeSpecials[1]); if (level >= 5 && activeSpecials.length > 2) availableColors.push(activeSpecials[2]); // 色の抽選ロジック if (availableColors.length > 1) { // レベルが高いほど特殊色が出やすくなる確率を調整 // 修正: 確率を底上げし、後半は黒よりも色付きが多く出るようにする let colorChance = 0.5; // Lv3初期値 if (level >= 4) colorChance = 0.7; // Lv4で7割 if (level >= 5) colorChance = 0.85; // Lv5で8.5割 if (Math.random() < colorChance) { const specialColors = availableColors.filter(c => c !== 'Black'); nextColor = specialColors[Math.floor(Math.random() * specialColors.length)]; } } const newLandolt: LandoltState = { id: Date.now(), direction: getRandomDirection(), color: nextColor, x: pos.x, y: pos.y, spawnTime: Date.now(), hitsRemaining: nextColor === 'Yellow' ? 3 : 1, }; setLandolt(newLandolt); if (redTimeoutRef.current) clearTimeout(redTimeoutRef.current); if (nextColor === 'Red') { redTimeoutRef.current = window.setTimeout(handleRedTimeout, RED_LIMIT_MS); } }, [level, getActiveSpecialColors, handleRedTimeout]); useEffect(() => { if (gameState === 'playing' && !landolt) { spawnLandolt(); } }, [gameState, landolt, spawnLandolt]); const startGame = () => { playSound('start'); setGameState('playing'); setScore(0.1); setLevel(1); setTimeLeft(GAME_DURATION); setFeedbacks([]); setLevelAlert(null); spawnLandolt(); }; const handleTitleBack = () => { playSound('back'); setGameState('title'); }; const endGame = useCallback(() => { playSound('gameover'); setGameState('gameover'); if (score > highScore) { setHighScore(score); localStorage.setItem('ultraVisionKidsHighScoreV3', score.toFixed(1)); } if (redTimeoutRef.current) clearTimeout(redTimeoutRef.current); if (alertTimerRef.current) clearTimeout(alertTimerRef.current); }, [score, highScore]); // Main Timer useEffect(() => { if (gameState === 'playing') { timerRef.current = window.setInterval(() => { setTimeLeft((prev) => { if (prev <= 1) { endGame(); return 0; } return prev - 1; }); }, 1000); } else if (timerRef.current) { clearInterval(timerRef.current); } return () => { if (timerRef.current) clearInterval(timerRef.current); }; }, [gameState, endGame]); // Level Management (Dynamic Adjustment) useEffect(() => { let newLevel = level; // 視力に応じたレベルアップ if (score >= 0.3 && level < 2) newLevel = 2; // ランダム位置 if (score >= 0.7 && level < 3) newLevel = 3; // 特殊色1 解禁 if (score >= 1.2 && level < 4) newLevel = 4; // 特殊色2 解禁 if (score >= 1.8 && level < 5) newLevel = 5; // 特殊色3 解禁 if (newLevel !== level) { setLevel(newLevel); // 新しく解禁された色に基づいてアラートを出す const activeSpecials = getActiveSpecialColors(); let message = ''; let targetColor: ColorType | undefined; if (newLevel === 3 && activeSpecials.length > 0) targetColor = activeSpecials[0]; else if (newLevel === 4 && activeSpecials.length > 1) targetColor = activeSpecials[1]; else if (newLevel === 5 && activeSpecials.length > 2) targetColor = activeSpecials[2]; if (targetColor) { if (targetColor === 'Red') message = 'あか色は 1.5びょう!'; if (targetColor === 'Yellow') message = 'きいろは 3かい れんだ!'; if (targetColor === 'Blue') message = 'あお色は はんたい!'; } if (message) { setLevelAlert(message); if (alertTimerRef.current) clearTimeout(alertTimerRef.current); alertTimerRef.current = window.setTimeout(() => { setLevelAlert(null); }, 3000); } } }, [score, level, getActiveSpecialColors]); const addFeedback = (text: string, type: Feedback['type'], x: number, y: number) => { const id = Date.now() + Math.random(); setFeedbacks(prev => [...prev, { id, text, type, x, y }]); setTimeout(() => { setFeedbacks(prev => prev.filter(f => f.id !== id)); }, 400); }; const handleInput = useCallback((inputDir: Direction) => { if (gameState !== 'playing' || !landolt) return; if (landolt.color === 'Red' && redTimeoutRef.current) { clearTimeout(redTimeoutRef.current); } let isCorrect = false; let scoreDelta = 0.1; let message = 'ナイス!'; if (landolt.color === 'Blue') { const targetDir = getOppositeDirection(landolt.direction); isCorrect = inputDir === targetDir; } else { isCorrect = inputDir === landolt.direction; } if (isCorrect) { if (landolt.color === 'Yellow') { const remaining = landolt.hitsRemaining - 1; if (remaining > 0) { setLandolt({ ...landolt, hitsRemaining: remaining }); return; } } if (landolt.color === 'Red') scoreDelta = 0.2; if (landolt.color === 'Blue') scoreDelta = 0.2; if (landolt.color === 'Yellow') scoreDelta = 0.3; setScore(prev => parseFloat((prev + scoreDelta).toFixed(1))); addFeedback(message, 'success', landolt.x, landolt.y); spawnLandolt(); } else { addFeedback('ちがう!', 'error', landolt.x, landolt.y); setScore(prev => Math.max(0.1, parseFloat((prev - 0.2).toFixed(1)))); } }, [gameState, landolt, spawnLandolt]); // Keyboard useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { if (e.key === 'Enter') { if (gameState === 'title') startGame(); else if (gameState === 'gameover') handleTitleBack(); return; } if (gameState !== 'playing') return; switch (e.key) { case 'ArrowUp': handleInput('Up'); break; case 'ArrowDown': handleInput('Down'); break; case 'ArrowLeft': handleInput('Left'); break; case 'ArrowRight': handleInput('Right'); break; } }; window.addEventListener('keydown', handleKeyDown); return () => window.removeEventListener('keydown', handleKeyDown); }, [gameState, handleInput]); // --- Components --- // Config Toggle Button const ToggleBtn = ({ label, isActive, colorClass, icon: Icon, onClick }: any) => ( ); const ArrowMark = ({ data }: { data: LandoltState }) => { let rotateDeg = 0; switch(data.direction) { case 'Up': rotateDeg = 0; break; case 'Right': rotateDeg = 90; break; case 'Down': rotateDeg = 180; break; case 'Left': rotateDeg = 270; break; } let iconColorClass = 'text-slate-800'; if (data.color === 'Blue') { iconColorClass = 'text-blue-500'; } if (data.color === 'Red') { iconColorClass = 'text-red-500'; } if (data.color === 'Yellow') { iconColorClass = 'text-yellow-500'; } return (
{data.color === 'Yellow' && ( {data.hitsRemaining} )}
); }; return (
{/* Background Decor */}
{/* --- HEADER (Game) --- */} {gameState === 'playing' && (
{/* やめるボタン (追加) */}
しりょく {score.toFixed(1)}
{/* HINT AREA */}
{levelAlert ? (
{levelAlert}
) : ( landolt && (
{landolt.color === 'Red' && いそげ!} {landolt.color === 'Blue' && はんたい!} {landolt.color === 'Yellow' && れんだ!} {landolt.color === 'Black' && むきは?}
) )}
のこり {timeLeft}
)} {/* --- TITLE SCREEN --- */} {gameState === 'title' && (

すーぱー
しりょく てすと

あそぶ ルールを えらぼう

setConfig(p => ({...p, useRed: !p.useRed}))} /> setConfig(p => ({...p, useYellow: !p.useYellow}))} /> setConfig(p => ({...p, useBlue: !p.useBlue}))} />
)} {/* --- GAME OVER SCREEN --- */} {gameState === 'gameover' && (

あなたの しりょくは...

{score.toFixed(1)}
最高きろく: {highScore.toFixed(1)}
)} {/* --- MAIN GAME CANVAS --- */}
{/* The Mark */} {gameState === 'playing' && landolt && } {/* Feedback Effects */} {feedbacks.map(fb => (
{fb.text}
))}
{/* --- CONTROL PAD --- */} {gameState === 'playing' && (
)}
); }