このページでは、ひなテックGamesで実際に動いている「ブロックパズル」のソースコードを丸ごと公開しています。ここに載っているのは説明用に書き直した特別なコードではなく、いまあなたが遊んでいるゲームそのものを動かしている本物のファイルです。
ブロックパズルは7種類のブロック(テトロミノ)を回転・移動させてラインをそろえる、テトリス風の世界中で愛されているパズルゲームです。「マス目をデータでどう持つか」「ブロックを回転させるとはどういうことか」「ぶつかり判定をどうやるか」という、ゲーム作りの大事な考え方がぎっしり詰まっています。
コードはコピーして自分のパソコンで自由に動かせます。「どうやってブロックが動いているんだろう?」と思ったら、解説を読みながらコードを追いかけてみてください。
ブロックパズルは、次のファイルが役割を分担して動いています。それぞれの中身は下で1つずつ解説します。
| ファイル | 役割 |
|---|---|
index.html | ゲーム画面の骨組み。盤面・スコア表示・ボタンなどの部品をHTMLで配置する |
style.css | 見た目のデザイン。色・大きさ・レイアウト・アニメーション |
game.js | ゲームのルールを動かす頭脳。操作の受け付け・判定・スコア計算など |
この「HTMLで構造、CSSで見た目、JavaScriptで動き」という分担は、ほとんどのWebページ・Webゲームに共通する考え方です。
index.htmlは、ブラウザが最初に読み込むファイルです。ここにはゲームの「部品」をHTMLで並べているだけで、動きそのものは書かれていません。
大きく分けると、(1)スコア・ベスト・レベルを並べるscore-area、(2)ホールド表示・ゲーム盤・ネクスト表示を横に並べたtetris-area、(3)スマホ用の操作ボタンmobile-controls、でできています。注目してほしいのはtetris-boardの中が空っぽなことです。盤面のマス(200個)はJavaScriptがあとから作って入れます。スタート画面とゲームオーバー画面はgame-overlayとして盤の上に重ねてあります。
ゲーム本体は<body>の中から始まります。
※ 下のコードでは、ゲームの動きと関係のない広告・アクセス解析用のタグ(ページ先頭の数行)を省いています。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<title>ブロックパズル|無料ブラウザゲーム|ひなテックGames</title>
<meta name="description" content="ブロックパズルは落ちてくるブロックを回転・移動させてラインをそろえて消す、テトリス風の無料パズルゲーム。PC・スマホ対応のブラウザゲーム。">
<link rel="canonical" href="https://hinata-ya.tech/games/games/tetris/">
<!-- OGP -->
<meta property="og:title" content="ブロックパズル|無料ブラウザゲーム|ひなテックGames">
<meta property="og:description" content="ブロックパズルは落ちてくるブロックを回転・移動させてラインをそろえて消す、テトリス風の無料パズルゲーム。PC・スマホ対応のブラウザゲーム。">
<meta property="og:type" content="website">
<meta property="og:url" content="https://hinata-ya.tech/games/games/tetris/">
<meta property="og:site_name" content="ひなテックGames">
<meta property="og:locale" content="ja_JP">
<!-- 構造化データ -->
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "Game",
"name": "ブロックパズル",
"description": "落ちてくるブロックを回転・移動させてラインを消す、テトリス風のクラシックなパズルゲーム。",
"url": "https://hinata-ya.tech/games/games/tetris/",
"genre": ["パズル", "クラシック"],
"gamePlatform": "Web Browser",
"publisher": {
"@type": "Organization",
"name": "ひなテック",
"url": "https://hinata-ya.tech"
}
}
</script>
<link rel="stylesheet" href="../../css/style.css">
<link rel="stylesheet" href="style.css?v=20260516">
<link rel="stylesheet" href="../../css/leaderboard.css">
</head>
<body>
<div id="header"></div>
<div class="game-page">
<h1 class="game-page-title">ブロックパズル</h1>
<div class="game-page-tags">
<span class="tag">パズル</span>
<span class="tag">クラシック</span>
</div>
<div class="game-container">
<!-- スコアエリア -->
<div class="score-area">
<div class="score-box">
<span class="score-label">スコア</span>
<span class="score-value" id="score">0</span>
</div>
<div class="score-box">
<span class="score-label">ベスト</span>
<span class="score-value" id="best-score">0</span>
</div>
<div class="score-box">
<span class="score-label">レベル</span>
<span class="score-value" id="level">1</span>
</div>
<button class="btn-new-game" id="btn-new-game">New Game</button>
</div>
<!-- ゲームエリア(ホールド + ボード + ネクスト表示) -->
<div class="tetris-area">
<!-- ホールドピース表示 -->
<div class="hold-piece-panel">
<div class="hold-piece-label">ホールド</div>
<div class="hold-piece-box" id="hold-piece-box"></div>
</div>
<!-- ゲームボード -->
<div class="board-wrapper">
<div class="tetris-board" id="tetris-board"></div>
<!-- ゲームオーバーオーバーレイ -->
<div class="game-overlay" id="game-over-overlay">
<div class="overlay-content">
<h2>ゲームオーバー</h2>
<p>スコア: <span id="final-score">0</span></p>
<p class="best-result" id="best-result"></p>
<button class="btn-primary" id="btn-retry">もう一度遊ぶ</button>
</div>
</div>
<!-- スタートオーバーレイ -->
<div class="game-overlay active" id="game-start-overlay">
<div class="overlay-content">
<h2>ブロックパズル</h2>
<p>ブロックを並べてラインを消そう!</p>
<button class="btn-primary" id="btn-start">ゲームスタート</button>
</div>
</div>
</div>
<!-- ネクストピース表示 -->
<div class="next-piece-panel">
<div class="next-piece-label">つぎ</div>
<div class="next-piece-box" id="next-piece-box"></div>
</div>
</div>
<!-- モバイル操作ボタン -->
<div class="mobile-controls" id="mobile-controls">
<div class="mobile-controls-row">
<button class="mobile-btn" id="btn-hold" aria-label="ホールド">☐ ホールド</button>
<button class="mobile-btn" id="btn-rotate" aria-label="回転">⟳</button>
</div>
<div class="mobile-controls-row">
<button class="mobile-btn" id="btn-left" aria-label="左">◀</button>
<button class="mobile-btn" id="btn-down" aria-label="下">▼</button>
<button class="mobile-btn" id="btn-right" aria-label="右">▶</button>
</div>
<div class="mobile-controls-row">
<button class="mobile-btn mobile-btn-wide" id="btn-drop" aria-label="ハードドロップ">▼▼ ドロップ</button>
</div>
</div>
</div>
<div class="game-instructions">
<p>「ブロックパズル」は、落ちてくるブロックを回転・移動させてラインをそろえて消す、テトリス風の無料パズルゲームです。</p>
<h3>遊び方</h3>
<ul>
<li><strong>PC</strong>: ← → で左右移動、↑ で回転、↓ でソフトドロップ、スペースでハードドロップ、Shift/Cでホールド</li>
<li><strong>スマホ</strong>: 画面下のボタンで操作</li>
<li>横一列をブロックで埋めるとラインが消えます</li>
<li>一度に多くのラインを消すと高得点!(1列=100, 2列=300, 3列=500, 4列=800)</li>
<li>10ライン消すごとにレベルアップ!スピードも上がります</li>
<li>ブロックが一番上まで積み上がるとゲームオーバー!</li>
</ul>
</div>
<div class="game-promo">
<h3>このゲーム、自分でも作れるよ!</h3>
<p>
ブロックパズルはJavaScriptの配列操作と<br>
タイマー処理で作られています。<br><br>
ひなテックでは、こうしたゲームの<br>
作り方を楽しく学べます!
</p>
<div class="promo-links">
<a href="tutorial/" class="btn-primary">このゲームの作り方を見る →</a>
<a href="https://hinata-ya.tech/contact/" class="btn-primary">無料体験に申し込む →</a>
<a href="source/" class="btn-secondary">ソースコードを見る →</a>
</div>
</div>
<div class="other-games">
<a href="../../index.html">← 他のゲームも遊ぶ</a>
</div>
</div>
<div id="footer"></div>
<script src="../../js/common.js?v=20260317" data-base="../../"></script>
<script src="../../js/leaderboard.js"></script>
<script src="game.js?v=20260516"></script>
</body>
</html>
style.cssは、ブロックパズルの見た目を作っているファイルです。7種類のブロックはそれぞれ色が違いますが、その色は.cell-I(むらさき)から.cell-L(赤)までのクラスで1つずつ指定しています。JavaScriptは「このマスはJ型の色」と決めるとき、マスにcell-Jというクラスを付けるだけで色がつきます。
.cell-ghostは「ゴーストピース」という、ブロックが落ちる先をうすく見せるための専用スタイルです。ライン消去のときの光る演出は.clearingクラスのアニメーションで作っています。
ゲーム盤はdisplay:gridで10列のマス目に並べています。ファイルの下のほうの@mediaはスマホ向けにマスを小さくする指定です。
/* ============================================
ブロックパズル - 固有スタイル
============================================ */
/* スコアエリア */
.score-area {
display: flex;
align-items: center;
gap: 0.75rem;
margin-bottom: 1rem;
flex-wrap: wrap;
}
.score-box {
background: #BBADA0;
border-radius: 8px;
padding: 0.5rem 1rem;
text-align: center;
min-width: 70px;
}
.score-label {
display: block;
font-size: 0.7rem;
color: #EEE4DA;
text-transform: uppercase;
letter-spacing: 0.05em;
}
.score-value {
display: block;
font-size: 1.25rem;
font-weight: 700;
color: #fff;
}
.btn-new-game {
margin-left: auto;
background: #8F7A66;
color: #fff;
border: none;
border-radius: 8px;
padding: 0.5rem 1.25rem;
font-size: 0.85rem;
font-weight: 600;
cursor: pointer;
transition: background 0.2s;
}
.btn-new-game:hover {
background: #7A6658;
}
/* ブロックエリア(ボード + ネクスト) */
.tetris-area {
display: flex;
gap: 1rem;
justify-content: center;
align-items: flex-start;
}
/* ゲームボード */
.board-wrapper {
position: relative;
width: 100%;
max-width: 300px;
aspect-ratio: 1 / 2;
touch-action: none;
}
.tetris-board {
position: absolute;
inset: 0;
display: grid;
grid-template-columns: repeat(10, 1fr);
grid-template-rows: repeat(20, 1fr);
gap: 1px;
padding: 4px;
background: #2C2C2E;
border-radius: 8px;
overflow: hidden;
}
.tetris-cell {
background: #1C1C1E;
border-radius: 2px;
}
/* ブロックの色(固定ブロック) */
.tetris-cell.cell-I { background: #8B5CF6; }
.tetris-cell.cell-O { background: #14B8A6; }
.tetris-cell.cell-T { background: #F59E0B; }
.tetris-cell.cell-S { background: #EC4899; }
.tetris-cell.cell-Z { background: #38BDF8; }
.tetris-cell.cell-J { background: #84CC16; }
.tetris-cell.cell-L { background: #EF4444; }
/* ゴーストピース */
.tetris-cell.cell-ghost {
background: rgba(255, 255, 255, 0.12);
border: 1px solid rgba(255, 255, 255, 0.2);
}
/* ホールドピースパネル */
.hold-piece-panel {
flex-shrink: 0;
width: 90px;
}
.hold-piece-label {
font-size: 0.75rem;
font-weight: 600;
color: #EEE4DA;
text-align: center;
background: #BBADA0;
border-radius: 8px 8px 0 0;
padding: 0.3rem 0.5rem;
letter-spacing: 0.05em;
}
.hold-piece-box {
background: #2C2C2E;
border-radius: 0 0 8px 8px;
padding: 10px;
display: grid;
grid-template-columns: repeat(4, 1fr);
grid-template-rows: repeat(4, 1fr);
gap: 2px;
aspect-ratio: 1 / 1;
}
.hold-piece-box.hold-locked {
opacity: 0.4;
}
.hold-cell {
background: #1C1C1E;
border-radius: 2px;
}
.hold-cell.cell-I { background: #8B5CF6; }
.hold-cell.cell-O { background: #14B8A6; }
.hold-cell.cell-T { background: #F59E0B; }
.hold-cell.cell-S { background: #EC4899; }
.hold-cell.cell-Z { background: #38BDF8; }
.hold-cell.cell-J { background: #84CC16; }
.hold-cell.cell-L { background: #EF4444; }
/* ネクストピースパネル */
.next-piece-panel {
flex-shrink: 0;
width: 90px;
}
.next-piece-label {
font-size: 0.75rem;
font-weight: 600;
color: #EEE4DA;
text-align: center;
background: #BBADA0;
border-radius: 8px 8px 0 0;
padding: 0.3rem 0.5rem;
letter-spacing: 0.05em;
}
.next-piece-box {
background: #2C2C2E;
border-radius: 0 0 8px 8px;
padding: 10px;
display: grid;
grid-template-columns: repeat(4, 1fr);
grid-template-rows: repeat(4, 1fr);
gap: 2px;
aspect-ratio: 1 / 1;
}
.next-cell {
background: #1C1C1E;
border-radius: 2px;
}
.next-cell.cell-I { background: #8B5CF6; }
.next-cell.cell-O { background: #14B8A6; }
.next-cell.cell-T { background: #F59E0B; }
.next-cell.cell-S { background: #EC4899; }
.next-cell.cell-Z { background: #38BDF8; }
.next-cell.cell-J { background: #84CC16; }
.next-cell.cell-L { background: #EF4444; }
/* ゲームオーバーレイ */
.game-overlay {
position: absolute;
inset: 0;
background: rgba(44, 44, 46, 0.9);
border-radius: 8px;
display: none;
align-items: center;
justify-content: center;
z-index: 10;
animation: overlay-appear 0.3s ease;
}
.game-overlay.active {
display: flex;
}
@keyframes overlay-appear {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
.overlay-content {
text-align: center;
padding: 1.5rem;
}
.overlay-content h2 {
font-size: 1.75rem;
color: #fff;
margin-bottom: 0.5rem;
}
.overlay-content p {
font-size: 1.1rem;
color: #ddd;
margin-bottom: 0.75rem;
}
.overlay-content .best-result {
font-size: 0.95rem;
color: #FFEB3B;
font-weight: 600;
margin-bottom: 1rem;
}
.overlay-content .btn-primary {
margin-bottom: 0.5rem;
}
/* モバイル操作ボタン */
.mobile-controls {
display: none;
margin: 1.25rem auto 0;
max-width: 280px;
}
/* PCでは非表示 */
@media (min-width: 769px) {
.mobile-controls {
display: none;
}
}
/* モバイルでは表示 */
@media (max-width: 768px) {
.mobile-controls {
display: block;
}
}
.mobile-controls-row {
display: flex;
justify-content: center;
gap: 8px;
margin-bottom: 8px;
}
.mobile-btn {
width: 72px;
height: 52px;
border: none;
border-radius: 12px;
background: #8F7A66;
color: #fff;
font-size: 1.25rem;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: background 0.15s, transform 0.1s;
user-select: none;
-webkit-user-select: none;
-webkit-tap-highlight-color: transparent;
}
.mobile-btn:active {
background: #7A6658;
transform: scale(0.92);
}
.mobile-btn-wide {
width: 100%;
max-width: 232px;
font-size: 0.95rem;
font-weight: 600;
gap: 0.25rem;
}
/* ライン消去アニメーション */
@keyframes line-clear {
0% {
background: #fff;
transform: scaleY(1);
}
50% {
background: #FFEB3B;
}
100% {
background: #fff;
transform: scaleY(0);
opacity: 0;
}
}
.tetris-cell.clearing {
animation: line-clear 0.3s ease forwards;
}
/* レスポンシブ */
@media (max-width: 500px) {
.tetris-area {
gap: 0.5rem;
}
.board-wrapper {
max-width: 250px;
}
.hold-piece-panel {
width: 72px;
}
.hold-piece-box {
padding: 6px;
}
.next-piece-panel {
width: 72px;
}
.next-piece-box {
padding: 6px;
}
.tetris-board {
gap: 1px;
padding: 3px;
}
.tetris-cell {
border-radius: 1px;
}
}
@media (max-width: 360px) {
.board-wrapper {
max-width: 210px;
}
.hold-piece-panel {
width: 60px;
}
.next-piece-panel {
width: 60px;
}
.tetris-board {
gap: 0px;
padding: 2px;
}
.score-box {
padding: 0.4rem 0.75rem;
min-width: 55px;
}
.score-value {
font-size: 1rem;
}
.mobile-btn {
width: 60px;
height: 46px;
border-radius: 10px;
font-size: 1.1rem;
}
.mobile-btn-wide {
max-width: 196px;
font-size: 0.85rem;
}
}
game.jsがこのゲームの心臓部です。少し長いですが、やっていることを順番に追えば必ず理解できます。全体は(function () { ... })();という「即時実行関数」で囲まれ、ゲームの変数が他のコードとぶつからないようになっています。
まず大事なのがboardという20行×10列の2次元配列です。board[y][x]が空文字ならそのマスは空き、色名('I'など)が入っていればブロックが固定されています。落下中のブロックはcurrentPieceという別のオブジェクトで、種類・回転状態・位置(x, y)を持っています。
このゲームでいちばん面白いのがTETROMINOSというブロックの設計図です。7種類それぞれに、回転した4つの形を「ブロックを構成する4マスの座標リスト」として書き並べています。だから「回転する」というのは難しい計算をしているわけではなく、rotationという番号を(rotation + 1) % 4で次に進めて、別の形のリストに切りかえているだけなのです。
ブロックが動けるかどうかはisValidPosition()が判定します。ブロックの4マスを1つずつ見て、盤の外に出ていないか、すでにあるブロックとぶつからないかを調べます。回転したいのに壁が邪魔なとき、左右に少しずらしてもう一度試すrotatePiece()の中の「ウォールキック」も、ブロックパズルならではの気持ちよさを生む工夫です。
横一列がそろったかはclearLines()が調べます。そろった行はboard.splice()で配列から抜き取り、かわりに空っぽの行をboard.unshift()で上に足します。これで上のブロックがストンと落ちます。消した列数に応じてLINE_SCORESから得点を決め、4列同時消しがいちばん高得点です。
自動で落下させているのはgameStep()です。setTimeoutでcurrentSpeedミリ秒ごとに自分をまた呼び出し、1マスずつ下げています。レベルが上がるとcalcSpeed()でこの間隔が短くなり、ブロックが速く落ちてきます。接地してすぐ固定せず、LOCK_DELAYのあいだ動かせる「ロック猶予」も本格的な仕組みです。ベストスコアはlocalStorageに保存されます。
// ============================================
// ブロックパズル ロジック
// ============================================
(function () {
'use strict';
// ============================================
// 定数
// ============================================
var COLS = 10; // 横のマス数
var ROWS = 20; // 縦のマス数
var INITIAL_SPEED = 800; // 初期落下速度(ミリ秒)
var MIN_SPEED = 80; // 最速(ミリ秒)
var SPEED_DECREASE = 60; // レベルごとの速度減少量
var LINES_PER_LEVEL = 10; // レベルアップに必要なライン数
var LOCK_DELAY = 500; // 接地後のロック猶予(ミリ秒)
// スコアテーブル(消去ライン数 → 得点)
var LINE_SCORES = {
1: 100,
2: 300,
3: 500,
4: 800
};
// テトロミノの定義(4つの回転状態)
var TETROMINOS = {
I: {
shapes: [
[[0,0],[1,0],[2,0],[3,0]],
[[0,0],[0,1],[0,2],[0,3]],
[[0,0],[1,0],[2,0],[3,0]],
[[0,0],[0,1],[0,2],[0,3]]
],
color: 'I'
},
O: {
shapes: [
[[0,0],[1,0],[0,1],[1,1]],
[[0,0],[1,0],[0,1],[1,1]],
[[0,0],[1,0],[0,1],[1,1]],
[[0,0],[1,0],[0,1],[1,1]]
],
color: 'O'
},
T: {
shapes: [
[[0,0],[1,0],[2,0],[1,1]],
[[0,0],[0,1],[0,2],[1,1]],
[[1,0],[0,1],[1,1],[2,1]],
[[1,0],[1,1],[1,2],[0,1]]
],
color: 'T'
},
S: {
shapes: [
[[1,0],[2,0],[0,1],[1,1]],
[[0,0],[0,1],[1,1],[1,2]],
[[1,0],[2,0],[0,1],[1,1]],
[[0,0],[0,1],[1,1],[1,2]]
],
color: 'S'
},
Z: {
shapes: [
[[0,0],[1,0],[1,1],[2,1]],
[[1,0],[0,1],[1,1],[0,2]],
[[0,0],[1,0],[1,1],[2,1]],
[[1,0],[0,1],[1,1],[0,2]]
],
color: 'Z'
},
J: {
shapes: [
[[0,0],[0,1],[1,1],[2,1]],
[[0,0],[1,0],[0,1],[0,2]],
[[0,0],[1,0],[2,0],[2,1]],
[[1,0],[1,1],[0,2],[1,2]]
],
color: 'J'
},
L: {
shapes: [
[[2,0],[0,1],[1,1],[2,1]],
[[0,0],[0,1],[0,2],[1,2]],
[[0,0],[1,0],[2,0],[0,1]],
[[0,0],[1,0],[1,1],[1,2]]
],
color: 'L'
}
};
var PIECE_NAMES = ['I', 'O', 'T', 'S', 'Z', 'J', 'L'];
// ============================================
// 状態変数
// ============================================
var board = []; // ボード配列 [row][col] = '' or color名
var currentPiece = null; // 現在のピース { type, rotation, x, y }
var nextPieceType = null; // 次のピースの種類
var holdPieceType = null; // ホールド中のピースの種類
var holdUsed = false; // 現在のピースでホールド済みか
var score = 0;
var bestScore = parseInt(localStorage.getItem('bestTetris') || '0', 10);
var level = 1;
var totalLines = 0;
var gameRunning = false;
var gameOverFlag = false;
var gameLoopTimer = null;
var currentSpeed = INITIAL_SPEED;
var lockTimer = null;
var isLocking = false;
// ============================================
// DOM要素
// ============================================
var boardEl = document.getElementById('tetris-board');
var scoreEl = document.getElementById('score');
var bestScoreEl = document.getElementById('best-score');
var levelEl = document.getElementById('level');
var gameOverOverlay = document.getElementById('game-over-overlay');
var gameStartOverlay = document.getElementById('game-start-overlay');
var finalScoreEl = document.getElementById('final-score');
var bestResultEl = document.getElementById('best-result');
var nextPieceBox = document.getElementById('next-piece-box');
var holdPieceBox = document.getElementById('hold-piece-box');
// セルの2次元配列(高速アクセス用)
var cells = [];
var nextCells = [];
var holdCells = [];
// ============================================
// ボードの初期化(DOMセルを生成)
// ============================================
function createBoard() {
boardEl.innerHTML = '';
cells = [];
for (var y = 0; y < ROWS; y++) {
cells[y] = [];
for (var x = 0; x < COLS; x++) {
var cell = document.createElement('div');
cell.className = 'tetris-cell';
boardEl.appendChild(cell);
cells[y][x] = cell;
}
}
}
function createNextPieceDisplay() {
nextPieceBox.innerHTML = '';
nextCells = [];
for (var y = 0; y < 4; y++) {
nextCells[y] = [];
for (var x = 0; x < 4; x++) {
var cell = document.createElement('div');
cell.className = 'next-cell';
nextPieceBox.appendChild(cell);
nextCells[y][x] = cell;
}
}
}
function createHoldPieceDisplay() {
holdPieceBox.innerHTML = '';
holdCells = [];
for (var y = 0; y < 4; y++) {
holdCells[y] = [];
for (var x = 0; x < 4; x++) {
var cell = document.createElement('div');
cell.className = 'hold-cell';
holdPieceBox.appendChild(cell);
holdCells[y][x] = cell;
}
}
}
// ============================================
// ボードデータ初期化
// ============================================
function initBoard() {
board = [];
for (var y = 0; y < ROWS; y++) {
board[y] = [];
for (var x = 0; x < COLS; x++) {
board[y][x] = '';
}
}
}
// ============================================
// ランダムなテトロミノを生成
// ============================================
function randomPieceType() {
return PIECE_NAMES[Math.floor(Math.random() * PIECE_NAMES.length)];
}
// ============================================
// 新しいピースを生成
// ============================================
function spawnPiece() {
var type = nextPieceType || randomPieceType();
nextPieceType = randomPieceType();
holdUsed = false;
var piece = {
type: type,
rotation: 0,
x: Math.floor(COLS / 2) - 1,
y: 0
};
// I型は少し左にずらす
if (type === 'I') {
piece.x = Math.floor(COLS / 2) - 2;
}
// 設置できるか確認
if (!isValidPosition(piece)) {
// ゲームオーバー
return null;
}
return piece;
}
// ============================================
// ピースの現在の形状を取得
// ============================================
function getShape(piece) {
return TETROMINOS[piece.type].shapes[piece.rotation];
}
// ============================================
// ピースの位置が有効か判定
// ============================================
function isValidPosition(piece) {
var shape = getShape(piece);
for (var i = 0; i < shape.length; i++) {
var newX = piece.x + shape[i][0];
var newY = piece.y + shape[i][1];
// 範囲外チェック
if (newX < 0 || newX >= COLS || newY < 0 || newY >= ROWS) {
return false;
}
// 他のブロックとの衝突チェック
if (board[newY][newX] !== '') {
return false;
}
}
return true;
}
// ============================================
// ゴースト位置の計算(ハードドロップ先)
// ============================================
function getGhostY(piece) {
var ghostY = piece.y;
while (true) {
var testPiece = { type: piece.type, rotation: piece.rotation, x: piece.x, y: ghostY + 1 };
if (!isValidPosition(testPiece)) {
break;
}
ghostY++;
}
return ghostY;
}
// ============================================
// ボードの描画更新
// ============================================
function render() {
// すべてのセルをリセット
for (var y = 0; y < ROWS; y++) {
for (var x = 0; x < COLS; x++) {
if (board[y][x] !== '') {
cells[y][x].className = 'tetris-cell cell-' + board[y][x];
} else {
cells[y][x].className = 'tetris-cell';
}
}
}
if (currentPiece) {
// ゴーストピースを描画
var ghostY = getGhostY(currentPiece);
if (ghostY !== currentPiece.y) {
var ghostShape = getShape(currentPiece);
for (var i = 0; i < ghostShape.length; i++) {
var gx = currentPiece.x + ghostShape[i][0];
var gy = ghostY + ghostShape[i][1];
if (gy >= 0 && gy < ROWS && gx >= 0 && gx < COLS && board[gy][gx] === '') {
cells[gy][gx].className = 'tetris-cell cell-ghost';
}
}
}
// 現在のピースを描画
var shape = getShape(currentPiece);
var color = TETROMINOS[currentPiece.type].color;
for (var i = 0; i < shape.length; i++) {
var px = currentPiece.x + shape[i][0];
var py = currentPiece.y + shape[i][1];
if (py >= 0 && py < ROWS && px >= 0 && px < COLS) {
cells[py][px].className = 'tetris-cell cell-' + color;
}
}
}
// ネクストピースを描画
renderNextPiece();
// ホールドピースを描画
renderHoldPiece();
}
// ============================================
// ネクストピースの描画
// ============================================
function renderNextPiece() {
// リセット
for (var y = 0; y < 4; y++) {
for (var x = 0; x < 4; x++) {
nextCells[y][x].className = 'next-cell';
}
}
if (!nextPieceType) return;
var shape = TETROMINOS[nextPieceType].shapes[0];
var color = TETROMINOS[nextPieceType].color;
// ピースを中央寄せするためのオフセットを計算
var minX = 4, maxX = 0, minY = 4, maxY = 0;
for (var i = 0; i < shape.length; i++) {
if (shape[i][0] < minX) minX = shape[i][0];
if (shape[i][0] > maxX) maxX = shape[i][0];
if (shape[i][1] < minY) minY = shape[i][1];
if (shape[i][1] > maxY) maxY = shape[i][1];
}
var pieceWidth = maxX - minX + 1;
var pieceHeight = maxY - minY + 1;
var offsetX = Math.floor((4 - pieceWidth) / 2) - minX;
var offsetY = Math.floor((4 - pieceHeight) / 2) - minY;
for (var i = 0; i < shape.length; i++) {
var nx = shape[i][0] + offsetX;
var ny = shape[i][1] + offsetY;
if (nx >= 0 && nx < 4 && ny >= 0 && ny < 4) {
nextCells[ny][nx].className = 'next-cell cell-' + color;
}
}
}
// ============================================
// ホールドピースの描画
// ============================================
function renderHoldPiece() {
// リセット
for (var y = 0; y < 4; y++) {
for (var x = 0; x < 4; x++) {
holdCells[y][x].className = 'hold-cell';
}
}
// ホールド使用済みならボックスを暗くする
if (holdUsed) {
holdPieceBox.classList.add('hold-locked');
} else {
holdPieceBox.classList.remove('hold-locked');
}
if (!holdPieceType) return;
var shape = TETROMINOS[holdPieceType].shapes[0];
var color = TETROMINOS[holdPieceType].color;
// ピースを中央寄せするためのオフセットを計算
var minX = 4, maxX = 0, minY = 4, maxY = 0;
for (var i = 0; i < shape.length; i++) {
if (shape[i][0] < minX) minX = shape[i][0];
if (shape[i][0] > maxX) maxX = shape[i][0];
if (shape[i][1] < minY) minY = shape[i][1];
if (shape[i][1] > maxY) maxY = shape[i][1];
}
var pieceWidth = maxX - minX + 1;
var pieceHeight = maxY - minY + 1;
var offsetX = Math.floor((4 - pieceWidth) / 2) - minX;
var offsetY = Math.floor((4 - pieceHeight) / 2) - minY;
for (var i = 0; i < shape.length; i++) {
var hx = shape[i][0] + offsetX;
var hy = shape[i][1] + offsetY;
if (hx >= 0 && hx < 4 && hy >= 0 && hy < 4) {
holdCells[hy][hx].className = 'hold-cell cell-' + color;
}
}
}
// ============================================
// ピースをボードに固定
// ============================================
function lockPiece() {
var shape = getShape(currentPiece);
var color = TETROMINOS[currentPiece.type].color;
for (var i = 0; i < shape.length; i++) {
var px = currentPiece.x + shape[i][0];
var py = currentPiece.y + shape[i][1];
if (py >= 0 && py < ROWS && px >= 0 && px < COLS) {
board[py][px] = color;
}
}
// ロックタイマーをクリア
clearLockTimer();
isLocking = false;
}
// ============================================
// ライン消去チェック
// ============================================
function clearLines() {
var linesCleared = 0;
var linesToClear = [];
for (var y = ROWS - 1; y >= 0; y--) {
var full = true;
for (var x = 0; x < COLS; x++) {
if (board[y][x] === '') {
full = false;
break;
}
}
if (full) {
linesToClear.push(y);
linesCleared++;
}
}
if (linesCleared > 0) {
// 消去アニメーション
for (var i = 0; i < linesToClear.length; i++) {
var row = linesToClear[i];
for (var x = 0; x < COLS; x++) {
cells[row][x].classList.add('clearing');
}
}
// アニメーション後にラインを実際に消去
setTimeout(function () {
// ラインを上から順にソート(下から消すため)
linesToClear.sort(function (a, b) { return a - b; });
for (var i = linesToClear.length - 1; i >= 0; i--) {
board.splice(linesToClear[i], 1);
}
// 上に空行を追加
for (var i = 0; i < linesCleared; i++) {
var emptyRow = [];
for (var x = 0; x < COLS; x++) {
emptyRow.push('');
}
board.unshift(emptyRow);
}
// スコア計算
var lineScore = LINE_SCORES[linesCleared] || (linesCleared * 200);
score += lineScore * level;
totalLines += linesCleared;
// レベルアップ
var newLevel = Math.floor(totalLines / LINES_PER_LEVEL) + 1;
if (newLevel > level) {
level = newLevel;
currentSpeed = calcSpeed();
levelEl.textContent = level;
}
updateScore();
render();
}, 300);
}
return linesCleared;
}
// ============================================
// スコア更新
// ============================================
function updateScore() {
scoreEl.textContent = score;
if (score > bestScore) {
bestScore = score;
localStorage.setItem('bestTetris', bestScore.toString());
}
bestScoreEl.textContent = bestScore;
}
// ============================================
// 速度の計算(レベルに応じて速くなる)
// ============================================
function calcSpeed() {
var speed = INITIAL_SPEED - ((level - 1) * SPEED_DECREASE);
return Math.max(speed, MIN_SPEED);
}
// ============================================
// ピースを左右に移動
// ============================================
function movePiece(dx) {
if (!currentPiece || !gameRunning) return;
var testPiece = {
type: currentPiece.type,
rotation: currentPiece.rotation,
x: currentPiece.x + dx,
y: currentPiece.y
};
if (isValidPosition(testPiece)) {
currentPiece.x = testPiece.x;
// ロック中に横移動した場合、ロックタイマーをリセット
if (isLocking) {
resetLockTimer();
}
render();
}
}
// ============================================
// ピースを回転
// ============================================
function rotatePiece() {
if (!currentPiece || !gameRunning) return;
var newRotation = (currentPiece.rotation + 1) % 4;
var testPiece = {
type: currentPiece.type,
rotation: newRotation,
x: currentPiece.x,
y: currentPiece.y
};
// 通常の回転を試す
if (isValidPosition(testPiece)) {
currentPiece.rotation = newRotation;
if (isLocking) {
resetLockTimer();
}
render();
return;
}
// ウォールキック: 左右にずらして試す
var kicks = [1, -1, 2, -2];
for (var i = 0; i < kicks.length; i++) {
testPiece.x = currentPiece.x + kicks[i];
if (isValidPosition(testPiece)) {
currentPiece.rotation = newRotation;
currentPiece.x = testPiece.x;
if (isLocking) {
resetLockTimer();
}
render();
return;
}
}
}
// ============================================
// ソフトドロップ(1マス下に移動)
// ============================================
function softDrop() {
if (!currentPiece || !gameRunning) return false;
var testPiece = {
type: currentPiece.type,
rotation: currentPiece.rotation,
x: currentPiece.x,
y: currentPiece.y + 1
};
if (isValidPosition(testPiece)) {
currentPiece.y = testPiece.y;
score += 1;
updateScore();
render();
return true;
}
return false;
}
// ============================================
// ハードドロップ(一番下まで落とす)
// ============================================
function hardDrop() {
if (!currentPiece || !gameRunning) return;
var ghostY = getGhostY(currentPiece);
var dropDistance = ghostY - currentPiece.y;
currentPiece.y = ghostY;
score += dropDistance * 2;
updateScore();
// 即座にロック
clearLockTimer();
isLocking = false;
lockPiece();
var linesCleared = clearLines();
// 次のピースを出す
currentPiece = spawnPiece();
if (!currentPiece) {
gameOver();
return;
}
render();
// ゲームループをリスタート
clearGameLoop();
if (linesCleared > 0) {
// ライン消去アニメーション後にゲームループ再開
gameLoopTimer = setTimeout(gameStep, 350);
} else {
gameLoopTimer = setTimeout(gameStep, currentSpeed);
}
}
// ============================================
// ホールド(ピースを保存・交換)
// ============================================
function holdPiece() {
if (!currentPiece || !gameRunning || holdUsed) return;
var currentType = currentPiece.type;
// ロック状態をリセット
clearLockTimer();
isLocking = false;
if (holdPieceType) {
// ホールド中のピースと交換
var swapType = holdPieceType;
holdPieceType = currentType;
var piece = {
type: swapType,
rotation: 0,
x: Math.floor(COLS / 2) - 1,
y: 0
};
if (swapType === 'I') {
piece.x = Math.floor(COLS / 2) - 2;
}
if (!isValidPosition(piece)) {
gameOver();
return;
}
currentPiece = piece;
} else {
// 初回ホールド: 現在のピースを保存して次のピースを出す
holdPieceType = currentType;
currentPiece = spawnPiece();
if (!currentPiece) {
gameOver();
return;
}
}
// 同じピースで2回ホールドできないようにする
holdUsed = true;
render();
// ゲームループをリスタート
clearGameLoop();
gameLoopTimer = setTimeout(gameStep, currentSpeed);
}
// ============================================
// ロックタイマー管理
// ============================================
function clearLockTimer() {
if (lockTimer) {
clearTimeout(lockTimer);
lockTimer = null;
}
}
function resetLockTimer() {
clearLockTimer();
lockTimer = setTimeout(function () {
if (!gameRunning || !currentPiece) return;
// まだ接地しているか確認
var testPiece = {
type: currentPiece.type,
rotation: currentPiece.rotation,
x: currentPiece.x,
y: currentPiece.y + 1
};
if (!isValidPosition(testPiece)) {
// ロック実行
lockPiece();
isLocking = false;
var linesCleared = clearLines();
currentPiece = spawnPiece();
if (!currentPiece) {
gameOver();
return;
}
render();
clearGameLoop();
if (linesCleared > 0) {
gameLoopTimer = setTimeout(gameStep, 350);
} else {
gameLoopTimer = setTimeout(gameStep, currentSpeed);
}
} else {
isLocking = false;
}
}, LOCK_DELAY);
}
// ============================================
// ゲームループのクリア
// ============================================
function clearGameLoop() {
if (gameLoopTimer) {
clearTimeout(gameLoopTimer);
gameLoopTimer = null;
}
}
// ============================================
// ゲームの1ステップ(自動落下)
// ============================================
function gameStep() {
if (!gameRunning || gameOverFlag || !currentPiece) return;
var testPiece = {
type: currentPiece.type,
rotation: currentPiece.rotation,
x: currentPiece.x,
y: currentPiece.y + 1
};
if (isValidPosition(testPiece)) {
currentPiece.y = testPiece.y;
isLocking = false;
clearLockTimer();
render();
gameLoopTimer = setTimeout(gameStep, currentSpeed);
} else {
// 接地した。ロック猶予を開始
if (!isLocking) {
isLocking = true;
resetLockTimer();
}
render();
// 自動落下ループは止まる。ロックタイマーが次のステップを引き受ける
gameLoopTimer = setTimeout(gameStep, currentSpeed);
}
}
// ============================================
// ゲームオーバー処理
// ============================================
function gameOver() {
gameRunning = false;
gameOverFlag = true;
clearGameLoop();
clearLockTimer();
finalScoreEl.textContent = score;
// ベストスコア更新チェック
if (score >= bestScore && score > 0) {
bestResultEl.textContent = 'ハイスコア更新!';
} else {
bestResultEl.textContent = 'ベスト: ' + bestScore;
}
// 少し遅延させてオーバーレイを表示
setTimeout(function () {
gameOverOverlay.classList.add('active');
if (window.Leaderboard) Leaderboard.show('tetris', score);
}, 300);
}
// ============================================
// ゲーム開始・リスタート
// ============================================
function startGame() {
// タイマーをクリア
clearGameLoop();
clearLockTimer();
// 状態をリセット
score = 0;
level = 1;
totalLines = 0;
gameOverFlag = false;
isLocking = false;
holdPieceType = null;
holdUsed = false;
currentSpeed = INITIAL_SPEED;
updateScore();
levelEl.textContent = level;
// オーバーレイを非表示
gameOverOverlay.classList.remove('active');
gameStartOverlay.classList.remove('active');
// ボードを初期化
initBoard();
// 最初のピースを生成
nextPieceType = randomPieceType();
currentPiece = spawnPiece();
render();
// ゲームループ開始
gameRunning = true;
gameLoopTimer = setTimeout(gameStep, currentSpeed);
}
// ============================================
// キーボード操作
// ============================================
document.addEventListener('keydown', function (e) {
if (!gameRunning || gameOverFlag) {
// スタート画面でキーが押されたらゲーム開始
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
if (!gameRunning && !gameOverFlag) {
startGame();
}
}
return;
}
switch (e.key) {
case 'ArrowLeft':
case 'a':
case 'A':
e.preventDefault();
movePiece(-1);
break;
case 'ArrowRight':
case 'd':
case 'D':
e.preventDefault();
movePiece(1);
break;
case 'ArrowUp':
case 'w':
case 'W':
e.preventDefault();
rotatePiece();
break;
case 'ArrowDown':
case 's':
case 'S':
e.preventDefault();
if (softDrop()) {
// ソフトドロップ成功時、ゲームループをリセット
clearGameLoop();
gameLoopTimer = setTimeout(gameStep, currentSpeed);
}
break;
case ' ':
e.preventDefault();
hardDrop();
break;
case 'Shift':
case 'c':
case 'C':
e.preventDefault();
holdPiece();
break;
}
});
// ============================================
// モバイル操作ボタン
// ============================================
function setupMobileButton(btnId, action) {
var btn = document.getElementById(btnId);
if (!btn) return;
btn.addEventListener('click', function (e) {
e.preventDefault();
if (!gameRunning && !gameOverFlag) {
startGame();
return;
}
if (gameRunning) {
action();
}
});
btn.addEventListener('touchstart', function (e) {
e.preventDefault();
if (!gameRunning && !gameOverFlag) {
startGame();
return;
}
if (gameRunning) {
action();
}
}, { passive: false });
}
setupMobileButton('btn-hold', function () { holdPiece(); });
setupMobileButton('btn-left', function () { movePiece(-1); });
setupMobileButton('btn-right', function () { movePiece(1); });
setupMobileButton('btn-rotate', function () { rotatePiece(); });
setupMobileButton('btn-down', function () {
if (softDrop()) {
clearGameLoop();
gameLoopTimer = setTimeout(gameStep, currentSpeed);
}
});
setupMobileButton('btn-drop', function () { hardDrop(); });
// ============================================
// ボタンイベント
// ============================================
document.getElementById('btn-new-game').addEventListener('click', function () {
startGame();
});
document.getElementById('btn-retry').addEventListener('click', function () {
startGame();
});
document.getElementById('btn-start').addEventListener('click', function () {
startGame();
});
// ============================================
// ページ離脱時にゲームループを停止
// ============================================
document.addEventListener('visibilitychange', function () {
if (document.hidden && gameRunning) {
clearGameLoop();
clearLockTimer();
gameRunning = false;
}
});
// ============================================
// 初期化
// ============================================
bestScoreEl.textContent = bestScore;
createBoard();
createNextPieceDisplay();
createHoldPieceDisplay();
initBoard();
// ネクストピースのプレビュー用に初期化
nextPieceType = randomPieceType();
renderNextPiece();
})();
board[行][列]でマス目のデータを管理するisValidPosition()でブロックの各マスが壁や他ブロックと重ならないか調べる上のコードをそれぞれのファイル名で同じフォルダに保存し、index.htmlをブラウザで開けばゲームが動きます。各コードブロックのファイル名バーにある「コピー」ボタンを使うと便利です。
このゲームはサイト全体で共通して使うファイル(共通CSSやヘッダー)も少し利用しているため、ソースのファイルだけで開くとヘッダーなどサイト共通の部分は表示されません。でもゲーム本体はちゃんと動くので、仕組みを学んだり改造してためすには十分です。
慣れてきたら、色を変えたりルールを少しいじったり、自由に改造してみてください。コードを「読む」次は「いじってみる」のがいちばんの上達法です。
このソースコードは、プログラミングの勉強のために自由に読んだり、コピーして動かしたり、改造したりして大丈夫です。学校の授業や自由研究で参考にするのも歓迎します。ぜひ「自分だったらどう作るか」を考えるきっかけにしてください。
ただし、コードをそのまままるごとコピーして自分の作品やサービスとして公開・配布するのはご遠慮ください。あくまで学習用としてお使いください。
コードを読んで「自分でも作ってみたい!」と思ったら、ブロックパズルの作り方ガイドがおすすめです。このページが「完成したコード」を見せるのに対して、作り方ガイドは何もない状態から1ステップずつ組み立てていく流れを解説しています。
ひなテックでは、こうしたゲームづくりを先生といっしょに楽しく学べる教室を開いています。「コードを読むだけじゃなく、ちゃんと作れるようになりたい」という人は、無料体験にぜひ来てみてください。