神経衰弱のソースコード全文 — JavaScript・HTML・CSS

▶ 神経衰弱で遊ぶ 📖 作り方を1から学ぶ

このページでは、ひなテックGamesで実際に動いている「神経衰弱」のソースコードを丸ごと公開しています。ここに載っているのは説明用に書き直したコードではなく、いまあなたが遊んでいるゲームそのものを動かしている本物のファイルです。

このゲームの見どころは2つあります。1つは、カードをタップするとくるりと裏返るCSSの3D回転アニメーション。もう1つは、絵柄をまぜて毎回ちがう配置にするシャッフルの仕組みです。どちらもゲームづくりでよく使う大切な技術です。

コードはコピーして自分のパソコンで自由に動かせます。「カードのめくり方ってどう作るんだろう?」と思ったら、解説を読みながらコードを追いかけてみてください。

神経衰弱は3つのファイルでできている

神経衰弱は、次のファイルが役割を分担して動いています。それぞれの中身は下で1つずつ解説します。

ファイル役割
index.htmlゲーム画面の骨組み。盤面・スコア表示・ボタンなどの部品をHTMLで配置する
style.css見た目のデザイン。色・大きさ・レイアウト・アニメーション
game.jsゲームのルールを動かす頭脳。操作の受け付け・判定・スコア計算など

この「HTMLで構造、CSSで見た目、JavaScriptで動き」という分担は、ほとんどのWebページ・Webゲームに共通する考え方です。

index.html — ゲーム画面の骨組み

index.htmlは、ブラウザが最初に読み込むファイルです。ここにはゲームの「部品」をHTMLで並べているだけで、カードをめくる動きそのものは書かれていません。

大きく分けると、(1)難易度を選ぶselectとNew Gameボタンを置くcontrol-area、(2)手数・タイム・ベストを表示するscore-area、(3)カードを並べるcard-grid、(4)クリア時に出るオーバーレイ、でできています。card-gridの中は空っぽで、カード1枚1枚はJavaScriptがあとから作って入れます。

ゲーム本体は<body>の中から始まります。

※ 下のコードでは、ゲームの動きと関係のない広告・アクセス解析用のタグ(ページ先頭の数行)を省いています。

index.html
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
  <title>神経衰弱|無料ブラウザゲーム・ダウンロード不要|ひなテックGames</title>
  <meta name="description" content="神経衰弱(メモリーマッチ)を無料でプレイ!ダウンロード不要・登録不要でブラウザからすぐ遊べる定番の脳トレ。カードをめくって同じ絵柄のペアを見つけよう。3段階の難易度と覚え方のコツも解説。PC・スマホ対応。">
  <link rel="canonical" href="https://hinata-ya.tech/games/games/memory/">
  <!-- I18N:hreflang -->
  <link rel="alternate" hreflang="ja" href="https://hinata-ya.tech/games/games/memory/">
  <link rel="alternate" hreflang="en" href="https://hinata-ya.tech/games/en/games/memory/">
  <link rel="alternate" hreflang="x-default" href="https://hinata-ya.tech/games/games/memory/">
  <!-- /I18N:hreflang -->
  <!-- OGP -->
  <meta property="og:title" content="神経衰弱|無料ブラウザゲーム・ダウンロード不要|ひなテックGames">
  <meta property="og:description" content="ダウンロード不要・登録不要でブラウザからすぐ遊べる定番の神経衰弱(メモリーマッチ)。カードをめくって同じ絵柄のペアを見つけよう。3段階の難易度で遊べます。PC・スマホ対応。">
  <meta property="og:type" content="website">
  <meta property="og:url" content="https://hinata-ya.tech/games/games/memory/">
  <meta property="og:site_name" content="ひなテックGames">
  <meta property="og:locale" content="ja_JP">
  <!-- SEO:meta-extra -->
  <meta property="og:image" content="https://hinata-ya.tech/games/images/og/memory.png">
  <meta property="og:image:width" content="1200">
  <meta property="og:image:height" content="630">
  <meta property="og:image:alt" content="神経衰弱">
  <meta name="twitter:card" content="summary_large_image">
  <meta name="twitter:title" content="神経衰弱|無料ブラウザゲーム・ダウンロード不要|ひなテックGames">
  <meta name="twitter:description" content="神経衰弱(メモリーマッチ)を無料でプレイ!ダウンロード不要・登録不要でブラウザからすぐ遊べる定番の脳トレ。カードをめくって同じ絵柄のペアを見つけよう。3段階の難易度と覚え方のコツも解説。PC・スマホ対応。">
  <meta name="twitter:image" content="https://hinata-ya.tech/games/images/og/memory.png">
  <!-- /SEO:meta-extra -->
  <link rel="stylesheet" href="../../css/style.css">
  <link rel="stylesheet" href="style.css?v=20260607">
  <link rel="stylesheet" href="../../css/leaderboard.css">
  <!-- FAQ:ld+json(手動管理。enhance-seo-games.js の SEO:ld+json マーカー外なので再生成で上書きされない) -->
  <script type="application/ld+json">
  {
    "@context": "https://schema.org",
    "@type": "FAQPage",
    "mainEntity": [
      {
        "@type": "Question",
        "name": "この神経衰弱は無料で遊べますか?",
        "acceptedAnswer": { "@type": "Answer", "text": "はい、完全無料です。料金や課金は一切ありません。" }
      },
      {
        "@type": "Question",
        "name": "ダウンロードやインストールは必要ですか?",
        "acceptedAnswer": { "@type": "Answer", "text": "不要です。ブラウザを開くだけですぐに遊べます。" }
      },
      {
        "@type": "Question",
        "name": "スマホでも遊べますか?",
        "acceptedAnswer": { "@type": "Answer", "text": "遊べます。カードをタップ(PCはクリック)してめくります。" }
      },
      {
        "@type": "Question",
        "name": "難易度は選べますか?",
        "acceptedAnswer": { "@type": "Answer", "text": "かんたん(3×4)・ふつう(4×4)・むずかしい(4×6)の3段階から選べます。" }
      },
      {
        "@type": "Question",
        "name": "記録は保存されますか?",
        "acceptedAnswer": { "@type": "Answer", "text": "ベスト記録(手数・タイム)は自動で保存され、ランキングに登録することもできます。" }
      }
    ]
  }
  </script>
  <!-- SEO:ld+json -->
  <script type="application/ld+json">
  {
    "@context": "https://schema.org",
    "@type": "VideoGame",
    "name": "神経衰弱",
    "description": "神経衰弱(メモリーマッチ)を無料でプレイ!ダウンロード不要・登録不要でブラウザからすぐ遊べる定番の脳トレ。カードをめくって同じ絵柄のペアを見つけよう。3段階の難易度と覚え方のコツも解説。PC・スマホ対応。",
    "url": "https://hinata-ya.tech/games/games/memory/",
    "image": "https://hinata-ya.tech/games/images/og/memory.png",
    "inLanguage": "ja",
    "genre": [
      "脳トレ",
      "カード"
    ],
    "gamePlatform": [
      "Web Browser"
    ],
    "applicationCategory": "GameApplication",
    "operatingSystem": [
      "Windows",
      "macOS",
      "iOS",
      "Android",
      "ChromeOS"
    ],
    "browserRequirements": "Requires JavaScript. Requires HTML5.",
    "isAccessibleForFree": true,
    "offers": {
      "@type": "Offer",
      "price": "0",
      "priceCurrency": "JPY",
      "availability": "https://schema.org/InStock"
    },
    "publisher": {
      "@type": "Organization",
      "name": "ひなテック",
      "url": "https://hinata-ya.tech"
    },
    "author": {
      "@type": "Organization",
      "name": "ひなテック",
      "url": "https://hinata-ya.tech"
    }
  }
  </script>
  <script type="application/ld+json">
  {
    "@context": "https://schema.org",
    "@type": "BreadcrumbList",
    "itemListElement": [
      {
        "@type": "ListItem",
        "position": 1,
        "name": "ホーム",
        "item": "https://hinata-ya.tech/games/"
      },
      {
        "@type": "ListItem",
        "position": 2,
        "name": "神経衰弱",
        "item": "https://hinata-ya.tech/games/games/memory/"
      }
    ]
  }
  </script>
  <!-- /SEO:ld+json -->
</head>
<body>
  <div id="header"></div>

  <div class="game-page">
    <h1 class="game-page-title">神経衰弱</h1>
    <div class="game-page-tags">
      <span class="tag">脳トレ</span>
      <span class="tag">カード</span>
    </div>

    <p class="game-lead">カードをめくって同じ絵柄のペアを見つける定番の神経衰弱(メモリーマッチ)。<strong>ダウンロード不要・登録不要</strong>で、PCでもスマホでもブラウザからすぐ無料で遊べます。</p>

    <div class="game-container">
      <!-- コントロールエリア -->
      <div class="control-area">
        <div class="difficulty-selector">
          <label for="difficulty">難易度:</label>
          <select id="difficulty">
            <option value="easy">かんたん (3x4)</option>
            <option value="normal" selected>ふつう (4x4)</option>
            <option value="hard">むずかしい (4x6)</option>
          </select>
        </div>
        <button class="btn-new-game" id="btn-new-game">New Game</button>
      </div>

      <!-- スコアエリア -->
      <div class="score-area">
        <div class="score-box">
          <span class="score-label">手数</span>
          <span class="score-value" id="moves">0</span>
        </div>
        <div class="score-box">
          <span class="score-label">タイム</span>
          <span class="score-value" id="timer">0:00</span>
        </div>
        <div class="score-box">
          <span class="score-label">ベスト</span>
          <span class="score-value" id="best-score">-</span>
        </div>
      </div>

      <!-- ゲームボード -->
      <div class="board-container" id="board-container">
        <div class="card-grid" id="card-grid">
          <!-- カードはJSで生成 -->
        </div>

        <!-- ゲームクリアオーバーレイ -->
        <div class="game-overlay" id="game-complete-overlay">
          <div class="overlay-content">
            <h2>クリア!</h2>
            <div class="stars" id="stars"></div>
            <p class="result-text">
              手数: <span id="final-moves">0</span><br>
              タイム: <span id="final-time">0:00</span>
            </p>
            <p class="best-text" id="best-text"></p>
            <button class="btn-primary" id="btn-play-again">もう一度遊ぶ</button>
          </div>
        </div>
      </div>
    </div>

    <div class="game-instructions">
      <h3>遊び方</h3>
      <ul>
        <li>カードをクリック(タップ)してめくります</li>
        <li>2枚めくって同じ絵柄ならペア成立! カードはそのまま表になります</li>
        <li>絵柄が違ったら、カードは裏に戻ります</li>
        <li>すべてのペアを見つけたらクリア!</li>
        <li>少ない手数でクリアするほど高評価(星3つが最高)</li>
        <li>難易度を変えてチャレンジしよう!</li>
      </ul>
    </div>

    <div class="game-instructions">
      <h3>神経衰弱を少ない手数でクリアするコツ</h3>
      <ul>
        <li><strong>位置を行・列で覚える</strong>:「左上から2番目」のように場所を座標で記憶すると思い出しやすい。</li>
        <li><strong>端や角から開ける</strong>:序盤は端から順にめくると、位置の基準ができて記憶を整理しやすい。</li>
        <li><strong>めくった絵柄を声・イメージで反復</strong>:頭の中で「ここはりんご」と繰り返すと忘れにくい。</li>
        <li><strong>既知のペアを優先</strong>:場所が分かっているペアから確実に取り、手数の無駄を減らす。</li>
        <li><strong>かんたんから慣らす</strong>:まず3×4で記憶のコツをつかみ、4×4・4×6へ段階的に挑戦する。</li>
      </ul>
    </div>

    <div class="game-instructions">
      <h3>よくある質問</h3>
      <ul>
        <li><strong>Q. 無料で遊べますか?</strong><br>A. はい、完全無料です。料金や課金は一切ありません。</li>
        <li><strong>Q. ダウンロードやインストールは必要?</strong><br>A. 不要です。ブラウザを開くだけですぐに遊べます。</li>
        <li><strong>Q. スマホでも遊べますか?</strong><br>A. 遊べます。カードをタップ(PCはクリック)してめくります。</li>
        <li><strong>Q. 難易度は選べますか?</strong><br>A. かんたん(3×4)・ふつう(4×4)・むずかしい(4×6)の3段階から選べます。</li>
        <li><strong>Q. 記録は保存されますか?</strong><br>A. ベスト記録(手数・タイム)は自動で保存され、ランキングに登録することもできます。</li>
      </ul>
    </div>

    <div class="game-promo">
      <h3>このゲーム、自分でも作れるよ!</h3>
      <p>
        神経衰弱はCSS 3Dトランスフォームの<br>
        カードめくりアニメーションと、<br>
        配列のシャッフルで作られています。<br><br>
        ひなテックでは、こうしたゲームの<br>
        作り方を楽しく学べます!
      </p>
      <div class="promo-links">
        <a href="tutorial/" class="btn-primary">このゲームの作り方を見る →</a>
        <a href="https://hinata-ya.tech/contact/" class="btn-primary">体験レッスンに申し込む →</a>
        <a href="source/" class="btn-secondary">ソースコードを見る →</a>
      </div>
    </div>

    <div class="other-games">
      <a href="../../index.html">他のゲームも遊ぶ</a>
    </div>
  </div>

  <div id="footer"></div>

  <script src="../../js/common.js?v=20260613c" data-base="../../"></script>
  <script src="../../js/leaderboard.js"></script>
  <script src="game.js?v=20260610a"></script>
</body>
</html>

style.css — カードがめくれて見える3Dの仕掛け

style.cssは、このゲームのいちばんの見せ場「カードがくるりと裏返る動き」を作っているファイルです。

仕組みはこうです。1枚のカードはcard-back(はてなマークの裏面)とcard-front(絵柄の表面)の2枚を重ねて作られています。backface-visibility: hiddenで「裏側を向いた面は見えなくする」ようにしておき、card-innerrotateY(180deg)でくるっと回すと、見えていた面がかくれて反対の面が現れます。これが「カードがめくれた」ように見える仕掛けです。perspectivetransform-style: preserve-3dが、平面のはずのカードを立体的に回転させているポイントです。

ペアがそろったときの@keyframes card-match-bounceはカードがポンと弾む演出、クリア画面のstar-popは星が1つずつ順番に飛び出す演出です。難易度ごとに列の数を変える.grid-3x4などのクラスも用意されています。

style.css
/* ============================================
   神経衰弱ゲーム - 固有スタイル
   ============================================ */

/* リード文(H1直下の説明) */
.game-lead {
  font-size: 0.95rem;
  line-height: 1.7;
  color: #555;
  margin-bottom: 1.5rem;
}

/* コントロールエリア */
.control-area {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.75rem;
  margin-bottom: 1rem;
}

.difficulty-selector {
  display: flex;
  align-items: center;
  gap: 0.5rem;
}

.difficulty-selector label {
  font-size: 0.9rem;
  font-weight: 600;
  color: #776E65;
}

.difficulty-selector select {
  background: #fff;
  border: 2px solid #BBADA0;
  border-radius: 8px;
  padding: 0.4rem 0.75rem;
  font-size: 0.85rem;
  font-weight: 600;
  color: #776E65;
  cursor: pointer;
  outline: none;
  transition: border-color 0.2s;
  font-family: inherit;
}

.difficulty-selector select:focus {
  border-color: #E67E22;
}

/* スコアエリア(2048風) */
.score-area {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  margin-bottom: 1rem;
}

.score-box {
  background: #BBADA0;
  border-radius: 8px;
  padding: 0.5rem 1rem;
  text-align: center;
  min-width: 70px;
  flex: 1;
}

.score-label {
  display: block;
  font-size: 0.7rem;
  color: #EEE4DA;
  text-transform: uppercase;
  letter-spacing: 0.05em;
}

.score-value {
  display: block;
  font-size: 1.25rem;
  font-weight: 700;
  color: #fff;
}

.btn-new-game {
  background: #8F7A66;
  color: #fff;
  border: none;
  border-radius: 8px;
  padding: 0.5rem 1.25rem;
  font-size: 0.85rem;
  font-weight: 600;
  cursor: pointer;
  transition: background 0.2s;
  white-space: nowrap;
  font-family: inherit;
}

.btn-new-game:hover {
  background: #7A6658;
}

/* ゲームボード */
.board-container {
  position: relative;
  width: 100%;
  max-width: 500px;
  margin: 0 auto;
}

/* カードグリッド */
.card-grid {
  display: grid;
  gap: 8px;
  padding: 12px;
  background: #BBADA0;
  border-radius: 12px;
}

/* 難易度別グリッド */
.card-grid.grid-3x4 {
  grid-template-columns: repeat(4, 1fr);
}

.card-grid.grid-4x4 {
  grid-template-columns: repeat(4, 1fr);
}

.card-grid.grid-4x6 {
  grid-template-columns: repeat(6, 1fr);
}

/* カード */
.card {
  aspect-ratio: 1 / 1;
  perspective: 600px;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}

.card-inner {
  position: relative;
  width: 100%;
  height: 100%;
  transition: transform 0.5s ease;
  transform-style: preserve-3d;
}

.card.flipped .card-inner {
  transform: rotateY(180deg);
}

.card.matched .card-inner {
  transform: rotateY(180deg);
}

.card-front,
.card-back {
  position: absolute;
  inset: 0;
  border-radius: 10px;
  display: flex;
  align-items: center;
  justify-content: center;
  backface-visibility: hidden;
  -webkit-backface-visibility: hidden;
}

/* カード裏面(最初に見える面) */
.card-back {
  background: linear-gradient(145deg, #F39C12, #E67E22);
  box-shadow: inset 0 2px 4px rgba(255, 255, 255, 0.3),
              inset 0 -2px 4px rgba(0, 0, 0, 0.1);
}

.card-back::after {
  content: '?';
  font-size: 1.5rem;
  font-weight: 700;
  color: rgba(255, 255, 255, 0.6);
}

/* カード表面(絵柄のある面) */
.card-front {
  background: #fff;
  transform: rotateY(180deg);
  font-size: 2rem;
  box-shadow: inset 0 0 0 2px #EEE4DA;
}

/* マッチしたカード */
.card.matched .card-front {
  background: #FFF8E1;
  box-shadow: 0 0 12px rgba(243, 156, 18, 0.4),
              inset 0 0 0 2px #F39C12;
}

/* マッチアニメーション */
@keyframes card-match-bounce {
  0% { transform: rotateY(180deg) scale(1); }
  30% { transform: rotateY(180deg) scale(1.15); }
  60% { transform: rotateY(180deg) scale(0.95); }
  100% { transform: rotateY(180deg) scale(1); }
}

.card.matched .card-inner {
  animation: card-match-bounce 0.5s ease;
}

/* カードホバー(デスクトップ) */
@media (hover: hover) {
  .card:not(.flipped):not(.matched):hover {
    transform: scale(1.05);
    transition: transform 0.15s ease;
  }

  .card:not(.flipped):not(.matched):hover .card-back {
    background: linear-gradient(145deg, #F5AB35, #E88F30);
  }
}

/* カード操作無効 */
.card.flipped,
.card.matched {
  cursor: default;
  pointer-events: none;
}

.card-grid.locked .card {
  pointer-events: none;
}

.card-grid.locked .card.flipped,
.card-grid.locked .card.matched {
  pointer-events: none;
}

/* ゲームクリアオーバーレイ */
.game-overlay {
  position: absolute;
  inset: 0;
  background: rgba(238, 228, 218, 0.85);
  border-radius: 12px;
  display: none;
  align-items: center;
  justify-content: center;
  z-index: 10;
  animation: overlay-appear 0.5s ease;
}

.game-overlay.active {
  display: flex;
}

@keyframes overlay-appear {
  0% {
    opacity: 0;
    transform: scale(0.9);
  }
  100% {
    opacity: 1;
    transform: scale(1);
  }
}

.overlay-content {
  text-align: center;
  padding: 2rem 1.5rem;
  background: #fff;
  border-radius: 16px;
  box-shadow: 0 8px 32px rgba(0, 0, 0, 0.15);
  max-width: 300px;
  width: 90%;
}

.overlay-content h2 {
  font-size: 1.75rem;
  color: #E67E22;
  margin-bottom: 0.75rem;
}

/* 星評価 */
.stars {
  font-size: 2.5rem;
  margin-bottom: 0.75rem;
  line-height: 1;
}

.star {
  display: inline-block;
  transition: transform 0.3s ease;
}

.star.filled {
  animation: star-pop 0.4s ease;
}

.star:nth-child(2).filled {
  animation-delay: 0.15s;
}

.star:nth-child(3).filled {
  animation-delay: 0.3s;
}

@keyframes star-pop {
  0% { transform: scale(0); opacity: 0; }
  50% { transform: scale(1.3); }
  100% { transform: scale(1); opacity: 1; }
}

.result-text {
  font-size: 1.05rem;
  color: #776E65;
  margin-bottom: 0.5rem;
  line-height: 1.8;
}

.best-text {
  font-size: 0.85rem;
  color: #E67E22;
  font-weight: 600;
  margin-bottom: 1rem;
  min-height: 1.2em;
}

.overlay-content .btn-primary {
  margin-top: 0.5rem;
}

/* レスポンシブ */
@media (max-width: 500px) {
  .card-grid {
    gap: 6px;
    padding: 8px;
  }

  .card-front {
    font-size: 1.5rem;
  }

  .card-back::after {
    font-size: 1.2rem;
  }

  .card-front,
  .card-back {
    border-radius: 8px;
  }
}

@media (max-width: 400px) {
  .control-area {
    flex-wrap: wrap;
  }

  .difficulty-selector {
    flex: 1;
  }

  .card-grid {
    gap: 4px;
    padding: 6px;
  }

  .card-front {
    font-size: 1.25rem;
  }

  .card-back::after {
    font-size: 1rem;
  }

  .card-front,
  .card-back {
    border-radius: 6px;
  }

  .score-box {
    padding: 0.4rem 0.5rem;
    min-width: 55px;
  }

  .score-value {
    font-size: 1rem;
  }

  .stars {
    font-size: 2rem;
  }
}

/* hard(4x6)の場合のカードサイズ調整 */
@media (max-width: 500px) {
  .card-grid.grid-4x6 .card-front {
    font-size: 1.2rem;
  }

  .card-grid.grid-4x6 .card-back::after {
    font-size: 0.9rem;
  }
}

@media (max-width: 360px) {
  .card-grid.grid-4x6 .card-front {
    font-size: 1rem;
  }

  .card-grid.grid-4x6 .card-back::after {
    font-size: 0.8rem;
  }
}

game.js — カードをまぜて、ペアを判定する頭脳

game.jsがこのゲームの心臓部です。全体は(function () { ... })();という「即時実行関数」で囲まれ、ゲームで使う変数が他のコードとぶつからないようになっています。

このゲームでいちばん大事なのがshuffle()です。ここで使われているのはフィッシャー・イェーツのシャッフルという、配列をかたよりなくまぜる有名なやり方。配列のうしろから順に、まだまぜていない範囲からランダムに1つ選んで入れかえる、をくり返します。これでカードが毎回ちがう並びになります。createCards()では、絵文字をシャッフルして必要な数だけ取り出し、それぞれを2枚ずつ並べてから、もう一度シャッフルして「ペアがバラバラに散らばった山」を作っています。

カードのクリックを受け取るのがonCardClick()です。ここの工夫はisLockedという見はり役。2枚めくって絵柄が違ったとき、すぐ次をめくれてしまうとゲームが崩れるので、isLockedtrueにして約0.8秒のあいだ操作を止めます。その間にhandleMismatch()がカードを裏へ戻します。絵柄が同じならhandleMatch()が呼ばれ、isMatchedを立ててカードを表のまま固定します。

ゲームの状態はcardsという配列で管理しています。1枚ごとにvalue(絵柄)・isFlipped(めくられているか)・isMatched(ペアがそろったか)を持たせ、見た目(DOM)と中身(データ)を分けて考えているのがきれいな設計です。

すべてのペアがそろうとhandleGameComplete()が動きます。calculateStars()は「ペア数に対して手数が少ないほど星が多い」という計算で評価を出します。ベストスコアは難易度ごとにlocalStorageへJSON形式で保存しているので、ブラウザを閉じても記録が残ります。

game.js
// ============================================
// 神経衰弱(メモリーマッチ)ゲームロジック
// ============================================

(function () {
  'use strict';

  // 言語別テキスト(英語版ページが window.GAME_TEXT を定義して上書きする。docs/i18n.md 参照)
  var TEXT = window.GAME_TEXT || {};

  // 絵文字リスト(ペア用)
  var EMOJI_POOL = [
    // 動物
    '\uD83D\uDC36', // 犬
    '\uD83D\uDC31', // 猫
    '\uD83D\uDC2D', // ねずみ
    '\uD83D\uDC39', // ハムスター
    '\uD83D\uDC30', // うさぎ
    '\uD83E\uDD8A', // きつね
    '\uD83D\uDC3B', // くま
    '\uD83D\uDC3C', // パンダ
    '\uD83D\uDC28', // コアラ
    '\uD83E\uDD81', // ライオン
    '\uD83D\uDC2F', // とら
    '\uD83D\uDC37', // ぶた
    // 食べ物
    '\uD83C\uDF4E', // りんご
    '\uD83C\uDF53', // いちご
    '\uD83C\uDF49', // すいか
    '\uD83C\uDF4A', // みかん
    '\uD83C\uDF51', // もも
    '\uD83C\uDF52', // さくらんぼ
    '\uD83C\uDF70', // ケーキ
    '\uD83C\uDF69', // ドーナツ
    '\uD83C\uDF66', // ソフトクリーム
    '\uD83C\uDF6B', // チョコ
    '\uD83C\uDF6D', // キャンディ
    '\uD83C\uDF54'  // ハンバーガー
  ];

  // 難易度設定
  var DIFFICULTY_CONFIG = {
    easy:   { rows: 3, cols: 4, pairs: 6  },
    normal: { rows: 4, cols: 4, pairs: 8  },
    hard:   { rows: 4, cols: 6, pairs: 12 }
  };

  // ゲーム状態
  var currentDifficulty = 'normal';
  var cards = [];
  var flippedCards = [];
  var matchedPairs = 0;
  var totalPairs = 0;
  var moves = 0;
  var timerInterval = null;
  var elapsedSeconds = 0;
  var gameStarted = false;
  var isLocked = false; // 比較中の操作ロック

  // DOM要素
  var cardGrid = document.getElementById('card-grid');
  var movesEl = document.getElementById('moves');
  var timerEl = document.getElementById('timer');
  var bestScoreEl = document.getElementById('best-score');
  var difficultySelect = document.getElementById('difficulty');
  var completeOverlay = document.getElementById('game-complete-overlay');
  var starsEl = document.getElementById('stars');
  var finalMovesEl = document.getElementById('final-moves');
  var finalTimeEl = document.getElementById('final-time');
  var bestTextEl = document.getElementById('best-text');

  // ============================================
  // ユーティリティ
  // ============================================

  // 配列をシャッフル(Fisher-Yates)
  function shuffle(array) {
    var arr = array.slice();
    for (var i = arr.length - 1; i > 0; i--) {
      var j = Math.floor(Math.random() * (i + 1));
      var temp = arr[i];
      arr[i] = arr[j];
      arr[j] = temp;
    }
    return arr;
  }

  // 秒数をm:ss形式にフォーマット
  function formatTime(seconds) {
    var m = Math.floor(seconds / 60);
    var s = seconds % 60;
    return m + ':' + (s < 10 ? '0' : '') + s;
  }

  // localStorageキー
  function bestScoreKey(difficulty) {
    return 'memoryBest_' + difficulty;
  }

  // ベストスコアの取得
  function getBestScore(difficulty) {
    var data = localStorage.getItem(bestScoreKey(difficulty));
    if (data) {
      try {
        return JSON.parse(data);
      } catch (e) {
        return null;
      }
    }
    return null;
  }

  // ベストスコアの保存
  function saveBestScore(difficulty, moves, time) {
    var current = getBestScore(difficulty);
    // 手数が少ない方が良い。同じならタイムが短い方が良い
    if (!current || moves < current.moves || (moves === current.moves && time < current.time)) {
      localStorage.setItem(bestScoreKey(difficulty), JSON.stringify({ moves: moves, time: time }));
      return true; // 新記録
    }
    return false;
  }

  // ベストスコアの表示を更新
  function updateBestDisplay() {
    var best = getBestScore(currentDifficulty);
    if (best) {
      bestScoreEl.textContent = best.moves + (TEXT.movesUnit || '手');
    } else {
      bestScoreEl.textContent = '-';
    }
  }

  // ============================================
  // タイマー
  // ============================================

  function startTimer() {
    if (timerInterval) return;
    timerInterval = setInterval(function () {
      elapsedSeconds++;
      timerEl.textContent = formatTime(elapsedSeconds);
    }, 1000);
  }

  function stopTimer() {
    if (timerInterval) {
      clearInterval(timerInterval);
      timerInterval = null;
    }
  }

  function resetTimer() {
    stopTimer();
    elapsedSeconds = 0;
    timerEl.textContent = '0:00';
  }

  // ============================================
  // 星評価の計算
  // ============================================

  function calculateStars(moves, pairs) {
    // 理想手数 = ペア数(全てを一発で当てた場合)
    // 星3: 手数 <= ペア数 * 1.5
    // 星2: 手数 <= ペア数 * 2.5
    // 星1: それ以上
    var ratio = moves / pairs;
    if (ratio <= 1.5) return 3;
    if (ratio <= 2.5) return 2;
    return 1;
  }

  // ============================================
  // カード生成
  // ============================================

  function createCards() {
    var config = DIFFICULTY_CONFIG[currentDifficulty];
    totalPairs = config.pairs;

    // 絵文字をシャッフルして必要な数だけ取得
    var shuffledEmoji = shuffle(EMOJI_POOL);
    var selectedEmoji = shuffledEmoji.slice(0, totalPairs);

    // ペアを作る(各絵文字を2つずつ)
    var cardValues = [];
    for (var i = 0; i < selectedEmoji.length; i++) {
      cardValues.push(selectedEmoji[i]);
      cardValues.push(selectedEmoji[i]);
    }

    // シャッフル
    cardValues = shuffle(cardValues);

    return cardValues;
  }

  // ============================================
  // 描画
  // ============================================

  function renderBoard() {
    var config = DIFFICULTY_CONFIG[currentDifficulty];
    var cardValues = createCards();

    // グリッドクラスの設定
    cardGrid.className = 'card-grid grid-' + config.rows + 'x' + config.cols;

    // カード要素を生成
    cardGrid.innerHTML = '';
    cards = [];

    for (var i = 0; i < cardValues.length; i++) {
      var cardData = {
        id: i,
        value: cardValues[i],
        isFlipped: false,
        isMatched: false
      };
      cards.push(cardData);

      var cardEl = document.createElement('div');
      cardEl.className = 'card';
      cardEl.setAttribute('data-id', i);

      var inner = document.createElement('div');
      inner.className = 'card-inner';

      var back = document.createElement('div');
      back.className = 'card-back';

      var front = document.createElement('div');
      front.className = 'card-front';
      front.textContent = cardValues[i];

      inner.appendChild(back);
      inner.appendChild(front);
      cardEl.appendChild(inner);

      // クリックイベント
      cardEl.addEventListener('click', onCardClick);

      cardGrid.appendChild(cardEl);
    }
  }

  // ============================================
  // カードクリック処理
  // ============================================

  function onCardClick(e) {
    // ロック中は何もしない
    if (isLocked) return;

    var cardEl = e.currentTarget;
    var id = parseInt(cardEl.getAttribute('data-id'), 10);
    var card = cards[id];

    // 既にめくられている or マッチ済みは無視
    if (card.isFlipped || card.isMatched) return;

    // 同じカードの二重クリック防止
    if (flippedCards.length === 1 && flippedCards[0].id === id) return;

    // 最初のカードをめくった時にタイマー開始
    if (!gameStarted) {
      gameStarted = true;
      startTimer();
    }

    // カードをめくる
    card.isFlipped = true;
    cardEl.classList.add('flipped');
    flippedCards.push(card);

    // 2枚めくった場合
    if (flippedCards.length === 2) {
      moves++;
      movesEl.textContent = moves;

      var card1 = flippedCards[0];
      var card2 = flippedCards[1];

      if (card1.value === card2.value) {
        // ペア成立
        handleMatch(card1, card2);
      } else {
        // ペア不成立
        handleMismatch(card1, card2);
      }
    }
  }

  // ============================================
  // マッチ/ミスマッチ処理
  // ============================================

  function handleMatch(card1, card2) {
    card1.isMatched = true;
    card2.isMatched = true;
    matchedPairs++;

    // マッチのDOM更新
    var el1 = cardGrid.querySelector('[data-id="' + card1.id + '"]');
    var el2 = cardGrid.querySelector('[data-id="' + card2.id + '"]');

    // 少し遅延させてアニメーションを適用
    setTimeout(function () {
      if (el1) el1.classList.add('matched');
      if (el2) el2.classList.add('matched');
    }, 300);

    flippedCards = [];

    // 全ペア完了チェック
    if (matchedPairs === totalPairs) {
      setTimeout(function () {
        handleGameComplete();
      }, 600);
    }
  }

  function handleMismatch(card1, card2) {
    isLocked = true;
    cardGrid.classList.add('locked');

    // 800ms後にカードを裏返す
    setTimeout(function () {
      card1.isFlipped = false;
      card2.isFlipped = false;

      var el1 = cardGrid.querySelector('[data-id="' + card1.id + '"]');
      var el2 = cardGrid.querySelector('[data-id="' + card2.id + '"]');
      if (el1) el1.classList.remove('flipped');
      if (el2) el2.classList.remove('flipped');

      flippedCards = [];
      isLocked = false;
      cardGrid.classList.remove('locked');
    }, 800);
  }

  // ============================================
  // ゲームクリア
  // ============================================

  function handleGameComplete() {
    stopTimer();
    if (window.Leaderboard) Leaderboard.show('memory', moves, { order: 'asc' });

    var starCount = calculateStars(moves, totalPairs);
    var isNewBest = saveBestScore(currentDifficulty, moves, elapsedSeconds);

    // 星の表示
    var starsHTML = '';
    for (var i = 0; i < 3; i++) {
      if (i < starCount) {
        starsHTML += '<span class="star filled">\u2B50</span>';
      } else {
        starsHTML += '<span class="star">\u2606</span>';
      }
    }
    starsEl.innerHTML = starsHTML;

    // 結果の表示
    finalMovesEl.textContent = moves;
    finalTimeEl.textContent = formatTime(elapsedSeconds);

    if (isNewBest) {
      bestTextEl.textContent = TEXT.newBest || '\uD83C\uDF89 \u65B0\u8A18\u9332\uFF01';
    } else {
      var best = getBestScore(currentDifficulty);
      if (best) {
        bestTextEl.textContent = (TEXT.best || '\u30D9\u30B9\u30C8: ') + best.moves + (TEXT.movesUnit || '\u624B') + ' / ' + formatTime(best.time);
      } else {
        bestTextEl.textContent = '';
      }
    }

    // ベスト表示の更新
    updateBestDisplay();

    // オーバーレイを表示
    completeOverlay.classList.add('active');
  }

  // ============================================
  // ゲーム初期化
  // ============================================

  function initGame() {
    // 状態のリセット
    flippedCards = [];
    matchedPairs = 0;
    moves = 0;
    gameStarted = false;
    isLocked = false;

    movesEl.textContent = '0';
    resetTimer();
    completeOverlay.classList.remove('active');

    // 難易度の読み取り
    currentDifficulty = difficultySelect.value;
    updateBestDisplay();

    // ボード生成
    renderBoard();
  }

  // ============================================
  // イベントリスナー
  // ============================================

  // New Gameボタン
  document.getElementById('btn-new-game').addEventListener('click', initGame);

  // もう一度遊ぶボタン
  document.getElementById('btn-play-again').addEventListener('click', initGame);

  // 難易度変更
  difficultySelect.addEventListener('change', initGame);

  // ============================================
  // ゲーム開始
  // ============================================

  initGame();

})();

このコードで学べるポイント

自分のパソコンで動かすには

上のコードをそれぞれのファイル名で同じフォルダに保存し、index.htmlをブラウザで開けばゲームが動きます。各コードブロックのファイル名バーにある「コピー」ボタンを使うと便利です。

このゲームはサイト全体で共通して使うファイル(共通CSSやヘッダー)も少し利用しているため、ソースのファイルだけで開くとヘッダーなどサイト共通の部分は表示されません。でもゲーム本体はちゃんと動くので、仕組みを学んだり改造してためすには十分です。

慣れてきたら、色を変えたりルールを少しいじったり、自由に改造してみてください。コードを「読む」次は「いじってみる」のがいちばんの上達法です。

このコードは使っていいの?

このソースコードは、プログラミングの勉強のために自由に読んだり、コピーして動かしたり、改造したりして大丈夫です。学校の授業や自由研究で参考にするのも歓迎します。ぜひ「自分だったらどう作るか」を考えるきっかけにしてください。

ただし、コードをそのまままるごとコピーして自分の作品やサービスとして公開・配布するのはご遠慮ください。あくまで学習用としてお使いください。

もっと学びたい人へ

コードを読んで「自分でも作ってみたい!」と思ったら、神経衰弱の作り方ガイドがおすすめです。このページが「完成したコード」を見せるのに対して、作り方ガイドは何もない状態から1ステップずつ組み立てていく流れを解説しています。

ひなテックでは、こうしたゲームづくりを先生といっしょに楽しく学べる教室を開いています。「コードを読むだけじゃなく、ちゃんと作れるようになりたい」という人は、体験レッスンにぜひ来てみてください。