AI開発収益化ラボ
HackSim

HackSim ができるまで — ブラウザだけで動くハッキングシミュレーターの設計と実装

「ハッキングってどんな感じなんだろう?」

映画でスクリーンに緑の文字が流れるシーン、真っ黒なターミナルに高速でコマンドが打ち込まれていくシーン——あの「かっこよさ」を、ブラウザ上で体験できたら面白いんじゃないか。

それが HackSim の出発点でした。架空のネットワークに侵入し、コマンドを駆使してミッションをクリアするゲームです。この記事では、なぜ作ったか・どう設計したか・何につまずいたかを全部書きます。


🎯 なぜハッキングゲームなのか

AI収益化ラボの文脈

このサービスを運営している「AI開発収益化ラボ」は、AIサービスで本当に収益化できるかを検証するプロジェクトです。これまでに作ったのは言い訳ジェネレーターやプロンプト辞典——いずれも「使い捨てされやすい系」のサービスでした。

HackSimは戦略を変えました。目標は 滞在時間と再訪率

指標既存サービス(言い訳など)HackSim
1回の滞在時間30秒〜3分10〜30分
リピート動機弱(飽きやすい)強(未クリアミッション・続編)
SNS拡散「結果をシェア」「クリアした!」報告
課金導線薄い「続きのミッションが欲しい」

ゲーミフィケーションは熱量を生む。熱量があるユーザーはお金を払う。この仮説を検証するために作りました。

ターゲット設定

設定理由
主ターゲットIT系・エンジニア・学生ターミナル操作への親和性が高い
副ターゲットゲーム好き一般ユーザー「映画みたいなハッキング」への憧れ
提供体験本物感のある架空ハッキングフィクションだから安全に楽しめる

🏗️ システム設計

HackSimの最大の特徴は、バックエンドが存在しないことです。

すべてのゲームロジック——仮想ファイルシステム、ネットワーク、コマンド実行エンジン——はブラウザ上のTypeScriptで動きます。

全体アーキテクチャ

HackSimApp(React)
  │
  ├─ HackSimMenu(ミッション選択画面)
  │
  └─ Terminal(ターミナル画面)
        ├─ TitleBar
        ├─ OutputBuffer(出力表示)
        ├─ InputLine(コマンド入力)
        ├─ StatusBar(状態表示)
        ├─ MatrixRain(マトリックス演出)
        ├─ CRTOverlay(CRTスキャンライン)
        └─ ScreenFlash(アクセス許可/拒否フラッシュ)
コマンド実行フロー:
ユーザー入力
  └─ CommandDispatcher
        ├─ navigation.ts  (ls, cd, cat, pwd, find, grep...)
        ├─ network.ts     (nmap, ssh, ping, wget...)
        ├─ hacking.ts     (brute, decrypt, crack, exfil...)
        ├─ mission.ts     (objectives, hint, status...)
        └─ system.ts      (help, whoami, clear...)

状態管理:Zustand

Reactのstateで管理するには状態が多すぎる——ミッション状態・コマンド履歴・接続ホスト・ファイルシステム・アラートレベル・検出されたパスワード……。Zustandを選んだ理由はシンプルさです。

// gameStore.ts の主要状態
interface GameStateSnapshot {
  missionStatus: "idle" | "briefing" | "active" | "complete" | "failed"
  currentHost: string
  currentPath: string
  currentUser: string
  currentAccess: "user" | "root"
  outputBuffer: OutputLine[]
  commandHistory: string[]
  connectedHosts: string[]
  discoveredHosts: string[]
  crackedPasswords: Record<string, string>
  alertLevel: number   // 0〜100(侵入検知レベル)
  objectivesCompleted: string[]
}

コマンドハンドラーは CommandContext を受け取り、CommandResult を返すだけ。副作用なし、テスタブル。

type CommandHandler = (ctx: CommandContext) => CommandResult

interface CommandResult {
  output: OutputLine[]
  stateUpdates?: Partial<GameStateSnapshot>
}

🗂️ 仮想ファイルシステム

仮想的なLinuxファイルシステムを、TypeScriptのオブジェクトツリーで表現しています。

interface FSNode {
  name: string
  type: "file" | "directory" | "binary" | "encrypted"
  permissions: string        // "rw-r--r--" など
  owner: string
  content?: string           // ファイルの中身
  children?: FSNode[]        // ディレクトリの場合
  hidden?: boolean           // ls -a でのみ表示
  readable?: boolean         // パーミッション制御
  requiredAccess?: "root"    // root権限が必要
  isTarget?: boolean         // ミッション目標ファイル
  isBonusTarget?: boolean    // ボーナス目標ファイル
  encrypted?: boolean        // 暗号化ファイル
}

boot_campミッションのファイルツリーはこんな構造:

/ (root)
├── home/
│   └── recruit/
│       ├── welcome.txt          ← 最初に読むファイル
│       ├── .hidden_note.txt     ← ボーナス(ls -la で発見)
│       └── documents/
│           ├── guide.txt        ← コマンドガイド
│           └── mission_brief.txt ← 抽出対象
├── var/
│   └── log/
│       └── auth.log             ← SSH接続ログ
└── etc/
    └── passwd                   ← ユーザー一覧

lscdcatfindgrep——それぞれがこのツリーをトラバースして結果を返します。パーミッションチェック・hidden属性・暗号化フラグも考慮します。


🌐 仮想ネットワーク

各ミッションは MissionData として定義され、仮想ネットワーク構成を含みます。

interface MissionData {
  id: string
  network: {
    localIp: string
    subnet: string
    hosts: VirtualHost[]
  }
  objectives: Objective[]
  bonusObjectives?: Objective[]
  hints: string[]
  briefing: DialogLine[]
  completionMessage: DialogLine[]
}

nmap コマンドが discoveredHosts に追加し、sshconnectedHosts に追加する。接続するとそのホストのファイルシステムが使えるようになる——という流れで、リアルなハッキングの手順を再現しています。

boot_campミッションの攻略フロー

1. help           → 利用可能コマンドを確認
2. nmap -sV 192.168.0.100
                  → ポート22/SSH を発見
                  → SSHバナーで「Default password = hostname」を確認
3. ssh recruit@192.168.0.100
                  → パスワード: training-server(ホスト名がそのまま)
4. cat welcome.txt
5. cd documents
6. cat documents/mission_brief.txt
7. exfil mission_brief.txt
                  → ファイルをベースに送信 → ミッション完了!

🎨 UX・演出設計

CRTオーバーレイ

/* ブラウン管テレビのスキャンライン効果 */
background: repeating-linear-gradient(
  0deg,
  transparent,
  transparent 2px,
  rgba(0, 0, 0, 0.15) 2px,
  rgba(0, 0, 0, 0.15) 4px
);

キャラクターが薄くてもこれだけで「古いモニター感」が出ます。アニメーションで微妙にちらつかせることで、本物のCRTっぽさを演出。

マトリックス演出

映画「マトリックス」の緑の文字が降り注ぐあの演出を、Canvas APIで実装しました。

  • ゲーム開始時(ブリーフィング開始) → 90フレーム(約1.5秒)
  • SSH接続時(ホスト切り替え) → 90フレーム(約1.5秒)
  • ミッション完了時 → 180フレーム(3秒、フルバージョン)
const CHARS = "アイウエオカキクケコ...0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"

// キャラクターを列ごとにランダムな位置から降らせる
for (let i = 0; i < drops.length; i++) {
  const char = CHARS[Math.floor(Math.random() * CHARS.length)]
  ctx.fillText(char, i * fontSize, drops[i] * fontSize)
  if (drops[i] * fontSize > canvas.height && Math.random() > 0.975) {
    drops[i] = 0  // ランダムにリセットで自然な流れを作る
  }
  drops[i]++
}

日本語カタカナを混ぜているのがポイントです。英数字だけより「和製サイバーパンク感」が出ます。

スクリーンフラッシュ

SSHログイン成功・失敗、権限昇格——それぞれのイベントで画面を一瞬フラッシュさせます。

イベントフラッシュ色意味
アクセス許可認証成功
アクセス拒否認証失敗・権限不足
ミッション失敗赤(強め)ゲームオーバー

日本語UIとキャラクター演出

ナレーターは「ZERO」というキャラクター。ブリーフィングと完了メッセージをタイプライター風に流します。

[ZERO] ようこそ、Phantom へ。新入りよ。
[ZERO] 現場に送り出す前に、基本を確認しておこう。
[ZERO] 各目標を達成しろ。'objectives' で現在の作戦目標を確認できる。

英語だと「ハッカーゲームあるある」になりすぎる。日本語にすることで独自性が生まれ、かつターゲットユーザーにとって理解しやすい。


🔊 サウンドエンジン

Web Audio APIを使ってサウンドを生成しています。サーバーからオーディオファイルをロードせず、ブラウザ上でリアルタイム合成。

class SoundEngine {
  // キータイプ音:ホワイトノイズ+フィルター
  playKeyType() {
    const noise = this.ctx.createOscillator()
    noise.type = "square"
    noise.frequency.value = 80 + Math.random() * 40
    // ...
  }

  // SSH接続音:上昇するビープ音
  playConnect() {
    const osc = this.ctx.createOscillator()
    osc.frequency.setValueAtTime(200, now)
    osc.frequency.exponentialRampToValueAtTime(800, now + 0.3)
    // ...
  }
}

サウンドはミュートボタン付き。スマホで触るユーザーへの配慮です。


😅 実装でハマったこと

1. モバイルでボタンが反応しない問題

スマートフォンで名前入力後のボタンが全く反応しない——という問題が発生しました。

原因は disabled 属性。入力値のバリデーション用に disabled={!isValid} としていたのですが、iOSではフォームの状態変化後に disabled が即座に反映されないケースがあります。

解決策: disabled を削除し、送信時にバリデーションを実行。エラーは赤字で表示する方式に変更。

2. 日本語IMEでボタンが有効化されない問題

スマホでひらがなを入力すると、テキストは入っているのに「ネットワークへ接続」ボタンが有効にならない——。

原因は onChange ではなく onCompositionEnd の問題。IME変換中(composition中)はReactのstateが正しく更新されないことがある。

解決策: inputMode="url" を指定。これでiOSがASCIIキーボードを優先表示するため、日本語IMEが起動しない。

3. マトリックスアニメーションが一瞬しか表示されない問題

実装したはずのマトリックス演出が、ゲーム開始時に全く表示されない(または一瞬で消える)という謎の問題。

原因はReactのuseEffectの依存配列。onComplete コールバックとして () => setShowMatrix(false) をインラインで渡していたため、ストアの outputBuffer が更新されるたびにTerminalコンポーネントが再レンダーされ、Matri xRainの useEffect の依存が変わり、アニメーションがフレーム0にリセットされ続けていました。

// ❌ 毎レンダーで新しい関数参照が生成される
<MatrixRain onComplete={() => setShowMatrix(false)} />

// ✓ useCallbackで参照を安定化
const handleMatrixComplete = useCallback(() => setShowMatrix(false), [])
<MatrixRain onComplete={handleMatrixComplete} />

4. 文字幅とボックス描画の問題

objectives や status コマンドの出力を「ボックス」で囲もうとしたとき、罫線文字(╔╗║╚╝)と日本語テキストの表示幅が合わない問題が発生しました。

文字Unicode幅ターミナル表示幅
半角英数11列
日本語漢字・ひらがな12列
罫線文字11列

JavaScriptで string.length を計算しても、実際の表示幅はフォントに依存してズレる。

解決策: 右辺の罫線を全廃。左辺と区切り線のみの「開放型」レイアウトに変更。

┌─ 作戦目標: BOOT_CAMP
────────────────────────────────
  [ ] 'help' コマンドを実行する
  [✓] 'ssh recruit@...' で接続する  (パスワード: training-server)
────────────────────────────────

5. ミッション完了画面が何度も表示される問題

ミッション完了後、ZEROのセリフが繰り返し表示されてしまう問題。

原因は useEffect の依存配列に onMissionComplete 関数を含めていたこと。親コンポーネントが再レンダーされるたびに関数参照が変わり、効果がトリガーし直されていました。

解決策: useRef でフラグ管理。

const completeFiredRef = useRef(false)

useEffect(() => {
  if (missionStatus === "complete" && !completeFiredRef.current) {
    completeFiredRef.current = true
    // 完了演出を実行
  }
}, [missionStatus])

📊 コマンド実装数

現時点で実装済みのコマンド:

カテゴリコマンド
ナビゲーションls, cd, cat, pwd, find, grep, head, history
ネットワークnmap, ssh, ping, wget, ifconfig, traceroute, connect
ハッキングbrute, decrypt, crack, exploit, inject, spoof, exfil, analyze
ミッションobjectives, hint, status, abort, reset
システムhelp, whoami, clear

合計 27コマンド。それぞれが仮想FSやネットワーク状態を参照し、ミッション進捗に応じて結果が変わります。


🚀 今後の展開

ミッション拡張(シリーズ化)

現在はチュートリアル(boot_camp)のみ本格稼働。以下のミッションが企画済みです:

ミッションコードネーム特徴
1FIRST BLOODWebサーバー侵入、SQLインジェクション体験
2INSIDE JOBメールサーバー、社内ネットワーク横断
3GHOST PROTOCOL侵入検知システムを回避しながら進む
4DEEP STATE多層ネットワーク、暗号化ファイル解読
5ENDGAME最終決戦、全スキルの集大成

収益化の設計(構想)

現時点では全機能無料です。ミッション全5本が完成した段階で、以下の課金モデルを導入予定です。

機能無料(予定)有料(予定)
boot_camp(チュートリアル)
ミッション1〜3
ミッション4〜5
リーダーボード閲覧のみランクイン可
ヒント無制限

チュートリアルで「面白い!」と思わせてから課金——クラシックなF2Pゲームの設計です。

技術的な発展アイデア

  • WebSocket対応: リアルタイムで「他のエージェントも侵入中」演出
  • プロシージャル生成: ミッションごとにファイル構造とパスワードをランダム変化させてリプレイ性を向上
  • アチーブメントシステム: 「コマンド100回」「ヒントなしクリア」などバッジ付与
  • カスタムミッションエディタ: ユーザーが自分でミッションを作れるUGC機能

💡 まとめ

HackSimを作って気づいたことを一言で言うと:

「ゲームはバグが面白さを殺す」

コマンドが動かない、演出が出ない、完了が何度も発火する——ゲームはちょっとしたバグで体験が壊れます。アプリより品質要件が高い。

その分、動いたときの「おっ、本物っぽい!」という体験の質も高い。

ハッキングゲームというニッチなジャンルで、ブラウザ完結・日本語UI・アニメーション演出——この組み合わせは意外と少ない。「知ってる人が作ったちゃんとしたもの」として差別化できると思っています。

ミッション全5本完成した暁には、また記事を書きます。