2048のソースコード全文 — JavaScript・HTML・CSS

▶ 2048で遊ぶ 📖 作り方を1から学ぶ

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

2048は、たった3つのファイル — index.htmlstyle.cssgame.js — だけでできています。HTMLでゲームの骨組みを作り、CSSで見た目を整え、JavaScriptでルールを動かす。Webゲームの基本がぎゅっと詰まった、プログラミング学習にぴったりの題材です。

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

2048は3つのファイルでできている

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

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

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

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

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

大きく分けると、(1)スコアと「New Game」ボタンを置くscore-area、(2)ゲーム盤のboard-container、(3)ゲームオーバーとクリア時に出る2つのオーバーレイ、でできています。盤面の中のgrid-backgroundには空のマス(grid-cell)を16個並べていますが、これは背景のマス目で、数字タイルそのものはJavaScriptがtile-containerの中に作って置きます。

ゲーム本体は<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>2048|無料で遊べるブラウザゲーム(ダウンロード不要)|ひなテックGames</title>
  <meta name="description" content="2048を無料でプレイ!ダウンロード不要・登録不要でブラウザからすぐ遊べる定番の数字パズル。同じ数字のタイルを合わせて2048を目指そう。高得点のコツや攻略、遊び方も解説。PC・スマホ対応。">
  <link rel="canonical" href="https://hinata-ya.tech/games/games/2048/">
  <!-- I18N:hreflang -->
  <link rel="alternate" hreflang="ja" href="https://hinata-ya.tech/games/games/2048/">
  <link rel="alternate" hreflang="en" href="https://hinata-ya.tech/games/en/games/2048/">
  <link rel="alternate" hreflang="x-default" href="https://hinata-ya.tech/games/games/2048/">
  <!-- /I18N:hreflang -->
  <!-- OGP -->
  <meta property="og:title" content="2048|無料で遊べるブラウザゲーム(ダウンロード不要)|ひなテックGames">
  <meta property="og:description" content="ダウンロード不要・登録不要でブラウザからすぐ遊べる定番の数字パズル。同じ数字を合わせて2048を目指そう。高得点のコツ・攻略も解説。PC・スマホ対応。">
  <meta property="og:type" content="website">
  <meta property="og:url" content="https://hinata-ya.tech/games/games/2048/">
  <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/2048.png">
  <meta property="og:image:width" content="1200">
  <meta property="og:image:height" content="630">
  <meta property="og:image:alt" content="2048">
  <meta name="twitter:card" content="summary_large_image">
  <meta name="twitter:title" content="2048|無料で遊べるブラウザゲーム(ダウンロード不要)|ひなテックGames">
  <meta name="twitter:description" content="2048を無料でプレイ!ダウンロード不要・登録不要でブラウザからすぐ遊べる定番の数字パズル。同じ数字のタイルを合わせて2048を目指そう。高得点のコツや攻略、遊び方も解説。PC・スマホ対応。">
  <meta name="twitter:image" content="https://hinata-ya.tech/games/images/og/2048.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": "2048は無料で遊べますか?",
        "acceptedAnswer": { "@type": "Answer", "text": "はい、完全無料です。料金や課金は一切ありません。" }
      },
      {
        "@type": "Question",
        "name": "ダウンロードやインストールは必要ですか?",
        "acceptedAnswer": { "@type": "Answer", "text": "不要です。ブラウザを開くだけですぐに遊べます。" }
      },
      {
        "@type": "Question",
        "name": "スマホでも遊べますか?",
        "acceptedAnswer": { "@type": "Answer", "text": "遊べます。スマホはスワイプ、PCは矢印キーまたはWASDで操作します。" }
      },
      {
        "@type": "Question",
        "name": "2048の次は?最大はいくつまでですか?",
        "acceptedAnswer": { "@type": "Answer", "text": "2048を作ったあとも「続けて遊ぶ」で継続でき、4096・8192とさらに大きな数字を目指せます。" }
      },
      {
        "@type": "Question",
        "name": "スコアは保存されますか?",
        "acceptedAnswer": { "@type": "Answer", "text": "ベストスコアは自動で保存され、ランキングにスコアを登録することもできます。" }
      }
    ]
  }
  </script>
  <!-- SEO:ld+json -->
  <script type="application/ld+json">
  {
    "@context": "https://schema.org",
    "@type": "VideoGame",
    "name": "2048",
    "description": "2048を無料でプレイ!ダウンロード不要・登録不要でブラウザからすぐ遊べる定番の数字パズル。同じ数字のタイルを合わせて2048を目指そう。高得点のコツや攻略、遊び方も解説。PC・スマホ対応。",
    "url": "https://hinata-ya.tech/games/games/2048/",
    "image": "https://hinata-ya.tech/games/images/og/2048.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": "2048",
        "item": "https://hinata-ya.tech/games/games/2048/"
      }
    ]
  }
  </script>
  <!-- /SEO:ld+json -->
</head>
<body>
  <div id="header"></div>

  <div class="game-page">
    <h1 class="game-page-title">2048</h1>
    <div class="game-page-tags">
      <span class="tag">パズル</span>
      <span class="tag">数字</span>
    </div>

    <p class="game-lead">同じ数字のタイルを合わせて「2048」を作る定番の数字パズル。<strong>ダウンロード不要・登録不要</strong>で、PCでもスマホでもブラウザからすぐ無料で遊べます。</p>

    <div class="game-container">
      <!-- スコアエリア -->
      <div class="score-area">
        <div class="score-box">
          <span class="score-label">スコア</span>
          <span class="score-value" id="score">0</span>
        </div>
        <div class="score-box">
          <span class="score-label">ベスト</span>
          <span class="score-value" id="best-score">0</span>
        </div>
        <button class="btn-new-game" id="btn-new-game">New Game</button>
      </div>

      <!-- ゲームボード -->
      <div class="board-container" id="board-container">
        <div class="grid-background">
          <div class="grid-cell"></div><div class="grid-cell"></div><div class="grid-cell"></div><div class="grid-cell"></div>
          <div class="grid-cell"></div><div class="grid-cell"></div><div class="grid-cell"></div><div class="grid-cell"></div>
          <div class="grid-cell"></div><div class="grid-cell"></div><div class="grid-cell"></div><div class="grid-cell"></div>
          <div class="grid-cell"></div><div class="grid-cell"></div><div class="grid-cell"></div><div class="grid-cell"></div>
        </div>
        <div class="tile-container" id="tile-container"></div>

        <!-- ゲームオーバーオーバーレイ -->
        <div class="game-overlay" id="game-over-overlay">
          <div class="overlay-content">
            <h2>ゲームオーバー</h2>
            <p>スコア: <span id="final-score">0</span></p>
            <button class="btn-primary" id="btn-retry">もう一度遊ぶ</button>
            <p class="overlay-promo">この2048、自分でも作れる!<a href="tutorial/" id="over-tutorial-link">作り方を見る →</a></p>
          </div>
        </div>

        <!-- 勝利オーバーレイ -->
        <div class="game-overlay game-win" id="game-win-overlay">
          <div class="overlay-content">
            <h2>🎉 2048達成!</h2>
            <p>おめでとう!スコア: <span id="win-score">0</span></p>
            <button class="btn-primary" id="btn-register-win">ランキングに登録</button>
            <button class="btn-primary" id="btn-continue">続けて遊ぶ</button>
            <button class="btn-new-game" id="btn-new-after-win">New Game</button>
            <p class="overlay-promo">気に入った?<a href="tutorial/" id="win-tutorial-link">このゲームの作り方を見る →</a></p>
          </div>
        </div>
      </div>
    </div>

    <div class="game-instructions">
      <h3>遊び方</h3>
      <ul>
        <li><strong>PC</strong>: 矢印キー または WASD でタイルを動かす</li>
        <li><strong>スマホ</strong>: スワイプ(上下左右)でタイルを動かす</li>
        <li>同じ数字のタイルがぶつかると合体して数字が倍に!</li>
        <li>2048のタイルを作ればクリア!それ以降も続けて遊べます</li>
      </ul>
    </div>

    <div class="game-instructions">
      <h3>2048を高得点でクリアするコツ・攻略</h3>
      <ul>
        <li><strong>大きい数字は隅に固定</strong>:いちばん大きいタイルを四隅のひとつに置き、なるべく動かさないのが基本。</li>
        <li><strong>動かす方向を絞る</strong>:上下左右のうち2方向(例:下と右)を中心に使うと盤面が崩れにくい。</li>
        <li><strong>端の1列をそろえる</strong>:隅に寄せた列を大きい順に並べると連鎖的に合体しやすくなる。</li>
        <li><strong>逆方向はなるべく使わない</strong>:固定した隅が崩れる原因になるので、上(逆向き)は最後の手段に。</li>
        <li><strong>埋まる前に合体</strong>:空きマスが少なくなったら、合体できる手を優先して空きを作る。</li>
      </ul>
    </div>

    <div class="game-instructions">
      <h3>2048 よくある質問</h3>
      <ul>
        <li><strong>Q. 無料で遊べますか?</strong><br>A. はい、完全無料です。料金や課金は一切ありません。</li>
        <li><strong>Q. ダウンロードやインストールは必要?</strong><br>A. 不要です。ブラウザを開くだけですぐに遊べます。</li>
        <li><strong>Q. スマホでも遊べますか?</strong><br>A. 遊べます。スマホはスワイプ、PCは矢印キーまたはWASDで操作します。</li>
        <li><strong>Q. 2048の次は?最大はいくつまで?</strong><br>A. 2048を作ったあとも「続けて遊ぶ」で継続でき、4096・8192…とさらに大きな数字を目指せます。</li>
        <li><strong>Q. スコアは保存されますか?</strong><br>A. ベストスコアは自動で保存され、ランキングにスコアを登録することもできます。</li>
      </ul>
    </div>

    <div class="game-promo">
      <h3>💡 このゲーム、自分でも作れるよ!</h3>
      <p>
        2048はJavaScriptの配列操作と<br>
        CSSアニメーションで作られています。<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 — 見た目とアニメーション

style.cssは、2048の「あの見た目」を作っているファイルです。数字ごとに違うタイルの色(2はうすいベージュ、2048は輝く黄色…)は、.tile-2から.tile-2048までのクラスで1つずつ指定しています。

注目してほしいのは2つのアニメーションです。@keyframes tile-appearは新しいタイルが出てくるときにポンッと拡大する動き、@keyframes tile-mergeはタイルが合体したときに一瞬大きくなる動きです。ほんの0.2秒の演出ですが、これがあるだけでゲームの手ざわりがぐっと気持ちよくなります。

ファイルの下のほうにある@media (max-width: 500px)などは、スマホのように画面が小さいときマスの間隔をせまくする「レスポンシブデザイン」の指定です。

style.css
/* ============================================
   2048ゲーム - 固有スタイル
   ============================================ */

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

/* スコアエリア */
.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;
}

.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 {
  margin-left: auto;
  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;
}

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

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

.grid-background {
  position: absolute;
  inset: 0;
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 10px;
  padding: 10px;
  background: #BBADA0;
  border-radius: 8px;
}

.grid-cell {
  background: #CDC1B4;
  border-radius: 4px;
}

.tile-container {
  position: absolute;
  inset: 0;
  padding: 10px;
}

/* タイル */
.tile {
  position: absolute;
  display: flex;
  align-items: center;
  justify-content: center;
  font-weight: 700;
  border-radius: 4px;
  transition: top 0.15s ease, left 0.15s ease;
  user-select: none;
}

.tile-2 {
  background: #EEE4DA;
  color: #776E65;
}

.tile-4 {
  background: #EDE0C8;
  color: #776E65;
}

.tile-8 {
  background: #F2B179;
  color: #F9F6F2;
}

.tile-16 {
  background: #F59563;
  color: #F9F6F2;
}

.tile-32 {
  background: #F67C5F;
  color: #F9F6F2;
}

.tile-64 {
  background: #F65E3B;
  color: #F9F6F2;
}

.tile-128 {
  background: #EDCF72;
  color: #F9F6F2;
}

.tile-256 {
  background: #EDCC61;
  color: #F9F6F2;
}

.tile-512 {
  background: #EDC850;
  color: #F9F6F2;
}

.tile-1024 {
  background: #EDC53F;
  color: #F9F6F2;
}

.tile-2048 {
  background: #EDC22E;
  color: #F9F6F2;
  box-shadow: 0 0 30px 10px rgba(237, 194, 46, 0.4);
}

.tile-super {
  background: #3C3A32;
  color: #F9F6F2;
}

/* タイルアニメーション */
@keyframes tile-appear {
  0% {
    transform: scale(0);
    opacity: 0;
  }
  100% {
    transform: scale(1);
    opacity: 1;
  }
}

@keyframes tile-merge {
  0% {
    transform: scale(1);
  }
  50% {
    transform: scale(1.2);
  }
  100% {
    transform: scale(1);
  }
}

.tile-new {
  animation: tile-appear 0.2s ease;
}

.tile-merged {
  animation: tile-merge 0.2s ease;
}

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

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

.game-win {
  background: rgba(237, 194, 46, 0.5);
}

@keyframes overlay-appear {
  0% {
    opacity: 0;
  }
  100% {
    opacity: 1;
  }
}

.overlay-content {
  text-align: center;
  padding: 1.5rem;
}

.overlay-content h2 {
  font-size: 1.75rem;
  color: #776E65;
  margin-bottom: 0.5rem;
}

.game-win .overlay-content h2 {
  color: #F9F6F2;
  text-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
}

.overlay-content p {
  font-size: 1.1rem;
  color: #776E65;
  margin-bottom: 1rem;
}

.overlay-content .btn-primary {
  display: block;
  margin: 0 auto 0.5rem;
}

/* オーバーレイ内の送客リンク(作り方へ) */
.overlay-content p.overlay-promo {
  font-size: 0.8rem;
  margin-top: 0.75rem;
  margin-bottom: 0;
}

.overlay-promo a {
  color: #E67E22;
  font-weight: 600;
  text-decoration: none;
}

.game-win .overlay-promo a {
  color: #5a4f3a;
}

.overlay-content .btn-new-game {
  margin-left: 0;
  display: block;
  margin: 0.5rem auto 0;
}

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

  .tile-container {
    padding: 8px;
  }
}

@media (max-width: 360px) {
  .grid-background {
    gap: 6px;
    padding: 6px;
  }

  .tile-container {
    padding: 6px;
  }

  .score-box {
    padding: 0.4rem 0.75rem;
    min-width: 60px;
  }

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

game.js — ゲームのルールを動かす頭脳

game.jsがこのゲームの心臓部です。少し長いですが、やっていることを順番に追えば必ず理解できます。コードを読む前に、大きな流れをつかんでおきましょう。

まず全体が(function () { ... })();という形で囲まれています。これは「即時実行関数」と呼ばれる書き方で、ゲームで使う変数(gridscoreなど)がページの他の部分とぶつからないよう、自分専用の部屋に閉じこめる役割があります。

ゲームの盤面はgridという4×4の2次元配列で表しています。grid[r][c]がr行c列のマスの数字で、0は空きマスです。addRandomTile()は空きマスをemptyCells()で探し、そのうち1つにランダムで2か4を置きます(90%が2、10%が4)。

このゲームでいちばん面白いのがslideRow()rotateGrid()です。slideRow()は1行を左に寄せて、となり合う同じ数字を合体させる関数。では、なぜ「左」だけ? 上下右の処理はどこに…? ここがこのコードの最大のアイデアです。rotateGrid()で盤面をくるっと回転させてしまえば、どの方向の移動も「左に寄せる」処理1つで済むのです。上向きの移動なら盤面を1回転、右向きなら2回転…という具合に。同じような処理を4回書かずに済む、とてもスマートなテクニックです。

move()がこれらをまとめる司令塔で、「回転 → スライド → 回転を戻す → 新しいタイルを追加」の順で1手を進めます。そのあとcanMove()で「もう動かせるマスがないか」を調べ、なければゲームオーバーです。

操作の受け付けは2種類あります。keydownイベントで矢印キーとWASDキーを、touchstarttouchendでスマホのスワイプを処理しています。指を動かした距離(dxdy)の大きいほうを見て、上下のスワイプか左右のスワイプかを判定しているのがポイントです。

ベストスコアはlocalStorageに保存しているので、ブラウザを閉じても記録が残ります。

game.js
// ============================================
// 2048 ゲームロジック
// ============================================

(function () {
  'use strict';

  var SIZE = 4;
  var grid = [];
  var score = 0;
  var bestScore = parseInt(localStorage.getItem('best2048') || '0', 10);
  var gameOver = false;
  var won = false;
  var keepPlaying = false;

  // DOM要素
  var tileContainer = document.getElementById('tile-container');
  var boardContainer = document.getElementById('board-container');
  var scoreEl = document.getElementById('score');
  var bestScoreEl = document.getElementById('best-score');
  var gameOverOverlay = document.getElementById('game-over-overlay');
  var gameWinOverlay = document.getElementById('game-win-overlay');
  var finalScoreEl = document.getElementById('final-score');
  var winScoreEl = document.getElementById('win-score');

  // タイルサイズの計算用
  var cellSize = 0;
  var gapSize = 0;
  var padSize = 0;

  // サイズ計算
  function calcSizes() {
    var w = boardContainer.offsetWidth;
    if (w <= 360) {
      gapSize = 6;
      padSize = 6;
    } else if (w <= 500) {
      gapSize = 8;
      padSize = 8;
    } else {
      gapSize = 10;
      padSize = 10;
    }
    cellSize = (w - padSize * 2 - gapSize * (SIZE - 1)) / SIZE;
  }

  // タイルの位置を計算
  function tilePos(row, col) {
    return {
      top: padSize + row * (cellSize + gapSize),
      left: padSize + col * (cellSize + gapSize)
    };
  }

  // タイルのフォントサイズ
  function tileFontSize(value) {
    if (value < 100) return cellSize * 0.45;
    if (value < 1000) return cellSize * 0.35;
    if (value < 10000) return cellSize * 0.28;
    return cellSize * 0.22;
  }

  // 初期化
  function init() {
    grid = [];
    for (var r = 0; r < SIZE; r++) {
      grid[r] = [];
      for (var c = 0; c < SIZE; c++) {
        grid[r][c] = 0;
      }
    }
    score = 0;
    gameOver = false;
    won = false;
    keepPlaying = false;
    updateScore();
    gameOverOverlay.classList.remove('active');
    gameWinOverlay.classList.remove('active');

    addRandomTile();
    addRandomTile();
    render();
  }

  // 空きセルを取得
  function emptyCells() {
    var cells = [];
    for (var r = 0; r < SIZE; r++) {
      for (var c = 0; c < SIZE; c++) {
        if (grid[r][c] === 0) cells.push({ r: r, c: c });
      }
    }
    return cells;
  }

  // ランダムにタイルを追加
  function addRandomTile() {
    var empty = emptyCells();
    if (empty.length === 0) return null;
    var cell = empty[Math.floor(Math.random() * empty.length)];
    var value = Math.random() < 0.9 ? 2 : 4;
    grid[cell.r][cell.c] = value;
    return { r: cell.r, c: cell.c, value: value };
  }

  // スコア更新
  function updateScore() {
    scoreEl.textContent = score;
    if (score > bestScore) {
      bestScore = score;
      localStorage.setItem('best2048', bestScore.toString());
    }
    bestScoreEl.textContent = bestScore;
  }

  // 描画
  function render(newTile, mergedPositions) {
    calcSizes();
    tileContainer.innerHTML = '';

    for (var r = 0; r < SIZE; r++) {
      for (var c = 0; c < SIZE; c++) {
        var value = grid[r][c];
        if (value === 0) continue;

        var pos = tilePos(r, c);
        var el = document.createElement('div');
        var cls = value <= 2048 ? 'tile-' + value : 'tile-super';
        el.className = 'tile ' + cls;
        el.textContent = value;
        el.style.width = cellSize + 'px';
        el.style.height = cellSize + 'px';
        el.style.top = pos.top + 'px';
        el.style.left = pos.left + 'px';
        el.style.fontSize = tileFontSize(value) + 'px';
        el.style.lineHeight = cellSize + 'px';

        // 新しいタイルのアニメーション
        if (newTile && newTile.r === r && newTile.c === c) {
          el.classList.add('tile-new');
        }

        // 合体タイルのアニメーション
        if (mergedPositions) {
          for (var m = 0; m < mergedPositions.length; m++) {
            if (mergedPositions[m].r === r && mergedPositions[m].c === c) {
              el.classList.add('tile-merged');
              break;
            }
          }
        }

        tileContainer.appendChild(el);
      }
    }
  }

  // 1行をスライド処理(左方向に統一)
  function slideRow(row) {
    // ゼロを除去
    var filtered = row.filter(function (v) { return v !== 0; });
    var merged = [];
    var mergedFlags = [];
    var scoreGain = 0;

    for (var i = 0; i < filtered.length; i++) {
      if (i + 1 < filtered.length && filtered[i] === filtered[i + 1]) {
        var newVal = filtered[i] * 2;
        merged.push(newVal);
        mergedFlags.push(true);
        scoreGain += newVal;
        i++; // 次をスキップ
      } else {
        merged.push(filtered[i]);
        mergedFlags.push(false);
      }
    }

    // ゼロで埋める
    while (merged.length < SIZE) {
      merged.push(0);
      mergedFlags.push(false);
    }

    return { row: merged, mergedFlags: mergedFlags, scoreGain: scoreGain };
  }

  // グリッドを回転(方向を左に統一するため)
  function rotateGrid(g, times) {
    var result = g;
    for (var t = 0; t < times; t++) {
      var newGrid = [];
      for (var r = 0; r < SIZE; r++) {
        newGrid[r] = [];
        for (var c = 0; c < SIZE; c++) {
          newGrid[r][c] = result[c][SIZE - 1 - r];
        }
      }
      result = newGrid;
    }
    return result;
  }

  // 逆回転のマッピング(座標の変換)
  function unrotatePos(r, c, times) {
    var row = r, col = c;
    for (var t = 0; t < (4 - times) % 4; t++) {
      var tmp = row;
      row = col;
      col = SIZE - 1 - tmp;
    }
    return { r: row, c: col };
  }

  // 移動処理
  function move(direction) {
    if (gameOver) return false;

    // 方向に応じた回転回数(左を基準)
    var rotations = { left: 0, up: 1, right: 2, down: 3 };
    var rot = rotations[direction];

    // グリッドを回転して左方向に統一
    var rotated = rotateGrid(grid, rot);
    var moved = false;
    var mergedPositions = [];
    var totalScore = 0;

    for (var r = 0; r < SIZE; r++) {
      var result = slideRow(rotated[r]);
      // 行が変化したかチェック
      for (var c = 0; c < SIZE; c++) {
        if (rotated[r][c] !== result.row[c]) {
          moved = true;
        }
        // 合体した位置を記録
        if (result.mergedFlags[c]) {
          var original = unrotatePos(r, c, rot);
          mergedPositions.push(original);
        }
      }
      rotated[r] = result.row;
      totalScore += result.scoreGain;
    }

    if (!moved) return false;

    // 回転を戻す
    grid = rotateGrid(rotated, (4 - rot) % 4);
    score += totalScore;
    updateScore();

    // 新しいタイルを追加
    var newTile = addRandomTile();
    render(newTile, mergedPositions);

    // 勝利判定
    if (!won && !keepPlaying) {
      for (var r2 = 0; r2 < SIZE; r2++) {
        for (var c2 = 0; c2 < SIZE; c2++) {
          if (grid[r2][c2] === 2048) {
            won = true;
            winScoreEl.textContent = score;
            gameWinOverlay.classList.add('active');
            return true;
          }
        }
      }
    }

    // ゲームオーバー判定
    if (!canMove()) {
      gameOver = true;
      finalScoreEl.textContent = score;
      gameOverOverlay.classList.add('active');
      if (window.Leaderboard) Leaderboard.show('2048', score);
    }

    return true;
  }

  // 移動可能か判定
  function canMove() {
    // 空きセルがあれば移動可能
    for (var r = 0; r < SIZE; r++) {
      for (var c = 0; c < SIZE; c++) {
        if (grid[r][c] === 0) return true;
      }
    }
    // 隣接する同じ数字があれば移動可能
    for (var r2 = 0; r2 < SIZE; r2++) {
      for (var c2 = 0; c2 < SIZE; c2++) {
        var val = grid[r2][c2];
        if (c2 + 1 < SIZE && grid[r2][c2 + 1] === val) return true;
        if (r2 + 1 < SIZE && grid[r2 + 1][c2] === val) return true;
      }
    }
    return false;
  }

  // キーボード入力
  document.addEventListener('keydown', function (e) {
    var keyMap = {
      ArrowLeft: 'left', ArrowRight: 'right', ArrowUp: 'up', ArrowDown: 'down',
      a: 'left', d: 'right', w: 'up', s: 'down',
      A: 'left', D: 'right', W: 'up', S: 'down'
    };
    var dir = keyMap[e.key];
    if (dir) {
      e.preventDefault();
      move(dir);
    }
  });

  // タッチ操作(スワイプ)
  var touchStartX = 0;
  var touchStartY = 0;
  var touchStartTime = 0;

  boardContainer.addEventListener('touchstart', function (e) {
    if (e.touches.length === 1) {
      touchStartX = e.touches[0].clientX;
      touchStartY = e.touches[0].clientY;
      touchStartTime = Date.now();
    }
  }, { passive: true });

  boardContainer.addEventListener('touchmove', function (e) {
    e.preventDefault();
  }, { passive: false });

  boardContainer.addEventListener('touchend', function (e) {
    if (e.changedTouches.length === 0) return;

    var dx = e.changedTouches[0].clientX - touchStartX;
    var dy = e.changedTouches[0].clientY - touchStartY;
    var dt = Date.now() - touchStartTime;

    // 最小スワイプ距離と最大時間
    var minDist = 30;
    var maxTime = 500;

    if (dt > maxTime) return;
    if (Math.abs(dx) < minDist && Math.abs(dy) < minDist) return;

    var dir;
    if (Math.abs(dx) > Math.abs(dy)) {
      dir = dx > 0 ? 'right' : 'left';
    } else {
      dir = dy > 0 ? 'down' : 'up';
    }

    move(dir);
  }, { passive: true });

  // ボタンイベント
  document.getElementById('btn-new-game').addEventListener('click', init);
  document.getElementById('btn-retry').addEventListener('click', init);
  document.getElementById('btn-new-after-win').addEventListener('click', init);
  document.getElementById('btn-continue').addEventListener('click', function () {
    keepPlaying = true;
    gameWinOverlay.classList.remove('active');
  });

  // 勝利時はゲームオーバーまで到達しないとスコアが登録されないため、
  // 勝利オーバーレイから明示的に登録できるようにする(表示中=勝利時スコア)。
  document.getElementById('btn-register-win').addEventListener('click', function () {
    if (window.Leaderboard) Leaderboard.show('2048', score);
  });

  // オーバーレイから「作り方」への送客導線を GA4 で計測する
  function trackTutorialCta(id, context) {
    var el = document.getElementById(id);
    if (!el) return;
    el.addEventListener('click', function () {
      if (window.gtag) gtag('event', 'overlay_cta_click', { game: '2048', cta: 'tutorial', context: context });
    });
  }
  trackTutorialCta('win-tutorial-link', 'win');
  trackTutorialCta('over-tutorial-link', 'gameover');

  // ウィンドウリサイズ時に再描画
  var resizeTimer;
  window.addEventListener('resize', function () {
    clearTimeout(resizeTimer);
    resizeTimer = setTimeout(function () {
      render();
    }, 100);
  });

  // ゲーム開始
  bestScoreEl.textContent = bestScore;
  init();
})();

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

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

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

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

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

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

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

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

もっと学びたい人へ

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

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