謎の書斎からの脱出のソースコード全文 — JavaScript・HTML・CSS

▶ 謎の書斎からの脱出で遊ぶ 📖 作り方を1から学ぶ

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

このゲームは、5つのシーンを行き来して暗号を解き、書斎から脱出するポイント&クリック型のアドベンチャーです。コードの見どころは、5つの部屋とその中の調べられるものをすべてデータとして書き出している設計と、「アイテムを拾って別のものに使う」というインベントリのしくみ。画像を1枚も使わず、背景をすべてSVGで描いているのも面白いところです。

コードはコピーして自分のパソコンで自由に動かせます。暗号や謎を自分で考えて差しかえれば、オリジナルの脱出ゲームが作れます。

謎の書斎からの脱出は3つのファイルでできている

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

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

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

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

index.htmlは、ブラウザが最初に読み込むファイルです。ここにはゲームの「枠」をHTMLで用意しているだけで、部屋の中身や謎の動きは書かれていません。

大きく分けると、(1)部屋の絵を映すscene-area、(2)状況を伝えるmessage-bar、(3)拾ったアイテムを並べるinventory-bar、(4)となりの部屋へ移動するnav-buttons、(5)番号錠などを表示するmodal-overlay、でできています。

部屋の背景や調べられるオブジェクト、アイテムのマスは、ここには1つも書かれていません。すべてJavaScriptが今いるシーンに合わせて作って入れています。

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

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="謎の書斎からの脱出!部屋に散らばるヒントを組み合わせて暗号を解読し、5つのシーンを攻略して脱出せよ。PC・スマホ対応の無料ブラウザゲーム。">
  <link rel="canonical" href="https://hinata-ya.tech/games/games/escape/">
  <!-- OGP -->
  <meta property="og:title" content="謎の書斎からの脱出|無料ブラウザゲーム|ひなテックGames">
  <meta property="og:description" content="謎の書斎からの脱出!部屋に散らばるヒントを組み合わせて暗号を解読し、5つのシーンを攻略して脱出せよ。PC・スマホ対応の無料ブラウザゲーム。">
  <meta property="og:type" content="website">
  <meta property="og:url" content="https://hinata-ya.tech/games/games/escape/">
  <meta property="og:site_name" content="ひなテックGames">
  <meta property="og:locale" content="ja_JP">
  <!-- 構造化データ -->
  <script type="application/ld+json">
  {
    "@context": "https://schema.org",
    "@type": "Game",
    "name": "謎の書斎からの脱出",
    "description": "書斎に閉じ込められた主人公が、部屋に隠されたヒントを組み合わせて謎を解き、脱出を目指すポイント&クリック型アドベンチャーゲーム",
    "url": "https://hinata-ya.tech/games/games/escape/",
    "genre": ["アドベンチャー", "謎解き", "脱出"],
    "gamePlatform": "Web Browser",
    "publisher": {
      "@type": "Organization",
      "name": "ひなテック",
      "url": "https://hinata-ya.tech"
    }
  }
  </script>
  <link rel="stylesheet" href="../../css/style.css">
  <link rel="stylesheet" href="style.css">
</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>

    <div class="game-container">
      <!-- ゲームメインエリア -->
      <div class="escape-wrapper" id="escape-wrapper">

        <!-- シーン表示エリア -->
        <div class="scene-area" id="scene-area">
          <!-- シーン背景とオブジェクトはJSで動的生成 -->
        </div>

        <!-- メッセージバー -->
        <div class="message-bar" id="message-bar">
          <span id="message-text">クリックして調べよう</span>
        </div>

        <!-- インベントリ -->
        <div class="inventory-bar" id="inventory-bar">
          <div class="inventory-label">所持品</div>
          <div class="inventory-slots" id="inventory-slots">
            <!-- アイテムスロットはJSで生成 -->
          </div>
        </div>

        <!-- 移動ボタン -->
        <div class="nav-buttons" id="nav-buttons">
          <button class="nav-btn" id="btn-left" title="左へ">&#8592;</button>
          <div class="scene-indicator" id="scene-indicator">1 / 5</div>
          <button class="nav-btn" id="btn-right" title="右へ">&#8594;</button>
        </div>

        <!-- モーダル(番号錠・テキスト調査など) -->
        <div class="modal-overlay" id="modal-overlay">
          <div class="modal-box" id="modal-box">
            <button class="modal-close" id="modal-close">&#10005;</button>
            <div class="modal-content" id="modal-content"></div>
          </div>
        </div>

        <!-- スタートオーバーレイ -->
        <div class="game-overlay active" id="game-start-overlay">
          <div class="overlay-content">
            <h2>&#128274; 謎の書斎からの脱出</h2>
            <p>古びた書斎に閉じ込められた。<br>部屋に隠されたヒントを組み合わせて<br>謎を解き、脱出せよ!</p>
            <p class="hint-note">&#128161; ヒント:シーン間を行き来して情報を集めよう</p>
            <button class="btn-primary" id="btn-start">ゲームスタート</button>
          </div>
        </div>

        <!-- クリアオーバーレイ -->
        <div class="game-overlay" id="game-clear-overlay">
          <div class="overlay-content">
            <h2>&#127881; 脱出成功!</h2>
            <p id="clear-time-text"></p>
            <p class="clear-msg">謎を全て解いて書斎から脱出した!</p>
            <button class="btn-primary" id="btn-retry">もう一度遊ぶ</button>
          </div>
        </div>

      </div><!-- /.escape-wrapper -->
    </div><!-- /.game-container -->

    <div class="game-instructions">
      <h3>遊び方</h3>
      <ul>
        <li>画面内のものをクリック(タップ)して調べよう</li>
        <li>アイテムを拾ったら下部のインベントリに表示される</li>
        <li>アイテムを選択した状態で別のものをクリックすると「使う」</li>
        <li>&#8592; &#8594; ボタンで隣のシーンに移動できる</li>
        <li>5つのシーンに隠されたヒントを組み合わせて謎を解こう</li>
        <li>一部のシーンは特定アイテムがないと入れない</li>
      </ul>
    </div>

    <div class="game-promo">
      <h3>このゲーム、自分でも作れるよ!</h3>
      <p>
        脱出ゲームはDOM操作と<br>
        ゲーム状態管理の良い練習になります。<br><br>
        ひなテックでは、こうしたゲームの<br>
        作り方を楽しく学べます!
      </p>
      <div class="promo-links">
        <a href="https://hinata-ya.tech/contact/" class="btn-primary">無料体験に申し込む →</a>
        <a href="https://github.com/mooosung/hinatech-games/tree/main/games/escape" 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=20260404" data-base="../../"></script>
  <script src="game.js"></script>
</body>
</html>

style.css — 見た目とふんいき作り

style.cssは、古びた書斎の重く落ちついた雰囲気を作っているファイルです。茶色や暗いオレンジを基調にして、薄暗い部屋の空気を表現しています。

注目してほしいのは.scene-objectの指定です。調べられるものにマウスを乗せると:hoverfilter: brightnessが効いて明るく光り、黄色いふちが付くので「これはクリックできる」とプレイヤーに伝わります。アイテムを選んでいるときは.selected-item-targetで青いふちに変わります。

暗い廊下の.flashlight-overlayはまっ黒な半透明の板で、懐中電灯を使うとJavaScriptがこれを取り除いて「照らした」演出を作ります。番号錠の数字は.lock-digitfont-family: monospaceを指定し、デジタル表示らしい見た目にしています。

style.css
/* ============================================
   謎の書斎からの脱出 - 固有スタイル
   ============================================ */

/* ゲームラッパー */
.escape-wrapper {
  position: relative;
  width: 100%;
  max-width: 560px;
  margin: 0 auto;
  background: #1a1208;
  border-radius: 8px;
  overflow: hidden;
  user-select: none;
}

/* シーンエリア */
.scene-area {
  position: relative;
  width: 100%;
  aspect-ratio: 4 / 3;
  overflow: hidden;
  cursor: default;
}

/* シーン背景 */
.scene-bg {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
}

/* クリッカブルオブジェクト */
.scene-object {
  position: absolute;
  cursor: pointer;
  border-radius: 4px;
  transition: filter 0.15s, outline 0.15s;
  display: flex;
  align-items: center;
  justify-content: center;
}

.scene-object:hover {
  filter: brightness(1.35);
  outline: 2px solid rgba(255, 220, 100, 0.7);
}

.scene-object.selected-item-target {
  outline: 2px solid rgba(100, 220, 255, 0.85);
  filter: brightness(1.2);
}

/* オブジェクトラベル(デバッグ用には非表示) */
.scene-object .obj-icon {
  font-size: 1.6em;
  pointer-events: none;
  line-height: 1;
}

/* メッセージバー */
.message-bar {
  background: #2a1e0e;
  border-top: 2px solid #5a3e1e;
  padding: 0.55rem 1rem;
  min-height: 2.4rem;
  display: flex;
  align-items: center;
}

#message-text {
  font-size: 0.88rem;
  color: #e8d5a0;
  line-height: 1.4;
}

/* インベントリ */
.inventory-bar {
  background: #231808;
  border-top: 2px solid #5a3e1e;
  padding: 0.5rem 0.75rem;
  display: flex;
  align-items: center;
  gap: 0.6rem;
  min-height: 3.5rem;
}

.inventory-label {
  font-size: 0.72rem;
  color: #8a7050;
  font-weight: 600;
  letter-spacing: 0.05em;
  white-space: nowrap;
  writing-mode: initial;
}

.inventory-slots {
  display: flex;
  gap: 0.4rem;
  flex-wrap: wrap;
}

.inventory-slot {
  width: 44px;
  height: 44px;
  background: #3a2810;
  border: 2px solid #5a3e1e;
  border-radius: 6px;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 1.4rem;
  cursor: pointer;
  transition: border-color 0.15s, background 0.15s;
  position: relative;
}

.inventory-slot:hover {
  border-color: #c8a050;
  background: #4a3418;
}

.inventory-slot.active {
  border-color: #64dcff;
  background: #1a3040;
  box-shadow: 0 0 8px rgba(100, 220, 255, 0.4);
}

.inventory-slot .slot-tooltip {
  position: absolute;
  bottom: calc(100% + 6px);
  left: 50%;
  transform: translateX(-50%);
  background: #1a1208;
  color: #e8d5a0;
  font-size: 0.7rem;
  padding: 3px 7px;
  border-radius: 4px;
  white-space: nowrap;
  pointer-events: none;
  opacity: 0;
  border: 1px solid #5a3e1e;
  transition: opacity 0.15s;
  z-index: 20;
}

.inventory-slot:hover .slot-tooltip {
  opacity: 1;
}

/* 移動ボタン */
.nav-buttons {
  background: #1a1208;
  border-top: 2px solid #3a2810;
  padding: 0.4rem 0.75rem;
  display: flex;
  align-items: center;
  justify-content: space-between;
}

.nav-btn {
  background: #3a2810;
  color: #c8a050;
  border: 2px solid #5a3e1e;
  border-radius: 6px;
  width: 40px;
  height: 36px;
  font-size: 1.1rem;
  cursor: pointer;
  transition: background 0.15s, border-color 0.15s;
}

.nav-btn:hover:not(:disabled) {
  background: #4a3820;
  border-color: #c8a050;
}

.nav-btn:disabled {
  opacity: 0.3;
  cursor: not-allowed;
}

.scene-indicator {
  font-size: 0.8rem;
  color: #8a7050;
}

/* モーダル */
.modal-overlay {
  position: absolute;
  inset: 0;
  background: rgba(10, 6, 2, 0.82);
  display: none;
  align-items: center;
  justify-content: center;
  z-index: 30;
}

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

.modal-box {
  background: #2a1e0e;
  border: 2px solid #8a6030;
  border-radius: 10px;
  padding: 1.25rem 1.5rem 1.5rem;
  width: 88%;
  max-width: 320px;
  position: relative;
}

.modal-close {
  position: absolute;
  top: 0.5rem;
  right: 0.6rem;
  background: none;
  border: none;
  color: #8a7050;
  font-size: 1rem;
  cursor: pointer;
  padding: 0.1rem 0.3rem;
}

.modal-close:hover {
  color: #e8d5a0;
}

.modal-content h3 {
  font-size: 1rem;
  color: #e8d5a0;
  margin-bottom: 0.75rem;
}

.modal-content p {
  font-size: 0.85rem;
  color: #c8b080;
  line-height: 1.6;
  margin-bottom: 0.75rem;
}

/* 番号錠UI */
.lock-display {
  display: flex;
  justify-content: center;
  gap: 0.4rem;
  margin: 0.75rem 0;
}

.lock-digit-group {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.25rem;
}

.lock-digit-btn {
  background: #3a2810;
  color: #e8d5a0;
  border: 1px solid #5a3e1e;
  border-radius: 4px;
  width: 28px;
  height: 22px;
  font-size: 0.75rem;
  cursor: pointer;
  line-height: 1;
  padding: 0;
}

.lock-digit-btn:hover {
  background: #4a3820;
  border-color: #c8a050;
}

.lock-digit {
  background: #1a1208;
  color: #ffdc64;
  border: 2px solid #5a3e1e;
  border-radius: 6px;
  width: 38px;
  height: 48px;
  font-size: 1.6rem;
  font-weight: 700;
  display: flex;
  align-items: center;
  justify-content: center;
  font-family: monospace;
}

.lock-submit {
  display: block;
  width: 100%;
  margin-top: 1rem;
  padding: 0.5rem;
  background: #6a4820;
  color: #e8d5a0;
  border: 2px solid #c8a050;
  border-radius: 6px;
  font-size: 0.9rem;
  font-weight: 600;
  cursor: pointer;
  transition: background 0.15s;
}

.lock-submit:hover {
  background: #8a6030;
}

/* テキスト入力(英単語錠) */
.word-input-area {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  margin: 0.75rem 0;
}

.word-input {
  background: #1a1208;
  color: #ffdc64;
  border: 2px solid #5a3e1e;
  border-radius: 6px;
  padding: 0.5rem 0.75rem;
  font-size: 1.1rem;
  font-family: monospace;
  text-align: center;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  width: 100%;
}

.word-input:focus {
  outline: none;
  border-color: #c8a050;
}

/* 懐中電灯演出(暗い廊下) */
.flashlight-overlay {
  position: absolute;
  inset: 0;
  pointer-events: none;
  z-index: 5;
}

/* ゲームオーバーレイ */
.game-overlay {
  position: absolute;
  inset: 0;
  background: rgba(10, 6, 2, 0.92);
  display: none;
  align-items: center;
  justify-content: center;
  z-index: 50;
  animation: overlay-appear 0.3s ease;
}

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

@keyframes overlay-appear {
  from { opacity: 0; }
  to   { opacity: 1; }
}

.overlay-content {
  text-align: center;
  padding: 1.5rem;
  max-width: 300px;
}

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

.overlay-content p {
  font-size: 0.95rem;
  color: #c8b080;
  line-height: 1.6;
  margin-bottom: 0.75rem;
}

.overlay-content .hint-note {
  font-size: 0.82rem;
  color: #8a7050;
  margin-bottom: 1.25rem;
}

.overlay-content .clear-msg {
  color: #ffdc64;
  font-weight: 600;
}

/* レスポンシブ */
@media (max-width: 480px) {
  .inventory-slot {
    width: 38px;
    height: 38px;
    font-size: 1.2rem;
  }

  .lock-digit {
    width: 32px;
    height: 40px;
    font-size: 1.3rem;
  }

  .lock-digit-btn {
    width: 24px;
    height: 20px;
    font-size: 0.7rem;
  }

  .modal-box {
    padding: 1rem 1.1rem 1.25rem;
  }
}

game.js — 謎解きを動かす頭脳

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

このゲームの設計の中心はSCENESという配列です。5つの部屋が1つずつオブジェクトになっていて、その中のobjectsにはその部屋で調べられるもの(本棚・机・金庫など)が、位置(x, y, w, h)とクリックしたときの動き(onExamine)をセットにして並んでいます。renderScene()は、今いる部屋のデータを読んで画面に部品を並べ直す関数。部屋を移動するたびにこれが呼ばれて、画面がまるごと作り直されます。

ゲームの進み具合はstateというオブジェクトが覚えています。今いる部屋(scene)、持ち物(inventory)、そしてflagsという「引き出しを開けたか」「金庫を開けたか」といったオン・オフの記録です。このflagsのおかげで、一度解いた謎は解けたままになり、まだ解いていない場所には進めない、といった判定ができます。

このゲームならではの工夫がアイテムを「使う」しくみです。handleObjectClick()は、アイテムを選んでいるかどうかで動きを変えます。何も選んでいなければ「調べる」(onExamine)、アイテムを選んでいれば「使う」(onUseWith)。たとえば暗い廊下の壁に懐中電灯を使うと、onUseWithが反応して隠し文字が浮かび上がります。

番号錠はshowSafeLock()showDoorLock()が作ります。▲▼ボタンで数字を回す部分は(current[i] + dir + 10) % 10という計算がポイント。+ 10% 10を組み合わせることで、9の次は0、0の前は9というふうに数字がくるりとループします。入力した数字をjoin('')でつなげ、正解のSAFE_CODEなどと比べて開錠を判定します。

背景の絵に画像ファイルは1枚も使っていません。drawStudy()drawFireplace()といった関数がdocument.createElementNSを使ってSVGの図形(四角や丸)をプログラムで描いているのです。クリアまでの時間はDate.now()でスタートからの差を計算しています。

game.js
/* ============================================
   謎の書斎からの脱出 - ゲームロジック
   ============================================
   【謎解きの流れ】
   シーン1(書斎全体): 本棚の5冊を厚さ順に並べると英単語 "LIGHT" が浮かぶ
   シーン2(机クローズアップ): "LIGHT" で引き出し開錠 → 星座図とページメモ入手
   シーン3(暖炉・壁): 本棚の該当ページ(p.47)を調べる → 星座記号→数字変換 → 金庫(4桁)開錠 → 懐中電灯入手
   シーン4(廊下・暗い): 懐中電灯で壁の隠し文字を照らす → 6桁番号を得る
   シーン5(玄関ドア): 6桁番号を逆から入力 → 脱出!
   ============================================ */

(function () {
  'use strict';

  // ---- 定数 ----
  var CORRECT_WORD    = 'LIGHT';       // 机の引き出しの合言葉
  var SAFE_CODE       = '4729';        // 暖炉の金庫4桁
  var HIDDEN_NUMBERS  = '839156';      // 廊下の隠し文字(懐中電灯で照らす)
  var DOOR_CODE       = '651938';      // 玄関ドア6桁(HIDDEN_NUMBERS を逆順)

  // ---- ゲーム状態 ----
  var state = {
    scene: 0,               // 現在シーン (0〜4)
    inventory: [],          // 所持アイテム ID リスト
    selectedItem: null,     // 選択中アイテム ID
    flags: {
      drawerOpen: false,    // 引き出し開いた
      safeOpen: false,      // 金庫開いた
      hallLit: false,       // 廊下を照らした
      doorUnlocked: false,  // ドア解錠済み
    },
    startTime: null,
    cleared: false,
  };

  // ---- シーン定義 ----
  // bg: 背景SVGのインライン文字列か色
  // objects: クリッカブルオブジェクト配列
  //   { id, icon, label, x, y, w, h, onExamine, onUseWith }
  var SCENES = [
    /* ---- シーン0: 書斎全体 ---- */
    {
      name: '書斎',
      bgColor: '#2b1d0e',
      bgDeco: drawStudy,
      objects: [
        {
          id: 'bookshelf',
          icon: '📚',
          label: '本棚',
          x: 5, y: 10, w: 30, h: 65,
          onExamine: function () {
            showModal(
              '本棚',
              '5冊の本が並んでいる。<br>' +
              'それぞれの背表紙に1文字ずつ刻まれている。<br><br>' +
              '背の厚さは左から:<strong>薄・厚・中・薄・中</strong><br><br>' +
              '薄い順に並べると……<br>' +
              '&nbsp;&nbsp;[<span style="color:#ffdc64">薄1</span>] [<span style="color:#ffdc64">薄2</span>] [<span style="color:#aaffaa">中1</span>] [<span style="color:#aaffaa">中2</span>] [<span style="color:#ff9977">厚</span>]<br><br>' +
              '背表紙の文字(薄い順):<strong style="color:#ffdc64;font-size:1.1em;letter-spacing:0.15em">L &nbsp;I &nbsp;G &nbsp;H &nbsp;T</strong><br><br>' +
              '<small style="color:#8a7050">「机の引き出しに使えそうだ」</small>',
              null
            );
          }
        },
        {
          id: 'desk_far',
          icon: '🪑',
          label: '机(遠景)',
          x: 38, y: 40, w: 35, h: 45,
          onExamine: function () {
            setMessage('机に近づいてみよう。→ボタンで次のシーンへ。');
          }
        },
        {
          id: 'window',
          icon: '🪟',
          label: '窓',
          x: 68, y: 10, w: 26, h: 35,
          onExamine: function () {
            setMessage('窓は固く閉ざされている。鍵もかかっている。');
          }
        },
        {
          id: 'portrait',
          icon: '🖼️',
          label: '肖像画',
          x: 68, y: 50, w: 26, h: 30,
          onExamine: function () {
            setMessage('古い肖像画。目が合う気がする。裏に何か書いてあるが届かない。');
          }
        }
      ]
    },

    /* ---- シーン1: 机クローズアップ ---- */
    {
      name: '机(クローズアップ)',
      bgColor: '#1e1408',
      bgDeco: drawDesk,
      objects: [
        {
          id: 'drawer_lock',
          icon: '🔒',
          label: '引き出し(錠付き)',
          x: 20, y: 35, w: 60, h: 40,
          onExamine: function () {
            if (state.flags.drawerOpen) {
              showDrawerOpen();
            } else {
              showWordLock();
            }
          },
          onUseWith: null
        },
        {
          id: 'desk_memo',
          icon: '📝',
          label: 'メモ用紙(机上)',
          x: 65, y: 20, w: 20, h: 20,
          onExamine: function () {
            showModal(
              'メモ用紙',
              'ボールペンで走り書きがある。<br><br>' +
              '「暖炉の上の本棚に<br>' +
              '&nbsp;天文学書がある。<br>' +
              '&nbsp;<strong>p.47</strong> を見よ」<br><br>' +
              '<small style="color:#8a7050">(ページメモを覚えた)</small>',
              null
            );
            setFlag('readDeskMemo');
          }
        },
        {
          id: 'ink_bottle',
          icon: '🖊️',
          label: 'インク瓶',
          x: 10, y: 15, w: 15, h: 22,
          onExamine: function () {
            setMessage('インク瓶。もうほとんど空だ。');
          }
        }
      ]
    },

    /* ---- シーン2: 暖炉・壁 ---- */
    {
      name: '暖炉と壁',
      bgColor: '#1a1208',
      bgDeco: drawFireplace,
      objects: [
        {
          id: 'astro_book',
          icon: '📖',
          label: '天文学書(p.47)',
          x: 62, y: 8, w: 14, h: 30,
          onExamine: function () {
            showModal(
              '天文学書 p.47',
              '星座と数字の対応表が載っている。<br><br>' +
              '<span style="font-family:monospace;color:#aaffaa">' +
              '♈ オリオン = <strong>4</strong><br>' +
              '♉ カシオペア = <strong>7</strong><br>' +
              '♊ おおぐま = <strong>2</strong><br>' +
              '♋ さそり = <strong>9</strong><br>' +
              '</span><br>' +
              '<small style="color:#8a7050">(星座の数字対応を覚えた)</small>',
              null
            );
          }
        },
        {
          id: 'fireplace_safe',
          icon: '🔐',
          label: '暖炉横の金庫',
          x: 10, y: 50, w: 28, h: 35,
          onExamine: function () {
            if (state.flags.safeOpen) {
              setMessage('金庫は開いている。懐中電灯はもう持っている。');
            } else {
              showSafeLock();
            }
          }
        },
        {
          id: 'star_chart',
          icon: '🌟',
          label: '星座図(壁)',
          x: 65, y: 42, w: 28, h: 40,
          onExamine: function () {
            // 引き出しを開けていれば星座図を持っているはず
            if (!state.flags.drawerOpen) {
              setMessage('壁に貼られた古い星座図。引き出しの中に詳しいものがありそうだ。');
              return;
            }
            showModal(
              '星座図',
              '引き出しで見つけた星座図だ。<br>' +
              '4つの星座が描かれ、矢印の順に並んでいる。<br><br>' +
              '<span style="color:#ffdc64;font-size:1.1em">' +
              '① ♈ オリオン<br>' +
              '② ♉ カシオペア<br>' +
              '③ ♊ おおぐま<br>' +
              '④ ♋ さそり' +
              '</span><br><br>' +
              '<small style="color:#8a7050">p.47 の対応表と照らし合わせよう</small>',
              null
            );
          }
        },
        {
          id: 'fireplace',
          icon: '🔥',
          label: '暖炉',
          x: 10, y: 30, w: 50, h: 20,
          onExamine: function () {
            setMessage('暖炉は消えている。灰が残っているが、何か燃やされたようだ。');
          }
        }
      ]
    },

    /* ---- シーン3: 廊下(暗い) ---- */
    {
      name: '廊下',
      bgColor: '#08060a',
      bgDeco: drawHallway,
      objects: [
        {
          id: 'hallway_wall',
          icon: '🧱',
          label: '壁',
          x: 5, y: 10, w: 90, h: 70,
          onExamine: function () {
            if (!hasItem('flashlight')) {
              setMessage('暗くて何も見えない。光が必要だ。');
            } else if (!state.flags.hallLit) {
              // 懐中電灯を自動的に使った演出
              state.flags.hallLit = true;
              showModal(
                '壁の隠し文字',
                '懐中電灯で照らすと…<br>' +
                '壁に蛍光塗料で書かれた文字が浮かび上がった!<br><br>' +
                '<span style="font-size:2rem;letter-spacing:0.3em;color:#aaffaa">839156</span><br><br>' +
                '<small style="color:#8a7050">(6桁の番号を覚えた)</small>',
                null
              );
            } else {
              showModal(
                '壁の隠し文字',
                '蛍光塗料で書かれた番号。<br><br>' +
                '<span style="font-size:2rem;letter-spacing:0.3em;color:#aaffaa">839156</span>',
                null
              );
            }
          },
          onUseWith: function (itemId) {
            if (itemId === 'flashlight') {
              state.flags.hallLit = true;
              showModal(
                '壁の隠し文字',
                '懐中電灯で照らすと…<br>' +
                '壁に蛍光塗料で書かれた文字が浮かび上がった!<br><br>' +
                '<span style="font-size:2rem;letter-spacing:0.3em;color:#aaffaa">839156</span><br><br>' +
                '<small style="color:#8a7050">(6桁の番号を覚えた)</small>',
                null
              );
              return true;
            }
            return false;
          }
        },
        {
          id: 'hallway_door_back',
          icon: '🚪',
          label: '書斎へ戻るドア',
          x: 75, y: 20, w: 20, h: 65,
          onExamine: function () {
            setMessage('書斎へ戻るドアだ。');
          }
        }
      ]
    },

    /* ---- シーン4: 玄関ドア ---- */
    {
      name: '玄関ドア',
      bgColor: '#0e0a06',
      bgDeco: drawEntrance,
      objects: [
        {
          id: 'front_door',
          icon: '🚪',
          label: '玄関ドア(番号錠)',
          x: 20, y: 10, w: 60, h: 80,
          onExamine: function () {
            if (state.flags.doorUnlocked) {
              triggerClear();
            } else {
              showDoorLock();
            }
          }
        },
        {
          id: 'door_note',
          icon: '📋',
          label: 'ドアの貼り紙',
          x: 68, y: 20, w: 12, h: 18,
          onExamine: function () {
            showModal(
              'ドアの貼り紙',
              '<span style="color:#ffaaaa;font-size:1.05em">「逆から読め」</span><br><br>' +
              '<small style="color:#8a7050">廊下で見つけた番号を逆順に入力するらしい…</small>',
              null
            );
          }
        }
      ]
    }
  ];

  // ---- DOM参照 ----
  var sceneArea    = document.getElementById('scene-area');
  var messageText  = document.getElementById('message-text');
  var invSlots     = document.getElementById('inventory-slots');
  var sceneInd     = document.getElementById('scene-indicator');
  var btnLeft      = document.getElementById('btn-left');
  var btnRight     = document.getElementById('btn-right');
  var modalOverlay = document.getElementById('modal-overlay');
  var modalContent = document.getElementById('modal-content');
  var modalClose   = document.getElementById('modal-close');
  var startOverlay = document.getElementById('game-start-overlay');
  var clearOverlay = document.getElementById('game-clear-overlay');
  var btnStart     = document.getElementById('btn-start');
  var btnRetry     = document.getElementById('btn-retry');

  // ---- 初期化 ----
  btnStart.addEventListener('click', startGame);
  btnRetry.addEventListener('click', function () {
    clearOverlay.classList.remove('active');
    startGame();
  });
  modalClose.addEventListener('click', closeModal);
  modalOverlay.addEventListener('click', function (e) {
    if (e.target === modalOverlay) closeModal();
  });
  btnLeft.addEventListener('click', function () { navigateScene(-1); });
  btnRight.addEventListener('click', function () { navigateScene(1); });

  function startGame() {
    state.scene       = 0;
    state.inventory   = [];
    state.selectedItem = null;
    state.flags       = { drawerOpen: false, safeOpen: false, hallLit: false, doorUnlocked: false };
    state.startTime   = Date.now();
    state.cleared     = false;
    startOverlay.classList.remove('active');
    renderScene();
    renderInventory();
    setMessage('書斎に閉じ込められた。手がかりを探そう。');
  }

  // ---- シーン描画 ----
  function renderScene() {
    var scene = SCENES[state.scene];
    sceneArea.innerHTML = '';
    sceneArea.style.background = scene.bgColor;

    // SVG背景デコレーション
    if (scene.bgDeco) {
      var svg = scene.bgDeco();
      if (svg) sceneArea.appendChild(svg);
    }

    // 懐中電灯シーン(廊下)の暗闇オーバーレイ
    if (state.scene === 3 && !state.flags.hallLit) {
      var dark = document.createElement('div');
      dark.className = 'flashlight-overlay';
      dark.style.background = 'rgba(0,0,0,0.88)';
      sceneArea.appendChild(dark);
    }

    // オブジェクト配置
    scene.objects.forEach(function (obj) {
      // 引き出しが開いた後はアイコン変更
      var icon = obj.icon;
      if (obj.id === 'drawer_lock' && state.flags.drawerOpen) icon = '📂';
      if (obj.id === 'fireplace_safe' && state.flags.safeOpen) icon = '🔓';

      var el = document.createElement('div');
      el.className = 'scene-object';
      el.style.left   = obj.x + '%';
      el.style.top    = obj.y + '%';
      el.style.width  = obj.w + '%';
      el.style.height = obj.h + '%';
      el.dataset.id   = obj.id;

      var iconEl = document.createElement('span');
      iconEl.className = 'obj-icon';
      iconEl.textContent = icon;
      el.appendChild(iconEl);

      el.addEventListener('click', function () { handleObjectClick(obj); });
      sceneArea.appendChild(el);
    });

    // ナビゲーション更新
    sceneInd.textContent = (state.scene + 1) + ' / ' + SCENES.length;
    btnLeft.disabled  = (state.scene === 0);
    // 廊下(シーン3)は懐中電灯なしで入れない
    var nextBlocked = (state.scene === 2 && !state.flags.safeOpen);
    btnRight.disabled = (state.scene >= SCENES.length - 1) || nextBlocked;

    updateObjectHighlights();
  }

  // ---- オブジェクトクリック ----
  function handleObjectClick(obj) {
    if (state.selectedItem) {
      // アイテムを使う
      var used = false;
      if (obj.onUseWith) {
        used = obj.onUseWith(state.selectedItem);
      }
      if (!used) {
        setMessage('それはここでは使えないようだ。');
      }
      state.selectedItem = null;
      renderInventory();
      updateObjectHighlights();
      renderScene();
    } else {
      // 調べる
      if (obj.onExamine) obj.onExamine();
    }
  }

  // ---- ナビゲーション ----
  function navigateScene(dir) {
    var next = state.scene + dir;
    if (next < 0 || next >= SCENES.length) return;
    // 廊下は懐中電灯がないと入れない
    if (next === 3 && !hasItem('flashlight')) {
      setMessage('暗くて進めない。何か光るものが必要だ。');
      return;
    }
    state.scene = next;
    state.selectedItem = null;
    closeModal();
    renderScene();
    setMessage(SCENES[next].name + 'を調べよう。');
  }

  // ---- インベントリ ----
  var ITEMS = {
    flashlight: { icon: '🔦', name: '懐中電灯' },
    star_map:   { icon: '🗺️', name: '星座図' },
    key:        { icon: '🗝️', name: '引き出しの鍵' },
  };

  function addItem(id) {
    if (!hasItem(id)) {
      state.inventory.push(id);
      renderInventory();
      setMessage('「' + ITEMS[id].name + '」を手に入れた!');
    }
  }

  function hasItem(id) {
    return state.inventory.indexOf(id) !== -1;
  }

  function renderInventory() {
    invSlots.innerHTML = '';
    state.inventory.forEach(function (id) {
      var item = ITEMS[id];
      if (!item) return;
      var slot = document.createElement('div');
      slot.className = 'inventory-slot' + (state.selectedItem === id ? ' active' : '');
      slot.textContent = item.icon;

      var tip = document.createElement('span');
      tip.className = 'slot-tooltip';
      tip.textContent = item.name;
      slot.appendChild(tip);

      slot.addEventListener('click', function () {
        state.selectedItem = (state.selectedItem === id) ? null : id;
        renderInventory();
        updateObjectHighlights();
        if (state.selectedItem) {
          setMessage('「' + item.name + '」を選択中。使いたいものをクリック。');
        } else {
          setMessage('選択を解除した。');
        }
      });
      invSlots.appendChild(slot);
    });
  }

  function updateObjectHighlights() {
    document.querySelectorAll('.scene-object').forEach(function (el) {
      el.classList.toggle('selected-item-target', !!state.selectedItem);
    });
  }

  // ---- フラグ ----
  function setFlag(key) {
    state.flags[key] = true;
  }

  // ---- メッセージ ----
  function setMessage(msg) {
    messageText.innerHTML = msg;
  }

  // ---- モーダル ----
  function showModal(title, bodyHtml, extraHtml) {
    var html = '<h3>' + title + '</h3><p>' + bodyHtml + '</p>';
    if (extraHtml) html += extraHtml;
    modalContent.innerHTML = html;
    modalOverlay.classList.add('active');
  }

  function closeModal() {
    modalOverlay.classList.remove('active');
  }

  // ---- 錠前UI ----

  /* 英単語錠(引き出し) */
  function showWordLock() {
    var html = '<h3>引き出しの錠</h3>' +
      '<p>5文字の英単語を入力して開けよう。</p>' +
      '<div class="word-input-area">' +
      '<input class="word-input" id="word-input" type="text" maxlength="5" placeholder="?????" autocomplete="off">' +
      '</div>' +
      '<button class="lock-submit" id="word-submit">入力</button>';
    modalContent.innerHTML = html;
    modalOverlay.classList.add('active');

    document.getElementById('word-submit').addEventListener('click', function () {
      var val = document.getElementById('word-input').value.toUpperCase().trim();
      if (val === CORRECT_WORD) {
        state.flags.drawerOpen = true;
        // 星座図を入手
        closeModal();
        renderScene();
        setMessage('引き出しが開いた!');
        setTimeout(function () {
          showModal(
            '引き出しの中身',
            '引き出しの中に <strong>星座図</strong> があった。<br>' +
            '4つの星座が描かれ、番号が振られている。<br><br>' +
            '<small style="color:#8a7050">(星座図を入手した)</small>',
            null
          );
          addItem('star_map');
        }, 300);
      } else {
        document.getElementById('word-input').style.borderColor = '#ff6644';
        setMessage('違う。もう一度考えよう。');
        setTimeout(function () {
          var el = document.getElementById('word-input');
          if (el) el.style.borderColor = '';
        }, 600);
      }
    });

    // Enterキーでも送信
    document.getElementById('word-input').addEventListener('keydown', function (e) {
      if (e.key === 'Enter') document.getElementById('word-submit').click();
    });
    setTimeout(function () {
      var el = document.getElementById('word-input');
      if (el) el.focus();
    }, 100);
  }

  /* 引き出しオープン状態 */
  function showDrawerOpen() {
    showModal(
      '引き出し(開)',
      '引き出しは開いている。<br>' +
      '中には <strong>星座図</strong> が入っていた。<br><br>' +
      '<small style="color:#8a7050">(すでに持っている)</small>',
      null
    );
  }

  /* 金庫4桁錠 */
  function showSafeLock() {
    var digits = [4, 7, 2, 9].map(function (d) { return d; }); // 正解はSAFE_CODE
    var current = [0, 0, 0, 0];

    function buildSafeHTML() {
      var html = '<h3>金庫の番号錠</h3><p>4桁の数字を入力しよう。</p>';
      html += '<div class="lock-display">';
      for (var i = 0; i < 4; i++) {
        html += '<div class="lock-digit-group">' +
          '<button class="lock-digit-btn" data-i="' + i + '" data-dir="1">▲</button>' +
          '<div class="lock-digit" id="safe-d-' + i + '">' + current[i] + '</div>' +
          '<button class="lock-digit-btn" data-i="' + i + '" data-dir="-1">▼</button>' +
          '</div>';
      }
      html += '</div>';
      html += '<button class="lock-submit" id="safe-submit">解錠する</button>';
      return html;
    }

    modalContent.innerHTML = buildSafeHTML();
    modalOverlay.classList.add('active');

    function bindSafeEvents() {
      document.querySelectorAll('.lock-digit-btn').forEach(function (btn) {
        btn.addEventListener('click', function () {
          var i   = parseInt(this.dataset.i);
          var dir = parseInt(this.dataset.dir);
          current[i] = (current[i] + dir + 10) % 10;
          document.getElementById('safe-d-' + i).textContent = current[i];
        });
      });
      document.getElementById('safe-submit').addEventListener('click', function () {
        var entered = current.join('');
        if (entered === SAFE_CODE) {
          state.flags.safeOpen = true;
          closeModal();
          renderScene();
          setMessage('金庫が開いた!');
          setTimeout(function () {
            showModal(
              '金庫の中身',
              '金庫の中に <strong>懐中電灯</strong> があった!<br>' +
              'バッテリーはまだ残っているようだ。<br><br>' +
              '<small style="color:#8a7050">(懐中電灯を入手した)</small>',
              null
            );
            addItem('flashlight');
            // 廊下へのナビを有効化
            renderScene();
          }, 300);
        } else {
          setMessage('違う番号だ。もう一度。');
          document.querySelectorAll('.lock-digit').forEach(function (el) {
            el.style.color = '#ff6644';
          });
          setTimeout(function () {
            document.querySelectorAll('.lock-digit').forEach(function (el) {
              el.style.color = '#ffdc64';
            });
          }, 600);
        }
      });
    }
    bindSafeEvents();
  }

  /* 玄関ドア6桁錠 */
  function showDoorLock() {
    var current = [0, 0, 0, 0, 0, 0];

    function buildDoorHTML() {
      var html = '<h3>玄関ドアの番号錠</h3>' +
        '<p>6桁の番号を入力しよう。</p>';
      html += '<div class="lock-display">';
      for (var i = 0; i < 6; i++) {
        html += '<div class="lock-digit-group">' +
          '<button class="lock-digit-btn" data-i="' + i + '" data-dir="1">▲</button>' +
          '<div class="lock-digit" id="door-d-' + i + '" style="width:30px;height:40px;font-size:1.3rem">' + current[i] + '</div>' +
          '<button class="lock-digit-btn" data-i="' + i + '" data-dir="-1">▼</button>' +
          '</div>';
      }
      html += '</div>';
      html += '<button class="lock-submit" id="door-submit">解錠する</button>';
      return html;
    }

    modalContent.innerHTML = buildDoorHTML();
    modalOverlay.classList.add('active');

    document.querySelectorAll('.lock-digit-btn').forEach(function (btn) {
      btn.addEventListener('click', function () {
        var i   = parseInt(this.dataset.i);
        var dir = parseInt(this.dataset.dir);
        current[i] = (current[i] + dir + 10) % 10;
        document.getElementById('door-d-' + i).textContent = current[i];
      });
    });

    document.getElementById('door-submit').addEventListener('click', function () {
      var entered = current.join('');
      if (entered === DOOR_CODE) {
        state.flags.doorUnlocked = true;
        closeModal();
        triggerClear();
      } else {
        setMessage('違う。貼り紙のヒントをもう一度確認しよう。');
        document.querySelectorAll('#modal-content .lock-digit').forEach(function (el) {
          el.style.color = '#ff6644';
        });
        setTimeout(function () {
          document.querySelectorAll('#modal-content .lock-digit').forEach(function (el) {
            el.style.color = '#ffdc64';
          });
        }, 700);
      }
    });
  }

  // ---- クリア ----
  function triggerClear() {
    if (state.cleared) return;
    state.cleared = true;
    var elapsed = Math.floor((Date.now() - state.startTime) / 1000);
    var min = Math.floor(elapsed / 60);
    var sec = elapsed % 60;
    var timeStr = (min > 0 ? min + '分' : '') + sec + '秒';
    document.getElementById('clear-time-text').textContent =
      'クリアタイム:' + timeStr;
    clearOverlay.classList.add('active');
  }

  // ---- 背景SVG描画関数 ----

  function makeSVG(w, h) {
    var svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
    svg.setAttribute('viewBox', '0 0 ' + w + ' ' + h);
    svg.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
    svg.style.position = 'absolute';
    svg.style.inset = '0';
    svg.style.width = '100%';
    svg.style.height = '100%';
    return svg;
  }

  function rect(svg, x, y, w, h, fill, rx) {
    var el = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
    el.setAttribute('x', x); el.setAttribute('y', y);
    el.setAttribute('width', w); el.setAttribute('height', h);
    el.setAttribute('fill', fill);
    if (rx) el.setAttribute('rx', rx);
    svg.appendChild(el);
  }

  function drawStudy() {
    var svg = makeSVG(400, 300);
    // 床
    rect(svg, 0, 220, 400, 80, '#3d2510');
    // 壁
    rect(svg, 0, 0, 400, 220, '#4a3020');
    // 腰板
    rect(svg, 0, 170, 400, 12, '#6a4828');
    // 本棚(左)
    rect(svg, 0, 20, 120, 185, '#5a3818');
    for (var i = 0; i < 5; i++) {
      rect(svg, 4, 30 + i * 32, 112, 28,
        ['#8b3a3a','#3a7a5a','#4a5a9a','#9a7a2a','#6a3a7a'][i], 2);
    }
    // 棚板
    for (var j = 0; j < 4; j++) {
      rect(svg, 0, 58 + j * 32, 120, 4, '#3a2510');
    }
    // 窓(右上)
    rect(svg, 280, 20, 110, 120, '#8ab4d4', 4);
    rect(svg, 280, 20, 110, 120, 'none');
    // 窓枠
    rect(svg, 278, 18, 114, 124, '#5a3818', 4);
    rect(svg, 280, 20, 110, 2, '#6a4828');
    rect(svg, 280, 78, 110, 2, '#6a4828');
    rect(svg, 334, 20, 2, 122, '#6a4828');
    // 机(右下)
    rect(svg, 140, 160, 180, 12, '#7a5028');
    rect(svg, 145, 172, 12, 55, '#6a4020');
    rect(svg, 303, 172, 12, 55, '#6a4020');
    // 肖像画
    rect(svg, 280, 155, 110, 75, '#5a3010', 3);
    rect(svg, 285, 160, 100, 65, '#7a6040', 2);
    return svg;
  }

  function drawDesk() {
    var svg = makeSVG(400, 300);
    rect(svg, 0, 0, 400, 300, '#2a1808');
    // 机面
    rect(svg, 20, 80, 360, 180, '#6a4a28', 6);
    rect(svg, 20, 80, 360, 10, '#8a6a3a', 6);
    // 引き出し
    rect(svg, 80, 130, 240, 100, '#4a3018', 4);
    rect(svg, 82, 132, 236, 96, '#5a3e22', 4);
    // 引き出しの取っ手
    rect(svg, 185, 175, 30, 8, '#c8a050', 3);
    // インク瓶エリア(左上)
    rect(svg, 30, 90, 50, 70, '#2a1808', 4);
    // メモ用紙エリア(右上)
    rect(svg, 290, 88, 80, 60, '#e8dcc0', 3);
    return svg;
  }

  function drawFireplace() {
    var svg = makeSVG(400, 300);
    rect(svg, 0, 0, 400, 300, '#1e160a');
    // 壁
    rect(svg, 0, 0, 400, 240, '#3a2810');
    // 床
    rect(svg, 0, 240, 400, 60, '#2a1c0c');
    // 暖炉本体
    rect(svg, 30, 110, 200, 130, '#2e2010', 4);
    rect(svg, 35, 120, 190, 100, '#1a1008', 4);
    // 暖炉上の棚
    rect(svg, 20, 100, 220, 14, '#5a3e1e', 3);
    // 天文学書(棚の上)
    rect(svg, 250, 20, 55, 110, '#3a2a5a', 3);
    rect(svg, 252, 22, 51, 106, '#4a3a7a', 3);
    // 金庫(暖炉横)
    rect(svg, 30, 120, 110, 100, '#1a1208', 3);
    rect(svg, 35, 125, 100, 90, '#3a2a18', 3);
    rect(svg, 68, 165, 28, 28, '#2a2010', 4);
    // 星座図(右壁)
    rect(svg, 258, 165, 115, 120, '#2a2010', 3);
    rect(svg, 263, 170, 105, 110, '#1a1808', 2);
    // 星座の点
    var stars = [[280,195],[320,210],[300,240],[340,250],[285,265],[350,230]];
    stars.forEach(function(s) {
      var c = document.createElementNS('http://www.w3.org/2000/svg','circle');
      c.setAttribute('cx',s[0]); c.setAttribute('cy',s[1]); c.setAttribute('r','3');
      c.setAttribute('fill','#ffdc64');
      svg.appendChild(c);
    });
    return svg;
  }

  function drawHallway() {
    var svg = makeSVG(400, 300);
    rect(svg, 0, 0, 400, 300, '#0a0810');
    // 遠近感のある廊下
    rect(svg, 0, 0, 400, 300, '#0e0c16');
    rect(svg, 60, 60, 280, 180, '#12101a', 0);
    rect(svg, 120, 100, 160, 120, '#16141e', 0);
    // 壁ライン(遠近)
    var lines = [[0,0,60,60],[400,0,340,60],[0,300,60,240],[400,300,340,240]];
    lines.forEach(function(l) {
      var line = document.createElementNS('http://www.w3.org/2000/svg','line');
      line.setAttribute('x1',l[0]); line.setAttribute('y1',l[1]);
      line.setAttribute('x2',l[2]); line.setAttribute('y2',l[3]);
      line.setAttribute('stroke','#2a2030'); line.setAttribute('stroke-width','2');
      svg.appendChild(line);
    });
    // ドア(奥)
    rect(svg, 300, 40, 80, 220, '#1e1828', 3);
    rect(svg, 302, 42, 76, 216, '#2a2230', 3);
    return svg;
  }

  function drawEntrance() {
    var svg = makeSVG(400, 300);
    rect(svg, 0, 0, 400, 300, '#0e0a06');
    // 壁
    rect(svg, 0, 0, 400, 260, '#2a1e10');
    // 床
    rect(svg, 0, 260, 400, 40, '#1e1408');
    // ドア
    rect(svg, 80, 20, 240, 270, '#3a2810', 4);
    rect(svg, 85, 25, 230, 255, '#4a3818', 4);
    // ドアパネル装飾
    rect(svg, 95, 40, 95, 100, '#5a4828', 4);
    rect(svg, 210, 40, 95, 100, '#5a4828', 4);
    rect(svg, 95, 155, 95, 100, '#5a4828', 4);
    rect(svg, 210, 155, 95, 100, '#5a4828', 4);
    // ドアノブ
    rect(svg, 285, 148, 18, 8, '#c8a050', 4);
    // 番号錠パネル
    rect(svg, 95, 120, 60, 30, '#1a1208', 4);
    // 貼り紙(右)
    rect(svg, 275, 80, 45, 60, '#e8dcc0', 3);
    return svg;
  }

})();

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

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

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

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

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

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

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

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

もっと学びたい人へ

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

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