タワーディフェンスのソースコード全文 — JavaScript・HTML・CSS

▶ タワーディフェンスで遊ぶ 📖 作り方を1から学ぶ

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

タワーディフェンスは、道を進んでくる敵をタワーで撃ち落とすゲームです。このゲームには「<canvas>に絵をかく」「敵を道に沿って動かす」「タワーがねらいを定めて弾を撃つ」「敵の波(ウェーブ)を管理する」という、本格的なゲーム作りの要素がぎっしり詰まっています。

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

タワーディフェンスは3つのファイルでできている

タワーディフェンスは、次のファイルが役割を分担して動いています。それぞれの中身は下で1つずつ解説します。

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

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

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

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

このゲームの絵は、たった1つの<canvas>(お絵かきボード)の上にすべてかかれます。マップも敵もタワーも弾も、JavaScriptがこの1枚のキャンバスに毎フレームかき直しているのです。HTMLには他に、ウェーブ・ライフ・ゴールドを出すgame-hud、6種類のタワーを選ぶtower-select、早送りボタンのspeed-controlが並んでいます。タワーのボタンにはdata-towerで種類の名前が付けてあります。

ゲーム本体は<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="タワーディフェンスを無料でプレイ!ダウンロード不要・登録不要でブラウザからすぐ遊べる。タワーを設置して敵の侵攻を防ぎ、ウェーブをクリアしてハイスコアを目指そう。攻略のコツも解説。PC・スマホ対応。">
  <link rel="canonical" href="https://hinata-ya.tech/games/games/tower-defense/">
  <!-- I18N:hreflang -->
  <link rel="alternate" hreflang="ja" href="https://hinata-ya.tech/games/games/tower-defense/">
  <link rel="alternate" hreflang="en" href="https://hinata-ya.tech/games/en/games/tower-defense/">
  <link rel="alternate" hreflang="x-default" href="https://hinata-ya.tech/games/games/tower-defense/">
  <!-- /I18N:hreflang -->
  <!-- OGP -->
  <meta property="og:title" content="タワーディフェンス|無料ブラウザゲーム・ダウンロード不要|ひなテックGames">
  <meta property="og:description" content="ダウンロード不要・登録不要でブラウザからすぐ遊べるタワーディフェンス。タワーを設置して敵の侵攻を防ぎ、ウェーブをクリアしてハイスコアを目指そう。PC・スマホ対応。">
  <meta property="og:type" content="website">
  <meta property="og:url" content="https://hinata-ya.tech/games/games/tower-defense/">
  <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/tower-defense.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="タワーディフェンスを無料でプレイ!ダウンロード不要・登録不要でブラウザからすぐ遊べる。タワーを設置して敵の侵攻を防ぎ、ウェーブをクリアしてハイスコアを目指そう。攻略のコツも解説。PC・スマホ対応。">
  <meta name="twitter:image" content="https://hinata-ya.tech/games/images/og/tower-defense.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": "スロウで足止め、ばくはつで密集処理、スナイパーで遠距離狙撃など、ウェーブに合わせて組み合わせます。" }
      },
      {
        "@type": "Question",
        "name": "スコアは保存されますか?",
        "acceptedAnswer": { "@type": "Answer", "text": "ベストスコアは自動で保存され、ランキングにスコアを登録することもできます。" }
      }
    ]
  }
  </script>
  <!-- SEO:ld+json -->
  <script type="application/ld+json">
  {
    "@context": "https://schema.org",
    "@type": "VideoGame",
    "name": "タワーディフェンス",
    "description": "タワーディフェンスを無料でプレイ!ダウンロード不要・登録不要でブラウザからすぐ遊べる。タワーを設置して敵の侵攻を防ぎ、ウェーブをクリアしてハイスコアを目指そう。攻略のコツも解説。PC・スマホ対応。",
    "url": "https://hinata-ya.tech/games/games/tower-defense/",
    "image": "https://hinata-ya.tech/games/images/og/tower-defense.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/tower-defense/"
      }
    ]
  }
  </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">6種類のタワーを設置して、道を進む敵の侵攻を防ぐ戦略ゲーム。<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="game-hud">
        <div class="hud-item">
          <span class="hud-label">WAVE</span>
          <span class="hud-value" id="wave">1</span>
        </div>
        <div class="hud-item">
          <span class="hud-label">LIFE</span>
          <span class="hud-value" id="lives">10</span>
        </div>
        <div class="hud-item">
          <span class="hud-label">GOLD</span>
          <span class="hud-value" id="gold">100</span>
        </div>
      </div>

      <div class="tower-select">
        <button class="tower-btn selected" data-tower="normal">
          <span class="tower-icon" style="background:#3498DB"></span>
          <span class="tower-info">ノーマル<br><small>20G</small></span>
        </button>
        <button class="tower-btn" data-tower="fast">
          <span class="tower-icon" style="background:#2ECC71"></span>
          <span class="tower-info">はやい<br><small>35G</small></span>
        </button>
        <button class="tower-btn" data-tower="strong">
          <span class="tower-icon" style="background:#E74C3C"></span>
          <span class="tower-info">つよい<br><small>50G</small></span>
        </button>
        <button class="tower-btn" data-tower="slow">
          <span class="tower-icon" style="background:#9B59B6"></span>
          <span class="tower-info">スロウ<br><small>30G</small></span>
        </button>
        <button class="tower-btn" data-tower="splash">
          <span class="tower-icon" style="background:#F39C12"></span>
          <span class="tower-info">ばくはつ<br><small>60G</small></span>
        </button>
        <button class="tower-btn" data-tower="sniper">
          <span class="tower-icon" style="background:#1ABC9C"></span>
          <span class="tower-info">スナイパー<br><small>75G</small></span>
        </button>
        <button class="tower-btn tower-btn-sell" data-tower="sell">
          <span class="tower-icon" style="background:#999"></span>
          <span class="tower-info">撤去<br><small>半額戻る</small></span>
        </button>
      </div>

      <div class="speed-control">
        <button class="speed-btn selected" data-speed="1" id="btn-speed-1">x1</button>
        <button class="speed-btn" data-speed="2" id="btn-speed-2">x2</button>
        <button class="speed-btn" data-speed="3" id="btn-speed-3">x3</button>
      </div>

      <div class="board-wrapper">
        <canvas id="game-canvas"></canvas>

        <div class="game-overlay" id="game-over-overlay">
          <div class="overlay-content">
            <h2>ゲームオーバー</h2>
            <p>スコア: <span id="final-score">0</span></p>
            <p class="best-result" id="best-result"></p>
            <button class="btn-primary" id="btn-retry">もう一度遊ぶ</button>
          </div>
        </div>

        <div class="game-overlay active" id="game-start-overlay">
          <div class="overlay-content">
            <h2>タワーディフェンス</h2>
            <p>タワーを設置して<br>敵の侵攻を防げ!</p>
            <button class="btn-primary" id="btn-start">ゲームスタート</button>
          </div>
        </div>
      </div>
    </div>

    <div class="game-instructions">
      <h3>遊び方</h3>
      <ul>
        <li><strong>PC</strong>: マップ上の空きマスをクリックしてタワーを設置</li>
        <li><strong>スマホ</strong>: 空きマスをタップしてタワーを設置</li>
        <li>敵は道に沿って進む。タワーは自動で攻撃する</li>
        <li>敵を倒すとゴールドを獲得。タワー設置に使おう</li>
        <li>敵がゴールに到達するとライフが減る。0になるとゲームオーバー</li>
        <li>6種類のタワーを使い分けてハイスコアを目指そう!</li>
        <li>設置済みタワーは「撤去」モードで壊すと半額のゴールドが戻る</li>
        <li>早送りボタン(x1/x2/x3)でスピードを変えられる</li>
      </ul>
    </div>

    <div class="game-instructions">
      <h3>タワーディフェンス攻略のコツ</h3>
      <ul>
        <li><strong>カーブや交差点に集中</strong>:敵が長く射程内に留まる曲がり角にタワーを固めると効率よく削れる。</li>
        <li><strong>序盤はノーマルで資金を貯める</strong>:いきなり高額タワーを置かず、安いタワーで稼いでから強化する。</li>
        <li><strong>スロウで足止め+火力で削る</strong>:スロウタワーで遅くした敵を、つよい・スナイパーでまとめて倒す。</li>
        <li><strong>ばくはつは密集に強い</strong>:敵が固まって出るウェーブには範囲攻撃のばくはつタワーが効く。</li>
        <li><strong>ゴールドを使い切らない</strong>:強いウェーブの前に少し残し、増援や強化に回せるようにする。</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. スロウで足止め、ばくはつで密集処理、スナイパーで遠距離狙撃など、ウェーブに合わせて組み合わせます。</li>
        <li><strong>Q. スコアは保存されますか?</strong><br>A. ベストスコアは自動で保存され、ランキングにスコアを登録することもできます。</li>
      </ul>
    </div>

    <div class="game-promo">
      <h3>このゲーム、自分でも作れるよ!</h3>
      <p>
        経路探索、タワーのAI、ウェーブ管理など<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">&larr; 他のゲームも遊ぶ</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は、ゲームのまわりの見た目を作っているファイルです。ゲームの絵そのものはJavaScriptがキャンバスにかくので、このCSSはおもにキャンバスの外側 — タワー選択ボタン・HUD・早送りボタンの配置や色を整えています。

いま選んでいるタワーや速度のボタンには.selectedクラスが付き、色が変わって「これを選んでいるよ」とわかるようになっています。

スタート画面やゲームオーバー画面のgame-overlayはキャンバスの上に重ねて表示します。ファイルの下のほうの@mediaは、スマホでボタンが小さくなりすぎないよう調整する指定です。

style.css
/* ============================================
   タワーディフェンス - 固有スタイル
   ============================================ */

/* リード文(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: 0.5rem;
}

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

.score-label {
  display: block;
  font-size: 0.7rem;
  color: #c4a45a;
  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: #8B4513;
  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: #6b3410;
}

/* HUD */
.game-hud {
  display: flex;
  gap: 0.5rem;
  margin-bottom: 0.5rem;
}

.hud-item {
  background: #2d5a27;
  border-radius: 6px;
  padding: 0.3rem 0.7rem;
  text-align: center;
  flex: 1;
}

.hud-label {
  display: block;
  font-size: 0.6rem;
  color: #a0d48a;
  text-transform: uppercase;
  letter-spacing: 0.05em;
}

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

/* タワー選択 */
.tower-select {
  display: flex;
  flex-wrap: wrap;
  gap: 0.35rem;
  margin-bottom: 0.5rem;
}

.tower-btn {
  display: flex;
  align-items: center;
  gap: 0.3rem;
  flex: 1 1 calc(25% - 0.35rem);
  min-width: 0;
  background: #444;
  border: 2px solid transparent;
  border-radius: 6px;
  padding: 0.3rem 0.4rem;
  cursor: pointer;
  transition: border-color 0.2s, background 0.2s;
  color: #ccc;
}

.tower-btn:hover {
  background: #555;
}

.tower-btn.selected {
  border-color: #F1C40F;
  background: #555;
}

.tower-btn-sell.selected {
  border-color: #E74C3C;
}

.tower-icon {
  display: block;
  width: 16px;
  height: 16px;
  border-radius: 50%;
  flex-shrink: 0;
}

.tower-info {
  font-size: 0.6rem;
  font-weight: 600;
  line-height: 1.2;
  text-align: left;
  white-space: nowrap;
  overflow: hidden;
}

.tower-info small {
  font-weight: 400;
  color: #F1C40F;
}

.tower-btn-sell .tower-info small {
  color: #E74C3C;
}

/* 速度コントロール */
.speed-control {
  display: flex;
  gap: 0.35rem;
  margin-bottom: 0.75rem;
}

.speed-btn {
  flex: 1;
  background: #444;
  color: #ccc;
  border: 2px solid transparent;
  border-radius: 6px;
  padding: 0.3rem;
  font-size: 0.75rem;
  font-weight: 700;
  cursor: pointer;
  transition: border-color 0.2s, background 0.2s;
}

.speed-btn:hover {
  background: #555;
}

.speed-btn.selected {
  border-color: #F1C40F;
  background: #555;
  color: #F1C40F;
}

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

#game-canvas {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  background: #2d5a27;
  border-radius: 8px;
  display: block;
}

/* ゲームオーバーレイ */
.game-overlay {
  position: absolute;
  inset: 0;
  background: rgba(30, 30, 20, 0.88);
  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;
}

@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: #c4a45a;
  margin-bottom: 0.5rem;
}

.overlay-content p {
  font-size: 1.1rem;
  color: #d4c9a0;
  margin-bottom: 0.75rem;
}

.overlay-content .best-result {
  font-size: 0.95rem;
  color: #F1C40F;
  font-weight: 600;
  margin-bottom: 1rem;
}

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

/* レスポンシブ */
@media (max-width: 500px) {
  .board-wrapper {
    border-radius: 6px;
  }

  #game-canvas {
    border-radius: 6px;
  }

  .tower-btn {
    flex: 1 1 calc(25% - 0.35rem);
  }
}

@media (max-width: 360px) {
  .score-area {
    gap: 0.5rem;
  }

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

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

  .score-label {
    font-size: 0.6rem;
  }

  .tower-btn {
    padding: 0.25rem 0.3rem;
  }

  .tower-info {
    font-size: 0.55rem;
  }

  .tower-icon {
    width: 14px;
    height: 14px;
  }
}

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

game.jsがこのゲームの心臓部です。少し長いですが、やっていることを順番に追えば必ず理解できます。全体は(function () { ... })();という「即時実行関数」で囲まれています。

このゲームの土台はMAPという2次元配列です。数字の0が空きマス、1が道、2がスタート、3がゴールを表しています。そしてPATHという配列に、スタートからゴールまで道のマスを順番に並べてあります。敵はこのPATHpathIndexという番号で1つずつたどって進みます。次のマスへの向き(dxdy)を求めて少しずつ近づける、というのが敵の移動の仕組みです。

このゲームのいちばんの中心はgameLoop()です。requestAnimationFrameを使って自分をくり返し呼び出し、毎回update()(計算)とdraw()(お絵かき)の2つを行います。早送りのときはupdate()gameSpeed回まとめて動かすので、絵はそのままで時間だけが速く進みます。

update()の中ではたくさんのことが起きています。タワーは射程内の敵をさがし、いちばんゴールに近い敵をねらってbullets配列に弾を1発追加します。Math.atan2()で砲身を敵の方へ向けているのも芸が細かいところです。弾が敵に当たるとhitEnemy()がHPを減らし、HPが0になると敵を消してゴールドとスコアを増やします。splash(ばくはつ)タワーは当たった敵のまわりにもダメージを与え、slow(スロウ)タワーは敵のspeedを一時的に遅くします。particlesはヒットや撃破のときに飛び散る小さな粒の演出です。

敵の波はstartWave()が管理します。ウェーブが進むほど敵のHPと数が増え、5の倍数のウェーブには大きくて固いボスが出てきます。TOWER_TYPESには6種類のタワーの値段・射程・攻撃力・連射速度がまとめて書いてあり、ここを変えるだけでゲームバランスを調整できます。ベストスコアはlocalStorageに保存されます。

game.js
// ============================================
// タワーディフェンス ゲームロジック
// ============================================

(function () {
  'use strict';

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

  // ============================================
  // マップ定義 (0=空, 1=道, 2=スタート, 3=ゴール)
  // ============================================
  var MAP_COLS = 12;
  var MAP_ROWS = 9;
  var MAP = [
    [0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0],
    [0, 0, 1, 0, 0, 1, 1, 1, 1, 1, 0, 0],
    [0, 0, 1, 0, 0, 1, 0, 0, 0, 1, 0, 0],
    [0, 0, 1, 1, 1, 1, 0, 0, 0, 1, 0, 0],
    [0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0],
    [0, 0, 1, 1, 1, 1, 1, 0, 0, 1, 0, 0],
    [0, 0, 1, 0, 0, 0, 1, 0, 0, 1, 0, 0],
    [0, 0, 1, 0, 0, 0, 1, 1, 1, 1, 0, 0],
    [0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0]
  ];

  var PATH = [
    {r:0,c:2},{r:1,c:2},{r:2,c:2},{r:3,c:2},{r:3,c:3},{r:3,c:4},{r:3,c:5},
    {r:2,c:5},{r:1,c:5},{r:1,c:6},{r:1,c:7},{r:1,c:8},{r:1,c:9},
    {r:2,c:9},{r:3,c:9},{r:4,c:9},{r:5,c:9},{r:6,c:9},{r:7,c:9},
    {r:7,c:8},{r:7,c:7},{r:7,c:6},{r:6,c:6},{r:5,c:6},{r:5,c:5},{r:5,c:4},{r:5,c:3},{r:5,c:2},
    {r:6,c:2},{r:7,c:2},{r:8,c:2}
  ];

  // ============================================
  // タワー定義(6種類)
  // ============================================
  var TOWER_TYPES = {
    normal: { cost: 20, range: 2.2, damage: 1,   fireRate: 50,  color: '#3498DB', bulletColor: '#85C1E9', name: 'ノーマル', splash: 0, slow: 0 },
    fast:   { cost: 35, range: 1.8, damage: 0.6,  fireRate: 18,  color: '#2ECC71', bulletColor: '#ABEBC6', name: 'はやい',   splash: 0, slow: 0 },
    strong: { cost: 50, range: 2.5, damage: 3.5,  fireRate: 90,  color: '#E74C3C', bulletColor: '#F5B7B1', name: 'つよい',   splash: 0, slow: 0 },
    slow:   { cost: 30, range: 2.0, damage: 0.3,  fireRate: 40,  color: '#9B59B6', bulletColor: '#D2B4DE', name: 'スロウ',   splash: 0, slow: 0.5 },
    splash: { cost: 60, range: 2.0, damage: 1.5,  fireRate: 70,  color: '#F39C12', bulletColor: '#F9E79F', name: 'ばくはつ', splash: 1.2, slow: 0 },
    sniper: { cost: 75, range: 4.0, damage: 8,    fireRate: 140, color: '#1ABC9C', bulletColor: '#A3E4D7', name: 'スナイパー', splash: 0, slow: 0 }
  };

  // ============================================
  // ゲーム定数
  // ============================================
  var INITIAL_GOLD = 100;
  var INITIAL_LIVES = 10;
  var GOLD_PER_KILL_BASE = 5;
  var ENEMY_BASE_HP = 3;
  var ENEMY_BASE_SPEED = 0.015;
  var ENEMIES_PER_WAVE_BASE = 6;
  var SPAWN_DELAY = 30;

  // ============================================
  // 状態変数
  // ============================================
  var canvas, ctx;
  var canvasW, canvasH, cellW, cellH;
  var towers = [];
  var enemies = [];
  var bullets = [];
  var particles = [];
  var gold, lives, wave, score;
  var bestScore = parseInt(localStorage.getItem('bestTowerDefense') || '0', 10);
  var gameRunning = false;
  var gameOverFlag = false;
  var animFrameId = null;
  var selectedTower = 'normal';
  var waveInProgress = false;
  var enemiesToSpawn = 0;
  var spawnTimer = 0;
  var waveDelay = 0;
  var frameCount = 0;
  var gameSpeed = 1;

  // ============================================
  // DOM要素
  // ============================================
  var scoreEl = document.getElementById('score');
  var bestScoreEl = document.getElementById('best-score');
  var waveEl = document.getElementById('wave');
  var livesEl = document.getElementById('lives');
  var goldEl = document.getElementById('gold');
  var gameOverOverlay = document.getElementById('game-over-overlay');
  var gameStartOverlay = document.getElementById('game-start-overlay');
  var finalScoreEl = document.getElementById('final-score');
  var bestResultEl = document.getElementById('best-result');

  canvas = document.getElementById('game-canvas');
  ctx = canvas.getContext('2d');

  // ============================================
  // キャンバス
  // ============================================
  function resizeCanvas() {
    var wrapper = canvas.parentElement;
    var rect = wrapper.getBoundingClientRect();
    var dpr = window.devicePixelRatio || 1;

    canvasW = rect.width;
    canvasH = rect.height;
    cellW = canvasW / MAP_COLS;
    cellH = canvasH / MAP_ROWS;

    canvas.width = canvasW * dpr;
    canvas.height = canvasH * dpr;
    ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
  }

  // ============================================
  // ユーティリティ
  // ============================================
  function cellCenter(r, c) {
    return { x: (c + 0.5) * cellW, y: (r + 0.5) * cellH };
  }

  function dist(x1, y1, x2, y2) {
    var dx = x1 - x2;
    var dy = y1 - y2;
    return Math.sqrt(dx * dx + dy * dy);
  }

  function findTowerAt(r, c) {
    for (var i = 0; i < towers.length; i++) {
      if (towers[i].r === r && towers[i].c === c) return i;
    }
    return -1;
  }

  // ============================================
  // 敵の生成
  // ============================================
  function createEnemy() {
    var hp = ENEMY_BASE_HP + wave * 1.5;
    var speed = ENEMY_BASE_SPEED + wave * 0.001;
    var start = cellCenter(PATH[0].r, PATH[0].c);

    var isBoss = (wave % 5 === 0) && enemiesToSpawn === 1;
    if (isBoss) {
      hp *= 5;
      speed *= 0.6;
    }

    return {
      x: start.x,
      y: start.y,
      pathIndex: 0,
      hp: hp,
      maxHp: hp,
      speed: speed,
      baseSpeed: speed,
      radius: isBoss ? Math.min(cellW, cellH) * 0.4 : Math.min(cellW, cellH) * 0.25,
      isBoss: isBoss,
      color: isBoss ? '#8E44AD' : '#E67E22',
      slowTimer: 0
    };
  }

  // ============================================
  // タワー設置・撤去
  // ============================================
  function placeTower(r, c) {
    if (r < 0 || r >= MAP_ROWS || c < 0 || c >= MAP_COLS) return;

    // 撤去モード
    if (selectedTower === 'sell') {
      var idx = findTowerAt(r, c);
      if (idx !== -1) {
        var refund = Math.floor(TOWER_TYPES[towers[idx].type].cost / 2);
        gold += refund;
        goldEl.textContent = gold;
        towers.splice(idx, 1);
      }
      return;
    }

    if (MAP[r][c] !== 0) return;
    if (findTowerAt(r, c) !== -1) return;

    var type = TOWER_TYPES[selectedTower];
    if (gold < type.cost) return;

    gold -= type.cost;
    goldEl.textContent = gold;

    var pos = cellCenter(r, c);
    towers.push({
      r: r,
      c: c,
      x: pos.x,
      y: pos.y,
      type: selectedTower,
      range: type.range * cellW,
      damage: type.damage,
      fireRate: type.fireRate,
      color: type.color,
      bulletColor: type.bulletColor,
      splash: type.splash,
      slow: type.slow,
      cooldown: 0,
      angle: 0
    });
  }

  // ============================================
  // 更新
  // ============================================
  function update() {
    if (!gameRunning || gameOverFlag) return;

    frameCount++;

    // ウェーブ間の待ち
    if (waveDelay > 0) {
      waveDelay--;
      if (waveDelay === 0) {
        startWave();
      }
      updateParticles();
      return;
    }

    // 敵のスポーン
    if (enemiesToSpawn > 0) {
      spawnTimer--;
      if (spawnTimer <= 0) {
        enemies.push(createEnemy());
        enemiesToSpawn--;
        spawnTimer = SPAWN_DELAY;
      }
    }

    // 敵の移動
    for (var i = enemies.length - 1; i >= 0; i--) {
      var e = enemies[i];

      // スロウタイマー
      if (e.slowTimer > 0) {
        e.slowTimer--;
        if (e.slowTimer <= 0) {
          e.speed = e.baseSpeed;
        }
      }

      if (e.pathIndex >= PATH.length - 1) {
        lives--;
        livesEl.textContent = lives;
        enemies.splice(i, 1);
        if (lives <= 0) {
          gameOver();
          return;
        }
        continue;
      }

      var target = cellCenter(PATH[e.pathIndex + 1].r, PATH[e.pathIndex + 1].c);
      var dx = target.x - e.x;
      var dy = target.y - e.y;
      var d = Math.sqrt(dx * dx + dy * dy);

      if (d < 2) {
        e.pathIndex++;
        e.x = target.x;
        e.y = target.y;
      } else {
        var moveSpeed = e.speed * cellW;
        e.x += (dx / d) * moveSpeed;
        e.y += (dy / d) * moveSpeed;
      }
    }

    // タワーの攻撃
    for (var t = 0; t < towers.length; t++) {
      var tower = towers[t];
      tower.cooldown--;

      var bestTarget = null;
      var bestProgress = -1;
      for (var ei = 0; ei < enemies.length; ei++) {
        var en = enemies[ei];
        var d = dist(tower.x, tower.y, en.x, en.y);
        if (d <= tower.range && en.pathIndex > bestProgress) {
          bestProgress = en.pathIndex;
          bestTarget = en;
        }
      }

      if (bestTarget) {
        tower.angle = Math.atan2(bestTarget.y - tower.y, bestTarget.x - tower.x);

        if (tower.cooldown <= 0) {
          tower.cooldown = tower.fireRate;
          bullets.push({
            x: tower.x,
            y: tower.y,
            tx: bestTarget.x,
            ty: bestTarget.y,
            target: bestTarget,
            damage: tower.damage,
            splash: tower.splash * cellW,
            slow: tower.slow,
            speed: 5,
            color: tower.bulletColor
          });
        }
      }
    }

    // 弾の移動
    for (var bi = bullets.length - 1; bi >= 0; bi--) {
      var b = bullets[bi];
      var tx = b.target ? b.target.x : b.tx;
      var ty = b.target ? b.target.y : b.ty;
      var dx = tx - b.x;
      var dy = ty - b.y;
      var d = Math.sqrt(dx * dx + dy * dy);

      if (d < 5) {
        if (b.target && b.target.hp > 0) {
          hitEnemy(b.target, b);

          // スプラッシュダメージ
          if (b.splash > 0) {
            for (var si = enemies.length - 1; si >= 0; si--) {
              if (enemies[si] === b.target) continue;
              if (dist(b.target.x, b.target.y, enemies[si].x, enemies[si].y) < b.splash) {
                hitEnemy(enemies[si], { damage: b.damage * 0.5, slow: b.slow, color: b.color, splash: 0 });
              }
            }
            // スプラッシュエフェクト
            for (var sp = 0; sp < 12; sp++) {
              var angle = (sp / 12) * Math.PI * 2;
              particles.push({
                x: b.target.x, y: b.target.y,
                vx: Math.cos(angle) * 3,
                vy: Math.sin(angle) * 3,
                life: 15,
                color: '#F39C12'
              });
            }
          }
        }
        bullets.splice(bi, 1);
      } else {
        b.x += (dx / d) * b.speed;
        b.y += (dy / d) * b.speed;
      }
    }

    updateParticles();

    // ウェーブ完了チェック
    if (waveInProgress && enemiesToSpawn === 0 && enemies.length === 0) {
      waveInProgress = false;
      wave++;
      waveEl.textContent = wave;
      waveDelay = 90;
    }
  }

  function hitEnemy(enemy, bullet) {
    enemy.hp -= bullet.damage;

    // スロウ適用
    if (bullet.slow > 0) {
      enemy.speed = enemy.baseSpeed * (1 - bullet.slow);
      enemy.slowTimer = 90;
    }

    // ヒットパーティクル
    for (var pi = 0; pi < 3; pi++) {
      particles.push({
        x: enemy.x, y: enemy.y,
        vx: (Math.random() - 0.5) * 3,
        vy: (Math.random() - 0.5) * 3,
        life: 15,
        color: bullet.color
      });
    }

    if (enemy.hp <= 0) {
      var killGold = GOLD_PER_KILL_BASE + Math.floor(wave * 0.5);
      if (enemy.isBoss) killGold *= 3;
      gold += killGold;
      goldEl.textContent = gold;
      var killScore = enemy.isBoss ? 50 + wave * 5 : 10 + wave;
      score += killScore;
      updateScore();

      for (var pi2 = 0; pi2 < 8; pi2++) {
        particles.push({
          x: enemy.x, y: enemy.y,
          vx: (Math.random() - 0.5) * 5,
          vy: (Math.random() - 0.5) * 5,
          life: 25,
          color: enemy.color
        });
      }

      var idx = enemies.indexOf(enemy);
      if (idx !== -1) enemies.splice(idx, 1);
    }
  }

  function updateParticles() {
    for (var i = particles.length - 1; i >= 0; i--) {
      var p = particles[i];
      p.x += p.vx;
      p.y += p.vy;
      p.life--;
      if (p.life <= 0) particles.splice(i, 1);
    }
  }

  function startWave() {
    waveInProgress = true;
    enemiesToSpawn = ENEMIES_PER_WAVE_BASE + Math.floor(wave * 1.5);
    spawnTimer = 0;
  }

  // ============================================
  // 描画
  // ============================================
  function draw() {
    ctx.clearRect(0, 0, canvasW, canvasH);

    drawMap();
    drawTowers();
    drawEnemies();
    drawBullets();
    drawParticles();

    if (waveDelay > 0 && gameRunning) {
      ctx.save();
      ctx.fillStyle = '#fff';
      ctx.font = 'bold ' + (cellH * 0.6) + 'px sans-serif';
      ctx.textAlign = 'center';
      ctx.textBaseline = 'middle';
      ctx.globalAlpha = 0.6;
      ctx.fillText('WAVE ' + wave, canvasW / 2, canvasH / 2);
      ctx.restore();
    }

    // 早送り表示
    if (gameSpeed > 1 && gameRunning) {
      ctx.save();
      ctx.fillStyle = '#F1C40F';
      ctx.font = 'bold ' + (cellH * 0.35) + 'px sans-serif';
      ctx.textAlign = 'right';
      ctx.textBaseline = 'top';
      ctx.globalAlpha = 0.5;
      ctx.fillText('x' + gameSpeed, canvasW - 6, 4);
      ctx.restore();
    }
  }

  function drawMap() {
    for (var r = 0; r < MAP_ROWS; r++) {
      for (var c = 0; c < MAP_COLS; c++) {
        var x = c * cellW;
        var y = r * cellH;
        var tile = MAP[r][c];

        if (tile === 0) {
          ctx.fillStyle = '#2d5a27';
          ctx.fillRect(x, y, cellW, cellH);
          ctx.fillStyle = 'rgba(0,0,0,0.08)';
          if ((r + c) % 2 === 0) ctx.fillRect(x, y, cellW, cellH);
        } else if (tile === 1) {
          ctx.fillStyle = '#c4a45a';
          ctx.fillRect(x, y, cellW, cellH);
          ctx.fillStyle = 'rgba(0,0,0,0.05)';
          if ((r + c) % 2 === 0) ctx.fillRect(x, y, cellW, cellH);
        } else if (tile === 2) {
          ctx.fillStyle = '#3498DB';
          ctx.fillRect(x, y, cellW, cellH);
          ctx.fillStyle = '#fff';
          ctx.font = 'bold ' + (cellH * 0.35) + 'px sans-serif';
          ctx.textAlign = 'center';
          ctx.textBaseline = 'middle';
          ctx.fillText('START', x + cellW / 2, y + cellH / 2);
        } else if (tile === 3) {
          ctx.fillStyle = '#E74C3C';
          ctx.fillRect(x, y, cellW, cellH);
          ctx.fillStyle = '#fff';
          ctx.font = 'bold ' + (cellH * 0.35) + 'px sans-serif';
          ctx.textAlign = 'center';
          ctx.textBaseline = 'middle';
          ctx.fillText('GOAL', x + cellW / 2, y + cellH / 2);
        }

        ctx.strokeStyle = 'rgba(0,0,0,0.15)';
        ctx.lineWidth = 0.5;
        ctx.strokeRect(x, y, cellW, cellH);
      }
    }

    // 撤去モード中はタワーに×マークを表示
    if (selectedTower === 'sell') {
      for (var i = 0; i < towers.length; i++) {
        var t = towers[i];
        ctx.save();
        ctx.fillStyle = 'rgba(231, 76, 60, 0.3)';
        ctx.fillRect(t.x - cellW / 2, t.y - cellH / 2, cellW, cellH);
        ctx.strokeStyle = '#E74C3C';
        ctx.lineWidth = 2;
        ctx.globalAlpha = 0.7;
        var s = Math.min(cellW, cellH) * 0.2;
        ctx.beginPath();
        ctx.moveTo(t.x - s, t.y - s);
        ctx.lineTo(t.x + s, t.y + s);
        ctx.moveTo(t.x + s, t.y - s);
        ctx.lineTo(t.x - s, t.y + s);
        ctx.stroke();
        ctx.restore();
      }
    }
  }

  function drawTowers() {
    for (var i = 0; i < towers.length; i++) {
      var t = towers[i];

      // 台座
      ctx.fillStyle = '#555';
      ctx.fillRect(t.x - cellW * 0.35, t.y - cellH * 0.35, cellW * 0.7, cellH * 0.7);

      // タワー本体
      ctx.fillStyle = t.color;
      ctx.beginPath();
      ctx.arc(t.x, t.y, Math.min(cellW, cellH) * 0.28, 0, Math.PI * 2);
      ctx.fill();

      // 砲身
      ctx.save();
      ctx.translate(t.x, t.y);
      ctx.rotate(t.angle);
      ctx.fillStyle = '#333';
      var barrelLen = t.type === 'sniper' ? 0.45 : 0.35;
      ctx.fillRect(0, -2, Math.min(cellW, cellH) * barrelLen, 4);
      ctx.restore();

      // レンジ
      ctx.beginPath();
      ctx.arc(t.x, t.y, t.range, 0, Math.PI * 2);
      ctx.strokeStyle = t.color;
      ctx.globalAlpha = 0.08;
      ctx.lineWidth = 1;
      ctx.stroke();
      ctx.globalAlpha = 1;
    }
  }

  function drawEnemies() {
    for (var i = 0; i < enemies.length; i++) {
      var e = enemies[i];

      ctx.beginPath();
      ctx.arc(e.x, e.y, e.radius, 0, Math.PI * 2);
      ctx.fillStyle = e.color;
      ctx.fill();
      ctx.strokeStyle = 'rgba(0,0,0,0.3)';
      ctx.lineWidth = 1;
      ctx.stroke();

      // スロウ中の表示
      if (e.slowTimer > 0) {
        ctx.beginPath();
        ctx.arc(e.x, e.y, e.radius + 2, 0, Math.PI * 2);
        ctx.strokeStyle = '#9B59B6';
        ctx.lineWidth = 1.5;
        ctx.globalAlpha = 0.6;
        ctx.stroke();
        ctx.globalAlpha = 1;
      }

      if (e.isBoss) {
        ctx.fillStyle = '#fff';
        ctx.font = 'bold ' + (e.radius * 0.8) + 'px sans-serif';
        ctx.textAlign = 'center';
        ctx.textBaseline = 'middle';
        ctx.fillText('B', e.x, e.y);
      }

      // HPバー
      var barW = e.radius * 2;
      var barH = 3;
      var barX = e.x - barW / 2;
      var barY = e.y - e.radius - 6;
      ctx.fillStyle = 'rgba(0,0,0,0.4)';
      ctx.fillRect(barX, barY, barW, barH);
      var hpRatio = Math.max(0, e.hp / e.maxHp);
      ctx.fillStyle = hpRatio > 0.5 ? '#2ECC71' : hpRatio > 0.25 ? '#F1C40F' : '#E74C3C';
      ctx.fillRect(barX, barY, barW * hpRatio, barH);
    }
  }

  function drawBullets() {
    for (var i = 0; i < bullets.length; i++) {
      var b = bullets[i];
      ctx.beginPath();
      ctx.arc(b.x, b.y, b.splash > 0 ? 4 : 3, 0, Math.PI * 2);
      ctx.fillStyle = b.color;
      ctx.fill();
    }
  }

  function drawParticles() {
    for (var i = 0; i < particles.length; i++) {
      var p = particles[i];
      ctx.beginPath();
      ctx.arc(p.x, p.y, 2, 0, Math.PI * 2);
      ctx.fillStyle = p.color;
      ctx.globalAlpha = p.life / 25;
      ctx.fill();
      ctx.globalAlpha = 1;
    }
  }

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

  // ============================================
  // ゲーム開始
  // ============================================
  function startGame() {
    resizeCanvas();
    towers = [];
    enemies = [];
    bullets = [];
    particles = [];
    gold = INITIAL_GOLD;
    lives = INITIAL_LIVES;
    wave = 1;
    score = 0;
    gameOverFlag = false;
    waveInProgress = false;
    enemiesToSpawn = 0;
    frameCount = 0;
    gameSpeed = 1;

    scoreEl.textContent = score;
    goldEl.textContent = gold;
    livesEl.textContent = lives;
    waveEl.textContent = wave;

    // 速度ボタンリセット
    var speedBtns = document.querySelectorAll('.speed-btn');
    for (var i = 0; i < speedBtns.length; i++) {
      speedBtns[i].classList.remove('selected');
    }
    document.getElementById('btn-speed-1').classList.add('selected');

    gameOverOverlay.classList.remove('active');
    gameStartOverlay.classList.remove('active');

    gameRunning = true;
    waveDelay = 60;

    if (animFrameId) cancelAnimationFrame(animFrameId);
    gameLoop();
  }

  // ============================================
  // ゲームオーバー
  // ============================================
  function gameOver() {
    gameRunning = false;
    gameOverFlag = true;

    if (animFrameId) {
      cancelAnimationFrame(animFrameId);
      animFrameId = null;
    }

    finalScoreEl.textContent = score;
    if (score >= bestScore && score > 0) {
      bestResultEl.textContent = TEXT.newBest || 'ハイスコア更新!';
    } else {
      bestResultEl.textContent = (TEXT.best || 'ベスト: ') + bestScore;
    }

    setTimeout(function () {
      gameOverOverlay.classList.add('active');
      if (window.Leaderboard) Leaderboard.show('tower-defense', score);
    }, 300);
  }

  // ============================================
  // ゲームループ(早送り対応)
  // ============================================
  function gameLoop() {
    for (var s = 0; s < gameSpeed; s++) {
      update();
      if (!gameRunning || gameOverFlag) break;
    }
    draw();
    if (gameRunning && !gameOverFlag) {
      animFrameId = requestAnimationFrame(gameLoop);
    }
  }

  // ============================================
  // 入力処理
  // ============================================
  function handleClick(px, py) {
    if (!gameRunning) return;
    var c = Math.floor(px / cellW);
    var r = Math.floor(py / cellH);
    placeTower(r, c);
  }

  canvas.addEventListener('click', function (e) {
    var rect = canvas.getBoundingClientRect();
    handleClick(e.clientX - rect.left, e.clientY - rect.top);
  });

  canvas.addEventListener('touchstart', function (e) {
    e.preventDefault();
    if (!gameRunning) return;
    var rect = canvas.getBoundingClientRect();
    handleClick(e.touches[0].clientX - rect.left, e.touches[0].clientY - rect.top);
  }, { passive: false });

  // タワー選択ボタン
  var towerBtns = document.querySelectorAll('.tower-btn');
  for (var i = 0; i < towerBtns.length; i++) {
    towerBtns[i].addEventListener('click', function () {
      for (var j = 0; j < towerBtns.length; j++) {
        towerBtns[j].classList.remove('selected');
      }
      this.classList.add('selected');
      selectedTower = this.getAttribute('data-tower');
    });
  }

  // 速度ボタン
  var speedBtns = document.querySelectorAll('.speed-btn');
  for (var si = 0; si < speedBtns.length; si++) {
    speedBtns[si].addEventListener('click', function () {
      for (var j = 0; j < speedBtns.length; j++) {
        speedBtns[j].classList.remove('selected');
      }
      this.classList.add('selected');
      gameSpeed = parseInt(this.getAttribute('data-speed'), 10);
    });
  }

  document.getElementById('btn-new-game').addEventListener('click', function () { startGame(); });
  document.getElementById('btn-retry').addEventListener('click', function () { startGame(); });
  document.getElementById('btn-start').addEventListener('click', function () { startGame(); });

  var resizeTimer;
  window.addEventListener('resize', function () {
    clearTimeout(resizeTimer);
    resizeTimer = setTimeout(function () {
      resizeCanvas();
      for (var i = 0; i < towers.length; i++) {
        var pos = cellCenter(towers[i].r, towers[i].c);
        towers[i].x = pos.x;
        towers[i].y = pos.y;
        towers[i].range = TOWER_TYPES[towers[i].type].range * cellW;
      }
      if (!gameRunning) draw();
    }, 100);
  });

  document.addEventListener('visibilitychange', function () {
    if (document.hidden && gameRunning) {
      if (animFrameId) {
        cancelAnimationFrame(animFrameId);
        animFrameId = null;
      }
      gameRunning = false;
    }
  });

  // ============================================
  // 初期化
  // ============================================
  bestScoreEl.textContent = bestScore;
  resizeCanvas();
  draw();
})();

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

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

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

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

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

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

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

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

もっと学びたい人へ

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

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