このページでは、ひなテックGamesで実際に動いている「神経衰弱」のソースコードを丸ごと公開しています。ここに載っているのは説明用に書き直したコードではなく、いまあなたが遊んでいるゲームそのものを動かしている本物のファイルです。
このゲームの見どころは2つあります。1つは、カードをタップするとくるりと裏返るCSSの3D回転アニメーション。もう1つは、絵柄をまぜて毎回ちがう配置にするシャッフルの仕組みです。どちらもゲームづくりでよく使う大切な技術です。
コードはコピーして自分のパソコンで自由に動かせます。「カードのめくり方ってどう作るんだろう?」と思ったら、解説を読みながらコードを追いかけてみてください。
神経衰弱は、次のファイルが役割を分担して動いています。それぞれの中身は下で1つずつ解説します。
| ファイル | 役割 |
|---|---|
index.html | ゲーム画面の骨組み。盤面・スコア表示・ボタンなどの部品をHTMLで配置する |
style.css | 見た目のデザイン。色・大きさ・レイアウト・アニメーション |
game.js | ゲームのルールを動かす頭脳。操作の受け付け・判定・スコア計算など |
この「HTMLで構造、CSSで見た目、JavaScriptで動き」という分担は、ほとんどのWebページ・Webゲームに共通する考え方です。
index.htmlは、ブラウザが最初に読み込むファイルです。ここにはゲームの「部品」をHTMLで並べているだけで、カードをめくる動きそのものは書かれていません。
大きく分けると、(1)難易度を選ぶselectとNew Gameボタンを置くcontrol-area、(2)手数・タイム・ベストを表示するscore-area、(3)カードを並べるcard-grid、(4)クリア時に出るオーバーレイ、でできています。card-gridの中は空っぽで、カード1枚1枚はJavaScriptがあとから作って入れます。
ゲーム本体は<body>の中から始まります。
※ 下のコードでは、ゲームの動きと関係のない広告・アクセス解析用のタグ(ページ先頭の数行)を省いています。
<!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は、このゲームのいちばんの見せ場「カードがくるりと裏返る動き」を作っているファイルです。
仕組みはこうです。1枚のカードはcard-back(はてなマークの裏面)とcard-front(絵柄の表面)の2枚を重ねて作られています。backface-visibility: hiddenで「裏側を向いた面は見えなくする」ようにしておき、card-innerをrotateY(180deg)でくるっと回すと、見えていた面がかくれて反対の面が現れます。これが「カードがめくれた」ように見える仕掛けです。perspectiveとtransform-style: preserve-3dが、平面のはずのカードを立体的に回転させているポイントです。
ペアがそろったときの@keyframes card-match-bounceはカードがポンと弾む演出、クリア画面のstar-popは星が1つずつ順番に飛び出す演出です。難易度ごとに列の数を変える.grid-3x4などのクラスも用意されています。
/* ============================================
神経衰弱ゲーム - 固有スタイル
============================================ */
/* リード文(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がこのゲームの心臓部です。全体は(function () { ... })();という「即時実行関数」で囲まれ、ゲームで使う変数が他のコードとぶつからないようになっています。
このゲームでいちばん大事なのがshuffle()です。ここで使われているのはフィッシャー・イェーツのシャッフルという、配列をかたよりなくまぜる有名なやり方。配列のうしろから順に、まだまぜていない範囲からランダムに1つ選んで入れかえる、をくり返します。これでカードが毎回ちがう並びになります。createCards()では、絵文字をシャッフルして必要な数だけ取り出し、それぞれを2枚ずつ並べてから、もう一度シャッフルして「ペアがバラバラに散らばった山」を作っています。
カードのクリックを受け取るのがonCardClick()です。ここの工夫はisLockedという見はり役。2枚めくって絵柄が違ったとき、すぐ次をめくれてしまうとゲームが崩れるので、isLockedをtrueにして約0.8秒のあいだ操作を止めます。その間にhandleMismatch()がカードを裏へ戻します。絵柄が同じならhandleMatch()が呼ばれ、isMatchedを立ててカードを表のまま固定します。
ゲームの状態はcardsという配列で管理しています。1枚ごとにvalue(絵柄)・isFlipped(めくられているか)・isMatched(ペアがそろったか)を持たせ、見た目(DOM)と中身(データ)を分けて考えているのがきれいな設計です。
すべてのペアがそろうとhandleGameComplete()が動きます。calculateStars()は「ペア数に対して手数が少ないほど星が多い」という計算で評価を出します。ベストスコアは難易度ごとにlocalStorageへJSON形式で保存しているので、ブラウザを閉じても記録が残ります。
// ============================================
// 神経衰弱(メモリーマッチ)ゲームロジック
// ============================================
(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();
})();
rotateYとbackface-visibilityでカードがめくれて見える仕掛けcards配列でゲームの状態を持ち、DOMはその表示にするisLockedでアニメーション中の入力を止め、ゲームの崩れを防ぐ上のコードをそれぞれのファイル名で同じフォルダに保存し、index.htmlをブラウザで開けばゲームが動きます。各コードブロックのファイル名バーにある「コピー」ボタンを使うと便利です。
このゲームはサイト全体で共通して使うファイル(共通CSSやヘッダー)も少し利用しているため、ソースのファイルだけで開くとヘッダーなどサイト共通の部分は表示されません。でもゲーム本体はちゃんと動くので、仕組みを学んだり改造してためすには十分です。
慣れてきたら、色を変えたりルールを少しいじったり、自由に改造してみてください。コードを「読む」次は「いじってみる」のがいちばんの上達法です。
このソースコードは、プログラミングの勉強のために自由に読んだり、コピーして動かしたり、改造したりして大丈夫です。学校の授業や自由研究で参考にするのも歓迎します。ぜひ「自分だったらどう作るか」を考えるきっかけにしてください。
ただし、コードをそのまままるごとコピーして自分の作品やサービスとして公開・配布するのはご遠慮ください。あくまで学習用としてお使いください。
コードを読んで「自分でも作ってみたい!」と思ったら、神経衰弱の作り方ガイドがおすすめです。このページが「完成したコード」を見せるのに対して、作り方ガイドは何もない状態から1ステップずつ組み立てていく流れを解説しています。
ひなテックでは、こうしたゲームづくりを先生といっしょに楽しく学べる教室を開いています。「コードを読むだけじゃなく、ちゃんと作れるようになりたい」という人は、体験レッスンにぜひ来てみてください。