中文多情感语音合成在智能家居场景的落地实践
2026/1/9 21:29:41
https://chat.xutongbao.top/nextjs/light/etris
'use client' import { useState, useEffect, useCallback, useRef } from 'react' import Header from '@/components/header' import { ArrowLeft, Play, Pause, RotateCw, Zap, Trophy, ArrowUp, ArrowDown, ArrowLeftIcon, ArrowRight, } from 'lucide-react' import { useRouter } from 'next/navigation' // 游戏配置 const BOARD_WIDTH = 10 const BOARD_HEIGHT = 20 const BLOCK_SIZE = 30 // 方块形状定义 const SHAPES = { I: [ [1, 1, 1, 1], [0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0], ], J: [ [1, 0, 0], [1, 1, 1], [0, 0, 0], ], L: [ [0, 0, 1], [1, 1, 1], [0, 0, 0], ], O: [ [1, 1], [1, 1], ], S: [ [0, 1, 1], [1, 1, 0], [0, 0, 0], ], T: [ [0, 1, 0], [1, 1, 1], [0, 0, 0], ], Z: [ [1, 1, 0], [0, 1, 1], [0, 0, 0], ], } // 方块颜色 const COLORS: { [key: string]: string } = { I: '#00f0f0', J: '#0000f0', L: '#f0a000', O: '#f0f000', S: '#00f000', T: '#a000f0', Z: '#f00000', } type ShapeKey = keyof typeof SHAPES type Board = (string | null)[][] interface Piece { shape: number[][] type: ShapeKey x: number y: number } export default function TetrisPage() { const router = useRouter() const [board, setBoard] = useState<Board>(createEmptyBoard()) const [currentPiece, setCurrentPiece] = useState<Piece | null>(null) const [nextPiece, setNextPiece] = useState<Piece | null>(null) const [score, setScore] = useState(0) const [lines, setLines] = useState(0) const [level, setLevel] = useState(1) const [gameOver, setGameOver] = useState(false) const [isPaused, setIsPaused] = useState(false) const [isPlaying, setIsPlaying] = useState(false) const gameLoopRef = useRef<NodeJS.Timeout | null>(null) // 创建空棋盘 function createEmptyBoard(): Board { return Array(BOARD_HEIGHT) .fill(null) .map(() => Array(BOARD_WIDTH).fill(null)) } // 生成随机方块 function createRandomPiece(): Piece { const shapes = Object.keys(SHAPES) as ShapeKey[] const type = shapes[Math.floor(Math.random() * shapes.length)] return { shape: SHAPES[type], type, x: Math.floor(BOARD_WIDTH / 2) - Math.floor(SHAPES[type][0].length / 2), y: 0, } } // 检查碰撞 function checkCollision(piece: Piece, newX: number, newY: number): boolean { for (let y = 0; y < piece.shape.length; y++) { for (let x = 0; x < piece.shape[y].length; x++) { if (piece.shape[y][x]) { const boardX = newX + x const boardY = newY + y if (boardX < 0 || boardX >= BOARD_WIDTH || boardY >= BOARD_HEIGHT) { return true } if (boardY >= 0 && board[boardY][boardX]) { return true } } } } return false } // 旋转方块 function rotatePiece(piece: Piece): number[][] { const rotated = piece.shape[0].map((_, i) => piece.shape.map((row) => row[i]).reverse()) return rotated } // 合并方块到棋盘 function mergePiece(piece: Piece): Board { const newBoard = board.map((row) => [...row]) for (let y = 0; y < piece.shape.length; y++) { for (let x = 0; x < piece.shape[y].length; x++) { if (piece.shape[y][x]) { const boardY = piece.y + y const boardX = piece.x + x if (boardY >= 0) { newBoard[boardY][boardX] = piece.type } } } } return newBoard } // 清除完整的行 function clearLines(boardToClear: Board): { newBoard: Board; clearedLines: number } { let clearedLines = 0 const newBoard = boardToClear.filter((row) => { if (row.every((cell) => cell !== null)) { clearedLines++ return false } return true }) while (newBoard.length < BOARD_HEIGHT) { newBoard.unshift(Array(BOARD_WIDTH).fill(null)) } return { newBoard, clearedLines } } // 移动方块 const movePiece = useCallback( (dx: number, dy: number) => { if (!currentPiece || gameOver || isPaused) return const newX = currentPiece.x + dx const newY = currentPiece.y + dy if (!checkCollision(currentPiece, newX, newY)) { setCurrentPiece({ ...currentPiece, x: newX, y: newY }) } else if (dy > 0) { // 方块触底 const newBoard = mergePiece(currentPiece) const { newBoard: clearedBoard, clearedLines } = clearLines(newBoard) setBoard(clearedBoard) setLines((prev) => prev + clearedLines) setScore((prev) => prev + clearedLines * 100 * level) // 检查游戏是否结束 if (nextPiece && checkCollision(nextPiece, nextPiece.x, nextPiece.y)) { setGameOver(true) setIsPlaying(false) return } setCurrentPiece(nextPiece) setNextPiece(createRandomPiece()) } }, [currentPiece, board, gameOver, isPaused, nextPiece, level] ) // 旋转 const rotate = useCallback(() => { if (!currentPiece || gameOver || isPaused) return const rotated = rotatePiece(currentPiece) const newPiece = { ...currentPiece, shape: rotated } if (!checkCollision(newPiece, newPiece.x, newPiece.y)) { setCurrentPiece(newPiece) } }, [currentPiece, board, gameOver, isPaused]) // 硬降 const hardDrop = useCallback(() => { if (!currentPiece || gameOver || isPaused) return let newY = currentPiece.y while (!checkCollision(currentPiece, currentPiece.x, newY + 1)) { newY++ } setCurrentPiece({ ...currentPiece, y: newY }) movePiece(0, 1) }, [currentPiece, gameOver, isPaused, movePiece]) // 键盘控制 useEffect(() => { const handleKeyPress = (e: KeyboardEvent) => { if (!isPlaying || gameOver) return switch (e.key) { case 'ArrowLeft': e.preventDefault() movePiece(-1, 0) break case 'ArrowRight': e.preventDefault() movePiece(1, 0) break case 'ArrowDown': e.preventDefault() movePiece(0, 1) break case 'ArrowUp': e.preventDefault() rotate() break case ' ': e.preventDefault() hardDrop() break case 'p': case 'P': e.preventDefault() setIsPaused((prev) => !prev) break } } window.addEventListener('keydown', handleKeyPress) return () => window.removeEventListener('keydown', handleKeyPress) }, [movePiece, rotate, hardDrop, isPlaying, gameOver]) // 游戏循环 useEffect(() => { if (isPlaying && !gameOver && !isPaused) { gameLoopRef.current = setInterval(() => { movePiece(0, 1) }, Math.max(100, 1000 - (level - 1) * 100)) } else { if (gameLoopRef.current) { clearInterval(gameLoopRef.current) } } return () => { if (gameLoopRef.current) { clearInterval(gameLoopRef.current) } } }, [isPlaying, gameOver, isPaused, movePiece, level]) // 开始游戏 const startGame = () => { setBoard(createEmptyBoard()) setCurrentPiece(createRandomPiece()) setNextPiece(createRandomPiece()) setScore(0) setLines(0) setLevel(1) setGameOver(false) setIsPaused(false) setIsPlaying(true) } // 更新等级 useEffect(() => { setLevel(Math.floor(lines / 10) + 1) }, [lines]) // 渲染棋盘 const renderBoard = () => { const displayBoard = board.map((row) => [...row]) // 绘制当前方块 if (currentPiece) { for (let y = 0; y < currentPiece.shape.length; y++) { for (let x = 0; x < currentPiece.shape[y].length; x++) { if (currentPiece.shape[y][x]) { const boardY = currentPiece.y + y const boardX = currentPiece.x + x if (boardY >= 0 && boardY < BOARD_HEIGHT && boardX >= 0 && boardX < BOARD_WIDTH) { displayBoard[boardY][boardX] = currentPiece.type } } } } } return displayBoard } return ( <> <Header /> <main className="min-h-screen bg-linear-to-br from-primary/5 via-background to-secondary/5 relative overflow-hidden"> {/* 背景装饰 */} <div className="absolute inset-0 overflow-hidden pointer-events-none"> <div className="absolute top-20 left-1/4 w-96 h-96 bg-primary/5 rounded-full blur-3xl animate-pulse-slow" /> <div className="absolute bottom-20 right-1/4 w-96 h-96 bg-secondary/5 rounded-full blur-3xl animate-pulse-slow" style={{ animationDelay: '2s' }} /> </div> {/* 内容区域 */} <div className="relative max-w-7xl mx-auto px-4 py-8"> {/* 返回按钮 */} <button onClick={() => router.push('/light')} className="group mb-8 flex items-center gap-2 px-4 py-3 rounded-2xl bg-card/80 backdrop-blur-xl border-2 border-border hover:border-primary shadow-lg hover:shadow-xl transition-all duration-300 hover:scale-105 animate-fade-in" > <div className="relative"> <div className="absolute inset-0 bg-primary/20 rounded-full blur-md scale-0 group-hover:scale-150 transition-transform duration-500" /> <ArrowLeft className="relative w-5 h-5 text-primary group-hover:text-primary transition-all duration-300 group-hover:-translate-x-1" /> </div> <span className="text-sm font-medium text-foreground group-hover:text-primary transition-colors duration-300"> 返回 </span> </button> {/* 页面标题 */} <div className="text-center mb-8 animate-fade-in"> <div className="inline-flex items-center gap-2 px-4 py-2 rounded-full bg-primary/10 border border-primary/20 mb-4"> <Zap className="w-4 h-4 text-primary" /> <span className="text-sm font-medium text-primary">经典游戏</span> </div> <h1 className="text-4xl md:text-5xl font-bold text-foreground mb-4"> 俄罗斯 <span className="bg-linear-to-r from-primary via-secondary to-accent bg-clip-text text-transparent"> {' '} 方块{' '} </span> </h1> <p className="text-lg text-muted-foreground">经典益智游戏,挑战你的反应速度</p> </div> {/* 游戏主体 */} <div className="grid grid-cols-1 lg:grid-cols-12 gap-8 max-w-6xl mx-auto"> {/* 左侧信息面板 */} <div className="lg:col-span-3 space-y-6"> {/* 分数 */} <div className="bg-card rounded-3xl p-6 shadow-xl border border-border animate-fade-in-up"> <div className="flex items-center gap-2 mb-4"> <Trophy className="w-5 h-5 text-primary" /> <h3 className="text-lg font-bold text-foreground">游戏统计</h3> </div> <div className="space-y-4"> <div> <div className="text-sm text-muted-foreground mb-1">得分</div> <div className="text-3xl font-bold text-primary">{score}</div> </div> <div> <div className="text-sm text-muted-foreground mb-1">消除行数</div> <div className="text-2xl font-bold text-secondary">{lines}</div> </div> <div> <div className="text-sm text-muted-foreground mb-1">等级</div> <div className="text-2xl font-bold text-accent">{level}</div> </div> </div> </div> {/* 下一个方块 */} {nextPiece && ( <div className="bg-card rounded-3xl p-6 shadow-xl border border-border animate-fade-in-up"> <h3 className="text-lg font-bold text-foreground mb-4">下一个</h3> <div className="flex justify-center"> <div className="grid gap-1 p-4 bg-muted/50 rounded-xl" style={{ gridTemplateColumns: `repeat(${nextPiece.shape[0].length}, ${BLOCK_SIZE}px)`, }} > {nextPiece.shape.map((row, y) => row.map((cell, x) => ( <div key={`${y}-${x}`} className="rounded transition-all duration-200" style={{ width: `${BLOCK_SIZE}px`, height: `${BLOCK_SIZE}px`, backgroundColor: cell ? COLORS[nextPiece.type] : 'transparent', boxShadow: cell ? `0 0 10px ${COLORS[nextPiece.type]}40` : 'none', }} /> )) )} </div> </div> </div> )} {/* 控制说明 */} <div className="bg-card rounded-3xl p-6 shadow-xl border border-border animate-fade-in-up"> <h3 className="text-lg font-bold text-foreground mb-4">操作说明</h3> <div className="space-y-3 text-sm"> <div className="flex items-center gap-2"> <ArrowLeftIcon className="w-4 h-4 text-primary" /> <span className="text-muted-foreground">左移</span> </div> <div className="flex items-center gap-2"> <ArrowRight className="w-4 h-4 text-primary" /> <span className="text-muted-foreground">右移</span> </div> <div className="flex items-center gap-2"> <ArrowDown className="w-4 h-4 text-primary" /> <span className="text-muted-foreground">加速下落</span> </div> <div className="flex items-center gap-2"> <ArrowUp className="w-4 h-4 text-primary" /> <span className="text-muted-foreground">旋转</span> </div> <div className="flex items-center gap-2"> <div className="w-4 h-4 flex items-center justify-center text-xs font-bold text-primary border border-primary rounded"> SP </div> <span className="text-muted-foreground">瞬间下落</span> </div> <div className="flex items-center gap-2"> <div className="w-4 h-4 flex items-center justify-center text-xs font-bold text-primary border border-primary rounded"> P </div> <span className="text-muted-foreground">暂停/继续</span> </div> </div> </div> </div> {/* 游戏棋盘 */} <div className="lg:col-span-6 flex flex-col items-center"> <div className="bg-card rounded-3xl p-6 shadow-xl border border-border animate-fade-in-up"> <div className="grid gap-[2px] p-2 bg-muted/50 rounded-xl relative" style={{ gridTemplateColumns: `repeat(${BOARD_WIDTH}, ${BLOCK_SIZE}px)`, }} > {renderBoard().map((row, y) => row.map((cell, x) => ( <div key={`${y}-${x}`} className="rounded transition-all duration-200" style={{ width: `${BLOCK_SIZE}px`, height: `${BLOCK_SIZE}px`, backgroundColor: cell ? COLORS[cell] : '#1a1a1a', boxShadow: cell ? `0 0 15px ${COLORS[cell]}60` : 'inset 0 0 5px rgba(0,0,0,0.3)', border: cell ? `1px solid ${COLORS[cell]}80` : '1px solid #2a2a2a', }} /> )) )} {/* 游戏结束遮罩 */} {gameOver && ( <div className="absolute inset-0 bg-black/80 backdrop-blur-sm flex items-center justify-center rounded-xl animate-fade-in"> <div className="text-center"> <div className="text-4xl font-bold text-white mb-4">游戏结束</div> <div className="text-xl text-muted-foreground mb-6">最终得分: {score}</div> <button onClick={startGame} className="px-8 py-3 bg-primary text-white rounded-xl font-semibold hover:bg-primary/90 transition-all duration-200 hover:scale-105 shadow-lg" > 重新开始 </button> </div> </div> )} {/* 暂停遮罩 */} {isPaused && !gameOver && ( <div className="absolute inset-0 bg-black/70 backdrop-blur-sm flex items-center justify-center rounded-xl animate-fade-in"> <div className="text-4xl font-bold text-white">游戏暂停</div> </div> )} </div> </div> </div> {/* 右侧控制面板 */} <div className="lg:col-span-3 space-y-6"> {/* 游戏控制 */} <div className="bg-card rounded-3xl p-6 shadow-xl border border-border animate-fade-in-up"> <h3 className="text-lg font-bold text-foreground mb-4">游戏控制</h3> <div className="space-y-3"> {!isPlaying ? ( <button onClick={startGame} className="w-full flex items-center justify-center gap-2 px-6 py-3 bg-primary text-white rounded-xl font-semibold hover:bg-primary/90 transition-all duration-200 hover:scale-105 shadow-lg" > <Play className="w-5 h-5" /> 开始游戏 </button> ) : ( <button onClick={() => setIsPaused(!isPaused)} className="w-full flex items-center justify-center gap-2 px-6 py-3 bg-secondary text-white rounded-xl font-semibold hover:bg-secondary/90 transition-all duration-200 hover:scale-105 shadow-lg" > {isPaused ? ( <> <Play className="w-5 h-5" /> 继续 </> ) : ( <> <Pause className="w-5 h-5" /> 暂停 </> )} </button> )} </div> </div> {/* 移动端控制按钮 */} <div className="bg-card rounded-3xl p-6 shadow-xl border border-border animate-fade-in-up lg:hidden"> <h3 className="text-lg font-bold text-foreground mb-4">触摸控制</h3> <div className="space-y-3"> <button onClick={rotate} disabled={!isPlaying || gameOver || isPaused} className="w-full flex items-center justify-center gap-2 px-6 py-3 bg-primary/10 text-primary rounded-xl font-semibold hover:bg-primary/20 transition-all duration-200 disabled:opacity-50 disabled:cursor-not-allowed" > <RotateCw className="w-5 h-5" /> 旋转 </button> <div className="grid grid-cols-3 gap-2"> <button onClick={() => movePiece(-1, 0)} disabled={!isPlaying || gameOver || isPaused} className="p-4 bg-secondary/10 text-secondary rounded-xl hover:bg-secondary/20 transition-all duration-200 disabled:opacity-50 disabled:cursor-not-allowed" > <ArrowLeftIcon className="w-6 h-6 mx-auto" /> </button> <button onClick={() => movePiece(0, 1)} disabled={!isPlaying || gameOver || isPaused} className="p-4 bg-secondary/10 text-secondary rounded-xl hover:bg-secondary/20 transition-all duration-200 disabled:opacity-50 disabled:cursor-not-allowed" > <ArrowDown className="w-6 h-6 mx-auto" /> </button> <button onClick={() => movePiece(1, 0)} disabled={!isPlaying || gameOver || isPaused} className="p-4 bg-secondary/10 text-secondary rounded-xl hover:bg-secondary/20 transition-all duration-200 disabled:opacity-50 disabled:cursor-not-allowed" > <ArrowRight className="w-6 h-6 mx-auto" /> </button> </div> <button onClick={hardDrop} disabled={!isPlaying || gameOver || isPaused} className="w-full flex items-center justify-center gap-2 px-6 py-3 bg-accent/10 text-accent rounded-xl font-semibold hover:bg-accent/20 transition-all duration-200 disabled:opacity-50 disabled:cursor-not-allowed" > <ArrowDown className="w-5 h-5" /> 瞬间下落 </button> </div> </div> {/* 游戏技巧 */} <div className="bg-card rounded-3xl p-6 shadow-xl border border-border animate-fade-in-up"> <h3 className="text-lg font-bold text-foreground mb-4">游戏技巧</h3> <ul className="space-y-2 text-sm text-muted-foreground"> <li className="flex items-start gap-2"> <span className="text-primary">•</span> <span>尽量保持底部平整</span> </li> <li className="flex items-start gap-2"> <span className="text-primary">•</span> <span>预留I型方块的空位</span> </li> <li className="flex items-start gap-2"> <span className="text-primary">•</span> <span>关注下一个方块</span> </li> <li className="flex items-start gap-2"> <span className="text-primary">•</span> <span>不要堆得太高</span> </li> <li className="flex items-start gap-2"> <span className="text-primary">•</span> <span>消除多行可获得更高分数</span> </li> </ul> </div> </div> </div> </div> </main> {/* 自定义动画样式 */} <style jsx global>{` @keyframes fade-in { from { opacity: 0; transform: translateY(20px); } to { opacity: 1; transform: translateY(0); } } @keyframes fade-in-up { from { opacity: 0; transform: translateY(30px); } to { opacity: 1; transform: translateY(0); } } @keyframes pulse-slow { 0%, 100% { opacity: 0.3; transform: scale(1); } 50% { opacity: 0.5; transform: scale(1.1); } } .animate-fade-in { animation: fade-in 0.8s ease-out; } .animate-fade-in-up { animation: fade-in-up 0.6s ease-out; animation-fill-mode: both; } .animate-pulse-slow { animation: pulse-slow 4s ease-in-out infinite; } `}</style> </> ) }