🧠

タスクを放置するとキャラクターが傷ついていき死に絶えるアプリを作った話

に公開

はじめに - なぜ作ったのか

ADHDの自分には普通のToDoリストが効かない。

「やらなきゃ」という曖昧な義務感では動けない。「やらないと誰かが死ぬ」という明確な損失が必要だった。

そこで作ったのがHOSTAGE。

"Your negligence kills."(あなたの怠慢が、誰かを殺す)

培養槽の中で眠るバーチャルキャラクター。彼・彼女の命はあなたのタスク消化状況に直結している。期限を破ればそれは衰弱し、画面は血のように赤く染まり、システムエラーが走る。習慣を完了すれば回復する。

これは生産性ツールではない。心理戦だ。

デモ・ソースコード

コンセプトと心理学的背景

なぜ「ホラー」なのか?

行動経済学の研究によれば、人間の損失回避の動機は利益追求の約2倍強い(プロスペクト理論)。

従来のタスク管理アプリは「タスクを完了すると嬉しい」という利益追求型。しかし、ADHDや先延ばし癖がある人間にとって、この程度の動機では不十分。

HOSTAGEは逆のアプローチ:

  • タスクを放置するとキャラクターが死ぬ(損失)
  • UIが崩壊し、視覚的に不快になる(損失)
  • 習慣を完了すると回復する(損失回避)

キャラクターの死は、脳が「自分の責任による損失」として認識する。これが罪悪感を生み、強力な行動動機となる。

主要機能

🎮 ゲームメカニクス

タスク完了 → HP回復(優先度連動)

優先度 回復量
Low +3 HP
Medium +5 HP
High +8 HP
Critical +12 HP

習慣完了 → HP回復 + ストリーク

  • 固定 +5 HP
  • 連続達成日数をトラッキング
  • 「🔥 7日連続達成!」のようなフィードバック

時間減衰 → 放置するほど加速的にダメージ

ダメージ = (経過時間²) × 0.5

1時間放置  → 0.5 ダメージ
10時間放置 → 50 ダメージ
24時間放置 → 288 ダメージ

💀 Death & Purge(死と廃棄)

HPが0になると被験体は死亡。ユーザーには2つの選択肢が提示される:

  • Reboot(蘇生): HP50で復活、同じ被験体を継続
  • Purge(検体廃棄): 完全削除し、新規被験体を作成

「失敗作を消して最初からやり直す」という選択肢が、心理的なリセット効果を生む。

📱 PWA対応(ネイティブアプリ体験)

スマートフォンの「ホーム画面に追加」で、アドレスバーのない全画面表示が可能。

  • オフライン対応(Service Worker)
  • プッシュ通知対応(将来実装予定)
  • インストール不要、ブラウザから直接追加

🌐 多言語対応(i18n)

英語・日本語の完全対応。ブラウザ言語を自動判定し、手動切り替えも可能。

翻訳の設計思想: 直訳ではなく、SF/サイバーパンクの世界観を維持した意訳を採用。

English 直訳(NG) SF調意訳(採用)
Sync Tasks タスク同期 外部記憶同期
Subject 対象 被験体
Signal Lost 信号喪失 反応消失
Your negligence has consequences あなたの怠慢には結果がある 怠惰には代償が伴う

技術スタック

┌─────────────────────────────────────┐
│         Frontend (Vercel)           │
│     Next.js 14 / App Router / PWA   │
└──────────────┬──────────────────────┘
               │ HTTPS
               ▼
┌─────────────────────────────────────┐
│         Backend (Railway)           │
│        FastAPI / Python 3.11        │
└──────────────┬──────────────────────┘
               │
               ▼
┌─────────────────────────────────────┐
│        Database (Supabase)          │
│    PostgreSQL / Auth / Realtime     │
└─────────────────────────────────────┘

選定理由

技術 選定理由
Next.js 14 App RouterでSSR/CSRの最適化、PWA対応が容易
FastAPI 型安全(Pydantic v2)、非同期処理、自動ドキュメント生成
Supabase PostgreSQL + 認証 + リアルタイム購読を一括提供
Vercel Next.jsとの親和性、Edge Functionsでグローバル配信
Railway Pythonバックエンドのデプロイが簡単、自動スケール

アーキテクチャの変遷

当初はNotion APIと連携し、ユーザーの既存タスクを読み取る設計だった。しかし以下の理由でSupabase完結型に移行:

  1. 仕様の不確定性: 「Notionのどのプロパティをどう同期するか」が未定義のまま肥大化
  2. 外部依存リスク: APIレート制限、トークン期限切れ、仕様変更に振り回される
  3. ゲームバランス制御: 自前DBなら「タスク完了 = +X HP」を明確に定義可能

結果として、シンプルで堅牢なシステムが完成した。

CI/CDパイプライン

GitHub Actionsによる自動品質チェックを実装。

# .github/workflows/ci.yml
jobs:
  backend-test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: '3.11'
      - run: pip install -r requirements.txt
      - run: python -m compileall app/

  frontend-build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
      - run: cd frontend && npm ci
      - run: cd frontend && npm run build
        env:
          NEXT_PUBLIC_API_BASE: "http://localhost:8000"
          NEXT_PUBLIC_SUPABASE_URL: ${{ secrets.SUPABASE_URL }}
          NEXT_PUBLIC_SUPABASE_ANON_KEY: ${{ secrets.SUPABASE_ANON_KEY }}

効果:

  • デプロイ前にビルドエラーを検知
  • 依存関係の不整合(存在しないバージョン指定など)を事前発見
  • 導入後のデプロイ失敗率: 0%

実装で最も苦労した点

1. Horror UI実装:「不快だけど目を離せない」絶妙なライン

一番こだわったのは**「ギリ不快で、ギリ目をそらさないホラー演出」**の調整。

CRTスキャンライン

.scanlines::before {
  content: " ";
  display: block;
  position: fixed;
  top: 0;
  left: 0;
  bottom: 0;
  right: 0;
  background: linear-gradient(
    to bottom,
    rgba(18, 16, 16, 0) 50%,
    rgba(0, 0, 0, 0.25) 50%
  );
  background-size: 100% 4px;
  z-index: 50;
  pointer-events: none;
}

グリッチテキストアニメーション

@keyframes glitch-anim-1 {
  0% { clip-path: inset(20% 0 80% 0); transform: translate(-2px, 1px); }
  20% { clip-path: inset(60% 0 10% 0); transform: translate(2px, -1px); }
  40% { clip-path: inset(40% 0 50% 0); transform: translate(-2px, 2px); }
  60% { clip-path: inset(80% 0 5% 0); transform: translate(2px, -2px); }
  80% { clip-path: inset(10% 0 70% 0); transform: translate(-1px, 1px); }
  100% { clip-path: inset(30% 0 50% 0); transform: translate(1px, -1px); }
}

.glitch-text::before {
  left: 2px;
  text-shadow: -1px 0 #ff00c1;
  animation: glitch-anim-1 2s infinite linear alternate-reverse;
}

.glitch-text::after {
  left: -2px;
  text-shadow: -1px 0 #00fff9;
  animation: glitch-anim-1 2s infinite linear alternate-reverse;
}

HPに応じた動的UI崩壊

const hpPercent = (pet.hp / pet.max_hp) * 100;
const isDead = pet.status === 'DEAD';

{isDead ? '† SIGNAL LOST †' : pet.name}

調整に時間がかかったポイント:

  • グリッチの頻度(激しすぎると不快、弱すぎると印象に残らない)
  • 色の選択(緑→黄→赤の段階的変化)
  • モバイル端末でのパフォーマンス(60fps維持)

2. HP減衰システム:時間でじわじわ死んでいく

from datetime import datetime, timezone

DECAY_COEFFICIENT = 0.5

def calculate_time_decay(pet: dict) -> dict:
    """
    指数関数的にダメージが増加(放置するほど加速度的に危険)
    """
    if pet['status'] == 'DEAD':
        return pet

    last_checked_str = pet.get('last_checked_at')
    if not last_checked_str:
        return pet

    try:
        last_checked = datetime.fromisoformat(
            last_checked_str.replace('Z', '+00:00')
        )
    except ValueError:
        return pet

    now = datetime.now(timezone.utc)
    hours_passed = (now - last_checked).total_seconds() / 3600.0

    if hours_passed <= 0:
        return pet

    damage = (hours_passed ** 2) * DECAY_COEFFICIENT

    current_hp = float(pet.get('hp', 100))
    new_hp = max(0.0, current_hp - damage)

    updated_pet = pet.copy()
    updated_pet['hp'] = new_hp

    if new_hp <= 0:
        updated_pet['status'] = 'DEAD'

    return updated_pet

ポイント: 単純な線形減衰ではなく、二乗関数を採用。「ちょっとくらい大丈夫」という油断を許さない設計。

3. Next.js App RouterとReact Contextの境界問題

多言語対応(i18n)実装時、Server ComponentとClient Componentの境界でContextが使えずHydrationエラーが頻発。

解決策: LayoutClientラッパーを作成し、境界を明確化。

// components/LayoutClient.tsx
'use client';
import dynamic from 'next/dynamic';
import { LanguageProvider } from '@/lib/i18n/LanguageContext';

const LanguageToggle = dynamic(
  () => import('@/components/ui/LanguageToggle'),
  { ssr: false }
);

export default function LayoutClient({ children }: { children: React.ReactNode }) {
  return (
    <LanguageProvider>
      <LanguageToggle />
      {children}
    </LanguageProvider>
  );
}

Phase 2構想:Solana Dynamic NFTs

現在のHOSTAGEはWeb2アプリだが、Phase 2でSolana統合を予定。

Dynamic NFTとは?

通常のNFTは静的だが、メタデータが変化するNFT。

HOSTAGEのキャラクターをDynamic NFTにすることで:

  • キャラクターの状態がNFTに反映(健康時は緑、クリティカル時は赤)
  • 生死記録が永続保存(完全な履歴がブロックチェーンに刻まれる)
  • Achievement NFT(7日生存、30日生存などのバッジ)
{
  "name": "HOSTAGE Subject #1337",
  "description": "A productivity companion whose fate depends on your discipline",
  "attributes": [
    {"trait_type": "Current Status", "value": "Critical"},
    {"trait_type": "Days Survived", "value": 23},
    {"trait_type": "Tasks Completed", "value": 156},
    {"trait_type": "Deaths", "value": 2}
  ]
}

フィードバック募集

聞きたいこと

  1. 心理学的効果: 実際に使ってみて、罪悪感は動機になりましたか?
  2. UI/UX: ホラー演出は「不快」と「興味深い」のバランスが取れていますか?
  3. 技術的改善点: コードレビュー、アーキテクチャ改善の提案があれば
  4. Phase 2構想: Solana統合について、面白そうな機能アイデアはありますか?

おわりに

「タスク管理が続かない」という悩みを持つ方、ぜひHOSTAGEを試してみてください。

そして、「こんな機能があったらもっと怖い(やる気が出る)」というアイデアがあれば、ぜひコメントで教えてください。

"Your negligence kills."

このコンセプトが、生産性ツールの新しい可能性を開くと信じています。


関連リンク:

Discussion