テトリスの7種類のブロックは、なぜその形なのか
遊びを見つける執筆にClaudeを使用しています。

落ちものパズルのブロックは、どれも4つの正方形をつないだ形です。ここで素朴な疑問が出てきます。4マスのつなぎ方は、本当に7通りしかないのでしょうか。ほかにも形があるのに、使われていないだけなのでしょうか。 この問いは、紙とペンで答えが出ます。ただし答えは「数え方」で変わります。回転して重なる形を同じとみなすか、裏返して重なる形まで同じとみなすかで、数は変わるからです。この記事では、4マスの形を自分の手で全部並べ、基準ごとに数え直す手順を示します。最後に、7という数がどの基準で出てくるかを読み解きます。 なお、なぜ4マスにしたのかという開発側の意図は、ここでは扱いません。見るのは、4マスの形が数の上でどう並ぶかだけです。
数える前に決める「同じ形」の三つの基準
四角を辺でつないだ図形を、一般にポリオミノと呼びます。4マスのものがテトロミノです。数える前に、どの形どうしを「同じ」とみなすかを決めておきます。基準は三つあります。
- 固定、同じとみなす操作:平行移動だけ、呼び方の例:向きが違えば別の形
- 回転のみ、同じとみなす操作:平行移動と回転、呼び方の例:裏返さないと重ならない形は別
- 回転と裏返し、同じとみなす操作:平行移動・回転・裏返し、呼び方の例:鏡に映して重なる形も同じ
「つなぐ」は辺で接することを指します。角だけで触れている二つのマスは、つながっているとは数えません。ここを曖昧にすると、数が合わなくなります。
3マスから育てて4マスを並べる手順
いきなり4マスを描くと、重複や抜けが出ます。3マスの形に1マス足していく方法なら、漏れなく数えられます。
準備するのは方眼紙(またはノートの方眼)と鉛筆、消しゴムです。
- 3マスの形を全部描きます。一列に並んだ直線形と、角を一つ曲げたL字形の2種類です。
- 直線形の周囲で、辺が接する空きマスを一つずつ探します。端の延長、端の横、中央の横といった位置があります。それぞれに1マス足した形を描きます。
- L字形でも同じことをします。足せる位置を一つ残らず試し、描きます。
- 描いた形を一枚ずつ切り抜くか、付箋に写します。
- 切り抜きを机に並べ、「回転だけで重なるもの」を見つけて重ねます。重なるものは、向きだけが違う同じ形です。
- 回転では重ならなくても、裏返すと重なる組を探します。鏡像の関係にある形です。
記録は、次の表を紙に作って行います。単位は「形一つにつき一行」です。
- 列1は、形の呼び名です。自分で付けた名前で構いません。
- 列2は、見つかった向きの数です。回転だけで何通りの向きがあるかを書きます。
- 列3は、裏返した形と重なるかどうかです。重なるなら「あり」、重ならないなら「なし」と書きます。
手作業は2回行うと確実です。1回目と2回目で形の総数が一致しなければ、足し忘れか重複があります。
コードで確かめる
手作業の結果は、短いプログラムで検算できます。下のコードをファイル(例: tetro.html)に保存してブラウザで開くと、1〜6マスの形を三つの基準で数えて表示します。外部のライブラリは使っていません。
<pre id="out"></pre>
<script>
const key = c => c.map(p => p.join(",")).sort().join(";");
const norm = c => {
const mx = Math.min(...c.map(p => p[0]));
const my = Math.min(...c.map(p => p[1]));
return c.map(([x, y]) => [x - mx, y - my]);
};
const rot = c => norm(c.map(([x, y]) => [-y, x]));
const flip = c => norm(c.map(([x, y]) => [-x, y]));
function grow(shapes) {
const next = new Map();
for (const s of shapes.values()) {
const have = new Set(s.map(p => p.join(",")));
for (const [x, y] of s) {
for (const [dx, dy] of [[1,0],[-1,0],[0,1],[0,-1]]) {
const q = [x + dx, y + dy];
if (have.has(q.join(","))) continue;
const t = norm([...s, q]);
next.set(key(t), t);
}
}
}
return next;
}
function classes(shapes, withFlip) {
const seen = new Set();
for (const s of shapes.values()) {
let forms = [s], cur = s;
for (let i = 0; i < 3; i++) { cur = rot(cur); forms.push(cur); }
if (withFlip) forms = forms.concat(forms.map(flip));
seen.add(forms.map(key).sort()[0]);
}
return seen.size;
}
let shapes = new Map([[key([[0,0]]), [[0,0]]]]);
let text = "";
for (let n = 1; n <= 6; n++) {
if (n > 1) shapes = grow(shapes);
text += `${n}マス: 固定 ${shapes.size} / 回転のみ ${classes(shapes, false)} / 回転と裏返し ${classes(shapes, true)}\n`;
}
document.getElementById("out").textContent = text;
</script>仕組みは手作業と同じです。1マスから始めて、辺で接する空きマスに1マスずつ足し、平行移動で重なる形をまとめます。そのうえで、回転した形、裏返した形を同じ組に入れて数えます。
3マスの行で「固定 6 / 回転のみ 2 / 回転と裏返し 2」と出れば、動作は正しいと判断できます。直線形とL字形の2種類を向きまで数えると6通りになるため、手作業の結果ともつながります。
結果の読み方:7はどの基準の数か
4マスの行を見てください。数学的によく知られた数え方では、固定が19、回転のみが7、回転と裏返しが5になります。手元の結果がこれと違う場合は、先に「起こりやすい誤り」の節を確認してください。
読み方は次のとおりです。
- 回転のみで数えると7になります。ゲームのブロックは盤上で回転させられますが、裏返す操作はありません。この操作と同じ基準で数えると、7種類になります。
- 回転と裏返しまで同じとみなすと5になります。S字とZ字、J字とL字が、それぞれ鏡像の組として一つにまとまるためです。
- 固定で数えると19です。内訳は、直線形2、正方形1、T字4、S字2、Z字2、J字4、L字4です。足し算すると19になるので、向きの数を数えた表と照らし合わせてください。
つまり、手元の表の列3が「なし」になる形が二組あります。S字とZ字、J字とL字です。この二組は、回転をどれだけ重ねても重ならず、鏡像どうしでだけ重なります。だから、回転しかできない前提では別々の部品として数え、5ではなく7になります。
ここで言えるのは、数の上での対応です。「4マスの形を、回転だけで同じとみなして並べ尽くすと、ちょうど7種類になる」という事実と、一般に知られる7種類の構成は一致します。作り手がその数え方を意図したかどうかは、この手順からは分かりません。
マス数を変えた行も見ておくと、数の変わり方がつかめます。3マスは回転のみで2種類で、並べるには少なめです。5マスは回転のみで18種類、回転と裏返しで12種類になり、種類が増えます。4マスは、全部の形を目で覚えられる範囲に収まっています。この「覚えられる量」という見方は、数から読み取れる範囲の感想として受け取ってください。
起こりやすい誤り
数が合わないときは、次の点を疑います。
- 角だけで接する形を数えている。斜めに触れるだけの二つのマスは、つながっていません。
- 向きが違うだけの形を二つ数えている。切り抜きを回して重ねて確かめます。
- 鏡像を同じ形として落としている。回転のみの基準では、S字とZ字は別の形です。基準を決めてから数えます。
- 3マスの形を描き漏らしている。直線形とL字形の2種類で全部です。ここが抜けると、4マスで漏れが出ます。
- コードの貼り付けで、記号が全角になっている。[ や ; が全角になると動きません。
次の行動
方眼紙に3マスの2種類を描き、1マス足して4マスの形を全部切り出してみてください。次に、回転で重なる組と、裏返さないと重ならない組を分けて、19・7・5のどの数まで自分の表で再現できるかを確かめます。
そのうえでコードを動かし、5マスの行も読んでみてください。基準を一つ変えるだけで、数がどれだけ変わるかが見えてきます。

