【ゲーム】JavaScript:06 テトリス(2人対戦)

 🕹️ テトリス 2人対戦は、20行×10列のフィールドを左右に並べて同時にプレイし、制限時間60秒または先に自分の盤面が積み上がったほうが負けとなるリアルタイム対戦パズルゲームです。7種類のテトリミノ(ブロック)を回転・移動させながら横一列を埋めて消去し、消した行数ぶんスコアを獲得します。

遊び方と操作方法

  • Player 1:左移動 A /右移動 D /下移動 S /回転 W
  • Player 2:左移動 ← /右移動 → /下移動 ↓ /回転 ↑
  1. ページ読み込み後、タイトル下の「残り時間: 60秒」から自動でカウントダウン開始。
  2. 落下してくるブロックを左右移動/回転し、隙間なく積み上げて一列を完成させると消去&スコア加算。
  3. 自分の「次のブロック」欄で、落ちてくる次の形をあらかじめ確認可能。

ルール

  • 横一列がすべて埋まるとその行が消え、上のブロックが下に落ちる。消去した行数ぶんスコア加算。
  • 自分の盤面上部(0行目)に固定ブロックが現れると積み上げ負けとなり、対戦相手の勝利。
  • 制限時間60秒経過時に残っているプレイヤー同士のスコアを比較し、高いほうが勝利。
  • どちらかが積み上げ負けしてもタイマーは停止し、即敗北となる。

🎮ゲームプレイ

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

06 テトリス(2人対戦)

素材のダウンロード

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

tetris2p_bg.png

ゲーム画面イメージ

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

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>🕹️ テトリス 2人対戦</title>
    <style>
        /* 背景画像 */
        body {
            display: flex;
            flex-direction: column;
            justify-content: center;
            align-items: center;
            height: 100vh;
            margin: 0;
            font-family: Arial, sans-serif;
            color: #fff;
            background: url('tetris2p_bg.png') no-repeat center / cover;
        }
        /* 背景半透明オーバーレイ */
        #overlay {
            position: absolute;
            top: 0; left: 0; right: 0; bottom: 0;
            background: rgba(0,0,0,0.5);
            z-index: 0;
        }
        /* 見出し・タイマー・メッセージの読みやすさ向上 */
        h1, #timer, #message {
            position: relative;
            z-index: 1;
            background: rgba(0,0,0,0.7);
            padding: 10px 20px;
            border-radius: 5px;
            text-align: center;
        }
        /* 残り時間とメッセージのフォントサイズ調整 */
        #timer, #message {
            font-size: 20px;
            font-weight: bold;
        }
        /* ゲームフィールド */
        #container {
            display: flex;
            gap: 50px;
            position: relative;
            z-index: 1;
        }
        .game-container {
            display: flex;
            flex-direction: column;
            align-items: center;
            gap: 10px;
        }
        /* game と info を横並びにする wrapper */
        .game-row {
            display: flex;
            align-items: center;
            gap: 10px;
            flex-wrap: nowrap;
        }
        /* Player1: info を左、game を右 */
        .player1-row {
            flex-direction: row;
        }
        /* Player2: game を左、info を右 */
        .player2-row {
            flex-direction: row-reverse;
        }
        .game {
            display: grid;
            grid-template-rows: repeat(20, 30px);
            grid-template-columns: repeat(10, 30px);
            gap: 1px;
            background-color: #111;
            border: 2px solid #fff;
        }
        .cell { width: 30px; height: 30px; background: #222; }
        .fixed { background-color: #555!important; }
        .block { /* ブロックはインラインスタイルで色設定 */ }
        .info { display: flex; flex-direction: column; align-items: center; }
        .score { font-size: 18px; margin-bottom: 10px; }
        .next-block { display: grid; grid-template-rows: repeat(4,30px); grid-template-columns: repeat(4,30px); gap:1px; }
        .next-cell { width:30px; height:30px; background:#222; }
        #restart-btn {
            position: relative;
            z-index: 1;
            margin-top: 20px;
            padding: 10px 20px;
            font-size: 16px;
            background-color: #007bff;
            border: none;
            border-radius: 5px;
            color: white;
            cursor: pointer;
        }
        #restart-btn:hover { background-color: #0056b3; }
    </style>
</head>
<body>
    <div id="overlay"></div>
    <!-- タイトル -->
    <h1>🕹️ テトリス 2人対戦</h1>
    <!-- タイマー -->
    <div id="timer">残り時間: 60秒</div>
    <!-- フィールド -->
    <div id="container">
        <!-- プレイヤー1 -->
        <div class="game-container">
            <h2>👤 Player 1</h2>
            <div class="game-row player1-row">
                <div class="info">
                    <div class="score" id="score1">スコア: 0</div>
                    <div class="next-block" id="next1"></div>
                </div>
                <div class="game" id="game1"></div>
            </div>
        </div>
        <!-- プレイヤー2 -->
        <div class="game-container">
            <h2>👤 Player 2</h2>
            <div class="game-row player2-row">
                <div class="game" id="game2"></div>
                <div class="info">
                    <div class="score" id="score2">スコア: 0</div>
                    <div class="next-block" id="next2"></div>
                </div>
            </div>
        </div>
    </div>
    <!-- メッセージ -->
    <div id="message"></div>
    <!-- リスタート -->
    <button id="restart-btn">🔄 もう一回プレイする</button>

    <script>
        // グローバルタイマー
        let time = 60;
        let timer = null;

        class Tetris {
            constructor(gameEl, scoreEl, nextEl, opponent, controls, name) {
                this.gameEl   = gameEl;
                this.scoreEl  = scoreEl;
                this.nextEl   = nextEl;
                this.opponent = opponent;
                this.controls = controls;
                this.name     = name;
                this.rows     = 20;
                this.cols     = 10;
                this.grid     = [];
                this.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]]
                ];
                this.colors = ['#e74c3c','#3498db','#f1c40f','#2ecc71','#9b59b6','#e67e22','#1abc9c'];

                this.initGrid();
                this.startGame();
                this.addKeyListeners();
            }

            initGrid() {
                this.grid = [];
                this.gameEl.innerHTML = '';
                for (let r = 0; r < this.rows; r++) {
                    this.grid[r] = [];
                    for (let c = 0; c < this.cols; c++) {
                        const cell = document.createElement('div');
                        cell.classList.add('cell');
                        this.gameEl.appendChild(cell);
                        this.grid[r][c] = cell;
                    }
                }
            }

            initNext() {
                this.nextEl.innerHTML = '';
                for (let i = 0; i < 16; i++) {
                    const cell = document.createElement('div');
                    cell.classList.add('next-cell');
                    this.nextEl.appendChild(cell);
                }
            }

            displayNext() {
                const cells = this.nextEl.querySelectorAll('.next-cell');
                cells.forEach(c => c.style.background = '#222');
                const { shape, color } = this.next;
                shape.forEach((r, i) => {
                    r.forEach((v, j) => {
                        if (v) cells[i * 4 + j].style.background = color;
                    });
                });
            }

            genBlock() {
                const idx = Math.floor(Math.random() * this.tetrominoes.length);
                return { shape: this.tetrominoes[idx], color: this.colors[idx] };
            }

            startGame() {
                this.initGrid();
                this.initNext();
                const first = this.genBlock();
                this.current = { ...first, row: 0, col: Math.floor(this.cols/2) - 1 };
                this.next    = this.genBlock();
                this.displayNext();
                this.score   = 0;
                this.scoreEl.textContent = `スコア: ${this.score}`;
                this.isGameOver = false;
                clearInterval(this.interval);
                this.interval = setInterval(() => { if (!this.isGameOver) this.move(0,1); }, 500);
            }

            draw() {
                this.clear();
                const { shape, row, col, color } = this.current;
                shape.forEach((r, i) => {
                    r.forEach((v, j) => {
                        if (v && this.grid[row+i]?.[col+j]) {
                            const cell = this.grid[row+i][col+j];
                            cell.classList.add('block');
                            cell.style.background = color;
                        }
                    });
                });
            }

            clear() {
                this.grid.flat().forEach(c => {
                    c.classList.remove('block');
                    if (!c.classList.contains('fixed')) c.style.background = '#222';
                });
            }

            valid(shape, row, col) {
                return shape.every((r,i) => r.every((v,j) => {
                    if (!v) return true;
                    const nr = row + i;
                    const nc = col + j;
                    return nr>=0 && nr<this.rows && nc>=0 && nc<this.cols && !this.grid[nr][nc].classList.contains('fixed');
                }));
            }

            move(dx, dy) {
                this.current.row += dy;
                this.current.col += dx;
                if (!this.valid(this.current.shape, this.current.row, this.current.col)) {
                    this.current.row -= dy;
                    this.current.col -= dx;
                    if (dy === 1) {
                        this.fix();
                        this.clearLines();
                        if (this.checkTop()) { this.handleStackLose(); return; }
                        this.spawnNext();
                    }
                }
                this.draw();
            }

            rotate() {
                const { shape, row, col } = this.current;
                const ns = shape[0].map((_,i) => shape.map(r => r[i]).reverse());
                if (this.valid(ns,row,col)) { this.current.shape=ns; this.draw(); }
            }

            fix() {
                const { shape,row,col } = this.current;
                shape.forEach((r,i) => r.forEach((v,j) => {
                    if (v && this.grid[row+i]?.[col+j]) {
                        const cell = this.grid[row+i][col+j];
                        cell.classList.add('fixed');
                        cell.style.background = '';
                    }
                }));
            }

            clearLines() {
                let cnt = 0;
                for (let i = 0; i < this.rows; i++) {
                    if (this.grid[i].every(c => c.classList.contains('fixed'))) {
                        this.grid[i].forEach(c => c.classList.remove('fixed'));
                        for (let r = i; r > 0; r--) {
                            for (let c = 0; c < this.cols; c++) {
                                if (this.grid[r-1][c].classList.contains('fixed')) {
                                    this.grid[r][c].classList.add('fixed');
                                } else {
                                    this.grid[r][c].classList.remove('fixed');
                                }
                            }
                        }
                        cnt++;
                    }
                }
                if (cnt > 0) {
                    this.score += cnt;
                    this.scoreEl.textContent = `スコア: ${this.score}`;
                    // 相手にブロックを送らない
                }
            }

            checkTop() {
                return this.grid[0].some(c => c.classList.contains('fixed'));
            }

            handleStackLose() {
                this.isGameOver = true;
                clearInterval(this.interval);
                if (this.opponent) {
                    this.opponent.isGameOver = true;
                    clearInterval(this.opponent.interval);
                }
                clearInterval(timer);
                time = 0;
                document.getElementById('timer').textContent = '残り時間: 0秒';
                const winner = this.opponent.name;
                document.getElementById('message').textContent = `💥 ${this.name} がブロックを積み上げました!🥇 勝者: ${winner}`;
            }

            spawnNext() {
                this.current = { ...this.next, row: 0, col: Math.floor(this.cols/2) - 1 };
                this.next = this.genBlock();
                this.displayNext();
            }

            addKeyListeners() {
                document.addEventListener('keydown', e => {
                    if (this.isGameOver) return;
                    if (this.controls.player1) {
                        if (e.key === 'a') this.move(-1,0);
                        if (e.key === 'd') this.move(1,0);
                        if (e.key === 's') this.move(0,1);
                        if (e.key === 'w') this.rotate();
                    }
                    if (this.controls.player2) {
                        if (e.key === 'ArrowLeft') this.move(-1,0);
                        if (e.key === 'ArrowRight') this.move(1,0);
                        if (e.key === 'ArrowDown') this.move(0,1);
                        if (e.key === 'ArrowUp') this.rotate();
                    }
                });
            }
        }

        // プレイヤーインスタンス生成
        const p1 = new Tetris(
            document.getElementById('game1'),
            document.getElementById('score1'),
            document.getElementById('next1'),
            null,
            { player1: true, player2: false },
            'Player 1'
        );
        const p2 = new Tetris(
            document.getElementById('game2'),
            document.getElementById('score2'),
            document.getElementById('next2'),
            p1,
            { player1: false, player2: true },
            'Player 2'
        );
        p1.opponent = p2;

        // タイマー制御
        function startTimer() {
            clearInterval(timer);
            time = 60;
            document.getElementById('timer').textContent = `残り時間: ${time}秒`;
            timer = setInterval(() => {
                time--;
                document.getElementById('timer').textContent = `残り時間: ${time}秒`;
                if (time <= 0) {
                    clearInterval(timer);
                    p1.isGameOver = true; clearInterval(p1.interval);
                    p2.isGameOver = true; clearInterval(p2.interval);
                    document.getElementById('timer').textContent = '残り時間: 0秒';
                    const s1 = p1.score, s2 = p2.score;
                    const msgEl = document.getElementById('message');
                    if (s1 > s2) msgEl.textContent = `⏰ 時間切れ!🥇 勝者: Player 1`;
                    else if (s2 > s1) msgEl.textContent = `⏰ 時間切れ!🥇 勝者: Player 2`;
                    else msgEl.textContent = `⏰ 時間切れ!🏅 引き分け`;
                }
            }, 1000);
        }
        startTimer();

        // リスタートボタン
        document.getElementById('restart-btn').addEventListener('click', () => {
            document.getElementById('message').textContent = '';
            p1.startGame(); p2.startGame(); startTimer();
        });
    </script>
</body>
</html>

アルゴリズムの流れ

ステップ処理概要
タイマー開始startTimer() で60秒カウントダウンを開始。0秒で両プレイヤー停止&勝者判定
盤面および次ブロック初期化initGrid() で20×10セルを生成、initNext() で4×4「次のブロック」欄を生成
テトロミノ生成genBlock() でランダムに形と色のオブジェクトを返却
ゲーム開始/リセットstartGame()currentnext をセットし、500ms間隔で move(0,1) を自動実行
描画・クリアdraw() で現在ブロックを表示、clear() で以前の描画を消去
移動検証valid() で移動・回転できるか判定(境界・固定セルとの衝突チェック)
固定化fix() で落下不能時にブロックを fixed セル化
行消去clearLines() で埋まった行を削除・上行を下へ落とし、スコア加算
積み上げ敗北判定checkTop() で0行目に固定セルがあるか判定。あれば handleStackLose() で敗北処理
次ブロックへ移行spawnNext()nextcurrent に昇格し、新たな next を生成・表示
入力監視addKeyListeners() で各プレイヤーごとのキー操作(移動・回転)を監視

関数の詳細

関数名機能
initGrid()ゲーム領域にセル要素を 20×10 個生成して this.grid に保持
initNext()次のブロック表示用に 4×4 のセル要素を生成
genBlock()7 種類のテトロミノからランダムに選択し、shapecolor をオブジェクトで返却
startGame()盤面初期化・次ブロック初期化・スコアリセット・落下ループ開始
draw()clear() 後、current の形状分だけセルに block クラスと色を適用
clear()すべてのセルから block クラスを除去し、固定セル以外を背景色 #222 に戻す
valid(shape,row,col)形状を与えた位置に置いても盤外/固定セルと衝突しないかをチェック
move(dx,dy)current を移動 → valid() 検証 → 落下失敗時に fix()clearLines()spawnNext() を実行
rotate()行列転置+反転で回転形状を作成 → valid() で通れば current.shape を更新
fix()current.fixed クラス化し、セルを固定
clearLines()完全に固定セルで埋まった行を削除し、上行を下にコピー → カウントした行数をスコアに加算
checkTop()0行目に固定セルがあるかを返し、あるときは積み上げ敗北
handleStackLose()積み上げ負け時に各ループ&タイマー停止、勝者メッセージ表示
spawnNext()nextcurrent に設定し、新たに next を生成・表示
addKeyListeners()ドキュメント全体でキーイベントを監視し、Player 1/Player 2 用の操作を振り分け

改造のポイント

  • ガベージ(ゴミ)送信機能:消した行数に応じて相手の盤面に「固定行」を送るメカニズムを追加
  • レベルアップ制:スコアに応じて落下速度(インターバル)を加速させ、ヒットエフェクトを強化
  • ホールド機能:任意のキーで「ホールド」できる枠を設け、一手だけ次のブロックを温存可能に
  • 複数次ブロック表示:先読み用に次の 2–3 個を表示する領域を拡張
  • スマホ対応:画面タッチ・スワイプ操作で移動・回転 を実装
  • サウンド・エフェクト:行消去や積み上げ敗北時に効果音/アニメーションを追加
  • リプレイ/観戦モード:操作履歴を記録して再生、または第三者が同時に観戦できるビューを追加

 これらを取り入れると、さらに深みのある2人対戦テトリスを実現できます。ぜひ自分だけのアレンジを加えてみてください!