※この記事は制作時の会話を元にClaudeに書いてもらい、最後に人間が校正しました。
おひさまをドラッグして影を動かし、日かげしか歩けないねこをおうちまで連れていくスマホ向けパズル「ひかげねこ」を作りました!ブラウザで開くだけで遊べて、インストールや登録はいりません。

「シンプルだけど、まだ誰も作っていないスマホ向けパズル」をClaudeに5案出してもらい、その1案目を形にしたものです。コードはほぼClaudeが書いていて、私は遊んでみて「ここを直して」「これを足して」と伝える係でした。作り始めてから最初の公開まで、半日ほどです。
この記事では、遊び方を簡単に紹介したあと、中の仕組みを中心に書いていきます。
遊び方
- 影のマスをタップすると、ねこがそこまで歩く
- 日なたは熱くて歩けない
- 画面上のおひさまをドラッグすると、時刻が6時〜18時の間で2時間ずつ(7段階)変わり、影も動く
- ねこの立っているマスが日なたになる時刻には動かせない(「アチッ!」と言って止まる)
- おひさまを動かした回数が少ないほど高評価。「めやす」(最少回数)どおりなら★3

ステージは「すみれ町」「こもれび公園」「なぎさ海岸」の3エリアで全30ステージ。池(入れない)やパラソル(いつでも日かげ)といった仕掛けが、エリアごとに増えていきます。これとは別に、日付ごとに変わる「きょうのおさんぽ」が毎日1ステージあります。
構成
- Vanilla JS(フレームワークを使わない素のJavaScript)+Canvas 2D(ブラウザの2D描画API)
- ねこや背景などの絵、効果音・BGMは、画像・音声ファイルを使わずにその場で描画・合成
- ソースは3ファイル。ルールとソルバー(解を自動で求めるプログラム)の
core.jsが約360行、描画・UI・音のgame.jsが約2,100行、HTML/CSSのshell.html - Pythonのスクリプトで1枚のHTML(約167KB)にまとめて配信
公開先はこのサイト(Astro+GitHub Pages)の /hikageneko/ です。Astro(静的サイトジェネレータ)の管理外のページなので public/hikageneko/index.html にそのまま置き、サイトマップには @astrojs/sitemap の customPages で手動追加しました。
影のルール
影の向きと長さは、時刻ごとの表で決めています。物の高さ(1〜3)に対して、朝夕は2倍、おひるは半分(切り上げ)。10時と14時は斜めに伸びるぶん、太めに落ちるようにしています。
const STOPS = [
{ hour: 6, dir: [1, 0], len: h => 2 * h },
{ hour: 8, dir: [1, 0], len: h => h },
{ hour: 10, dir: [1, 1], len: h => h, thick: true },
{ hour: 12, dir: [0, 1], len: h => Math.ceil(h / 2) },
{ hour: 14, dir: [-1, 1], len: h => h, thick: true },
{ hour: 16, dir: [-1, 0], len: h => h },
{ hour: 18, dir: [-1, 0], len: h => 2 * h },
];
盤面を読み込んだ時点で、7つの時刻それぞれについて「影になっているマス」を全部計算しておきます。ねこが歩けるかどうかは、この表を引くだけで分かります。
(マス, 時刻) の組で最短手順を解く
このゲームの状態は「ねこがいるマス」と「いまの時刻」の2つだけで決まります。そこで (マス, 時刻) の組を1つの頂点としたグラフを考えて、
- 同じ時刻のまま、隣の日かげマスへ歩く … コスト0
- その場で時刻を1つ進める/戻す … コスト1
という辺を張ると、「おひさまを動かす最少回数」はこのグラフの最短経路になります。コストが0と1しかないので、0-1 BFS(両端キューを使い、コスト0の辺は先頭、コスト1の辺は末尾に積む幅優先探索)で解けます。盤面は最大7×10マスなので、状態数は最大490個。一瞬で終わります。
// 歩く(コスト0)→ 先頭に積む
for (const j of neighbors(b, i)) {
if (!b.safe[t][j]) continue;
const kk = key(j, t);
if (dist[kk] > d) { dist[kk] = d; par[kk] = k; dq[--head] = kk; }
}
// おひさまを1段動かす(コスト1)→ 末尾に積む
for (const nt of [t - 1, t + 1]) {
if (nt < 0 || nt >= NT || !b.safe[nt][i]) continue;
const kk = key(i, nt);
if (dist[kk] > d + 1) { dist[kk] = d + 1; par[kk] = k; dq[tail++] = kk; }
}
このソルバーは、各ステージの「めやす」回数の算出、ヒントボタン(いまの状態から解き直して次の一手を出す)、タイトル画面の自動デモ、そして後述のステージ生成と、あちこちで使い回しています。
ステージは探索で作る
30ステージのうち、チュートリアルの1-1〜1-3だけ手作りで、残りの27ステージは探索で作りました。流れはこんな感じです。
- 大きさと物の数を指定して、盤面をランダムに作る
- ソルバーで解いて、解けない盤面は捨てる
- 取り除いても最少回数が変わらない物を消す(盤面の整理)
- 解きごたえが下がらない範囲で、飾りの物を足し直す
- 最少回数・ドラッグ回数・向きの切り替え回数(時刻を行ったり来たりさせる回数)が、そのステージの条件に入っていれば採用
条件はステージごとに1行で書いています。例えば最終ステージは「7×10マス、最少13〜20回、向きの切り替え4回以上」という指定です。
30: { W:7, H:10, blocks:[9,13], parasols:[1,2], ponds:[1,3], need:['p'],
cost:[13,20], drags:[7,12], rev:4, decor:13, decorChars:'123~' },
結果として、めやす回数は1-1の0回から3-10の14回まで、だんだん増えていく並びになりました。乱数はシード付き(同じ種からは同じ乱数列が出る)なので、何度作り直しても同じステージが出てきます。
「きょうのおさんぽ」も同じ仕組みで、日付をシードにしてブラウザ上でその場で生成しています。同じ日なら誰が開いても同じ盤面になるので、サーバーなしで「今日の問題」が作れます。クリアすると結果をテキストでコピーしてシェアできます。
画像も音もファイルなし
ねこや自販機、灯台などは、全部Canvasのパスで描いています。ねこの模様(しろ・くろ・サバトラ・ハチワレ・みけ など)や帽子などの着せ替えも、同じ描画関数のパラメータ違いです。
鳴き声はWeb Audio API(ブラウザで音を合成・再生する仕組み)で作っています。のこぎり波をバンドパスフィルタ(特定の周波数帯だけを通すフィルタ)2つに通し、フィルタの中心周波数を時間で動かすと、それっぽい「にゃー」になります。人の声の母音を作る「フォルマント」と同じ考え方です。
long: { dur: 0.46, f: [[0, 520], [0.12, 780], [0.42, 430]],
fc: [[0, 900], [0.13, 2000], [0.42, 1000]] },
f が声の高さ、fc がフィルタの中心周波数の時間変化です。これを長さ違い・気分違いで6種類用意して、歩き出し・アチッ・クリアなどで使い分けています。ショップで選べる「こえ」(こねこ・おおねこ・ロボねこ・オペラ など)は、高さ・長さ・ビブラートの量・波形を変えているだけです。
BGMも同じく合成で、104BPM・8小節のコード進行(C-Am-F-G-C-Am-Dm-G)を、少し先の音を予約しながら鳴らしています。ブラウザはユーザーが操作するまで音を出せないので、BGMは初期オフにしました。
かくしメダルの置き場所も計算で決める
各ステージには、自販機やしげみ、海の家などの置物に1枚ずつメダルが隠れています。ねこがその置物の隣にいる時にタップすると拾えます。あるかどうかは事前には分かりませんが、1-1だけはたまにキラッと光ります。

隠し場所は「いかにもありそうだけど、攻略には要らなくて、行くのが少し大変な所」にしたかったので、これも計算で選んでいます。スタートから前向きに、ゴールから後ろ向きに、それぞれ0-1 BFSを回しておくと、ある状態を経由する最短コストが「スタートからの距離+ゴールまでの距離」で出ます。そこから最適解のコストを引いたものが「寄り道のコスト」です。
const detour = dS[k] + dG[k] - opt;
置物ごとに、隣のマスへ立ち寄る寄り道コストの最小値を求め、基本は2〜5回(1-1だけは1〜3回)の範囲に入るものを候補にします。同じくらいなら、自販機や段ボール箱のように「下に何か落ちていそうな」物を優先するよう、置物の種類ごとに重みを付けました。
最初は「寄り道が大きいほど良い」という単純な点数にしていたのですが、太陽を8回も余分に動かさないと届かない場所や、逆に通り道のすぐ横が選ばれてしまいました。上限と下限を付けて、なるべくちょうどいい範囲に収まるようにしています(どうしても見つからない時は1〜8回まで広げています)。
候補が無いステージには、めやす回数が変わらないことを確認しながら置物を1つ足しました(30ステージ中25ステージ)。それでも太陽の回数で寄り道を作れなかった3ステージは、「歩く距離」で遠回りになる場所にしています。
ショップと動画ボーナス
メダルは、ステージの初クリアで1枚、かくしメダルで1枚、きょうのおさんぽのクリアで1枚もらえます。貯まった枚数に応じて、ショップで着せ替え・模様・声がアンロックされます(管理が面倒な使って減る方式ではなく、枚数に達したら解放)。
さらに、1日1回だけ動画を最後まで見ると10枚もらえます。ここはGoogleのH5 Games Ads(Webゲーム向けの広告の仕組み)の Ad Placement API で実装しました。
adBreak({
type: 'reward', name: 'daily_medals',
beforeReward(showAdFn) { /* 「見る」ボタンで showAdFn() を呼ぶ */ },
adViewed() { grantAd(); }, // 最後まで見たら +10
adDismissed() { /* 途中で閉じたらなし */ },
});
ただ、現時点ではまだ広告の配信が始まっていないので、代わりにひかげねこのPVが流れるようにしてあります。広告を見てもらうつもりが、自分のゲームの宣伝を見てもらう形になりました。(広告案件募集中です!笑)
PVも同じコードで録画
X(旧Twitter)用に、縦(1080×1920)と横(1920×1080)のPVも作りました。これも画面録画ではなく、ゲーム本体をPlaywright(ブラウザを自動操作するツール)で動かして1コマずつ撮っています。
ポイントは時計の差し替えです。performance.now() と requestAnimationFrame を偽物に置き換えて、1コマ撮るごとに時間を1/30秒だけ進めます。
const F = window.__fake = { now: 1000, raf: [], timers: [], id: 1 };
performance.now = () => F.now;
window.requestAnimationFrame = cb => { F.raf.push(cb); return F.raf.length; };
こうすると、スクリーンショットにどれだけ時間がかかってもコマ落ちしません。タップやドラッグも、台本(途中で一時停止できるジェネレータ関数で書いたもの)から実際のポインターイベントとして送っているので、指カーソルの動きとゲームの反応がずれません。
音は、録画中に鳴った効果音のタイミングをログに残し、BGMと合わせてPython(numpy)でWAVを合成、最後にffmpeg(動画変換ツール)で映像とまとめています。ゲーム内のBGMも、このPVと同じ曲です。
テスト
ステージを作り直すたびに、Playwrightで30ステージを自動プレイしています。ソルバーの答えを実際のタップ・ドラッグ操作に変換して流し込み、全ステージがめやす回数ちょうどでクリアできることと、コンソールにエラーが出ないことを確認しています。かくしメダルを配置した後も、全ステージでめやす回数が変わっていないこと、メダルが本当に置物の上にあることを別途チェックしました。
さいごに
ルールはシンプルですが、後半のステージは私もヒントなしだと結構悩みます。スマホのブラウザですぐ遊べますので、よかったら遊んでみて下さい。
ひかげねこ|太陽を動かして影でねこを導くパズルゲームsin1.studio/hikageneko/
「影を足場にする」という1つのルールを状態グラフにしたことで、ソルバー・ステージ生成・ヒント・メダル配置・PVまで同じ仕組みを使い回せたのが、作っていて一番面白かったところです。時刻の代わりに潮の満ち引きや照明のON/OFFにしても、同じ作り方で別のパズルが作れそうです。




