「ネオンでギラギラ光るテトリス作りたいな〜」という思いつきが先で、技術選定は後から考えるパターン。結局 Svelte + Vite で組んで、光らせて、鳴らして、途中で何回か笑いながら完成させた。そのときの記録。
完成したもの
普通に遊べるテトリス。移動と回転、ソフトドロップにハードドロップ。ここまでは教科書通り。
https://my-tetris-mu.vercel.app/
ただ、せっかくなので 7-Bag ランダマイザを入れた。これがないと「I ピース全然来ないんだけど!?」という悲鳴が発生するので。あと壁蹴り(SRS 風)も実装して、壁際でもちゃんと回転できるようにした。
見た目はネオングロー全開。レベルアップと TETRIS 達成時にギラッと光る。音も Web Audio API で自作して、Classic(ピコピコ)・Cyber(ジャキジャキ)・Mellow(ぽよぽよ)の 3 パターンから選べる。スマホでも遊べるようにタッチ用のコントロールパッドもつけた。
7-Bag ってなに
テトリス初心者向けに説明すると、7-Bag は「7 種類のブロックを袋に入れてシャッフルして、1 袋分を全部出し切ってから次の袋を開ける」という仕組み。見た目はランダムなんだけど、最悪でも 13 手以内には欲しいピースが来る。「I ピースが 40 手来ない」みたいな地獄を防げる。
実装は logic.js の fillPieceQueue で、やってることは素朴なシャッフルだけ。でも 7 個単位で補充するルールを守ってるから、ちゃんと機能する。
SRS(壁蹴り)ってなに
Super Rotation System の略。公式テトリスで使われてる回転ルールで、壁や床に当たっても「ちょっとずらして回る」という親切設計。
今回は簡易版で、config.js に KICK_OFFSETS として [ [0,0], [-1,0], [1,0], [0,-1], [-1,-1] ] を定義してる。本家ほど複雑じゃないけど、壁際で回れなくてイライラする問題は解消できた。
開発中にハマったこと・笑ったこと
シークレットモードで裸のテトリス
開発中、シークレットモードで開いたら一瞬スタイルが崩壊する現象に遭遇。スタートボタン押すと直るんだけど、最初の一瞬がひどい。
原因は Tailwind CDN。シークレットモードだとキャッシュが効かないし、サードパーティへの接続も抑制されがち。結果、CSS が届く前に素の HTML が一瞬見えてしまう。
直し方は単純で、Tailwind をビルドに組み込んだ。app.css に @tailwind base/components/utilities 書いて、CDN への依存を消した。これで初回表示から安定。
Analytics のコピペで爆死
Vercel Analytics を入れようとして、Next.js のサンプルをそのまま持ってきた。import { Analytics } from "@vercel/analytics/next" って書いて、当然動かない。Svelte なのに。
正解は @vercel/analytics から inject() を呼ぶだけ。main.js に 1 行追加して終わり。フレームワーク専用のインポートパスには気をつけような、という教訓。
pnpm のアップデートが効かない
pnpm self-update を実行しても何も起きない。しばらく悩んで、「あ、asdf で管理してるんだった」と気づく。
asdf 経由だとこうなる:
asdf plugin update pnpm && asdf install pnpm 10.25.0 && asdf set pnpm 10.25.0 && asdf reshim pnpm
シェルを開き直して PATH をリフレッシュすれば完了。自分の環境くらい覚えておけという話。
OG 画像が Vite のアイコン
Twitter でシェアしたら、OG 画像が Vite のロゴだった。「チュートリアル始めたの?」みたいな空気になって恥ずかしい。
public/og-image.svg を自作して差し替えた。ネオンブロックとタイトルを入れた 1200x630 の画像。これでようやくテトリスっぽくなった。
デバッグ用のテキストがうるさい
config.js を見返したら「🛠️ FEATURE CHECK」とか「💥 TRIGGER SHAKE」とか書いてある。深夜テンションで書いたんだろうな。まあ、どこで何が起きてるか分かりやすいからいいか。
コードのちょっとした見どころ
7-Bag の実装
export function fillPieceQueue(queue) {
const pieces = 'TJLOSZI';
while (queue.length < 7) {
const bag = pieces.split('').sort(() => Math.random() - 0.5);
for (let type of bag) queue.push(getPieceMatrix(type));
}
return queue;
}
10 行もない。でも「1 袋 7 個を出し切る」というルールを守ってるから、ちゃんと偏りが抑えられる。アルゴリズム的には何も難しいことしてない。
音のパラメータ
効果音は Web Audio API で全部 JS で書いてる。SOUND_PATTERNS に波形と周波数を定義してあって、Classic は四角波でピコピコ、Cyber はノコギリ波でジャキジャキ、Mellow はサイン波でぽよぽよ。
パラメータが全部コードなので、「もうちょっと高い音にしたいな」と思ったら数字をいじるだけ。耳で聞きながら調整できるのが楽しかった。
UI まわりの話
スマホ対応はタッチ用の ControlPad を作って対応した。親指で押しやすいサイズを意識。PC でも出るけど、まあ邪魔にはならない。
レベルアップと TETRIS 達成時の光るやつは、CSS のテキストシャドウとアニメーションだけで実装。重い処理は何もしてない。「光れば楽しいでしょ」という雑な発想だけど、実際楽しいからヨシ。
あと、メニュー画面で Space キー押すとゲームが始まる。油断してると急に始まる。仕様です。
動かし方
pnpm install
pnpm run dev -- --open # 開発サーバー起動
pnpm run build # 本番ビルド
pnpm run preview # ビルド結果の確認
Vercel にデプロイするなら、リポジトリ連携するだけ。Vite を自動検出してくれる。Analytics はダッシュボードで ON にすれば inject() が動く。
学んだこと
CDN 依存は危険。 シークレットモードとか、ネットワークが不安定な環境で露呈する。最初からビルドに組み込んでおくのが無難。
フレームワーク専用のサンプルコードはそのまま使えない。 Next.js 用、Svelte 用、React 用……ちゃんと確認しないと動かない。当たり前だけど、急いでるとやりがち。
ロジックと UI を分けると楽。 logic.js にゲームロジック、config.js に設定値を寄せたおかげで、見通しがよくなった。テストも書きやすい(書いてないけど)。
Web Audio API は思ったより簡単で楽しい。 波形と周波数をいじって音を作るのは、地味に時間を忘れる作業だった。
連絡先
バグ報告や感想があれば: