<! DOCTYPE html >
< html lang = " zh-CN" >
< head> < meta charset = " UTF-8" > < title> 俄罗斯方块</ title> < style> body { margin : 0; display : flex; justify-content : center; align-items : center; height : 100vh; background : #333; color : white; font-family : Arial, sans-serif; } .game-container { display : flex; gap : 20px; } canvas { border : 2px solid #fff; } .info-panel { width : 150px; } .controls { margin-top : 20px; } .controls button { padding : 10px 20px; margin : 5px; font-size : 16px; cursor : pointer; } </ style>
</ head>
< body> < div class = " game-container" > < canvas id = " tetris" width = " 300" height = " 600" > </ canvas> < div class = " info-panel" > < h2> 俄罗斯方块</ h2> < p> 分数: < span id = " score" > 0</ span> </ p> < p> 等级: < span id = " level" > 1</ span> </ p> < p> 下一个:</ p> < canvas id = " nextPiece" width = " 120" height = " 120" > </ canvas> < div class = " controls" > < button id = " startBtn" > 开始游戏</ button> < button id = " pauseBtn" disabled > 暂停</ button> < button id = " restartBtn" disabled > 重新开始</ button> </ div> </ div> </ div> < script> const canvas = document. getElementById ( 'tetris' ) ; const ctx = canvas. getContext ( '2d' ) ; const nextCanvas = document. getElementById ( 'nextPiece' ) ; const nextCtx = nextCanvas. getContext ( '2d' ) ; const scoreElement = document. getElementById ( 'score' ) ; const levelElement = document. getElementById ( 'level' ) ; const ROW = 20 ; const COL = 10 ; const SQ = 30 ; const VACANT = 'black' ; const SPEED = 500 ; const LINES_PER_LEVEL = 10 ; const Z = [ [ [ 1 , 1 , 0 ] , [ 0 , 1 , 1 ] , [ 0 , 0 , 0 ] ] , [ [ 0 , 0 , 1 ] , [ 0 , 1 , 1 ] , [ 0 , 1 , 0 ] ] ] ; const S = [ [ [ 0 , 1 , 1 ] , [ 1 , 1 , 0 ] , [ 0 , 0 , 0 ] ] , [ [ 1 , 0 , 0 ] , [ 1 , 1 , 0 ] , [ 0 , 1 , 0 ] ] ] ; const T = [ [ [ 1 , 1 , 1 ] , [ 0 , 1 , 0 ] , [ 0 , 0 , 0 ] ] ] , O = [ [ [ 1 , 1 ] , [ 1 , 1 ] ] ] , L = [ [ [ 1 , 0 , 0 ] , [ 1 , 1 , 1 ] , [ 0 , 0 , 0 ] ] ] , I = [ [ [ 1 , 1 , 1 , 1 ] ] ] , J = [ [ [ 0 , 0 , 1 ] , [ 1 , 1 , 1 ] , [ 0 , 0 , 0 ] ] ] ; const PIECES = [ [ Z , 'red' ] , [ S , 'green' ] , [ T , 'yellow' ] , [ O , 'blue' ] , [ L , 'purple' ] , [ I , 'cyan' ] , [ J , 'orange' ] ] ; let board = [ ] ; let score = 0 ; let level = 1 ; let lines = 0 ; let dropStart = Date. now ( ) ; let piece; let nextPiece; let isPaused = false ; let isRunning = false ; let rAF; function initBoard ( ) { for ( let r = 0 ; r < ROW ; r++ ) { board[ r] = [ ] ; for ( let c = 0 ; c < COL ; c++ ) { board[ r] [ c] = VACANT ; } } } function drawSquare ( x, y, color ) { ctx. fillStyle = color; ctx. fillRect ( x* SQ , y* SQ , SQ , SQ ) ; ctx. strokeStyle = '#333' ; ctx. strokeRect ( x* SQ , y* SQ , SQ , SQ ) ; } function drawBoard ( ) { for ( let r = 0 ; r < ROW ; r++ ) { for ( let c = 0 ; c < COL ; c++ ) { drawSquare ( c, r, board[ r] [ c] ) ; } } } class Piece { constructor ( tetromino, color ) { this . tetromino = tetromino; this . color = color; this . tetrominoN = 0 ; this . activeTetromino = this . tetromino[ this . tetrominoN] ; this . x = 3 ; this . y = - 2 ; } draw ( ) { for ( let r = 0 ; r < this . activeTetromino. length; r++ ) { for ( let c = 0 ; c < this . activeTetromino. length; c++ ) { if ( this . activeTetromino[ r] [ c] ) { drawSquare ( this . x + c, this . y + r, this . color) ; } } } } unDraw ( ) { for ( let r = 0 ; r < this . activeTetromino. length; r++ ) { for ( let c = 0 ; c < this . activeTetromino. length; c++ ) { if ( this . activeTetromino[ r] [ c] ) { drawSquare ( this . x + c, this . y + r, VACANT ) ; } } } } moveDown ( ) { if ( ! this . collision ( 0 , 1 , this . activeTetromino) ) { this . unDraw ( ) ; this . y++ ; this . draw ( ) ; } else { if ( this . y < 0 ) { gameOver ( ) ; return ; } this . lock ( ) ; piece = nextPiece; nextPiece = randomPiece ( ) ; drawNextPiece ( ) ; } } moveLeft ( ) { if ( ! this . collision ( - 1 , 0 , this . activeTetromino) ) { this . unDraw ( ) ; this . x-- ; this . draw ( ) ; } } moveRight ( ) { if ( ! this . collision ( 1 , 0 , this . activeTetromino) ) { this . unDraw ( ) ; this . x++ ; this . draw ( ) ; } } rotate ( ) { if ( this . y < 0 ) { return ; } let nextPattern = this . tetromino[ ( this . tetrominoN + 1 ) % this . tetromino. length] ; let kick = 0 ; if ( this . collision ( 0 , 0 , nextPattern) ) { if ( this . x > COL / 2 ) { kick = - 1 ; } else { kick = 1 ; } if ( ! this . collision ( kick, 0 , nextPattern) ) { this . x += kick; } else { return ; } } this . unDraw ( ) ; this . tetrominoN = ( this . tetrominoN + 1 ) % this . tetromino. length; this . activeTetromino = this . tetromino[ this . tetrominoN] ; this . draw ( ) ; } collision ( x, y, piece ) { for ( let r = 0 ; r < piece. length; r++ ) { for ( let c = 0 ; c < piece. length; c++ ) { if ( ! piece[ r] [ c] ) continue ; let newX = this . x + c + x; let newY = this . y + r + y; if ( newX < 0 || newX >= COL || newY >= ROW ) { return true ; } if ( newY >= 0 && board[ newY] [ newX] !== VACANT ) { return true ; } } } return false ; } lock ( ) { for ( let r = 0 ; r < this . activeTetromino. length; r++ ) { for ( let c = 0 ; c < this . activeTetromino. length; c++ ) { if ( ! this . activeTetromino[ r] [ c] ) continue ; if ( this . y + r < 0 ) { gameOver ( ) ; return ; } board[ this . y + r] [ this . x + c] = this . color; } } clearLines ( ) ; } } function randomPiece ( ) { let r = Math. floor ( Math. random ( ) * PIECES . length) ; return new Piece ( PIECES [ r] [ 0 ] , PIECES [ r] [ 1 ] ) ; } function clearLines ( ) { let linesCleared = 0 ; for ( let r = ROW - 1 ; r >= 0 ; r-- ) { if ( board[ r] . every ( cell => cell !== VACANT ) ) { board. splice ( r, 1 ) ; board. unshift ( new Array ( COL ) . fill ( VACANT ) ) ; linesCleared++ ; r++ ; } } if ( linesCleared > 0 ) { updateScore ( linesCleared) ; } } function updateScore ( linesCleared ) { const points = [ 0 , 40 , 100 , 300 , 1200 ] [ linesCleared] * level; score += points; lines += linesCleared; if ( lines >= LINES_PER_LEVEL ) { level++ ; lines -= LINES_PER_LEVEL ; } scoreElement. textContent = score; levelElement. textContent = level; } function drawNextPiece ( ) { nextCtx. clearRect ( 0 , 0 , nextCanvas. width, nextCanvas. height) ; for ( let r = 0 ; r < nextPiece. tetromino[ 0 ] . length; r++ ) { for ( let c = 0 ; c < nextPiece. tetromino[ 0 ] . length; c++ ) { if ( nextPiece. tetromino[ 0 ] [ r] [ c] ) { nextCtx. fillStyle = nextPiece. color; nextCtx. fillRect ( c* 30 , r* 30 , 30 , 30 ) ; nextCtx. strokeStyle = '#333' ; nextCtx. strokeRect ( c* 30 , r* 30 , 30 , 30 ) ; } } } } function gameOver ( ) { cancelAnimationFrame ( rAF) ; isRunning = false ; document. getElementById ( 'startBtn' ) . disabled = false ; document. getElementById ( 'pauseBtn' ) . disabled = true ; document. getElementById ( 'restartBtn' ) . disabled = false ; ctx. fillStyle = 'rgba(0,0,0,0.75)' ; ctx. fillRect ( 0 , 0 , canvas. width, canvas. height) ; ctx. fillStyle = 'white' ; ctx. font = '40px Arial' ; ctx. fillText ( '游戏结束!' , 30 , canvas. height/ 2 - 20 ) ; ctx. font = '20px Arial' ; ctx. fillText ( '按R键重新开始' , 50 , canvas. height/ 2 + 20 ) ; } function init ( ) { initBoard ( ) ; score = 0 ; level = 1 ; lines = 0 ; scoreElement. textContent = score; levelElement. textContent = level; piece = randomPiece ( ) ; nextPiece = randomPiece ( ) ; drawNextPiece ( ) ; drawBoard ( ) ; piece. draw ( ) ; } function gameLoop ( ) { if ( isPaused || ! isRunning) return ; let now = Date. now ( ) ; let delta = now - dropStart; if ( delta > SPEED / level) { piece. moveDown ( ) ; dropStart = Date. now ( ) ; } drawBoard ( ) ; piece. draw ( ) ; rAF = requestAnimationFrame ( gameLoop) ; } document. getElementById ( 'startBtn' ) . addEventListener ( 'click' , ( ) => { if ( ! isRunning) { init ( ) ; isRunning = true ; isPaused = false ; document. getElementById ( 'startBtn' ) . disabled = true ; document. getElementById ( 'pauseBtn' ) . disabled = false ; document. getElementById ( 'restartBtn' ) . disabled = false ; dropStart = Date. now ( ) ; gameLoop ( ) ; } } ) ; document. getElementById ( 'pauseBtn' ) . addEventListener ( 'click' , ( ) => { if ( isRunning) { isPaused = ! isPaused; document. getElementById ( 'pauseBtn' ) . textContent = isPaused ? '继续' : '暂停' ; if ( ! isPaused) { dropStart = Date. now ( ) ; gameLoop ( ) ; } } } ) ; document. getElementById ( 'restartBtn' ) . addEventListener ( 'click' , ( ) => { cancelAnimationFrame ( rAF) ; init ( ) ; isRunning = true ; isPaused = false ; document. getElementById ( 'pauseBtn' ) . textContent = '暂停' ; dropStart = Date. now ( ) ; gameLoop ( ) ; } ) ; document. addEventListener ( 'keydown' , ( event ) => { if ( ! isRunning || isPaused) return ; if ( event. keyCode === 37 ) { piece. moveLeft ( ) ; dropStart = Date. now ( ) ; } else if ( event. keyCode === 38 ) { if ( piece. y >= 0 ) { piece. rotate ( ) ; dropStart = Date. now ( ) ; } } else if ( event. keyCode === 39 ) { piece. moveRight ( ) ; dropStart = Date. now ( ) ; } else if ( event. keyCode === 40 ) { piece. moveDown ( ) ; } else if ( event. keyCode === 80 ) { document. getElementById ( 'pauseBtn' ) . click ( ) ; } } ) ; </ script>
</ body>
</ html>