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 ← ユーザー一覧
ls・cd・cat・find・grep——それぞれがこのツリーをトラバースして結果を返します。パーミッションチェック・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 に追加し、ssh が connectedHosts に追加する。接続するとそのホストのファイルシステムが使えるようになる——という流れで、リアルなハッキングの手順を再現しています。
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幅 | ターミナル表示幅 |
|---|---|---|
| 半角英数 | 1 | 1列 |
| 日本語漢字・ひらがな | 1 | 2列 |
| 罫線文字 | 1 | 1列 |
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)のみ本格稼働。以下のミッションが企画済みです:
| ミッション | コードネーム | 特徴 |
|---|---|---|
| 1 | FIRST BLOOD | Webサーバー侵入、SQLインジェクション体験 |
| 2 | INSIDE JOB | メールサーバー、社内ネットワーク横断 |
| 3 | GHOST PROTOCOL | 侵入検知システムを回避しながら進む |
| 4 | DEEP STATE | 多層ネットワーク、暗号化ファイル解読 |
| 5 | ENDGAME | 最終決戦、全スキルの集大成 |
収益化の設計(構想)
現時点では全機能無料です。ミッション全5本が完成した段階で、以下の課金モデルを導入予定です。
| 機能 | 無料(予定) | 有料(予定) |
|---|---|---|
| boot_camp(チュートリアル) | ✓ | ✓ |
| ミッション1〜3 | ✗ | ✓ |
| ミッション4〜5 | ✗ | ✓ |
| リーダーボード | 閲覧のみ | ランクイン可 |
| ヒント無制限 | ✗ | ✓ |
チュートリアルで「面白い!」と思わせてから課金——クラシックなF2Pゲームの設計です。
技術的な発展アイデア
- WebSocket対応: リアルタイムで「他のエージェントも侵入中」演出
- プロシージャル生成: ミッションごとにファイル構造とパスワードをランダム変化させてリプレイ性を向上
- アチーブメントシステム: 「コマンド100回」「ヒントなしクリア」などバッジ付与
- カスタムミッションエディタ: ユーザーが自分でミッションを作れるUGC機能
💡 まとめ
HackSimを作って気づいたことを一言で言うと:
「ゲームはバグが面白さを殺す」
コマンドが動かない、演出が出ない、完了が何度も発火する——ゲームはちょっとしたバグで体験が壊れます。アプリより品質要件が高い。
その分、動いたときの「おっ、本物っぽい!」という体験の質も高い。
ハッキングゲームというニッチなジャンルで、ブラウザ完結・日本語UI・アニメーション演出——この組み合わせは意外と少ない。「知ってる人が作ったちゃんとしたもの」として差別化できると思っています。
ミッション全5本完成した暁には、また記事を書きます。