【ゲーム】JavaScript:61 カップシャッフルゲーム

 「🥤 カップシャッフルゲーム 🥤」は、ボールを隠したカップを高速でシャッフルし、どのカップにボールが入っているかを当てる集中力テスト・マインドゲームです。カップの動きをしっかり目で追い、タイミングよくクリックして正解を狙います。

遊び方・操作方法

  1. タイトル画面で「スタート」ボタンをクリック。
  2. ゲーム画面では、まずカップの下にボール(🔴)を一瞬だけ表示。
  3. ボールを隠すと同時にカップが自動でシャッフルを開始します(12回ランダムスワップ)。
  4. シャッフルが完了したら、「どのカップだ?」というメッセージに切り替わり、カップをクリックして選択。
  5. 正解なら「正解!」、不正解なら「残念!」が表示され、タイトル画面に戻ります。

ルール

  • カップ数:3個
  • シャッフル回数:12手順
  • シャッフル速度:1ステップあたり約0.54秒のアニメーション
  • 正解判定:プレイヤーがクリックしたカップIDと実際にボールを持つカップIDが一致
  • 終了:一発勝負。正解/不正解後に結果画面へ。

🎮ゲームプレイ

以下のリンク先から実際にプレイできます。

61 カップシャッフルゲーム

素材のダウンロード

以下のリンクから使用する素材をダウンロードできます。

cup_shuffle_title.pngcup_shuffle_bg.png

ゲーム画面イメージ

プログラム全文(cup_shuffle.html)

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>🥤 カップシャッフルゲーム 🥤</title>
  <style>
    html, body {
      margin: 0; padding: 0;
      width: 100vw; height: 100vh;
      font-family: 'Yu Gothic', 'Meiryo', sans-serif;
      background: url('cup_shuffle_bg.png') no-repeat center center fixed;
      background-size: cover;
    }
    body {
      width: 100vw;
      height: 100vh;
      overflow: auto;
    }
    .container {
      width: 800px;
      margin: 40px auto;
      background: rgba(255,255,255,0.94);
      border-radius: 22px;
      box-shadow: 0 4px 24px rgba(0,0,0,0.13);
      padding-bottom: 32px;
      min-height: 500px;
      position: relative;
    }
    .title-img {
      display: block;
      margin: 20px auto 18px auto;
      width: 340px;
      max-width: 80%;
      height: auto;
    }
    h1 {
      text-align: center;
      font-size: 2.1em;
      margin: 0.6em 0 0.15em 0;
      color: #1976d2;
      text-shadow: 1px 1px 7px #fff;
      letter-spacing: 0.04em;
    }
    .rule-section {
      background: rgba(235,245,255,0.98);
      border-radius: 14px;
      margin: 28px 32px 14px 32px;
      padding: 16px 24px;
      box-shadow: 0 2px 8px #4d90fe22;
    }
    .rule-title {
      text-align: center;
      font-weight: bold;
      font-size: 1.3em;
      margin-bottom: 10px;
      color: #1976d2;
    }
    .rule-text {
      text-align: left;
      font-size: 1.09em;
      line-height: 1.65;
      color: #174078;
      letter-spacing: 0.01em;
    }
    .btn {
      display: block;
      margin: 28px auto 0 auto;
      padding: 14px 50px;
      font-size: 1.2em;
      border: none;
      border-radius: 28px;
      background: linear-gradient(90deg, #87c9fa, #1565c0);
      color: #fff;
      font-weight: bold;
      box-shadow: 0 2px 8px #1976d266;
      cursor: pointer;
      transition: background 0.2s;
    }
    .btn:hover { background: linear-gradient(90deg, #b7e4fe, #1976d2); }
    .game-area {
      width: 740px;
      margin: 36px auto 0 auto;
      background: rgba(240,248,255,0.96);
      border-radius: 16px;
      box-shadow: 0 1px 10px #1565c055;
      display: flex;
      flex-direction: column;
      align-items: center;
      min-height: 250px;
      padding: 16px 0 20px 0;
      user-select: none;
    }
    .cups-area {
      width: 600px;
      height: 160px;
      margin: 32px auto 16px auto;
      position: relative;
    }
    .cup {
      width: 120px;
      height: 130px;
      background: #fffbe6;
      border: 3px solid #1976d2;
      border-radius: 0 0 90px 90px / 0 0 110px 110px;
      position: absolute;
      left: 0; top: 0;
      box-shadow: 0 6px 14px #1565c044;
      cursor: pointer;
      display: flex;
      align-items: flex-end;
      justify-content: center;
      transition: left 0.55s cubic-bezier(.53,.1,.5,1.2), border-color 0.16s, box-shadow 0.16s;
      z-index: 1;
    }
    .cup.selected {
      border-color: #e53935;
      box-shadow: 0 0 25px #e5393544;
      z-index: 2;
    }
    .ball {
      position: absolute;
      left: 50%;
      bottom: 10px;
      transform: translateX(-50%);
      font-size: 2.6em;
      z-index: 10;
      pointer-events: none;
      opacity: 1;
      transition: opacity 0.2s;
    }
    .hide-ball {
      opacity: 0;
    }
    .message-box {
      text-align: center;
      background: rgba(235,245,255,0.98);
      font-size: 1.7em;
      color: #1976d2;
      font-weight: bold;
      border-radius: 16px;
      box-shadow: 0 4px 24px #1976d233;
      width: 80%;
      max-width: 480px;
      margin: 34px auto 0 auto;
      padding: 32px 18px;
      position: relative;
      z-index: 2;
    }
    .center-btn { margin: 22px auto 0 auto; }
    .info {
      text-align: center;
      color: #1976d2;
      font-size: 1.13em;
      margin: 4px 0 0 0;
    }
    @media (max-width: 900px) {
      .container, .game-area { width: 98vw !important; min-width: 0; }
      .cups-area { width: 97vw !important;}
      .cup { width: 22vw; height: 23vw; min-width:80px; min-height:90px; }
    }
  </style>
</head>
<body>
  <div class="container" id="main-container"></div>
  <script>
    // =============================
    // カップシャッフルゲーム
    // =============================

    const NUM_CUPS = 3;
    const SWAP_COUNT = 12;
    let ballPos = 1;         // ボールの位置(0,1,2)
    let cups = [0,1,2];      // カップのインデックス(実際の位置)
    let swapIndex = 0;
    let shuffling = false;
    let canSelect = false;
    let resultMsg = "";

    // カップのX座標(ピクセル単位)
    const cupX = [40, 240, 440];

    // タイトル画面を表示
    function showTitleScreen() {
      document.getElementById('main-container').innerHTML = `
        <h1>🥤 カップシャッフルゲーム 🥤</h1>
        <img src="cup_shuffle_title.png" class="title-img" alt="カップシャッフルゲーム タイトル">
        <div class="rule-section">
          <div class="rule-title">🥤 遊び方・ルール 🥤</div>
          <div class="rule-text">
            ・ボールが入ったカップがシャッフルされます。<br>
            ・よく見て、ボールがどのカップに入っているか当てましょう!<br>
            ・カップをクリックして答えを選択します。<br>
            ・正解なら「正解!」、外れたら「残念!」と表示されます。
          </div>
        </div>
        <button class="btn" id="start-btn">スタート</button>
      `;
      document.getElementById('start-btn').onclick = showGameScreen;
    }

    // ゲーム画面
    function showGameScreen() {
      ballPos = Math.floor(Math.random() * NUM_CUPS); // ボール初期位置をランダム
      cups = [0,1,2];
      swapIndex = 0;
      shuffling = false;
      canSelect = false;
      resultMsg = "";

      renderGame();
      setTimeout(()=>{
        revealBall(false); // 一瞬見せる
        setTimeout(()=>{
          revealBall(true); // ボール隠す
          setTimeout(startShuffle, 600);
        }, 900);
      }, 400);
    }

    // ボールを隠す/見せる
    function revealBall(hide) {
      // すべてのcupを確認し、ballPos(ボールの位置)のみボールを見せる
      const cupsNodes = document.querySelectorAll('.cup');
      cupsNodes.forEach((cupNode, idx) => {
        const ball = cupNode.querySelector('.ball');
        // ballPosは「どのカップIDにボールがあるか」、cups[idx]は「この位置にいるカップID」
        // 位置idxのカップIDがballPosと一致するものだけボールを見せる
        if (cups[idx] === ballPos) {
          if(hide) ball.classList.add('hide-ball');
          else ball.classList.remove('hide-ball');
        } else {
          ball.classList.add('hide-ball');
        }
      });
    }

    // ゲーム画面描画(カップをアニメ配置)
    function renderGame(selectedIdx = null) {
      let cupHtml = '';
      for(let i=0; i<NUM_CUPS; i++) {
        // cups配列は現在「座標に何番カップがあるか」= cups[物理位置]=カップID
        let cupId = cups[i];
        // cupIdがballPosと一致するカップだけボールを描画
        let showBall = (cupId === ballPos && !shuffling && !canSelect);
        cupHtml += `
          <div class="cup${selectedIdx===i?' selected':''}" data-cup="${i}" 
            style="left:${cupX[i]}px;" onclick="onSelectCup(${i})">
            ${showBall ? `<div class="ball">🔴</div>` : `<div class="ball hide-ball">🔴</div>`}
          </div>
        `;
      }
      document.getElementById('main-container').innerHTML = `
        <h1>🥤 カップシャッフルゲーム 🥤</h1>
        <div class="game-area">
          <div class="info" id="info">${shuffling ? "シャッフル中..." : canSelect ? "どのカップ?クリックで選ぼう!" : "カップの位置を覚えてください。"}</div>
          <div class="cups-area">${cupHtml}</div>
          ${resultMsg ? `<div class="message-box">${resultMsg}</div>` : ""}
        </div>
      `;
    }

    // シャッフル開始
    function startShuffle() {
      shuffling = true;
      canSelect = false;
      swapIndex = 0;
      shuffleStep();
    }

    // シャッフル手順(アニメで交換)
    function shuffleStep() {
      if (swapIndex >= SWAP_COUNT) {
        shuffling = false;
        canSelect = true;
        setTimeout(()=>{ renderGame(); }, 250);
        return;
      }
      // 2つのカップ物理位置をランダムで選ぶ
      let a = Math.floor(Math.random()*NUM_CUPS);
      let b;
      do { b = Math.floor(Math.random()*NUM_CUPS); } while (b === a);

      // 見た目の位置をアニメでスワップ
      let cupNodes = document.querySelectorAll('.cup');
      let [leftA, leftB] = [cupX[a], cupX[b]];
      cupNodes[a].style.left = leftB + "px";
      cupNodes[b].style.left = leftA + "px";

      // 実際の配列順もスワップ(見た目の動きが終わるまで配列は保留)
      setTimeout(()=>{
        [cups[a], cups[b]] = [cups[b], cups[a]];
        renderGame();
        swapIndex++;
        setTimeout(shuffleStep, 380);
      }, 540); // アニメduration+α
    }

    // カップ選択時
    function onSelectCup(idx) {
      if (!canSelect) return;
      canSelect = false;
      let cupId = cups[idx];
      let isCorrect = (cupId === ballPos);
      resultMsg = isCorrect ? "🎉 正解!お見事!" : "😢 残念!またチャレンジしてね。";
      renderGame(idx);
      setTimeout(showEndScreen, 1500, isCorrect);
    }

    // 終了画面
    function showEndScreen(isCorrect) {
      document.getElementById('main-container').innerHTML = `
        <h1>🥤 カップシャッフルゲーム 🥤</h1>
        <img src="cup_shuffle_title.png" class="title-img" alt="カップシャッフルゲーム タイトル">
        <div class="message-box">
          ${isCorrect ? "🎉 おめでとう!正解です!🎉" : "😢 残念!また挑戦してね。"}
        </div>
        <button class="btn center-btn" id="back-title-btn">タイトル画面に戻る</button>
      `;
      document.getElementById('back-title-btn').onclick = showTitleScreen;
    }

    // windowに選択関数を生やす
    window.onSelectCup = onSelectCup;

    // 初期表示
    showTitleScreen();
  </script>
</body>
</html>

アルゴリズムの流れ

手順処理内容
1showTitleScreen() でタイトル画面の HTML を描画
2「スタート」クリック → showGameScreen() でボール位置をランダム設定→一瞬見せ→隠す→シャッフル
3startShuffle()shuffleStep() で12回ランダムペアを選び、見た目と配列の両方をスワップ
4シャッフル完了後に canSelect=true、画面内のカップをクリック可能
5クリックされた位置 idx のカップID と ballPos を比較 → 正誤メッセージをセット
61.5秒後に showEndScreen() を呼び出し、正解/不正解画面を描画
7「タイトルに戻る」ボタンで再びタイトル画面へ

関数の解説

関数名機能概要詳細説明
showTitleScreen()タイトル画面描画スタートボタン・ルール説明を含む初期画面を描画。クリックで showGameScreen() を呼び出し。
showGameScreen()ゲーム画面初期化ballPos をランダム設定し、カップを初期配置。ボールの一瞬表示→隠蔽を経て startShuffle() を呼び出す。
revealBall(hide)ボールの見せ/隠し切り替えcups 配列と ballPos の一致判定で対象カップのみボール要素 (.ball) のクラスを制御。
renderGame(sel)画面再描画現在の cups 配列・状態フラグ (shuffling / canSelect) を反映し、カップ要素とメッセージを動的に再構築。
startShuffle()シャッフル開始フラグ初期化後、shuffleStep() によって非同期スワップを連続実行。
shuffleStep()シャッフルステップ(アニメ+配列)物理位置インデックス a,b のランダム選出 → CSS アニメーションで左右入れ替え → 配列内もスワップ → 次ステップへ再帰呼び出し。
onSelectCup(i)プレイヤー選択処理canSelect フラグチェック後、選択位置 i のカップID と ballPos を比較。結果メッセージ設定→ showEndScreen() 呼び出し。
showEndScreen(c)結果画面描画正誤に応じたメッセージと「タイトルに戻る」ボタンを表示。ボタンで showTitleScreen() を再呼び出し。

改造のポイント

  • シャッフル難易度SWAP_COUNT を増減させるほか、ステップごとのアニメ速度(setTimeoutの値)を調整し、初心者~上級者向けモードを実装。
  • カップ数変更NUM_CUPS を増やして難易度を上げる。多いほど視認が困難になります。
  • マルチラウンド制:3ラウンド制など複数回チャレンジ制を導入し、得点合計を競わせるランキング要素を追加。
  • 視覚・サウンド演出:シャッフル中のBGM、クリック時のSE、正解時のエフェクトを加えると没入感アップ。
  • モバイル最適化:タッチ操作時のアニメ速度やクリック判定領域を微調整し、指でタップしやすくする。

アドバイス
まずはシャッフル/選択の安定動作を最優先で実装し、次にUI演出・サウンドを足して「見て楽しい・聞いて楽しい」ゲーム体験を作りましょう。難易度やモードを段階的に増やすことで、長期的に遊んでもらえるコンテンツになります!