バイブコーディングで作った記憶ゲーム、中身を覗く

AI活用

前回は、計算ゲーム「陣取りペンキ」(内部リンク: 陣取りペンキ解説)の中身をプログラムごと覗く記事を書きました。今回はその第2弾。記憶力を試すブラウザゲーム「きおくフラッシュ」(内部リンク: きおくフラッシュ紹介)を、同じように覗いていきます。

今回のポイントは4つ。

  • 28種類のイラストを画像ファイル0枚で内蔵する仕組み
  • ランダムなのに理不尽にならない出題の作り方
  • 絵とことばがウソをつく、ひっかけカードの正体
  • 笑えるランク判定の裏側

今回も、バイブコーディング(AIとの対話だけでプログラムを組み立てる作り方)で仕上げました。一発では動かず、遊んでは直す作業の連続。顛末は最後の章で正直に書きます。

「きおくフラッシュ プログラムの中身を覗く」のアイキャッチ画像。SVGコード断片とカードイラスト

まずは遊んでみてください

説明の前に、まず触ってみてください。理不尽に見えるひっかけも、遊んでみれば「ああ、こういうことか」とすぐ分かります。

▶ 全画面で遊ぶ

ルールはシンプルです。

  • 数秒間だけ、絵とことばのカードが表示される
  • カードが裏返ったら、4択やタップで思い出す
  • 全5ラウンド・13問を通して、記憶力ランクが決まる

詳しい遊び方やコツは、紹介記事(内部リンク: きおくフラッシュ紹介)にまとめてあります。ここからは、プログラムの中身に集中します。

28種のイラスト、実は画像ファイルを1枚も使っていない

種明かしをすると、りんごやネコなどのイラストは全部SVG(線や図形でベクター画像を描くための仕組み)で直接描かれています。しかも画像ファイルは1枚も使っていません。例えばりんごは、円と棒と葉っぱのたった3パーツ。

<symbol id="ic-apple" viewBox="0 0 100 100">
    <circle cx="50" cy="58" r="32" fill="#e0392b"/>
    <rect x="47" y="18" width="6" height="14" rx="3" fill="#7a4a21"/>
    <ellipse cx="63" cy="22" rx="11" ry="6" fill="#3f9b46" transform="rotate(-20 63 22)"/>
  </symbol>

<symbol>というタグの中に、絵の”原本”を1回だけ描いておく。あとは呼び出したい場所で名前を指定するだけで、何度でも同じ絵を使い回せます。

const iconSvg = (id, cls = "icn") => `<svg class="${cls}" aria-hidden="true"><use href="#ic-${id}"/></svg>`;

iconSvg("apple")と書けば、<use href="#ic-apple">という短いタグに変換され、先ほどのりんごがそのまま画面に呼び出されます。一つの絵の”原本”をHTMLの中に隠しておいて、使いたい場所で「あの絵、ここにも出して」と呼んでいるだけ、というわけです。

SVGスプライトの仕組み図解。1つのsymbol(原本)から複数箇所にuseで呼び出される様子を示す図

28種類のイラストをこの方式で内蔵しているので、画像は1枚もサーバーから読み込みません。表示が一瞬で済むのは、地味に効いています。iframeで埋め込んでも重くならないのは、そのおかげ。前作「陣取りペンキ」で見つけた「単一ファイルは軽くて壊れにくい」(内部リンク: 陣取りペンキ解説)という発見の延長線ですね。

ランダムなのに理不尽にならない、出題の作り方

4択の「ダミー」(誤答の選択肢)は、ただの乱数で選ばれているわけではありません。unusedItems()という関数が、ルールに沿って選んでいます。

// 未使用アイテム(絵にもことばにも出ていない)を抽選。sameCatが指定されたら同カテゴリ優先
function unusedItems(cards, count, sameCatOf) {
  const used = new Set();
  cards.forEach(c => { used.add(c.icon.id); used.add(c.label.id); });
  const free = ITEMS.filter(x => !used.has(x.id));
  const sameCat = sameCatOf ? free.filter(x => x.cat === sameCatOf.cat) : [];
  const rest = free.filter(x => !sameCat.includes(x));
  return shuffle(sameCat).concat(shuffle(rest)).slice(0, count);
}

やっていることは日本語にすると単純です。表示済みのアイテムを除外し、残った中から同じカテゴリのものを優先的に並べる。それでも足りなければ、他のカテゴリから補充する、という順番です。

りんごの問題なら、全然関係ない「くるま」より、同じくだもの仲間の「ミカン」を混ぜたほうが記憶力を試せます。この考え方を、そのままコードにしているわけですね。

もう一つ、地味だけれど大事な工夫も。「表示されていなかったのはどれ?」という問題で、正解が2つになってしまう事故を防ぐ仕組みです。unusedItems()は絵とことば両方をチェック済みリストに入れているので、既に画面に出ていたアイテムをダミーとして混入させません。

「絵とことばのウソ」は、どうやって作っているか

後半のラウンドになると、絵はミカンなのに、ことばは「りんご」となっているカードが混ざります。この“ひっかけ”の正体は、buildRound()というカード生成の関数の中にあります。

const cards = pool.slice(0, def.n).map(it => ({ icon: it, label: it, mis: false }));
const used = new Set(cards.map(c => c.icon.id));
// ひっかけカード: 絵はそのまま、ことばを「同カテゴリ・未使用」のアイテムに差し替え
for (const i of shuffle(cards.map((_, k) => k)).slice(0, def.mis)) {
  const cand = ITEMS.filter(x => x.cat === cards[i].icon.cat && !used.has(x.id));
  if (cand.length) {
    cards[i].label = pick(cand);
    cards[i].mis = true;
    used.add(cards[i].label.id);
  }
}

やっていることは、絵はそのまま、ラベルだけこっそり別の仲間にすり替える処理です。しかも差し替え先は、カテゴリを揃えて選んでいます。全く関係ない「くるま」にすり替えるより、同じくだもの仲間の中でウソをついたほうが効果的。それらしくて、ひっかかりやすいという考え方です。

ふつうのカードとひっかけカードの違いを示す図解。ひっかけカードは絵とラベルが別アイテム由来であることを表す

このひっかけカードは、誤答の選択肢作りだけでは終わりません。ラウンド4・5では、ひっかけカードそのものから「『りんご』の“ことば”が付いていたのは、どの絵?」「この絵に付いていた“ことば”は?」という出題そのものが生まれます。絵とことばのすり替わりに気づけるかどうかを、直接問う形。

笑えるランク判定は、境界値をいくつか並べているだけ

const RANKS = [
  { min:.92, rank:"S", comment:"メモ帳いらずの達人。買い物メモはむしろ趣味" },
  { min:.75, rank:"A", comment:"豆腐を買い忘れない人。今日も冷蔵庫は平和" },
  { min:.55, rank:"B", comment:"たまに同じものを2度買う人。ポン酢が2本ありませんか?" },
  { min:.30, rank:"C", comment:"メモを書いたことを忘れる人。スマホのメモ帳に同じリストが3つ" },
  { min:0,   rank:"D", comment:"今この画面を開いた理由、思い出せますか?(大丈夫、ゲームです)" },
];

判定の仕組みは、この配列を上から順に見ていって、正答率が条件を満たした最初の1つを返しているだけです。92%以上ならS、75%以上ならA、というふうに。複雑な採点ロジックがあるわけではありません。

コメントのゆるさが個人的には気に入っています。「豆腐を買い忘れない人」「ポン酢が2本ありませんか?」——採点というより、あるある感を狙って書いた文章ですよね。念のため、ここでも改めて。※お遊びの判定で、医学的な根拠はありません。

ベスト記録は前作と同じくlocalStorage(ブラウザに保存される簡単な記憶領域)に残しています。前回のスコアと比較して一喜一憂できる、という程度の仕組み。深掘りは前作記事(内部リンク: 陣取りペンキ解説)に譲ります。

これも、バイブコーディングで組み立てました

今回も、企画からプロトタイプの完成まで、AIとの対話だけで進めました。ただし一発で思い通りに動いたわけではありません。

分かりやすい例が、ラウンド間の「つぎへ」ボタンです。1ラウンド目をクリアして「つぎへ」を押しても、画面が固まったように進まない。コードを読んだだけでは気づきにくいバグで、実際に動かしてみて初めて発覚しました。原因は単純で、ボタンにクリックのイベントリスナーを付け忘れていただけ。1行足すだけで直りました。

もう一つは、表示時間の調整です。最初の秒数設定は6秒・7秒・5秒・7秒・7秒でしたが、私が実際に遊んでみると「短すぎて覚えきれない」。フィードバックは「1.5倍くらい欲しい」というだけの一言でした。それを受けてAIが、秒数を9秒・10秒・8秒・10秒・10秒に調整してくれました。数字を1箇所変えるだけで難易度が動く——こういう作りの気持ちよさ、ですね。

思い通りに一発でできたわけではなく、遊んでは直す、の繰り返しでした。前作「陣取りペンキ」(内部リンク: 陣取りペンキ解説)やバイブコーディング連載(内部リンク: バイブコーディング入門)も、同じような試行錯誤の積み重ね。業務用の小さなツールも、この程度の試行錯誤で形になることが多いというのが、実務での実感です。

まとめ

  • イラストは画像ファイルなしで、SVGスプライトとしてHTMLに内蔵
  • 出題のダミーは、同じカテゴリを優先して公平に自動生成
  • ひっかけカードは、絵かことばの片方だけをこっそり差し替えているだけ
  • ランク判定は、正答率の境界値をいくつか並べているだけ
  • これも全部、AIとの対話と試行錯誤で数日かけて組み立てた

もう一度遊んでみると、仕組みが分かった分、見え方が変わるかもしれません(内部リンク: きおくフラッシュ紹介)。前作「陣取りペンキ」の解説(内部リンク: 陣取りペンキ解説)も、合わせて読むとバイブコーディングという開発スタイルの手応えがつかみやすいはずです。

「うちの業務にも、こういう小さなツールが使えそう」——そう感じた方は、個別相談も承っています。まずはお問い合わせから、お気軽にご連絡ください。

コメント

タイトルとURLをコピーしました