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

HTMLテトリスの作り方解説!難易度とレベルアップの実装方法【ver1.7.0対応】

作ってみた!

「HTMLでテトリスゲームを作ってみたいけど、難易度ってどうやって調整するの?」そんな疑問をお持ちのあなたへ、今回はHTML、JavaScript、Canvasを使ってテトリスゲームを作る方法を解説します。特に、ver1.6からver1.7.0へのアップデートで追加されたレベルアップ機能に注目して、難易度の調整の仕組みを詳しく見ていきます。ソースコードも交えて、初心者でも分かりやすく説明するので、ぜひ挑戦してみてくださいね♪

あんちゃん
あんちゃん

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

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

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

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

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

テトリスゲームの基本構造

テトリスのようなシンプルでありながらも奥深いゲームを作るには、基本的な構造がしっかりしていることが大切です。テトリスゲームの基本構造は、HTMLを使って視覚的な要素を配置し、JavaScriptでゲームロジックを動かす、という流れで構成されています。この基本構造を理解することで、初心者でもゲームの作り方をイメージしやすくなりますよ!

まずは、ゲーム画面の表示を担当するHTMLと、ゲームの動きを制御するJavaScriptの役割分担をしっかり整理します。この2つの技術を使えば、ブラウザ上で動く簡単なテトリスゲームを作成できます。さらに、canvas要素を使用して、動的にゲーム画面を描画するのがポイントです。このcanvas要素によって、リアルタイムでブロックの動きを表現できるようになるんです。

また、ゲームロジックでは、ブロックの生成・操作・衝突判定など、ゲームの進行に必要な機能がJavaScriptによって管理されます。特に、プレイヤーがブロックを操作するためのキーボード操作や、スマホやタブレット向けのタッチ操作などのインターフェースを整えることも重要です。これにより、幅広いデバイスでプレイ可能なテトリスが完成します。

ここで押さえておきたいのは、テトリスの基本的な動作は「ブロックが上から降ってきて、ラインを揃えて消す」というシンプルな仕組みですが、内部では複雑なアルゴリズムが動いています。たとえば、ブロック同士の衝突やライン消去の判定、スコアの計算、そして今回のバージョン1.7.0で追加されたレベルアップの機能などがゲームの核となります。

テトリスの基本構造をしっかり理解しておけば、この後の難易度調整やレベルアップ機能の実装がスムーズに行えるようになりますよ!

HTMLの基本構造

テトリスゲームをHTMLで作成する際の最初のステップは、HTMLの基本構造を整えることです。テトリスのようなゲームでは、<canvas>要素がゲーム画面の中心となります。このキャンバス要素は、JavaScriptを使ってゲームのブロックを描画し、リアルタイムでの動きを表現するために使われます。

canvas要素でゲーム画面を作成

<canvas>要素は、ゲームの視覚的な部分を描画するための領域を提供します。HTMLで定義し、JavaScriptから操作することで、ブロックの動きや衝突判定などのゲームのロジックがキャンバス内で行われます。サイズは自由に設定できますが、テトリスでは縦長の画面が一般的なので、widthheightを設定して縦方向のスペースを確保します。

<canvas id="tetris" width="240" height="400"></canvas>

このように、<canvas>要素をページ内に設置し、ゲームの画面領域を確保します。JavaScriptではこのキャンバスを通じて、動的にブロックの描画や消去が行われます。

操作ボタンを配置(スマホ対応も考慮)

テトリスは通常、キーボードで操作されますが、スマホでのプレイにも対応する場合は操作ボタンの配置が必要です。例えば、<button>要素を用いて、画面上に「左」「右」「回転」などのボタンを追加することで、タッチ操作に対応させることができます。これにより、タッチパネルデバイスでも快適にプレイできるようになります。

コード例:

<button id="left">左</button>
<button id="rotate">回転</button>
<button id="right">右</button>

ボタンの配置は自由ですが、ユーザーが快適にプレイできるように、操作性に配慮したレイアウトが大切です。また、JavaScriptで各ボタンに機能を割り当て、タップするとブロックが動くように設定します。

スコア表示エリア

テトリスゲームでは、プレイヤーがラインを消すごとにスコアが加算される仕組みがあり、スコアの表示エリアも重要な要素です。これを表示するために、<div>要素を使ってスコアの表示領域を作成し、JavaScriptでリアルタイムに更新します。

<div id="score">スコア: 0</div>

このdiv要素に対して、JavaScriptでスコアを計算し、ゲーム進行中に更新していきます。視覚的にプレイヤーにフィードバックを与えることで、ゲームの達成感を高めることができますね♪

BGM再生用のaudio要素

最後に、BGM(バックグラウンドミュージック)をゲームに加えることで、ゲーム体験が一層盛り上がります。<audio>要素を使って、ゲーム中に音楽を再生することができます。HTMLで<audio>タグを追加し、JavaScriptで再生・停止の制御を行います。

コード例:

<audio id="bgm" src="bgm.mp3" loop></audio>

ゲームが始まるときにBGMを再生し、ゲームオーバー時に停止するなどの演出が可能です。音楽があることで、プレイヤーはより深くゲームの世界に没頭することができます。


このように、テトリスゲームのHTML構造では、キャンバスを中心に操作ボタンやスコアエリア、BGMの再生機能などをバランス良く配置することがポイントです。これが整えば、あとはJavaScriptでゲームのロジックを追加していく流れになりますよ♪

JavaScriptの役割

テトリスゲームを作る上で、JavaScriptはまさにゲームのエンジン部分を担当しています。画面に表示されるブロックがどのように生成され、どのように動き、どうやって操作されるか、そしてゲームの進行やスコア計算まで、すべてJavaScriptがその背後で支えているんですよ。ここでは、特に重要なブロックの生成と落下処理プレイヤーの操作衝突判定ラインの消去、そしてゲームオーバーの判定について詳しく説明しますね!

ブロックの生成と落下処理、プレイヤーの操作、衝突判定、ラインの消去、ゲームオーバーの判定

ブロックの生成と落下処理

まず、テトリスゲームの一番大事な部分である「ブロックの生成」です!テトリスでおなじみの7種類のブロック、T、O、L、J、I、S、Zがランダムに生成されるんですけど、これをどうやって実現しているかというと、JavaScriptの配列という仕組みを使っています。各ブロックの形状は、2次元配列で定義されていて、その形に基づいて画面に描かれるんですね。

そして、この生成されたブロックは、ゲームが進行するにつれて落下していきます。この落下処理がなければテトリスになりませんよね?ブロックが1マスずつ下に動くのは、JavaScriptのplayerDrop()という関数で行われています。この関数が一定の時間ごとに呼び出されることで、ブロックが少しずつ落ちてくるんです。この落下速度は、レベルが上がるごとに速くなっていくようにdropIntervalという変数で管理されています。これで、ゲームの難易度が上がっていくんですね!すごくシンプルな仕組みですが、プレイヤーにはスリルを与えてくれる大事な要素です。

キーボード・タッチ操作の実装

さて、次は操作の話です。テトリスでは、ブロックを左右に動かしたり、回転させたりする操作が必要ですが、それを実現するのもJavaScriptの役割です。ここで登場するのが、キーボードやタッチ操作ですね。キーボードでは、矢印キーを使って左右に動かしたり、下矢印でブロックを早く落としたりします。この処理は、keydownイベントを使って実現されています。

コード例:

document.addEventListener('keydown', event => {
    if (event.keyCode === 37) {
        // 左に動かす
    } else if (event.keyCode === 39) {
        // 右に動かす
    } else if (event.keyCode === 40) {
        // 早く落とす
    }
});

スマホやタブレットでもテトリスを楽しめるように、タッチ操作の実装も可能です。画面上に操作用のボタンを配置して、それをタッチするとブロックが動くように設定することで、どんなデバイスでもプレイできるようになるんです。

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

ブロックが下まで落ちたり、他のブロックにぶつかったときに、そのブロックが固定される仕組みが必要ですよね。それが衝突判定という機能です。JavaScriptのcollide()関数を使って、ブロックがアリーナ(ゲームの盤面)や他のブロックにぶつかるかどうかを確認します。もしぶつかったら、そのブロックはその場所に固定され、次のブロックが生成される仕組みになっています。

コード例:

function collide(arena, player) {
    // 衝突判定の処理
}

この機能がなければ、ブロックがどこまでも落ち続けてしまってゲームになりませんよね。テトリスの面白さは、この固定される瞬間にもあるんですよ!

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

テトリスの醍醐味のひとつは、ブロックを積み重ねて横一列が揃ったら、そのラインが消去されることですよね。この処理ももちろんJavaScriptで行われています。arenaSweep()という関数が、アリーナ全体をスキャンして、完全にブロックで埋まったラインがないかをチェックします。そして、埋まったラインがあればそのラインを消して、その分だけスコアが加算されます。

コード例:

function arenaSweep() {
    // ライン消去の処理
}

消去されたラインの数によってスコアが変わり、プレイヤーの腕が上がるごとに高いスコアが出せるようになるんです。さらに、バージョン1.7.0では、レベルに応じてスコアが変わるような仕組みも追加されていますよ!どんどん難しくなっていくゲームに夢中になっちゃいますね♪

ゲームオーバー判定

最後に、ゲームオーバーの判定もJavaScriptの重要な役割です。ブロックが画面の上端まで積み上がってしまった場合、ゲームは終了します。この時、gameOverという変数をtrueにして、ゲーム全体を停止する処理が行われます。また、ゲームオーバーになるとスコアが表示され、BGMが停止するなどの演出もJavaScriptで制御されます。


こうしてみると、テトリスのすべての動きがJavaScriptによって支えられていることがわかりますよね!ブロックの生成から落下、操作、衝突判定、ラインの消去、そしてゲームオーバーまで、すべてがスムーズに連携しているからこそ、テトリスはこんなに楽しいゲームになっているんです。JavaScriptって本当にすごいですね!

Canvasによる描画

テトリスゲームのような2Dゲームを作る際、Canvas要素を使用することで、ゲーム内のすべての視覚的な要素をリアルタイムで描画・更新できます。特に、テトリスではブロックを次々と生成し、プレイヤーの操作やゲームの進行に合わせて、これを描画・動かしていく必要があります。ここでは、JavaScriptを使ってCanvas要素内にブロックをどのように描画していくのか、その詳細をじっくり見ていきましょう。

Canvasによる描画でテトリスゲームの2Dゲームを作る

contextオブジェクトでブロックを描画

コード例:

const canvas = document.getElementById('tetris');
const context = canvas.getContext('2d');

このコードで、まずHTML内の<canvas>タグを取得し、そこから2D描画用のcontextを取得します。contextを取得した後は、いよいよ具体的なブロックの描画処理に移りますが、このcontextがすべての描画の起点となるんです。

Canvasでの描画では、基本的に全てのオブジェクト(テトリスのブロックや背景、スコア表示など)は、contextを使って描かれます。このcontextを通じて、ゲーム内で動き回るブロックを次々と描画していきます。

fillRect()メソッドで四角形を描画

コード例:

context.fillRect(x, y, width, height);

コード例:

context.fillRect(100, 200, 20, 20);  // 座標(100, 200)に20x20の四角形を描画

このシンプルなメソッドでブロックの1マスを描くことができ、これを繰り返してブロック全体を描画していく仕組みです。fillRect()を使用すると、ブロックを描く位置やサイズを細かくコントロールできるため、テトリスの画面全体に対して正確にブロックを配置することが可能です。

色やサイズを指定してブロックを表現

コード例:

context.fillStyle = 'blue';  // ブロックを青色に設定
context.fillRect(100, 200, 20, 20);  // 座標(100, 200)に青色の四角形を描画

fillStyleプロパティに色を指定することで、その後の描画処理がすべてその色で行われるようになります。テトリスでは、各ブロックの種類ごとに色が異なるため、このプロパティを使ってブロックごとの個性を表現します。たとえば、L字型のブロックはオレンジ色、T字型のブロックは紫色という風に色を分けていきます。色の違いは、プレイヤーに視覚的な楽しさを与え、ゲームの進行をよりわかりやすくします。

また、ブロックのサイズも重要です。テトリスでは、すべてのブロックが同じサイズで描画されますが、このサイズはゲーム全体のバランスを取るために決定されます。多くの場合、1マスは20×20ピクセルとされていますが、これも自由にカスタマイズ可能です。サイズを変更することで、ゲームの見た目や難易度を調整できるのもCanvasを使った描画の魅力です。

複数のブロックを描画する

テトリスでは、1つのブロックは4つの正方形から構成されています。それを描画するには、fillRect()を4回使って、それぞれのマスを描いていく必要があります。例えば、以下のコードでL字型ブロックを描画します。

コード例:

context.fillStyle = 'orange';  // L字型のブロックをオレンジ色に設定
context.fillRect(100, 200, 20, 20);  // 座標(100, 200)に1つ目のマス
context.fillRect(120, 200, 20, 20);  // 座標(120, 200)に2つ目のマス
context.fillRect(140, 200, 20, 20);  // 座標(140, 200)に3つ目のマス
context.fillRect(140, 220, 20, 20);  // 座標(140, 220)に4つ目のマス

これでL字型のブロックが描かれます。このように、複数の四角形を並べてブロック全体を構成します。さらに、ブロックの回転や移動もJavaScriptのコードで実現できます。各マスの座標を動かしたり、回転に応じて座標を再計算することで、プレイヤーが自由にブロックを動かせるようになるんです。

Canvasを使ったアニメーション

テトリスでは、ブロックが上から下へと自然に落ちてきます。この動きもCanvasJavaScriptでアニメーションとして実装されます。ブロックの落下は時間と共に位置を変えることで実現されます。requestAnimationFrame()関数を使うことで、スムーズなアニメーションが可能です。

コード例:

function update() {
    context.clearRect(0, 0, canvas.width, canvas.height);  // 画面をクリア
    // 新しいブロックの位置を計算して描画
    context.fillRect(player.x, player.y, 20, 20);
    player.y += 20;  // ブロックを下に移動
    requestAnimationFrame(update);  // 次のフレームを呼び出す
}

requestAnimationFrame(update);  // アニメーションの開始

このアプローチでは、フレームごとに画面をクリアして新しいブロックの位置を描画し、リアルタイムで落下アニメーションを実現します。テトリスのスムーズなゲームプレイは、こうしたアニメーションの連続によって実現されているんです。


Canvasによる描画は、テトリスの見た目を作り上げる非常に大切な部分です。contextオブジェクトを使って四角形を描画し、色やサイズを設定することで、テトリスのブロックが画面に表現されます。そして、アニメーションを

通じてブロックが動くことで、テトリス特有のプレイ感が生まれるのです。こうして、HTML5とJavaScriptを使ってリアルタイムで動くテトリスの世界が作り上げられていくんですよ!

ブロックの生成と落下

テトリスブロックの生テトリス

テトリスゲームにおいて、ブロックの生成と落下はゲームの根幹を担う重要なメカニズムです。ゲームが始まると、次々とブロックが上から降りてきて、プレイヤーはそれらを操作し、画面上でブロックを積み上げていきます。この「ブロックの生成と落下」の仕組みがスムーズでなければ、テトリスの爽快感や難易度調整のバランスが崩れてしまいます。

まず、テトリスの特徴的な部分は、ブロックがランダムに生成されることです。プレイヤーに予測不可能な形状のブロックが降ってくることで、戦略的にブロックを積み上げる楽しさが生まれます。ランダムな生成というのは、プレイヤーに常に新しい挑戦を提供するための重要な要素であり、ゲームを飽きさせない工夫なんです。

そして、この生成されたブロックは、重力に従うように上から下に向かって落下していきます。ブロックの落下速度は、ゲームの難易度に直結しています。例えば、ゲームが進むにつれてブロックの落下速度が速くなることで、プレイヤーにとってのチャレンジが増し、よりスリリングな体験が生まれます。このように、ブロックの生成と落下速度のバランスをとることが、テトリスの難易度調整において非常に重要な役割を果たしているんです。

ゲームの最初は比較的ゆっくりとブロックが落ちてくるため、プレイヤーには考える時間があります。しかし、ゲームが進行するに連れてブロックの落下速度が加速し、プレイヤーは瞬時に判断を下さなければならなくなります。これは、レベルアップの仕組みによっても管理されています。ブロックが消去されるラインの数が一定数に達すると、レベルが上がり、ブロックの落下速度が上昇します。このシンプルな変化が、プレイヤーにとっての緊張感を生み、テトリスならではのドキドキ感が増すんです。

また、ブロックが一定の速度で落下してくるだけではなく、プレイヤーが操作している間は、プレイヤーの入力に応じてブロックが左右に動いたり、回転したりします。この操作も、ゲームの進行をスムーズにするための重要な要素です。プレイヤーがどのようにブロックを配置するかが、最終的にゲームの結果を大きく左右します。

そして、忘れてはならないのが、ゲームのバランスです。ブロックの生成と落下があまりにも速すぎると、初心者には難しすぎるゲームになってしまいますし、逆に遅すぎるとゲームが単調になりがちです。そこで、テトリスではプレイヤーのスキルに応じて適切な速度でゲームが進行するように、ブロックの生成と落下が巧妙に調整されています。これが、テトリスの奥深さであり、長く愛される理由のひとつでもあるんですね。

このように、「ブロックの生成と落下」というメカニズムは、単純に見えて実は非常に繊細で緻密なバランスの上に成り立っています。ゲームの難易度やプレイヤーの楽しさに直結するため、この仕組みがしっかりと設計されていることが、テトリスを成功に導いている大きな要因です。

ブロックの種類と形状

テトリスの醍醐味のひとつは、さまざまな形をしたブロックがランダムに落ちてくるところにあります。このブロックには全部で7種類あり、それぞれに特徴的な形と動きがあるんです。テトリスをプレイしていると、次にどんなブロックが来るのかを考えながら戦略を立てることが楽しいポイントですね♪

7種類のブロック

テトリスに登場する7種類のブロックには、全てアルファベットで名前が付けられています。それぞれがゲームにおいて異なる役割を果たし、プレイヤーの戦略によって積み方が変わってきます。それでは、代表的なブロックを簡単に紹介しますね。

  1. T字型ブロック: このブロックは、その名の通りT字型をしていて、回転させることで様々な形に適応する万能タイプです。穴埋めにも使いやすいので、プレイヤーにとって非常に頼りになる存在です。
  2. O字型ブロック: こちらは正方形の形をしたブロックで、回転しても形が変わらないのが特徴です。特に平らな部分を作るのに役立ち、安定した積み方が可能です。
  3. L字型ブロック: L字型は、隅にぴったりとフィットする形状です。横に寝かせたり、縦に立てたりして、狭いスペースに収めることができる便利なブロックです。
  4. J字型ブロック: L字型の逆パターンのブロックで、こちらも狭い場所に入れやすいです。L字型との組み合わせでうまくラインを揃えることができるんですよ。
  5. I字型ブロック: これは縦長の棒のような形をしていて、4マス分の高さを持つブロックです。テトリスの中ではもっとも重要なブロックで、一気に4ラインを消すことができる「テトリス」を狙うために欠かせません。
  6. S字型ブロック: このブロックは左右対称ではない波のような形をしており、平坦ではない地形の穴埋めにぴったりです。
  7. Z字型ブロック: S字型の逆パターンで、こちらも凹凸のある地形にフィットします。S字型と同様に、複雑な形状の場所に置きやすいです。

各ブロックは2次元配列で形状を定義

これら7種類のブロックは、2次元配列でその形状が定義されています。2次元配列というのは、縦と横の両方を持ったデータの並び方で、テトリスのブロックの各マスがどこに位置するのかを正確に表現するために使われます。たとえば、I字型ブロックなら4つの縦のマスが並んだ形で定義されますし、O字型ブロックは2×2の正方形で定義されます。

この2次元配列を使うことで、ゲーム内のどの座標にブロックが描画され、どの方向に回転させるかといった操作も可能になります。ブロックの形状がプログラムで正確に管理されているため、プレイヤーの操作に応じて、ゲーム内でブロックをスムーズに動かしたり、回転させたりすることができるんです。

テトリスのゲームプレイは、このような多様な形状のブロックと、その動きをリアルタイムで管理する仕組みによって成り立っています。プレイヤーはこれらのブロックの形状を活用して、効率的にラインを揃えていく戦略を立てるわけですが、どんなブロックが次に落ちてくるのかは完全にランダムです。これにより、毎回異なるゲーム展開が楽しめるという点が、テトリスが長く愛される理由の一つなんです♪

ブロックの生成と落下

テトリスにおけるブロックの生成と落下は、ゲームの進行を左右する非常に重要な仕組みです。この部分がうまく機能しなければ、ゲームのバランスやプレイ感が崩れてしまいます。ブロックが生成されて、ゆっくりと、あるいはスピードアップして落ちていく動作は、ゲームのテンポを決定する大きな要素です。

playerDrop()関数でブロックを1マスずつ落下

ブロックが落下する仕組みは、playerDrop()という関数によって実現されます。この関数は、ブロックが自然に重力のように下に落ちていく動作を1マスずつ処理するために使われます。プレイヤーが操作していなくても、ゲーム内のブロックは一定のリズムで下に進んでいきます。この動作をスムーズに行うことで、ゲーム全体の流れが自然なものとなり、プレイヤーが直感的にブロックを操作できる環境が整うのです。

playerDrop()関数が呼ばれるたびに、ブロックの位置が1マスずつ下に移動していき、ブロックが他のブロックやアリーナの底にぶつかるまで落下を続けます。こうして、プレイヤーがブロックを動かして最適な位置に配置する間も、ゲームは休まず進行し続けるわけです。この「落下」という動きが、テトリスの持つ緊張感を生み出す一因となっているんですね。

dropInterval変数で落下速度を調整

ブロックの落下速度は、ゲームの難易度を左右する大きなポイントです。この落下速度は、dropIntervalという変数によって管理されています。この変数によって、ブロックがどれくらいのスピードで落ちてくるかが設定されており、ゲームの進行度に合わせて動的に変更されます。

最初の段階では、ブロックの落下速度は比較的ゆっくりですが、プレイヤーが一定のラインを消すとレベルアップし、dropIntervalが短縮されることで、ブロックがより速く落ちてくるようになります。この変化により、ゲームが進行するにつれてプレイヤーにさらなる挑戦を与えることができるのです。どんどんスピードが上がっていく感覚が、プレイヤーに緊張感と集中力を要求し、テトリス特有の「やめられない」ゲーム体験を作り出しています。

dropCounter変数で落下タイミングを管理

一方で、単に落下速度を調整するだけではなく、ブロックがいつ落下するかも重要です。ここで使われるのが、dropCounterという変数です。dropCounterは、ブロックが次に落ちるタイミングを管理するためのカウンターであり、ゲームが進むごとにカウントが進んでいきます。一定のカウントに達すると、playerDrop()関数が呼び出されてブロックが1マス落下します。

この仕組みを使うことで、プレイヤーが操作するタイミングやゲームのテンポが調整され、スムーズにゲームが進行します。また、dropCounterとdropIntervalを組み合わせることで、ゲームの進行状況に応じてプレイヤーに適切なペースでブロックが提供されるため、ゲームが単調にならず、プレイヤーが常に適度な緊張感を保ちながらプレイできるように設計されています。


こうして見てみると、テトリスの「ブロックの生成と落下」の仕組みは非常に奥深いもので、ゲームの面白さを支える重要な要素です。単にブロックを落とすだけではなく、その速度やタイミングをうまくコントロールすることで、プレイヤーに新しい挑戦を与え続け、飽きの来ないゲーム体験を提供しているのです!

衝突判定とブロック固定

衝突判定とブロック固定

テトリスでの衝突判定ブロック固定って、本当にゲームの進行を支える大事な仕組みなんです。ブロックが上から降ってきて、他のブロックや画面の底にぶつかったら、その場所でピタッと止まって固定される──この動きがないと、テトリスの楽しさは成り立たないんですよ。

まず、衝突判定って何かというと、ブロックがどこかにぶつかるかどうかをチェックすることです。プレイヤーがブロックを動かしている間も、ブロックは重力に従って下に降りていきますよね。でも、ブロックが他のブロックや画面の端にぶつかる瞬間をしっかり見極めて、そこで止めなければいけません。それをやってくれるのがcollide()関数なんです。この関数がしっかり働いて、ブロックが他のブロックやアリーナ(ゲーム盤面)にぶつかると、「あ、ここで止まるんだな!」と判断してくれるわけです。

衝突が検知された後は、ブロックをその場に固定します。この固定があるからこそ、次に新しいブロックが上から降ってきて、また違うパズルが生まれるんです。もしもこの固定がなかったら、ブロックはどんどん動き続けて、ゲームとして成立しなくなっちゃいますね。この固定があることで、プレイヤーは次のブロックを使ってラインを揃えたり、空いている隙間を埋めたりといった戦略が展開できるんです。

ブロックが固定されると、そのブロックはアリーナに統合されます。つまり、もうそのブロックは動かせないけど、その上に次のブロックを積み重ねたり、周りに配置したりして、新しいラインを揃えるためのパズルがどんどん進んでいく感じですね。この一連の流れが、テトリスならではの独特のテンポを作り出しているんです。

衝突判定とブロック固定は、ゲーム全体のリズムを保つ重要な役割を持っています。プレイヤーがブロックを自由に動かせるのは、この仕組みが正しく機能しているからこそです。次々に落ちてくるブロックが適切な場所で止まり、ラインを揃えるための新たな挑戦が生まれる──これがテトリスの中毒性を生み出している部分なんですよね。

こうしたシンプルだけど重要な仕組みが、プレイヤーにスリリングなゲーム体験を提供しているんです。たまに「あ、このブロック、もうちょっとここに動かしたかったのに!」って思うときがあるけど、それもまたテトリスの醍醐味ですよね。

衝突判定とブロック固定

テトリスゲームをスムーズに進行させるためには、衝突判定ブロック固定が欠かせない重要な役割を担っています。この仕組みがあることで、ブロックは適切なタイミングで止まり、次のブロックが出現し、ゲームが進んでいくんですよね♪

collide()関数で衝突を検知

まず、衝突判定ですが、これはブロックがアリーナ(ゲームの盤面)や他のブロックにぶつかったかどうかをチェックする役割です。プレイヤーがブロックを動かし、ブロックが下に向かって落ちていくとき、ブロックが他のブロックやアリーナの底に到達した瞬間に止まる必要があります。これを実現するのが、collide()関数です。この関数は、ブロックと他のオブジェクトの衝突をリアルタイムで検知し、「ここで止まるべきだよ!」とブロックに知らせます。

例えば、ブロックが動きながら他のブロックに近づいたり、下に落ちてくるとき、collide()関数がそのブロックの位置を常にチェックしています。もしブロックが衝突する位置に来たら、その場で停止し、次のステップとしてブロックが固定されるわけです。この判定がないと、ブロックはずっと動き続けてしまい、テトリスとしてのゲーム性が崩れてしまいますよね。

衝突後のブロック固定

衝突が検知された後は、ブロックをその場に固定します。テトリスでは、ブロックが一度固定されるともう動かすことはできず、そのブロックはアリーナの一部となって次のブロックが降りてくる準備が整います。この「固定」という動作がないと、ゲームが止まることなく、いつまでもブロックが動き続けてしまいます。この固定によって、ブロックがプレイヤーの操作を超えて「そこで動かないもの」として新しい戦略を生み出す基盤になります。

この固定の仕組みが、テトリスのゲームプレイにおいて非常に重要な要素となります。固定されたブロックをベースにして、次にどのブロックをどう配置するかを考える戦略がプレイヤーに求められるわけです。ラインが揃ったらそのラインが消え、ゲームが続行する――このサイクルが、テトリスの魅力を作り出しているんですよ。

ブロックが固定されると、それはアリーナの一部として統合され、次のブロックが上から落ちてくる準備が完了します。固定されたブロックはもう動かすことができないため、プレイヤーは新しいブロックを使って、次のラインを完成させるための戦略を立てることになります。この「固定」があるからこそ、テトリスは進行し続け、絶え間ないチャレンジを提供できるんです。

ゲームのテンポを支える衝突と固定

衝突判定とブロック固定は、ゲームのテンポを作り出す要です。ブロックがどこで止まり、次にどのブロックをどう動かすか、プレイヤーは常に考え続けなければなりません。もしこの仕組みがなければ、ゲームは無秩序になり、テトリスならではのリズムや戦略性が失われてしまいます。

衝突判定とブロック固定は、テトリスのゲームプレイをスムーズに進行させるために欠かせない要素であり、この仕組みがあることでプレイヤーは次々と新しい挑戦に向かうことができるんです!

衝突判定とブロック固定

テトリスでは、ブロックが他のブロックやアリーナに衝突した時、そのブロックがその場に固定され、ゲームが次の段階に進むという重要なプロセスがあります。このプロセスをスムーズに進めるためには、いくつかの仕組みが組み込まれているんですよ。まずは衝突判定、そしてその後のブロック固定が基本の流れです。

merge()関数でブロックをアリーナに統合

ブロックが他のブロックやアリーナに衝突すると、そのブロックはそれ以上動かなくなります。この時、ブロックはアリーナ(ゲーム盤面)に統合され、固定されるわけです。この処理を実現するのがmerge()関数です。この関数は、ブロックをアリーナに加えて、そこで動かない「固定された部分」として認識させます。固定されたブロックはもう動かせないため、プレイヤーは次のブロックをどう配置するかを考える必要がありますね。

merge()関数がうまく機能することで、ゲームは自然なリズムで進みます。ブロックがアリーナに統合されることで、次のブロックがスムーズに登場し、プレイヤーは新たなラインを揃えるための戦略を立てることができるんです。この統合がスムーズに行われるかどうかは、テトリスのゲーム体験を左右する大事な部分なんです。

ラインの消去判定

ブロックがアリーナに統合された後、次に大事なのはラインの消去判定です。テトリスでは、ブロックを積み重ねて横一列がすべて埋まると、そのラインが消去され、スコアが加算されます。この消去の仕組みが、テトリスの爽快感や達成感を生むポイントですね。

この判定は、アリーナ全体をスキャンしてラインが揃っているかどうかを確認することで行われます。ラインが揃っている場合、そのラインはすぐに消去され、ブロックが消えたスペースに新たなブロックが上から積み重なります。これによってプレイヤーは新たなチャンスを得て、ゲームを続けていくことができるんです。

テトリスのライン消去の仕組みは、ゲームのテンポを大きく左右します。一気に複数のラインを消す「テトリス」と呼ばれる技も、この消去判定のスムーズな処理があるからこそ生まれるんです!プレイヤーが狙った通りにラインが消える瞬間は、何度プレイしてもやっぱり気持ちがいいものですよね。


このように、merge()関数でブロックをアリーナに統合し、その後にラインの消去判定が行われることで、テトリスは次々と新しいチャレンジをプレイヤーに与え続けているんです。このプロセスがスムーズに進行することで、ゲームのテンポは保たれ、プレイヤーに常に適度な緊張感と楽しさを提供することができるんですよ!

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

ラインの消去とスコア計算はゲームの進行に大きく関わる重要な要素

テトリスにおいて、ラインの消去とスコア計算はゲームの進行に大きく関わる重要な要素です。プレイヤーがブロックを積み上げていき、横一列にすべてのブロックが揃うと、そのラインは自動的に消去されます。このラインの消去がゲームの爽快感と達成感を生み出す瞬間で、揃えたラインの数に応じてスコアが加算される仕組みが導入されています。一度に複数のラインを消すほど、スコアは大きく加算されますし、特に「テトリス」と呼ばれる4ラインを一度に消す技は、高得点を得るための一つの大きな目標です。ラインを消すことはゲームの進行に直接影響し、消去されたラインの上に積み重なったブロックが落下することで、新たなラインを作るチャンスが生まれます。

スコア計算のシステムはシンプルで、消去したライン数に応じて加算されますが、ゲームが進むにつれてブロックの落下速度が速くなるため、ラインを消す難易度も上がっていきます。それに伴って、スコアの加算もレベルごとに増加していくため、ゲーム後半になるほど一気に高得点を狙えるチャンスが増えるわけです。プレイヤーにとって、スコアを伸ばすためには効率よくブロックを配置し、できるだけ多くのラインを同時に消すことが鍵となります。特に、連続してラインを消すことができれば、大量のスコアを短期間で獲得できるため、プレイヤーは戦略的にブロックを積み上げる必要があります。

このライン消去とスコア計算の流れが、ゲームのテンポを作り出し、プレイヤーに緊張感と達成感を提供しています。難易度が上がる中でどれだけスコアを伸ばせるかがテトリスの面白さの一つであり、プレイヤーは常に次のライン消去を狙って集中力を高めていくことになります。このシンプルな仕組みが、テトリスの奥深さとゲームの魅力を支え、スコアを競い合う楽しさを生み出しています。

ライン消去の判定

テトリスでは、ブロックを積み上げて横一列が全て揃うと、そのラインが消去される仕組みになっています。このライン消去の判定を行うためには、arenaSweep()関数が活躍します。この関数は、アリーナ内のブロックが揃っているかどうかをチェックし、一列に揃っているラインが見つかると、そのラインを自動的に消去します。この動作により、ブロックが消えた空間に上からブロックが落下し、ゲームの進行に新しい展開が生まれます。

arenaSweep()関数は、プレイヤーが効率よくラインを揃えられるように、常にアリーナ全体の状態を監視しています。例えば、プレイヤーが複数のラインを同時に揃えることができれば、より多くのラインが一度に消去され、得点もその分大きく加算されます。この判定はゲームのテンポを決定する非常に大切な要素であり、ライン消去の爽快感をプレイヤーに与える重要な部分なんです。

さらに、この関数はラインが揃っているかどうかを素早く判定し、揃っている場合は即座に消去されるため、ゲームのリズムを崩すことなくスムーズに進行します。ラインが消えるとき、消去されたブロックの上に積み上がっていたブロックが一段ずつ下に落ちていくため、新しいラインを作るチャンスが生まれます。この連続的な動きが、テトリス特有の流れを生み出し、プレイヤーに次の行動を考えさせる余地を与えるんですよ。

このように、arenaSweep()関数は、プレイヤーがラインを消してゲームを進めるための根幹となる仕組みであり、ラインが消える瞬間の達成感や爽快感を演出する大切な役割を果たしています。

スコア計算

テトリスにおいて、スコア計算はプレイヤーのモチベーションを支える重要な仕組みです。ブロックを消去することで得点が加算されるため、プレイヤーは効率的にラインを揃え、スコアを伸ばすことを目指します。ラインが揃って消去されるたびに、その消去されたライン数に応じてスコアが加算されます。1ラインを消したときと、2ライン、3ライン、そして一度に4ラインを消す「テトリス」と呼ばれる技では、加算されるスコアが大きく異なります。特に「テトリス」を成功させると、スコアが急激に増えるため、プレイヤーにとって大きな達成感を感じられる瞬間でもあります。

このスコア計算は、シンプルなようでいてゲームの進行に大きく影響を与えます。プレイヤーが上手くブロックを回転させたり、動かしたりして複数のラインを同時に消すことができれば、スコアは一気に上昇します。これはプレイヤーに対して、戦略的なブロックの配置を促す仕組みになっているんです。また、複数のラインを連続で消すことができれば、そのたびにスコアが積み上がり、ゲームのテンポが一層早まっていきます。

特にver1.7.0では、レベルに応じてスコアの加算も変わる仕組みが導入されています。レベルが上がると、ブロックの落下速度が速くなり、より速い判断と操作が求められるようになりますが、その分スコアの加算量も増えていきます。つまり、難易度が上がるにつれて、スコアを稼ぐチャンスが広がるんです。これにより、ゲーム後半になるとより緊張感のあるプレイが求められ、その分高得点を狙うプレイヤーにとっては絶好のタイミングとなります。

さらに、レベルが上がるたびに、プレイヤーが消すべきラインの数も増え、スコアの加算も一層ダイナミックになります。このスコア計算の仕組みが、プレイヤーに挑戦し続けるモチベーションを与え、ゲームの進行をスムーズかつエキサイティングにしているんですよ。スコアを高めるためには、一度に多くのラインを消すこと、そしてレベルアップに伴うスピードアップに対応できる操作スキルが求められます。スコア計算はテトリスの中核であり、プレイヤーのやり込み要素を強く刺激する仕組みとなっています。

このように、スコア計算はテトリスのゲームデザインの要であり、プレイヤーがより高い得点を目指してブロックを巧みに操作する楽しさを生み出しているんです。

レベルアップ機能の実装(ver1.7.0)

レベルアップ機能の実装(ver1.7.0)

テトリスのver1.7.0におけるレベルアップ機能の実装は、プレイヤーに新たな挑戦を提供し、ゲームの難易度を段階的に上げる重要な要素です。レベルアップは、ゲームが進むにつれて難しくなるだけでなく、スコア獲得のペースも上がるため、プレイヤーにとっては楽しさと達成感を倍増させる仕組みとなっています。このレベルアップの条件は、消去したライン数に基づいています。例えば、ある一定数のラインを消去すると、ゲーム内のlinesCleared変数が更新され、その値が設定された閾値に達した時点で自動的にレベルアップが発生します。このとき、ゲームの進行に新たなスピードが加わり、プレイヤーにさらなるスリルが生まれます。

レベルアップが発生すると、次に行われるのはlevelUp()関数による処理です。この関数は、現在のレベルに基づいてブロックの落下速度を調整します。具体的には、dropInterval変数が短縮され、ブロックの落下スピードが速くなります。これによって、プレイヤーは素早く次のブロックを操作しなければならず、集中力と反応速度が求められる場面が増えてきます。このスピード感がゲームの緊張感を増し、プレイヤーをさらに夢中にさせる要因になっているんです。

また、ver1.7.0ではレベルに応じてスコアの加算も変わる仕組みが実装されています。これにより、レベルが上がるごとに消去されたライン数に対するスコアが増加し、より高い得点を狙えるようになります。これはプレイヤーに対して、ゲームの後半に向かうほどスコアを稼ぐチャンスが広がることを示しています。このシステムが、プレイヤーにとってのやりがいや挑戦する意欲を高め、ゲーム全体のモチベーションを保つ鍵となっているんです。

レベルアップはゲームの流れを活性化させ、プレイヤーに常に新しい戦略を考えさせる要素として機能しています。プレイヤーが順調にラインを消していくと、自然にレベルが上がり、さらにスコアを獲得するチャンスが増える。これにより、単にブロックを積み上げるだけではなく、効率的にスコアを伸ばすための戦略を考える楽しさが加わり、テトリスの深みを感じられるようになっているんですよ。

レベルアップの条件

テトリスのレベルアップの条件は、消去したライン数に基づいて設定されています。このラインの数をカウントするのに使用されるのがlinesCleared変数です。この変数は、プレイヤーがどれだけのラインを消去したかを記録しており、ゲームの進行に伴って増加していきます。特定のライン数が達成されると、プレイヤーは次のレベルに進むことができます。たとえば、一定のライン数(例:10ライン)が消去されると、自動的にレベルアップが発生します。レベルが上がると、ブロックの落下速度が速くなり、プレイヤーにとっての挑戦がさらに難しくなりますが、それと同時にスコアを稼ぐ機会も増えていきます。

このレベルアップの仕組みは、プレイヤーに常に新たな挑戦を提供し続けるものです。最初はゆっくりと落ちてくるブロックも、レベルが上がるにつれてスピードが増し、次第に素早い反応や正確な操作が求められるようになります。テトリスの魅力は、この変化するスピード感にあり、プレイヤーは次々と訪れる難題をクリアするために集中力を高め、戦略を練り続けることになります。linesCleared変数は、ゲームの進行をスムーズに管理し、適切なタイミングでレベルアップをトリガーするための重要な役割を果たしています。

また、レベルアップが発生することで、プレイヤーは一層スコアを稼ぐ機会を得ます。ver1.7.0では、レベルに応じたスコア加算の仕組みが実装されており、レベルが上がるごとに消去されたラインに対するスコアが増加します。このシステムは、ゲーム後半になるほど高得点を狙いやすくなり、プレイヤーにとっての達成感がより一層増すよう設計されています。

このレベルアップの条件により、ゲーム全体のバランスが保たれ、プレイヤーが無限に楽しめるようになっているのです。

レベルアップ時の処理

テトリスのゲーム内でレベルアップが発生した際、levelUp()関数が大きな役割を果たします。この関数は、レベルアップと同時にゲームの進行速度を調整するためのキーとなる処理を担っています。まず、プレイヤーが消去したライン数が一定に達すると、levelUp()関数が呼び出され、現在のレベルが一段階上がります。これにより、ゲーム全体の難易度が一気に高まるんです。特に重要なのは、dropIntervalという変数の短縮です。この変数が短くなることで、ブロックの落下速度が上昇し、プレイヤーはより素早くブロックを配置する必要が出てきます。

このように、レベルが上がるごとにブロックの落下速度が速くなるため、ゲームのテンポが急激に変わります。最初の方では比較的ゆっくりとしたペースで進行するため、プレイヤーは余裕を持ってブロックを操作できるのですが、レベルが上がるにつれて、次第に余裕がなくなり、瞬時の判断が求められるようになるんです。この変化こそがテトリスの醍醐味であり、プレイヤーを飽きさせない要素となっています。

さらに、ver1.7.0では、レベルアップに伴ってスコア加算も強化されています。つまり、レベルが上がるたびに、消去されたライン数に応じたスコアがより多く加算されるようになっているんです。これにより、ゲーム後半ではスコアを一気に伸ばすチャンスが増え、ハイスコアを狙うプレイヤーにとって大きなモチベーションになります。例えば、同じラインを消しても、レベルが高いほど得られるスコアも大きくなり、プレイヤーは高得点を狙うためにより一層集中力を高めてプレイすることになります。

このレベルアップ時の処理は、単に難易度を上げるだけでなく、ゲーム全体のバランスを保ちながら、プレイヤーに新たな挑戦を提供する仕組みです。スピードが増すことで戦略的な操作が求められ、スコア加算が増えることでやりがいがアップし、ゲームに一層の深みが生まれるんですよ。

ゲームオーバー判定

ザ・ゲームオーバー

テトリスにおけるゲームオーバーの判定は、プレイヤーが画面上でのブロック配置に失敗し、ブロックがアリーナの上限に達した時に発生します。ブロックが積み上がり、配置するスペースがなくなった場合、もう次のブロックを置くことができなくなり、ゲームオーバーとなります。この判定は非常にシンプルでありながら、テトリスの緊張感を生む重要な要素です。

ゲームオーバーの条件が満たされると、gameOver変数trueに設定され、これによりゲーム全体の進行が停止します。この処理は、プレイヤーがそれ以上ブロックを操作できないようにするためのものです。同時に、スコアの表示が行われ、ゲーム終了時の結果がプレイヤーにフィードバックされます。プレイヤーがどれだけのラインを消去し、どれだけのスコアを稼いだかがここで明確に示されます。

さらに、ゲームオーバー時にはBGM(バックグラウンドミュージック)も停止します。これは、プレイヤーにゲームの終了を明確に伝えるための視覚的、聴覚的なサインです。BGMの停止は、ゲームのテンションを一気に落とし、プレイヤーに「ここでゲームが終わった」という区切りを感じさせます。

ゲームオーバーの瞬間は、プレイヤーにとって悔しさもありますが、その一方で、自分のスコアを見直し、次の挑戦への意欲を高める瞬間でもあります。このシステムは、シンプルでありながらプレイヤーのモチベーションを維持する大事な要素となっているんです。

まとめ

この記事では、HTML、JavaScript、Canvasを使ってテトリスゲームを作る方法を詳しく解説しました!特に、テトリスのver1.7.0で新しく追加されたレベルアップ機能に注目して、その実装方法と難易度調整の仕組みについても丁寧に説明しましたよ。テトリスを作るための基本的な構造から、ブロックの生成や落下、操作方法、そして衝突判定やゲームオーバーの判定まで、さまざまな要素がこのゲームを作るためには必要なんです。

特に重要なポイントだったのは、レベルアップの仕組みですね。テトリスでは、消したライン数によってゲームの難易度が上がり、ブロックの落下速度がどんどん速くなります。この機能を実装することで、プレイヤーに挑戦的な体験を提供し、ゲームに緊張感を持たせることができます。さらに、レベルアップごとにスコア加算のルールが変わるので、スコアを狙うプレイヤーにもやりがいを感じてもらえますよ。

また、テトリスのゲームオーバー判定やBGMの停止など、ゲームが終了する際の演出も重要です。プレイヤーにとって、ゲームが終わった瞬間にスコアが表示され、BGMが静かにフェードアウトする演出は、次の挑戦へのモチベーションを高めてくれます。これも、ゲーム作りにおいて忘れてはならないポイントですね。

今回の記事を参考に、ぜひ自分だけのテトリスゲームを作ってみてください!シンプルでありながらも、奥深いゲームデザインが詰まったテトリスは、プログラミングの練習にもピッタリです。少しずつ改良を加えたり、カスタマイズしたりして、自分だけのオリジナルテトリスを完成させてみましょう!応援してますよ〜!

TETRISまとめ
あんちゃん
あんちゃん

さぁさぁおまたせしました!!

それではテトリスコード、バージョン1.7を大公開しまーす!

ごらんなされ~♬

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>テトリスゲーム Ver1.7.0</title>
    <style>
        /* 既存のCSSはそのまま */
        body, html {
            margin: 0;
            padding: 0;
            width: 100%;
            height: 100%;
            overflow: hidden;
            display: flex;
            flex-direction: column;
            background-image: url('https://tokodomo.xyz/wp-content/uploads/2024/09/tetris_ver1.7_background.webp');
            background-size: cover;
            background-position: center;
        }
        #gameContainer {
            position: relative;
            width: 100%;
            flex-grow: 1;
            display: flex;
            justify-content: center;
            align-items: center;
            background-color: rgba(0, 0, 0, 0.7);
            z-index: 5;
        }
        #game {
            background-color: #000;
        }
        #scoreContainer {
            font-size: 20px;
            background-color: rgba(255, 255, 255, 0.7);
            padding: 10px;
            position: fixed;
            top: 0;
            width: 100%;
            display: flex;
            justify-content: space-between;
            box-sizing: border-box;
            z-index: 10;
        }
        #nextPieceContainer {
            position: absolute;
            top: 100px;
            right: 80px;
            text-align: center;
        }
        #nextPiece {
            width: 60px;
            height: 60px;
            background-color: rgba(255, 255, 255, 0.7);
            margin-bottom: -5px;
        }
        #nextLabel {
            font-size: 14px;
            color: white;
            letter-spacing: 2px;
            text-transform: uppercase;
            font-weight: bold;
        }
        #finalScore {
            font-size: 30px;
            color: red;
            position: absolute;
            top: 50%;
            left: 50%;
            transform: translate(-50%, -50%);
            background-color: rgba(255, 255, 255, 0.7);
            padding: 10px;
            display: none;
            width: 100%;
            text-align: center;
        }
        #controls {
            position: fixed;
            bottom: 0;
            width: 100%;
            display: flex;
            flex-direction: row;
            justify-content: space-between;
            align-items: center;
            padding: 10px 20px;
            box-sizing: border-box;
            z-index: 10;
            background-color: rgba(0, 0, 0, 0.5);
        }
        .control-button {
            width: 60px;
            height: 60px;
            background-color: rgba(255, 255, 255, 0.7);
            border: none;
            border-radius: 50%;
            font-size: 20px;
            font-weight: bold;
            color: black;
            display: flex;
            justify-content: center;
            align-items: center;
        }
        #moveControl {
            width: 140px;
            display: flex;
            justify-content: space-between;
        }
        .connected-button {
            border-radius: 0;
            flex-grow: 1;
            margin: 0;
        }
        .connected-button:first-child {
            border-top-left-radius: 50%;
            border-bottom-left-radius: 50%;
        }
        .connected-button:last-child {
            border-top-right-radius: 50%;
            border-bottom-right-radius: 50%;
        }
        @media (min-width: 601px) {
            #controls {
                display: none;
            }
        }
    </style>
</head>
<body>
    <div id="scoreContainer">
        <div id="score">Score: 0</div>
        <button id="startButton" onclick="startGame()">START</button>
    </div>
    <div id="gameContainer">
        <canvas id="game" width="240" height="400"></canvas>
        <div id="finalScore">GAME OVER<br>SCORE: 0</div>
        <div id="nextPieceContainer">
            <canvas id="nextPiece" width="60" height="60"></canvas>
            <div id="nextLabel">Next</div>
        </div>
    </div>
    <div id="controls">
        <div id="moveControl">
            <button class="control-button connected-button" id="leftButton">&larr;</button>
            <button class="control-button connected-button" id="rightButton">&rarr;</button>
        </div>
        <button class="control-button" id="rotateButton">&#x21BB;</button>
        <button class="control-button" id="dropButton">&#x2B07;</button>
    </div>

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

    <script>
        const canvas = document.getElementById('game');
        const context = canvas.getContext('2d');
        const nextCanvas = document.getElementById('nextPiece');
        const nextContext = nextCanvas.getContext('2d');
        const scale = 20;
        context.scale(scale, scale);
        nextContext.scale(10, 10);

        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 dropStart = false;
        let dropSpeed = 50;

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

        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.fillStyle = '#000';
            context.fillRect(0, 0, canvas.width / scale, canvas.height / scale);

            const xOffset = (canvas.width / scale - arena[0].length) / 2;
            drawMatrix(arena, {x: xOffset, y: 0}, context);
            drawMatrix(player.matrix, {x: player.pos.x + xOffset, y: player.pos.y}, context);

            nextContext.fillStyle = '#000';
            nextContext.fillRect(0, 0, nextCanvas.width, nextCanvas.height);

            const nextPieceOffset = calculateCenterOffset(player.nextPiece);
            drawMatrix(player.nextPiece, nextPieceOffset, nextContext);
        }

        function calculateCenterOffset(matrix) {
            const matrixWidth = matrix[0].length;
            const matrixHeight = matrix.length;

            const canvasSize = nextCanvas.width / 10; 
            const offsetX = (canvasSize - matrixWidth) / 2;
            const offsetY = (canvasSize - matrixHeight) / 2;

            return {x: offsetX, y: offsetY};
        }

        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();
                if (collide(arena, player)) {
                    gameOver = true;
                    displayFinalScore();
                }
            }
            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;
                }
            }
        }

        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() {
            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;  // レベルに応じたスコア加算
                linesCleared++;
            }

            // 一定のライン数が消えたらレベルアップ
            if (linesCleared >= level * 5) {
                levelUp();
            }
        }

        function levelUp() {
            level++;
            dropInterval = Math.max(100, dropInterval * 0.8);  // 落下速度をレベルごとに20%短縮
            console.log(`Level Up! Current Level: ${level}, Drop Interval: ${dropInterval}`);
        }

        let dropCounter = 0;

        let lastTime = 0;
        function update(time = 0) {
            if (!gameOver) {
                const deltaTime = time - lastTime;

                dropCounter += deltaTime;
                if (dropCounter > (dropStart ? dropSpeed : dropInterval)) {
                    playerDrop();
                }

                lastTime = time;

                draw();
                requestAnimationFrame(update);
            }
        }

        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();
                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();
            dropStart = true;
        });

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

        document.getElementById('dropButton').addEventListener('touchstart', (event) => {
            event.preventDefault();
            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;
            document.getElementById('finalScore').style.display = "none";
            playerReset();
            updateScore();
            update();

            // BGMの再生
            document.getElementById('bgm').play();
        }

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

            // BGMの停止
            document.getElementById('bgm').pause();
            document.getElementById('bgm').currentTime = 0;
        }

        playerReset();
        updateScore();
    </script>
</body>
</html>

コメント

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