このサイトはアフィリエイトリンクを含んでいます

【HTML/CSS/JS】レスポンシブ対応テトリスゲームの作り方を解説!スマホでも遊べる! (ver1.9.9)

テトリスレスポンシブルデザイン強化 作ってみた!

HTML、CSS、JavaScriptを使って、誰でも簡単に作れるレスポンシブ対応のテトリスゲームの作り方を解説します。ver1.8.8からver1.9.9へのアップデートで強化されたモバイルフレンドリーなデザインと実装ポイントを、実際のソースコードを交えながら初心者にも分かりやすく説明します。PCだけでなくスマホでも快適に遊べるテトリスゲームを自作してみましょう!

あんちゃん
あんちゃん

PCブラウザ版での操作方法はこれさ↓
Qキーは左回り、Wキーは右回りでブロックが回るよ。

左右の矢印キーでブロックを左右に動かせるよ!

下矢印キーはブロックが下に落ちます~。

最初に「START」ボタンを押してね~。

スマホ版はゲーム下のボタンで操作してください!

テトリスゲームの基本:HTMLでゲームの土台を作ろう

テトリス 最初 土台 ゲーム 枠組み

テトリスゲームの土台を作るには、まずHTMLがとても大切なんです。ここでしっかりとした基礎を作っておくと、後のステップがスムーズになりますよ。ゲームの中心になるのは、やっぱりcanvasタグ。これが、ゲーム画面を表すところなんですね。テトリスのブロックが降ってきて、積み重なっていく場所は、このcanvasの中で描かれるんです。なんだか、キャンバスに絵を描いていくみたいで楽しいですね!

それから、ゲームを遊ぶために、プレイヤーに必要な情報やボタンもHTMLで用意してあげることが大事です。例えば、スコアの表示スタートボタン。これらがちゃんと配置されていると、ゲームが始まったときに「今どのくらいのスコアかな?」とか「ゲームをスタートさせるぞ!」って気持ちになりますよね。HTMLでは、こういう大事な部分をしっかりと決めてあげるんです。

それに、テトリスはパソコンだけじゃなくて、スマホでも遊べるようにしたいです。今の時代、みんなスマホを使いますからね。そのためには、レスポンシブデザインがとても役に立ちます。画面の大きさに合わせて、うまくレイアウトを変えてあげれば、どんなデバイスでも快適に遊べるんです。スマホで遊ぶ時に画面が見にくかったり操作しにくかったりしないように、しっかりと考えておきましょう。

さらに、タッチパネルで操作する場合には、タッチに対応したボタンを用意してあげると、スマホでも気持ちよく操作できますよね。ここでは、HTMLでそのボタンの配置を考えてあげるだけでも、後のプログラミングがとっても楽になります。タッチする場所がわかりやすいと、プレイヤーも安心して遊べるんです。

こんな感じで、HTMLでしっかりとゲームの基礎を作り上げたら、テトリスのゲーム部分がぐっと形になってきますね。テトリスの世界が少しずつできてくるのを感じる瞬間です。

canvas要素でゲーム画面を準備

テトリスゲームを作る際に重要な要素の一つが、canvas要素です。このcanvasは、テトリスのブロックが落ちてくるゲームの舞台となる場所です。canvas要素を使うことで、JavaScriptを通じて自由に絵やアニメーションを描くことができるので、テトリスのような動きのあるゲームを表現するのにぴったりです。

さて、このcanvas要素の役割についてお話ししますね。canvasは、画面上に設置されたキャンバスのようなもので、まさにテトリスのブロックが落ちてくる場所なんです。普通のHTML要素と違って、canvasは静的な表示ではなく、JavaScriptを使って動的に描画を行います。たとえば、ブロックの移動、回転、積み重なり、ラインの消去といった動きが、このcanvasの中で実現されるんですよ。

そして、canvas要素はただ配置するだけではなく、サイズ指定がとても大切です。ゲームの見栄えやプレイ感覚に直結する部分なので、canvasの幅や高さを適切に設定しなければなりません。ここで気をつけたいのは、テトリスはPCやスマホなど、様々なデバイスで遊ばれることが想定されていますよね?そのため、レスポンシブ対応が非常に重要になってきます。

特に、ver1.9.9では、canvas要素のサイズがレスポンシブ対応として最適化されています。つまり、画面の幅や高さに応じて自動的にサイズが調整されるように設定されているんです。これによって、PCの大きな画面でもスマホの小さな画面でも、同じように快適にゲームが楽しめるようになっています。具体的には、画面のサイズに合わせて、canvasのサイズが自動でリサイズされ、ゲームの表示が崩れないようにする機能が追加されました。

たとえば、スマホでは縦長の画面が一般的ですが、その場合でもゲーム画面がぴったりフィットするように設定できるんです。また、横画面にしたときも、違和感なくブロックが落ちてくる場所が広がって見えるようになっています。これが、テトリスをどんなデバイスでも快適に楽しめる理由のひとつです。

また、canvas要素のサイズはCSSやJavaScriptを使って柔軟に変更できます。例えば、スマホの縦画面ではcanvasの縦幅を長めに設定して、ブロックが落ちてくる距離を長く見せたり、PC画面では横幅を広げて、より多くのブロックが一度に見えるようにしたりと、いろいろな工夫ができるんです。このように、サイズの調整ひとつでゲームのプレイ体験が大きく変わるんですね。

このcanvas要素がうまく機能することで、テトリスの基礎となるゲーム画面が完成します。次にJavaScriptを使ってブロックの動きやスコアの計算などを実装していくわけですが、このcanvasの準備がしっかりしていることが、スムーズなゲーム作りへの第一歩となります。

これで、canvas要素の基本と、サイズのレスポンシブ対応についてのお話はおしまいです。

スコア表示やスタートボタンなどのUI要素を追加

テトリスゲームを作るとき、ゲームの楽しさを引き立てるために必要なのが、スコア表示やスタートボタンなどのUI要素です。このUI要素は、プレイヤーがゲームを操作したり、進行状況を確認したりするために欠かせない部分なんですよ。では、どうやってこれらのUIをHTMLとCSSで実装するのか、見ていきましょうね。

まず、HTMLを使って、スコア表示エリアスタートボタン、そしてテトリスのNEXTピース表示エリアを設置します。スコア表示エリアでは、ゲームの進行中にプレイヤーが現在のスコアをすぐに確認できるようにしておきます。これは、プレイヤーが自分の成績を追いながらゲームを進めるモチベーションにもなりますよね。スタートボタンはゲーム開始の合図となる重要な操作部で、ゲームをリセットしたり再スタートするためにも必要です。NEXTピースの表示エリアは、次に落ちてくるブロックを事前に表示して、プレイヤーが戦略を立てやすくするための工夫です。

これらのUI要素を設置することで、ゲームをよりインタラクティブで操作しやすいものにできます。HTMLのコードでは、それぞれのエリアを<div>タグなどを使って、見やすく整理して配置します。スコア表示は、シンプルに<div>タグの中にスコアを表示する仕組みを入れるのが基本です。そして、スタートボタンは<button>タグで作成し、「スタート」や「リセット」といった文字を入れて、視覚的にわかりやすいものにします。NEXTピース表示エリアは、次に来るブロックを小さなサンプルとして表示するための場所です。

HTMLでレイアウトを作ったら、次はCSSでデザインを整えます。ここで大事なのは、見やすさと操作しやすさを考慮することです。スコア表示エリアはゲーム画面に溶け込むようなシンプルなデザインにすることがポイントです。例えば、フォントサイズを適切に設定して、プレイヤーがゲーム中でもスコアが一目で確認できるようにしましょう。また、スタートボタンも重要で、プレイヤーがすぐに見つけられる位置に配置し、押しやすい大きさや色でデザインします。色や大きさをCSSで調整して、視覚的にも押したくなるようなデザインに仕上げるのがコツです。

ver1.9.9のテトリスゲームでは、これらのUI要素のデザインが特に強化されています。レスポンシブ対応を意識して、PCやスマホ、タブレットなど、さまざまなデバイスでも同じように使いやすいUIを作ることができます。CSSのメディアクエリを使って、画面サイズに応じたレイアウトの調整を行うことで、デバイスごとに最適化されたUIデザインを提供できるんです。例えば、スマホではボタンを少し大きめにしてタッチ操作しやすくしたり、スコアの表示を画面の上部に配置してゲーム画面が広く使えるように工夫したりするんです。

また、モバイルフレンドリーな設計を意識することも重要です。スマホでゲームを楽しむ場合、タッチ操作が中心になるため、ボタンの配置やサイズが特に大事になります。CSSを使って、ボタンのサイズや配置を適切に調整することで、プレイヤーがゲームに集中しやすい環境を作ってあげましょう。

これで、テトリスゲームのUI要素を追加するための準備が整いました。これらのUI要素がしっかりと配置され、見やすくデザインされていれば、プレイヤーはスムーズにゲームを進めることができ、より楽しめるようになりますね。

JavaScriptでゲームロジックを実装するための準備

テトリスゲームを作るために、JavaScriptでゲームロジックを実装する準備はとても大事なステップです。まずは、canvas要素に描画するためのJavaScriptの基本的なコード構造をしっかりと理解しましょうね。この段階でしっかりとした準備をすることで、後からブロックの動きやスコア計算などのゲームロジックをスムーズに実装できるようになります。

最初にすることは、canvasに実際に描画するためのJavaScriptコードの枠組みを作ることです。この枠組みは、テトリスのようなゲームで動的なグラフィックスを扱うための基礎となります。canvasはHTMLで描画する領域を確保していますが、実際にその中にブロックを描いたり動かしたりするのはJavaScriptの役目です。

ゲームロジックを進めるためには、まず変数と関数の定義が必要です。変数はゲームの状態を管理するために使われます。例えば、現在のスコア、今落ちているブロックの位置や形、次に来るブロックの情報などが変数で管理されます。これらの変数を適切に設定し、ゲームの状態を追跡できるようにすることがとても大切です。

次に、テトリスゲームのような動的な動きを作り出すためには、関数の定義が欠かせません。関数は、ブロックの動きや描画、スコアの更新など、特定の処理をまとめて実行するものです。例えば、ブロックが1段下に落ちるたびに呼び出される関数や、ブロックを回転させるための関数などが必要になります。

さらに、ゲーム全体の進行を管理するためのメインループとなる関数も重要です。このメインループでは、一定のタイミングで画面を更新し、ブロックが落ちる動作や、ユーザーの入力に応じたアクション(左右に動かしたり、回転させたり)を処理します。このように、関数を使ってゲームの流れをスムーズに管理することが、快適なプレイ体験につながりますよ。

ver1.9.9のアップデートでは、特にレスポンシブ対応が強化されています。例えば、canvasのサイズがデバイスによって自動で調整されるため、スマホやPC、タブレットなど、どんなデバイスでもプレイがしやすいようになっています。JavaScriptを使って画面のリサイズに対応するコードもこの時点で準備しておくと、さまざまなデバイスに対応したスムーズなゲーム体験を提供できます。

また、スマホ対応のためにはタッチイベントの準備も必要です。JavaScriptでは、キーボードの操作だけでなく、タッチパネルでの操作にも対応できるようにします。これによって、スマホユーザーが直感的にブロックを動かせるようになるので、モバイルフレンドリーな設計を意識した作りが求められますね。

こうした準備がしっかりできていれば、次に進んで実際のゲームロジック(ブロックの落下やスコア管理)を実装する際に、スムーズに進められます。しっかりした基礎作りがあれば、テトリスの楽しさが存分に表現できるようになりますよ!

テトリスの心臓部:JavaScriptでゲームロジックを実装

テトリスの心臓部 Javascript ロジック

テトリスゲームの心臓部とも言える部分は、JavaScriptで実装するゲームロジックです。このゲームロジックがしっかりと動作することで、テトリスの楽しさが全開になるんです。ブロックが上から落ちてきて、回転しながらフィールドにぴったりとはまり、ラインがそろうと消えていく…。そのすべての動作をコントロールするのがJavaScriptの役割です。

このH2で触れる「テトリスの心臓部」では、テトリスの基本的なゲームロジックをJavaScriptでどのように実装するかを考えていきます。テトリスはシンプルなゲームに見えるけれど、実際に作るとなると、ブロックの動き、衝突判定、ライン消去など、さまざまな要素がしっかり組み合わさって成り立っています。

まず、JavaScriptでブロックが自然に落下していく動きを実装します。この部分はゲームの核となるアニメーション機能で、時間経過とともにブロックが少しずつ下に移動します。プレイヤーが何も操作しなくても、ブロックは一定の速度で自動的に落ちていく仕組みです。プレイヤーが介入してブロックを動かしたり、回転させたりできるのは、ゲームが動いている間中ずっと実行されるメインループの一部として考えられます。

次に、ブロック同士やフィールドの端との衝突判定です。ブロックが他のブロックにぶつかったり、フィールドの底に到達すると、それ以上落ちないように停止する必要があります。この判定が正確でないと、ゲームのバランスが崩れてしまいますので、とても大事な処理なんです。そして、衝突したブロックはフィールドに固定され、新しいブロックが落ちてくる流れへと進んでいきます。

さらに、JavaScriptでは、プレイヤーの操作もきちんと取り込む必要があります。キーボードの矢印キーやスペースキーを使って、ブロックを左右に移動させたり、回転させたり、高速で落としたりする機能を実装します。ここで使うのが、JavaScriptのイベントリスナー。プレイヤーがキーを押した瞬間に、その入力に応じた動作をブロックに反映させます。リアルタイムで反応する操作感は、テトリスの魅力の一つですよね。

そして、この操作や動作を組み合わせて、ゲームオーバーの条件もJavaScriptで設定します。ブロックが積み重なって画面の上まで到達してしまったらゲームオーバー。プレイヤーがどれだけ長くブロックを消してスコアを積み上げられるかが、テトリスの大きな挑戦となります。このゲームオーバーの処理もJavaScriptのロジックで管理されます。

JavaScriptでこうしたゲームロジックを組み立てることで、テトリスの「動き」と「戦略」が生まれてきます。単純にブロックを積み重ねるだけではなく、いかにラインを消していくか、そしてどれだけ長く続けられるかという要素が加わることで、ゲームがどんどん面白くなるんです。

これが、テトリスの心臓部となるJavaScriptによるゲームロジックの概要です。しっかりしたゲームロジックを構築することで、テトリスの世界がより深みを増し、プレイヤーに楽しさを提供することができるんですよ!

ブロックの落下と制御

  • ブロックのデータ構造と生成
  • 落下処理の実装 (ver1.9.9でのupdate関数とrequestAnimationFrame)
  • キーボード入力による左右移動、回転、高速落下の実装
  • タッチイベントによるスマホ操作への対応 (ver1.8.8からの変更点)

ブロックの衝突判定と固定

  • ブロックがフィールドの底辺や他のブロックに衝突したかどうかの判定
  • 衝突時のブロックの固定処理

ラインの消去とスコアの加算

  • 一行揃ったラインの検出と消去処理
  • 消去したライン数に応じたスコアの加算 (ver1.9.9でのレベルアップ)
  • スコア表示の更新
  • 効果音の再生

レスポンシブデザインでスマホ対応!

CSSメディアクエリで画面サイズに合わせたレイアウト調整

  • PCとスマホで異なるレイアウトを実現するメディアクエリの活用
  • 画面サイズに応じたゲーム画面とUI要素のサイズ調整 (ver1.9.9でのresizeCanvas関数)
  • 縦画面と横画面への対応 (ver1.9.9でのCSS改善)

タッチ操作に最適化されたボタン配置

  • スマホでの操作性を考慮したボタンのサイズと配置 (ver1.8.8からの改善点)

モバイルファーストな設計

  • スマホでの表示を優先したCSS設計

ゲームオーバー処理

ゲームオーバー判定

  • ブロックがフィールドの上限に達した場合のゲームオーバー判定

ゲームオーバー時の表示

  • ゲームオーバー画面の表示と最終スコアの表示

ゲームの再スタート

  • スタートボタンによるゲームの再スタート処理 (ver1.9.9でのstartGame関数)

まとめ

今回の記事では、HTML、CSS、JavaScriptを使ってレスポンシブ対応のテトリスゲームを作る方法を解説しました。ver1.8.8からver1.9.9へのアップデートによって、スマホでも快適に遊べるように、タッチ操作の改善や画面サイズの自動調整などが行われました。ぜひ今回の記事を参考に、自分だけのテトリスゲームを作ってみてください!

あんちゃん
あんちゃん

ずっともやもやしてたんです。それはテトリスのレスポンシブルデザインがちょっと微妙だなと・・・

ようやく、レスポンシブルデザイン強化版ができました。。ハァハァ。。。

どうぞー

<!DOCTYPE html> 
<html lang="ja">
<head>
    <meta charset="utf-8">
    <title>テトリスゲーム Ver1.9.9</title> 
    <style>
        /* リセットCSS */
        * {
            box-sizing: border-box;
            margin: 0;
            padding: 0;
        }
        html, body {
            width: 100%;
            height: 100%;
            overflow: hidden;
            background-image: url('https://tokodomo.xyz/wp-content/uploads/2024/09/tetris_ver1.9_background.webp');
            background-size: cover;
            background-position: center;
            display: flex;
            flex-direction: column;
        }
        #gameContainer {
            flex-grow: 1;
            display: flex;
            justify-content: center;
            align-items: center;
            position: relative;
        }
        #gameWrapper {
            position: relative;
            display: inline-block;
        }
        #game {
            background-color: #000;
            display: block;
        }
        #nextPieceContainer {
            position: absolute;
            top: 10px;
            left: 100%; /* 左端をcanvasの右端に合わせる */
            margin-left: 10px; /* canvasとの間に余白を設定 */
            text-align: center;
        }
        #nextPiece {
            background-color: rgba(255, 255, 255, 0.7);
            margin-bottom: 5px;
        }
        #nextLabel {
            font-size: 0.8em;
            color: white;
            letter-spacing: 2px;
            text-transform: uppercase;
            font-weight: bold;
        }
        #scoreContainer {
            position: absolute;
            top: 10px;
            left: 50%;
            transform: translateX(-50%);
            background-color: rgba(255, 255, 255, 0.7);
            padding: 5px 10px;
            border-radius: 5px;
            font-size: 1em;
            z-index: 10;
            display: flex;
            align-items: center;
        }
        #scoreContainer button {
            margin-left: 10px;
            padding: 5px 10px;
            font-size: 1em;
        }
        #finalScore {
            position: absolute;
            top: 50%;
            left: 50%;
            transform: translate(-50%, -50%);
            background-color: rgba(255, 255, 255, 0.9);
            padding: 20px;
            font-size: 1.5em;
            color: red;
            text-align: center;
            display: none;
            z-index: 20;
        }
        #controls {
            position: absolute;
            bottom: 10px;
            left: 50%;
            transform: translateX(-50%);
            display: flex;
            gap: 10px;
            z-index: 10;
        }
        .control-button {
            width: 50px;
            height: 50px;
            background-color: rgba(255, 255, 255, 0.7);
            border: none;
            border-radius: 50%;
            font-size: 1.5em;
            font-weight: bold;
            color: black;
            display: flex;
            justify-content: center;
            align-items: center;
        }
        /* レスポンシブデザイン */
        @media (min-width: 768px) {
            #controls {
                bottom: 20px;
            }
            #scoreContainer {
                top: 20px;
                font-size: 1.2em;
            }
            #scoreContainer button {
                font-size: 1em;
            }
            #nextLabel {
                font-size: 1em;
            }
            #finalScore {
                font-size: 2em;
            }
            .control-button {
                width: 60px;
                height: 60px;
                font-size: 2em;
            }
        }
    </style>
</head>
<body>
    <div id="gameContainer">
        <div id="gameWrapper">
            <canvas id="game"></canvas>
            <div id="nextPieceContainer">
                <canvas id="nextPiece"></canvas>
                <div id="nextLabel">Next</div>
            </div>
        </div>
        <div id="scoreContainer">
            <div id="score">Score: 0</div>
            <button id="startButton" onclick="startGame()">START</button>
        </div>
        <div id="finalScore">GAME OVER<br>SCORE: 0</div>
        <div id="controls">
            <button class="control-button" id="leftButton">&larr;</button>
            <button class="control-button" id="rotateButton">&#x21BB;</button>
            <button class="control-button" id="rightButton">&rarr;</button>
            <button class="control-button" id="dropButton">&#x2B07;</button>
        </div>
    </div>

    <!-- BGM用のオーディオ要素 -->
    <audio id="bgm" loop>
        <source src="https://tokodomo.xyz/wp-content/uploads/2024/09/Fall-of-the-Blocks.mp3" type="audio/mpeg">
    </audio>

    <script>
        // Canvasの初期設定
        const canvas = document.getElementById('game');
        const context = canvas.getContext('2d');
        const nextCanvas = document.getElementById('nextPiece');
        const nextContext = nextCanvas.getContext('2d');

        // レスポンシブ対応のためのサイズ調整関数
        function resizeCanvas() {
            const gameWidth = Math.min(window.innerWidth * 0.4, 300);
            const gameHeight = gameWidth * (20 / 12); // 縦横比に基づく

            canvas.width = gameWidth;
            canvas.height = gameHeight;

            context.setTransform(gameWidth / 12, 0, 0, gameHeight / 20, 0, 0);

            // 次のピース表示のサイズ調整
            nextCanvas.width = gameWidth * 0.3;
            nextCanvas.height = nextCanvas.width;

            nextContext.setTransform(nextCanvas.width / 4, 0, 0, nextCanvas.height / 4, 0, 0);

            draw();
        }

        window.addEventListener('resize', resizeCanvas);

        // 効果音を再生する関数(リアルタイムに対応)
        function playSound(src) {
            const sound = new Audio(src);
            sound.volume = 0.8;  // 効果音のボリュームを上げる
            sound.play();
        }

        const bgm = document.getElementById('bgm');
        bgm.volume = 0.3;  // BGMのボリュームを下げる

        const arena = createMatrix(12, 20);

        const player = {
            pos: {x: 0, y: 0},
            matrix: null,
            score: 0,
            nextPiece: null,
        };

        const colors = [
            null,
            '#FF0D72',
            '#0DC2FF',
            '#0DFF72',
            '#F538FF',
            '#FF8E0D',
            '#FFE138',
            '#3877FF',
        ];

        let gameOver = false;
        let manualDrop = false;  // 手動ドロップを判定するフラグ
        let dropStart = false;
        let dropSpeed = 50;

        let linesCleared = 0;  // 消去されたライン数
        let level = 1;  // 初期レベル
        let dropInterval = 1000;  // 初期の落下速度(1秒)

        // スコアに基づいてレベルを更新する関数
        function updateLevel() {
            level = Math.floor(player.score / 100) + 1;
            dropInterval = Math.max(1000 - (level * 100), 100);  // レベルが上がるごとに落下速度が速くなる
        }

        function createMatrix(w, h) {
            const matrix = [];
            while (h--) {
                matrix.push(new Array(w).fill(0));
            }
            return matrix;
        }

        function createPiece(type) {
            if (type === 'T') {
                return [
                    [0, 0, 0],
                    [1, 1, 1],
                    [0, 1, 0],
                ];
            } else if (type === 'O') {
                return [
                    [2, 2],
                    [2, 2],
                ];
            } else if (type === 'L') {
                return [
                    [0, 3, 0],
                    [0, 3, 0],
                    [0, 3, 3],
                ];
            } else if (type === 'J') {
                return [
                    [0, 4, 0],
                    [0, 4, 0],
                    [4, 4, 0],
                ];
            } else if (type === 'I') {
                return [
                    [0, 5, 0, 0],
                    [0, 5, 0, 0],
                    [0, 5, 0, 0],
                    [0, 5, 0, 0],
                ];
            } else if (type === 'S') {
                return [
                    [0, 6, 6],
                    [6, 6, 0],
                    [0, 0, 0],
                ];
            } else if (type === 'Z') {
                return [
                    [7, 7, 0],
                    [0, 7, 7],
                    [0, 0, 0],
                ];
            }
        }

        function drawMatrix(matrix, offset, context) {
            matrix.forEach((row, y) => {
                row.forEach((value, x) => {
                    if (value !== 0) {
                        context.fillStyle = colors[value];
                        context.fillRect(x + offset.x, y + offset.y, 1, 1);
                    }
                });
            });
        }

        function draw() {
            context.clearRect(0, 0, canvas.width, canvas.height);
            drawMatrix(arena, {x: 0, y: 0}, context);
            drawMatrix(player.matrix, player.pos, context);

            nextContext.clearRect(0, 0, nextCanvas.width, nextCanvas.height);

            const nextPiece = player.nextPiece;

            // NEXTピースのバウンディングボックスを計算
            const bounds = getPieceBounds(nextPiece);
            const offsetX = Math.floor((4 - bounds.width) / 2 - bounds.minX);
            const offsetY = Math.floor((4 - bounds.height) / 2 - bounds.minY);

            drawMatrix(nextPiece, {x: offsetX, y: offsetY}, nextContext);
        }

        // ピースのバウンディングボックスを取得する関数
        function getPieceBounds(piece) {
            let minX = piece[0].length;
            let maxX = 0;
            let minY = piece.length;
            let maxY = 0;

            piece.forEach((row, y) => {
                row.forEach((value, x) => {
                    if (value !== 0) {
                        if (x < minX) minX = x;
                        if (x > maxX) maxX = x;
                        if (y < minY) minY = y;
                        if (y > maxY) maxY = y;
                    }
                });
            });

            return {
                minX,
                maxX,
                minY,
                maxY,
                width: maxX - minX + 1,
                height: maxY - minY + 1
            };
        }

        function merge(arena, player) {
            player.matrix.forEach((row, y) => {
                row.forEach((value, x) => {
                    if (value !== 0) {
                        arena[y + player.pos.y][x + player.pos.x] = value;
                    }
                });
            });
        }

        function rotate(matrix, dir) {
            for (let y = 0; y < matrix.length; ++y) {
                for (let x = 0; x < y; ++x) {
                    [matrix[x][y], matrix[y][x]] = [matrix[y][x], matrix[x][y]];
                }
            }

            if (dir > 0) {
                matrix.forEach(row => row.reverse());
            } else {
                matrix.reverse();
            }
        }

        function playerDrop() {
            player.pos.y++;
            if (collide(arena, player)) {
                player.pos.y--;
                merge(arena, player);
                playerReset();
                arenaSweep();
                updateScore();
                updateLevel();  // レベルを更新
                if (collide(arena, player)) {
                    gameOver = true;
                    displayFinalScore();
                }
                if (manualDrop) {
                    playSound("https://tokodomo.xyz/wp-content/uploads/2024/09/drop.mp3");
                    manualDrop = false;  // フラグをリセット
                }
            }
            dropCounter = 0;
        }

        function playerMove(offset) {
            player.pos.x += offset;
            if (collide(arena, player)) {
                player.pos.x -= offset;
            }
        }

        function playerReset() {
            if (player.nextPiece === null) {
                player.nextPiece = createPiece('TJLOSZI'[Math.random() * 7 | 0]);
            }
            player.matrix = player.nextPiece;
            player.nextPiece = createPiece('TJLOSZI'[Math.random() * 7 | 0]);
            player.pos.y = 0;
            player.pos.x = (arena[0].length / 2 | 0) - (player.matrix[0].length / 2 | 0);
            if (collide(arena, player)) {
                gameOver = true;
                displayFinalScore();
            }
        }

        function playerRotate(dir) {
            const pos = player.pos.x;
            let offset = 1;
            rotate(player.matrix, dir);
            while (collide(arena, player)) {
                player.pos.x += offset;
                offset = -(offset + (offset > 0 ? 1 : -1));
                if (offset > player.matrix[0].length) {
                    rotate(player.matrix, -dir);
                    player.pos.x = pos;
                    return;
                }
            }
            playSound("https://tokodomo.xyz/wp-content/uploads/2024/09/rotate.mp3"); // 回転時の効果音
        }

        function collide(arena, player) {
            const [m, o] = [player.matrix, player.pos];
            for (let y = 0; y < m.length; ++y) {
                for (let x = 0; x < m[y].length; ++x) {
                    if (m[y][x] !== 0 &&
                        (arena[y + o.y] &&
                        arena[y + o.y][x + o.x]) !== 0) {
                        return true;
                    }
                }
            }
            return false;
        }

        function arenaSweep() {
            let lines = 0;
            outer: for (let y = arena.length - 1; y > 0; --y) {
                for (let x = 0; x < arena[y].length; ++x) {
                    if (arena[y][x] === 0) {
                        continue outer;
                    }
                }

                const row = arena.splice(y, 1)[0].fill(0);
                arena.unshift(row);
                ++y;

                player.score += 10 * level;
                lines++;
            }

            if (lines > 0) {
                if (lines === 4) {
                    playSound("https://tokodomo.xyz/wp-content/uploads/2024/09/tetris.mp3");
                } else {
                    playSound("https://tokodomo.xyz/wp-content/uploads/2024/09/line-clear.mp3");
                }
            }

            linesCleared += lines;
        }

        let dropCounter = 0;
        let lastTime = 0;

        let animationFrameId;

        function update(time = 0) {
            if (!gameOver) {
                const deltaTime = time - lastTime;
                dropCounter += deltaTime;
                if (dropCounter > (dropStart ? dropSpeed : dropInterval)) {
                    playerDrop();
                }
                lastTime = time;
                draw();
                animationFrameId = requestAnimationFrame(update);
            } else {
                cancelAnimationFrame(animationFrameId);
            }
        }

        function updateScore() {
            document.getElementById('score').innerText = `Score: ${player.score}`;
            document.getElementById('finalScore').innerText = `GAME OVER\nSCORE: ${player.score}`;
        }

        document.addEventListener('keydown', event => {
            if (event.keyCode === 37) {
                playerMove(-1);
            } else if (event.keyCode === 39) {
                playerMove(1);
            } else if (event.keyCode === 40) {
                event.preventDefault();
                manualDrop = true;
                playerDrop();
            } else if (event.keyCode === 81) {
                playerRotate(-1);
            } else if (event.keyCode === 87) {
                playerRotate(1);
            }
        });

        document.getElementById('leftButton').addEventListener('click', () => playerMove(-1));
        document.getElementById('rightButton').addEventListener('click', () => playerMove(1));
        document.getElementById('rotateButton').addEventListener('click', () => playerRotate(1));

        document.getElementById('dropButton').addEventListener('mousedown', (event) => {
            event.preventDefault();
            manualDrop = true;
            dropStart = true;
        });

        document.getElementById('dropButton').addEventListener('mouseup', (event) => {
            event.preventDefault();
            dropStart = false;
        });

        document.getElementById('dropButton').addEventListener('touchstart', (event) => {
            event.preventDefault();
            manualDrop = true;
            dropStart = true;
        });

        document.getElementById('dropButton').addEventListener('touchend', (event) => {
            event.preventDefault();
            dropStart = false;
        });

        function startGame() {
            arena.forEach(row => row.fill(0));
            player.score = 0;
            gameOver = false;
            player.nextPiece = null;
            document.getElementById('finalScore').style.display = "none";
            playerReset();
            updateScore();
            lastTime = 0;
            dropCounter = 0;
            cancelAnimationFrame(animationFrameId);
            update();
            bgm.play();
        }

        function displayFinalScore() {
            document.getElementById('finalScore').style.display = "block";
            document.getElementById('finalScore').innerText = `GAME OVER\nSCORE: ${player.score}`;
            bgm.pause();
            bgm.currentTime = 0;
        }

        // 初期化処理
        resizeCanvas();
        playerReset();
        updateScore();
        draw();
    </script>
</body>
</html>

コメント

タイトルとURLをコピーしました