【ゲーム】JavaScript:05 テトリス

 このテトリスは、落下するブロック(テトリミノ)を左右移動・回転させ、横一列が埋まったら消去してスコアを稼ぐクラシックなパズルゲームです。20行×10列のグリッド上でプレイし、次に現れるブロックをあらかじめ確認しながら積み上げを図ります。画面右に「次のブロック」を表示し、スコアは消した行数に応じて加算。最上部まで積み上がるとゲームオーバーです。

遊び方

 左右矢印キーでブロックを左右に移動、↓キーで下方向に強制移動、Zキー/Xキーでそれぞれ反時計回り/時計回りに回転できます。コンボを意識して隙間なく積み上げ、できるだけ長く続けるほど高得点を狙えます。

 置ける場所がなくなる、またはブロックが上部を超えた時点でゲームオーバー。消せる行が複数ある場合は一度にまとめて消去し、消した行数²×100点が加算されます。

  • ← / →キー:ブロックを左右に移動
  • ↓キー:ブロックを下に強制移動(早く落とす)
  • Zキー:ブロックを反時計回りに回転
  • Xキー:ブロックを時計回りに回転
  • 目的:隙間なくブロックを積み上げ、ラインを消して得点を獲得
  • コツコンボを狙って連続でラインを消すと高得点が狙える。
  • 目標:できるだけ長くプレイを続けてスコアを伸ばす

🎮ゲームプレイ

以下のリンク先から実際にプレイできます。

05 テトリス

素材のダウンロード

以下のリンクから使用する素材をダウンロードできます。

tetris_bg.png

ゲーム画面イメージ

プログラム全文(tetris.html)

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>🕹️ テトリス 🎮</title>
    <style>
        /* 全体リセット & 背景 */
        * { margin:0; padding:0; box-sizing:border-box; }
        body {
            display:flex; flex-direction:column;
            justify-content:center; align-items:center;
            height:100vh;
            background:url('tetris_bg.png') no-repeat center center fixed, #333;
            background-size:cover;
            font-family:Arial, sans-serif;
            color:#fff;
        }
        /* タイトル */
        h1 {
            font-size:2.5rem;
            margin-bottom:10px;
            background:rgba(0,0,0,0.7);
            padding:10px 20px;
            border-radius:8px;
            text-shadow:2px 2px 4px #000;
        }
        /* スコア */
        #score {
            font-size:1.2rem;
            margin-bottom:10px;
            background:rgba(0,0,0,0.7);
            padding:6px 12px;
            border-radius:4px;
        }
        /* ゲーム & 次ブロック */
        #game-container { display:flex; gap:20px; }
        #game {
            display:grid;
            grid-template-rows:repeat(20,30px);
            grid-template-columns:repeat(10,30px);
            gap:1px;
            background:rgba(0,0,0,0.7);
            padding:4px;
            border:2px solid #fff;
        }
        .cell { width:30px; height:30px; background:#222; }
        .block { /* color set inline */ }
        .fixed { background:#555; }
        #next-block-container { display:flex; flex-direction:column; align-items:center; }
        #next-block-container p { margin-bottom:5px; font-weight:bold; }
        #next-block {
            display:grid;
            grid-template-rows:repeat(4,30px);
            grid-template-columns:repeat(4,30px);
            gap:1px;
            background:rgba(0,0,0,0.7);
            padding:4px;
            border:2px solid #fff;
        }
        /* リスタートボタン */
        #restart-btn {
            display:none;
            margin-top:10px;
            padding:10px 20px;
            font-size:1rem;
            background:#007bff;
            border:none;
            border-radius:5px;
            color:#fff;
            cursor:pointer;
            text-shadow:1px 1px 2px #000;
        }
        #restart-btn:hover { background:#0056b3; }
    </style>
</head>
<body>
    <h1>🕹️ テトリス 🎮</h1>
    <div id="score">Score: 0</div>
    <div id="game-container">
        <div id="game"></div>
        <div id="next-block-container">
            <p>次のブロック</p>
            <div id="next-block"></div>
        </div>
    </div>
    <button id="restart-btn">🔄 もう1回プレイする</button>
    <script>
        const ROWS=20, COLS=10;
        const game=document.getElementById('game');
        const nextContainer=document.getElementById('next-block');
        const scoreEl=document.getElementById('score');
        const restartBtn=document.getElementById('restart-btn');
        let grid=[], current, next, intervalId, score=0, gameOverFlag=false;
        const tetrominoes=[
            [[1,1,1],[0,1,0]],[[1,1],[1,1]],[[1,1,0],[0,1,1]],
            [[0,1,1],[1,1,0]],[[1,1,1,1]],[[1,1,1],[1,0,0]],[[1,1,1],[0,0,1]]
        ];
        const colors=['#f00','#0f0','#00f','#ff0','#0ff','#f0f','#fa0'];
        function initGrid(){
            game.innerHTML=''; grid=[];
            for(let r=0;r<ROWS;r++){
                grid[r]=[];
                for(let c=0;c<COLS;c++){
                    const cell=document.createElement('div'); cell.className='cell';
                    game.appendChild(cell); grid[r][c]=cell;
                }
            }
        }
        function spawn(){
            current=next||randomTet(); current.row=0; current.col=Math.floor(COLS/2)-1;
            next=randomTet(); drawNext();
        }
        function randomTet(){
            const idx=Math.floor(Math.random()*tetrominoes.length);
            return {shape:tetrominoes[idx],color:colors[idx]};
        }
        function drawGrid(){
            grid.forEach(row=>row.forEach(cell=>{
                // 既定のcell or fixedを保持
                if(cell.classList.contains('fixed')){
                    cell.className='cell fixed'; cell.style.background='#555';
                } else {
                    cell.className='cell'; cell.style.background='';
                }
            }));
        }
        function drawBlock(){
            drawGrid();
            const {shape,row,col,color}=current;
            shape.forEach((r,i)=>r.forEach((v,j)=>{
                if(v){
                    const cell=grid[row+i][col+j];
                    cell.classList.add('block'); cell.style.background=color;
                }
            }));
        }
        function drawNext(){
            nextContainer.innerHTML='';
            const {shape,color}=next;
            for(let i=0;i<4;i++) for(let j=0;j<4;j++){
                const cell=document.createElement('div'); cell.className='cell';
                if(shape[i]&&shape[i][j]){cell.classList.add('block');cell.style.background=color;}
                nextContainer.appendChild(cell);
            }
        }
        function valid({shape,row,col}){
            return shape.every((r,i)=>r.every((v,j)=>{
                if(!v) return true;
                const x=row+i,y=col+j;
                return x>=0&&x<ROWS&&y>=0&&y<COLS&&!grid[x][y].classList.contains('fixed');
            }));
        }
        function move(dx,dy){
            current.row+=dy; current.col+=dx;
            if(!valid(current)){current.row-=dy;current.col-=dx;return false;}drawBlock();return true;
        }
        function rotate(dir){
            const {shape}=current;
            const trans=shape[0].map((_,i)=>shape.map(r=>r[i]));
            const rot=dir==='right'?trans.map(r=>r.reverse()):trans.reverse();
            const old=shape;current.shape=rot;
            if(!valid(current))current.shape=old;drawBlock();
        }
        function fix(){
            const {shape,row,col,color}=current;
            shape.forEach((r,i)=>r.forEach((v,j)=>{
                if(v){
                    const cell=grid[row+i][col+j];
                    cell.classList.add('fixed');cell.style.background='#555';
                }
            })); clearLines();
        }
        function clearLines(){
            let lines=0;
            for(let r=ROWS-1;r>=0;r--){
                if(grid[r].every(c=>c.classList.contains('fixed'))){
                    lines++; grid[r].forEach(c=>{c.className='cell';c.style.background='';});
                    for(let rr=r;rr>0;rr--)for(let cc=0;cc<COLS;cc++){
                        const src=grid[rr-1][cc]; const tgt=grid[rr][cc];
                        tgt.className=src.className; tgt.style.background=src.style.background;
                    }
                    r++; }
            }
            if(lines>0){ score+=lines*lines*100;scoreEl.textContent=`Score: ${score}`; }
        }
        function checkEnd(){return !valid({shape:current.shape,row:0,col:current.col});}
        function end(){clearInterval(intervalId);gameOverFlag=true;restartBtn.style.display='block';}
        function step(){if(!move(0,1)){if(current.row<0||checkEnd()){end();return;}fix();spawn();drawBlock();}}        
        function start(){initGrid();score=0;scoreEl.textContent='Score: 0';spawn();gameOverFlag=false;restartBtn.style.display='none';
            clearInterval(intervalId);intervalId=setInterval(step,500);
        }
        document.addEventListener('keydown',e=>{
            if(gameOverFlag)return;
            if(e.key==='ArrowLeft')move(-1,0);
            if(e.key==='ArrowRight')move(1,0);
            if(e.key==='ArrowDown')move(0,1);
            if(e.key==='z')rotate('left');
            if(e.key==='x')rotate('right');
        });
        restartBtn.addEventListener('click',start);
        start();
    </script>
</body>
</html>

アルゴリズムの流れ

ステップ処理内容
グリッド初期化initGrid() で20×10 のセルを DOM 上に生成し、spawn() で最初のブロックと次ブロックを準備
ブロック落下step() を 500ms ごとに呼び、move(0,1) で下に移動。移動不可なら fix() で固定化→行消去→次ブロック生成
行消去各行をチェックし、全て固定セルならクリア→上行をコピー→スコア加算
入力処理矢印キーで左右・下移動、Z/X キーで回転を move / rotate 関数で検証・実行
終了判定固定後に最上段でブロックが無効位置なら end() でループ停止・リスタートボタン表示

主な組み込みメソッド

メソッド/命令説明
setIntervalstep() を定期実行してブロックを自動で落下させる。
clearIntervalゲームオーバー時に自動落下を停止
document.createElementセル・ブロック要素を動的生成
element.classListCSS クラス追加・削除で表示状態を制御
addEventListenerキーボード/ボタンクリックイベントを監視・処理

関数の詳細

関数名機能・処理内容
initGrid()グリッドセルを生成して grid[r][c] に格納
spawn()next に格納しておいたテトロミノを current に昇格させ、次のテトロミノをランダム生成
randomTet()7 種類のテトロミノからランダムに選択し、色と形状オブジェクトを返す
valid()指定位置にテトロミノを置けるか衝突判定(境界 or 固定セル)
move(dx,dy)valid() 検証付きで current を左右/下/回転移動し、問題なければ描画
rotate(dir)形状行列を転置+左右反転で回転を生成し、valid() で戻すか適用
fix()current を固定セルに変えて clearLines() を呼び、盤を更新
clearLines()消せる行を検出→クリア→下行コピー→スコア更新
end()ゲームオーバー時にループ停止、リスタートボタン表示

改造のポイント

  • 自動落下速度(500ms)をレベルに応じて加速する機能を追加
  • ハードドロップ(スペースキーで即底まで落下)を実装
  • 「ホールド」機能で1つ前のテトロミノを一時保管できるよう拡張
  • 次ブロックを複数個表示し、先読みプレイをサポート
  • ラインクリア時のエフェクト(アニメーション/効果音)を追加
  • モバイル対応:画面タッチで左右スワイプやタップ回転を扱うイベントを実装

これらを参考に、自分だけのオリジナルテトリスを作りこんでみてください!