※本記事にはアフィリエイト広告(Udemy)を含みます。
インストール不要で、PCとスマートフォンから遊べます。
前回のおさらい
前回は、パイプが持つ上下左右の接続をデータで表し、入口から植物まで水が届く処理を作りました。
これで一つの盤面は遊べます。しかし、ゲームとして完成させるには、少しずつ難しくなるステージ、操作に対する反応、進捗の保存、スマートフォン対応、解けることを保証するテストが必要です。
今回は『雨まで10秒』の仕上げとして、7つのステージを作り、UIと効果音を整えます。
今回のゴール
今回追加する内容は、次の5つです。
- 10秒スピードとパズルガーデンを分ける
- 新しいルールを一つずつ加えた7ステージを作る
- 効果音と結果表示で操作の反応を伝える
- 進捗をブラウザへ保存する
- PC、スマートフォン、自動テストで完成を確認する
機能を増やすこと自体が目的ではありません。プレイヤーが迷わず操作でき、各ステージを最後まで解ける状態を完成とします。
2つの遊び方を分ける
開発を始めたときの中心は、雨が降るまでの10秒間にパイプを回し、4回の雨で植物を育てるモードでした。
短時間で判断する面白さがある一方、パイプの接続をじっくり考えたい場合には時間制限が合いません。
そこで、タイトル画面から次の2つを選べるようにしました。
- 10秒スピード:10秒×4回で植物を育て、得点を競う
- パズルガーデン:時間制限なしで7ステージを順番に解く
ルールを一つの画面へ詰め込まず、入口で遊び方を分けています。どちらのモードからもホームへ戻れるようにし、遊び直すときの迷いも減らしました。
ステージを少しずつ難しくする
パズルガーデンは、最初から複雑なルールをすべて使いません。

各ステージでは、新しい要素を一つずつ加えています。
ここでいうパーは、クリアに必要な最短手数です。
| ステージ | 主な学習要素 | パー |
|---|---|---|
| S1 はじめての庭 | 基本の水路 | 7 |
| S2 石のまわり道 | 回せない岩 | 9 |
| S3 動かない水路 | 固定パイプ、右からの入口 | 10 |
| S4 南からの総合水路 | 下からの入口、長い水路 | 8 |
| S5 石を越える二つ道 | 岩を避ける分岐 | 10 |
| S6 西からの固定水路 | 左からの入口、固定パイプ | 12 |
| S7 一滴もこぼさない庭 | 行き止まりパイプ、漏水ゼロ | 12 |
S1で基本操作を覚え、S2で岩、S3で固定パイプと入口方向を追加します。S4以降は、それまでの要素を組み合わせた挑戦です。
最後のS7では、3つの植物へ水が届くだけではクリアになりません。余分な出口を行き止まりパイプでふさぎ、水漏れを0にする必要があります。
パーとスターで繰り返し遊べるようにする
各ステージには、クリアに必要な最短手数をパーとして設定しました。
スターの判定はシンプルです。
export function calculatePuzzleStars(
moves: number,
par: number,
): PuzzleStarRating {
if (moves <= par) {
return 3;
}
if (moves <= par + 2) {
return 2;
}
return 1;
}
パー以内なら星3、パーより2手以内なら星2、それ以上なら星1です。
最初はクリアできれば十分です。その後で手数を減らす目標ができるため、同じステージを遊び直す理由になります。
進捗をブラウザへ保存する
ステージのクリア状況、最少手数、最高スターはlocalStorageへ保存します。
localStorageは、ブラウザに文字列を保存できる標準機能です。今回はアカウント機能やサーバーを用意せず、同じブラウザで続きを遊べれば目的を満たせます。
export function writePuzzleProgress(
progress: PuzzleProgress,
): void {
try {
localStorage.setItem(
PUZZLE_STORAGE_KEY,
JSON.stringify(progress),
);
} catch {
// 保存できない環境でもゲームは続ける
}
}
オブジェクトはそのまま保存できないため、JSON.stringify()で文字列へ変換します。
保存が使えない環境も考え、エラーが起きてもゲーム本体は止めません。読み込み時にはデータの形を確認し、矛盾した進捗なら初期状態へ戻します。
この方法は端末間の同期には向きません。将来、ユーザー登録やオンラインランキングが必要になった段階で、サーバー保存を検討します。
操作と結果を音で伝える
パイプを回したときに見た目だけが変わると、操作できたのか分かりにくいことがありました。
そこで、Web Audio APIで短い効果音を作りました。音声ファイルを再生するのではなく、ブラウザ上で周波数と長さを指定して音を鳴らします。
function playRotationSound(): void {
withAudioContext(true, (context) => {
const start = context.currentTime + 0.01;
scheduleTone(context, 620, 360, start, 0.13, 0.09, "triangle");
scheduleTone(context, 330, 240, start + 0.035, 0.11, 0.065, "square");
});
}
回転音は短く、給水成功は上がる音、失敗は下がる音にしています。開花や最終クリアでは音を重ね、通常の操作と区別しました。
ブラウザは、ページを開いただけで音を鳴らす処理を制限することがあります。そのため、最初のボタン操作をきっかけにAudioContextを準備します。効果音は画面右上からオン、オフを切り替えられます。
スマートフォンでも同じ操作にする
ブラウザゲームにした理由の一つは、PCとスマートフォンで同じURLを開けることです。

盤面はCSS Gridで5列に並べ、画面幅に合わせてマスを縮めます。操作対象にはbutton要素を使っているため、PCではクリックとキーボード、スマートフォンではタップで同じ回転処理を呼び出せます。
画面を小さくしたときは、「水を流す」を盤面の直下へ置きました。パイプを確認してから主操作へ進み、その下に「元に戻す」「リセット」「ステージ選択」を並べています。
ほかにも、次の内容を確認しました。
- キーボードで選んだマスにフォーカス表示が出る
- 水を流した結果を画面上のメッセージでも伝える
- 効果音をオフにできる
- 動きを減らす設定ではアニメーションを短縮する
- ボタンと文字がスマートフォン幅で重ならない
音だけ、色だけに頼らず、文字と状態の変化も組み合わせています。
解けないステージを作らないためのテスト
ステージを増やしたときに最も注意したのは、見た目がそれらしくても正解できない盤面を作ってしまうことです。
そこで、完成形を登録するだけでなく、すべてのステージを自動テストしています。
Vitestでは、各ステージの完成形を次のように確認します。
it.each(PUZZLE_STAGES.map((stage) => [stage.id, stage] as const))(
"%s solved grid connects all seeds from entry",
(_id, stage) => {
expect(
isAllSeedsConnected(
stage.solvedGrid,
stage.entry,
stage.seeds,
),
).toBe(true);
if (stage.requiresLeakFree) {
expect(getWaterLeaks(stage.solvedGrid, stage.entry)).toEqual([]);
}
},
);
このテストでは、各ステージの完成形で3つの植物へ水が届くことを確認します。S7だけは、水漏れが0であることも条件です。
さらに、各パイプの向きを列挙して最短手数を求め、設定したパーと一致することも確認しました。
const minimum = findMinimumMovesStrict(
stage.initialGrid,
stage.entry,
stage.seeds,
stage.requiresLeakFree,
);
expect(minimum).toBe(stage.par);
これにより、「7手がパーと表示されるのに、本当は6手で解ける」といったずれを防げます。
画面の確認では、S1をパーの7手で解き、給水後に3つの植物へ水が届き、星3でクリアになるところまで操作しました。
完成と判断した条件
『雨まで10秒』は、次の条件を満たした時点で最初の完成としました。
- 10秒スピードを4回の雨まで遊べる
- パズルガーデンの7ステージを順番にクリアできる
- I字、L字、T字、行き止まり、岩、固定パイプが正しく動く
- 水の到達、漏水、植物の成長、手数、スターが正しく判定される
- タイトル、各モード、ステージ選択の間を移動できる
- 操作音、給水音、クリア音を確認できる
- PCとスマートフォンで操作できる
- 型チェック、自動テスト、コード検査、完成版ビルドが通る
機能をいくらでも追加できる状態でも、中心となる遊びが成立し、最初から最後まで遊べるところで一区切りにしました。
今回のまとめ
全3回で、企画、技術選定、パイプの接続、水の到達、ステージ設計、UI、効果音、保存、自動テストまで進めました。
ブラウザの標準機能を中心に、TypeScript、Vite、Vitestなど少数の開発ツールを組み合わせれば、小規模なパズルゲームに必要な仕組みを一通り作れます。
大切だったのは、最初から7ステージを作ることではありません。まず5×5の盤面で中心となる遊びを確認し、その後で時間無制限モードや特殊パイプを追加したことです。
次に作り込むなら、問題数を増やす前に、実際に遊んだ人がどこで迷うかを確認します。その結果から、チュートリアル、難易度、演出のどれを直すか決めます。
TypeScriptやゲームプログラミングを学ぶオンライン講座
今回の制作では、TypeScriptの型、配列、経路探索、Web Audio API、localStorage、自動テストを使いました。
一つずつ試したい場合は、動画講座で基本を確認し、5×5より小さい盤面から自作してみる方法もあります。
※本リンクはアフィリエイトリンクです。


コメント