Skip to content

Soshi1234/wikigolf-portfolio

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

2 Commits
 
 
 
 

Repository files navigation

Wikiゴルフオンライン

🌐 Wikiゴルフオンライン

Wikipediaのリンクだけでゴール記事に辿り着け。
リアルタイム対戦 × NLP搭載CPU AI × ジャンル投票で、知識と直感を競い合うフルスタック対戦ゲーム。


📌 概要

「Wikipedia のリンクを辿って、ゴール記事に最短クリックでたどり着いた方が勝ち」——"Wiki Golf" という遊びをリアルタイムオンライン対戦ゲームとして、個人で0から企画・設計・開発しました。

CPU対戦(甘口 / 中辛 / 激辛の3段階AI)・オンラインPvP・ジャンル投票→ゴール投票の2段階合意・鬼モード(即ゲーム開始)など、繰り返し遊べるゲームデザインを実装。iOS App Store にもリリース済みです。

🌍 Web版 wiki-golf.com — インストール不要、ブラウザで即プレイ
📱 iOS App App Store(Capacitor製ネイティブアプリ)
開発体制 個人開発 — 企画・設計・実装・デプロイ・ストア申請を全て自分が担当

📂 ソースコードは非公開リポジトリで管理しています。閲覧をご希望の方は kanpailock@gmail.com までお気軽にご連絡ください。


🛠 技術スタック

サーバーサイド

技術 用途
Python 3.11 メイン言語(サーバー / AI / スクレイピング)
Flask Webフレームワーク(ルーティング / テンプレート)
Flask-SocketIO リアルタイム双方向通信(WebSocket + ロングポーリング)
Gunicorn + eventlet 本番WSGIサーバー(非同期ワーカー)
BeautifulSoup4 Wikipedia HTMLスクレイピング + リンク抽出

NLP / AI エンジン

技術 用途
正規表現トークナイザー(自作) 日本語テキストの形態素分割(漢字・カタカナ・英単語抽出)
セマンティッククラスター辞書(自作) 10カテゴリの意味分類(スポーツ・歴史・科学・技術等)
ハブ記事戦略 ジャンルタグ付きハブ記事による経路誘導
難易度パラメータ制御 top_n / lookahead / miss_rate で3段階AI(甘口 / 中辛 / 激辛)

フロントエンド

技術 用途
JavaScript (ES6+) クライアントロジック(Socket.IO / UI制御 / タイマー)
CSS3 (Glassmorphism) モダンUI(ガラス効果 / アニメーション / レスポンシブ)
HTML5 SPA 単一ページアプリケーション構成
Service Worker オフラインキャッシュ / PWA対応

iOS ネイティブ

技術 用途
Capacitor 8 Web → iOS ネイティブラッパー
Haptics リンクタップ時の振動フィードバック(LIGHT / SUCCESS)
StatusBar ダークモード / 背景色カスタマイズ
SplashScreen 起動画面制御

インフラ / DevOps

技術 用途
Render (Web Service) 本番サーバーホスティング(Python 3.11)
独自ドメイン (wiki-golf.com) カスタムドメイン運用
PWA (manifest.json) ホーム画面追加 / アプリライクな体験
sitemap.xml / robots.txt / OGP SEO最適化 / SNSシェア対応

セキュリティ

技術 用途
html_escape + 文字数制限 プレイヤー名のXSS防止
レート制限 (自作) 連打 / bot 防止(0.3〜0.5秒間隔制限)
ルーム数制限 DoS防止(同一SID最大2ルーム)
自動クリーンアップ 非アクティブルーム / レート制限エントリの定期削除

📊 プロジェクト規模

指標 数値
総コミット数 92+
Python 総行数 2,570 行(server.py / cpu_ai.py / wiki_api.py / config.py)
JavaScript 総行数 698 行(game.js)
CSS 総行数 2,035 行(style.css — Glassmorphism UI)
HTML テンプレート 484 行(SPA構成)
ジャンル別ゴール記事DB 1,061 行(genres.py — 8ジャンル × 難易度別)
合計コード行数 5,787 行
開発期間 約1ヶ月

🎮 ゲームフロー

                    ┌─────────────┐
                    │   ロビー     │
                    │  名前入力    │
                    │  モード選択  │
                    └──────┬──────┘
                           │
              ┌────────────┼────────────┐
              ▼            ▼            ▼
        ┌──────────┐ ┌──────────┐ ┌──────────┐
        │ CPU対戦   │ │ PvP作成  │ │ PvP参加  │
        │ 難易度選択│ │ ルーム発行│ │ コード入力│
        └────┬─────┘ └────┬─────┘ └────┬─────┘
             └────────────┼────────────┘
                          ▼
              ┌───────────────────────┐
              │  ジャンル選択 (10秒)   │  ← 鬼モードはスキップ
              │  8カテゴリから投票     │
              └───────────┬───────────┘
                          ▼
              ┌───────────────────────┐
              │  ゴール投票 (10秒)     │
              │  8候補から投票         │
              └───────────┬───────────┘
                          ▼
              ┌───────────────────────┐
              │     ゲーム開始         │
              │  スタート(人気記事)   │
              │  ゴール(投票で決定)   │
              └───────────┬───────────┘
                          ▼
              ┌───────────────────────┐
              │  リンク選択ループ      │
              │  ・20秒タイマー       │
              │  ・相手の進捗リアルタイム│
              │  ・相手の訪問リンク封鎖 │
              │  ・30クリック上限      │
              └───────────┬───────────┘
                          ▼
              ┌───────────────────────┐
              │  結果画面              │
              │  勝者 / クリック数     │
              │  両者の経路表示        │
              │  リマッチ or ロビーへ   │
              └───────────────────────┘

🏗 技術的な工夫とチャレンジ

1. リアルタイム対戦を支える Socket.IO 双方向通信設計

課題: ブラウザベースの対戦ゲームでは、複数ルームの並行管理・プレイヤーの接続切断・状態遷移の競合を安全に処理する必要がある。特にタイムアウトによる自動処理(投票締切・ゲーム開始)がスレッド間で競合すると、二重実行のリスクがある。

解決策: 「状態エポック」パターンを導入。状態遷移のたびにエポック値をインクリメントし、タイマースレッドは実行前に現在のエポックを検証。古い遷移に基づくスレッドは自動スキップされる。切断時は即削除せず60秒の再接続猶予を設定し、threading.Lock でルーム辞書の排他制御を実施。

def _bump_epoch(room):
    """状態エポックをインクリメント。古いスレッドの実行を防ぐ。"""
    room['state_epoch'] = room.get('state_epoch', 0) + 1
    return room['state_epoch']

# タイマースレッド: エポック一致時のみ実行(二重実行を完全防止)
def genre_timeout():
    time.sleep(GENRE_VOTE_TIMEOUT + 1)
    with app.app_context():
        if (code in rooms
            and rooms[code].get('state_epoch') == epoch
            and rooms[code]['state'] == 'genre_selection'):
            _resolve_genre_vote(code)

設計ポイント:

  • ルーム状態は waiting → genre_selection → goal_selection → wiki_playing → wiki_over の有限状態マシンで管理
  • rooms_lock による排他制御で、複数クライアントからの同時操作を安全に処理
  • 切断時は _pending_delete フラグで60秒のグレースピリオドを設け、再接続を許容

🎯 学び: リアルタイムシステムにおける「古いスレッドの実行防止」は、楽観的ロックのエポック概念で解決できる。分散システム設計の基本パターンとして応用が効く。


2. NLP ベースの CPU AI エンジン(3段階難易度)

課題: CPU対戦で「人間らしい」リンク選択を実現したい。単純なランダム選択では面白みがなく、外部NLP APIはレイテンシとコストの問題がある。

解決策: 外部API不要の軽量NLPエンジンを自作。3つのスコアリング要素を組み合わせ、難易度パラメータで知性の「強さ」を調整。

スコアリング要素 仕組み 重み
トークンマッチ リンクタイトルとゴールの形態素一致度 ×20
クラスターマッチ 10カテゴリの意味辞書で近接度を判定 ×8
ハブ記事ボーナス ジャンルタグ付きハブ記事を優先 ×15
# 日本語トークン化(漢字・カタカナ2文字以上 / 英単語3文字以上)
_TOKEN_PATTERN = re.compile(
    r'[\u4e00-\u9fff\u3040-\u309f\u30a0-\u30ff]{2,}|[a-zA-Z]{3,}'
)

# 難易度パラメータで「人間らしさ」を制御
CPU_CONFIG = {
    'mild':    {'top_n': 10, 'lookahead': False, 'miss_rate': 0.05, 'delay': (2,4)},
    'medium':  {'top_n': 5,  'lookahead': False, 'miss_rate': 0.05, 'delay': (3,6)},
    'extreme': {'top_n': 3,  'lookahead': True,  'miss_rate': 0.0,  'delay': (4,8)},
}

🎯 学び: 外部APIに依存せず正規表現+辞書ベースで実用的な日本語NLPを構築。難易度は「アルゴリズム変更」ではなく「パラメータ調整」で制御し、メンテナンス性と拡張性を両立。


3. Wikipedia HTML スクレイピングと3層フィルタリング

課題: Wikipedia には日付リンク・管理用名前空間・曖昧さ回避ページなどゲームに不適切なリンクが大量に存在し、そのままではゲーム体験が著しく低下する。

解決策: 3層のフィルタリングパイプラインを設計。DOM操作→正規表現→ブラックリストの順に不要リンクを除去し、キャッシュでパフォーマンスを最適化。

# Layer 1: navbox/metadataをDOM上で事前除去
for navbox in content_div.find_all(['div', 'table'],
        class_=re.compile(r'navbox|navbar|mw-authority-control')):
    navbox.decompose()

# Layer 2: 正規表現で日付・数量比較ページ等を排除
_BORING_RE = re.compile(r'^(\d{1,2}月\d{1,2}日|\d{1,4}年|...)', re.UNICODE)

# Layer 3: 名前空間ブラックリストで管理ページを完全排除
_EXCLUDE_PATTERNS = [r'^Wikipedia[:\-‐]', r'^Category:', ...]

パフォーマンス:

  • リンクキャッシュ: TTL 5分 / 最大200件(LRU風自動削除)
  • 人気記事キャッシュ: Wikimedia Pageviews API 3日分を1時間キャッシュ
  • エラー時リトライ: 最大3回 / ネットワーク失敗時はクリック数を消費しない(UX配慮)

🎯 学び: スクレイピングでは「何を取るか」より「何を除くか」の設計がUXを左右する。多段フィルタ構成により各層の責務が明確になりメンテナンス性が向上。


4. ジャンル投票 → ゴール投票の2段階合意プロトコル

課題: 対戦でお題に納得感を持たせるには合意形成が必要だが、投票に時間をかけすぎるとテンポが悪くなる。

解決策: 「ジャンル選択(8カテゴリ)→ ゴール投票(8候補)」の2段階投票。各フェーズ10秒タイムアウト付きで、未投票時はランダム決定。CPU対戦ではプレイヤーの投票を即採用するショートカットも実装。

難易度別の候補切り替え:

  • 普通: GOAL_BY_GENRE_EASY から8候補(定番の有名記事)
  • 難: EASY + HARD を結合して8候補
  • 鬼: 投票フェーズを完全スキップ → 即ゲーム開始

🎯 学び: 対戦ゲームのUXでは「待ち時間の許容範囲」をタイムアウトで明示管理し、必ずフォールバック(ランダム決定)を用意してデッドロックを回避する。


5. iOS ネイティブアプリ化(Capacitor 8 + Server-driven UI)

課題: Webアプリをネイティブとして配信しつつ、サーバー更新がアプリ審査なしに反映される仕組みが欲しい。

解決策: Capacitor 8 の server.url に本番URLを指定し、WKWebView からサーバーのHTMLを直接ロードする Server-driven UI パターンを採用。ネイティブ機能はクライアントJSで window.Capacitor の存在を検知して条件分岐。

// ネイティブ環境を自動検知してHapticsを統合
const isCapacitor = typeof window.Capacitor !== 'undefined';
function hapticLight() {
    if (isCapacitor && window.Capacitor.Plugins.Haptics) {
        window.Capacitor.Plugins.Haptics.impact({ style: 'LIGHT' });
    }
}

🎯 学び: Server-driven UI でストア審査なしの即時デプロイを実現。Web/ネイティブの分岐は最小限に留め、共通コードベースの保守性を維持。


6. セキュリティ: 多層防御設計

課題: 公開対戦ゲームではXSS攻撃・連打bot・DoSを想定した防御が必要。

解決策: 4層のセキュリティ対策を実装。

対策 実装
入力層 XSS防止 html_escape + 20文字制限
通信層 連打防止 クリック0.3秒 / ルーム作成0.5秒のレート制限
リソース層 DoS防止 同一SID最大2ルーム / 30分で自動削除
メモリ層 リーク防止 レート制限5分定期クリーンアップ / キャッシュ上限200件

🎯 学び: オンラインゲームでは「悪意あるユーザー」前提の多層防御が必須。各層が独立して機能するため、1層が突破されても他の層で防御できる。


🏛 アーキテクチャ

[ユーザー]
├── Web (PWA)                          ┌──────────────────────────┐
│   └── wiki-golf.com                  │    外部API               │
└── iOS App (Capacitor 8)              │    ├── Wikipedia HTML     │
        │                              │    │   └── リンク取得     │
        │  WebSocket / HTTP            │    └── Wikimedia Pageviews│
        │                              │        └── 人気記事       │
        ▼                              └────────────┬─────────────┘
┌───────────────────────────────────┐                │
│   Flask + Socket.IO / Render      │ ◄──────────────┘
│                                   │   HTTP (スクレイピング)
│   ┌─ server.py (843行) ─────┐    │
│   │  ルーム管理 (CRUD)        │    │
│   │  状態マシン (FSM)         │    │
│   │  ジャンル/ゴール投票       │    │
│   │  ゲームロジック            │    │
│   │  結果判定 / リマッチ       │    │
│   └──────────────────────────┘    │
│   ┌─ cpu_ai.py (299行) ─────┐    │
│   │  トークナイザー (正規表現)  │    │
│   │  セマンティックスコアリング │    │
│   │  難易度パラメータ制御      │    │
│   │  非同期ターン実行          │    │
│   └──────────────────────────┘    │
│   ┌─ wiki_api.py (233行) ───┐    │
│   │  HTMLスクレイピング        │    │
│   │  3層リンクフィルタ         │    │
│   │  LRUキャッシュ (200件)     │    │
│   │  人気記事キャッシュ (1h)    │    │
│   └──────────────────────────┘    │
└───────────────────────────────────┘
        │
        │  Socket.IO (双方向リアルタイム)
        ▼
┌───────────────────────────────────┐
│   クライアント                     │
│   ├── game.js (698行)  イベント駆動│
│   ├── style.css (2,035行) Glass UI │
│   ├── index.html (484行) SPA構成   │
│   └── sw.js            PWA対応    │
└───────────────────────────────────┘

📂 ディレクトリ構成

wikigolf/
├── server.py              # メインサーバー (Flask + Socket.IO, 843行)
│                          #   ルーム管理 / 投票 / ゲームロジック / リマッチ
├── cpu_ai.py              # CPU AIエンジン (299行)
│                          #   正規表現トークナイザー / セマンティックスコアリング
├── wiki_api.py            # Wikipedia スクレイパー (233行)
│                          #   HTML解析 / 3層フィルタ / キャッシュ
├── config.py              # 設定・定数 (134行)
│                          #   CPU難易度 / クラスター / ジャンル定義
├── genres.py              # ジャンル別ゴール記事DB (1,061行)
│                          #   8ジャンル × 普通・難の候補記事
├── gunicorn.conf.py       # Gunicorn設定 (eventlet, single process)
├── render.yaml            # Render デプロイ設定
├── requirements.txt       # Python依存パッケージ
│
├── templates/
│   ├── index.html         # メインSPA (484行)
│   ├── privacy.html       # プライバシーポリシー
│   └── terms.html         # 利用規約
│
├── static/
│   ├── game.js            # クライアントロジック (698行)
│   ├── style.css          # Glassmorphism UI (2,035行)
│   ├── manifest.json      # PWA マニフェスト
│   ├── sw.js              # Service Worker
│   ├── ogp.png            # OGP画像
│   ├── icon-192.png       # アプリアイコン
│   ├── icon-512.png       # アプリアイコン (大)
│   ├── sitemap.xml        # サイトマップ
│   └── robots.txt         # クローラー設定
│
└── ios-app/
    ├── capacitor.config.ts # Capacitor設定 (Server-driven UI)
    ├── package.json        # iOS依存 (Capacitor 8 / Haptics / StatusBar)
    └── ios/                # Xcodeプロジェクト

🚀 ローカル環境での起動

# 依存パッケージのインストール
pip install -r requirements.txt

# 開発サーバー起動
python server.py

# 本番実行 (Gunicorn + eventlet)
gunicorn --config gunicorn.conf.py server:app

# iOS ビルド (要Xcode)
cd ios-app
npm install
npx cap sync ios && npx cap open ios

🔒 セキュリティ設計

  • 入力サニタイズ: html_escape + 20文字制限でプレイヤー名のXSSを防止
  • レート制限: リンククリック 0.3秒 / ルーム操作 0.5秒の間隔制限で連打・botを防止
  • DoS対策: 同一SIDのルーム作成を2つに制限、30分非アクティブルームを自動削除
  • メモリ保護: レート制限エントリの5分定期クリーンアップ / キャッシュ上限200件
  • 環境変数: SECRET_KEY は Render Secrets で管理

👥 チーム

Yohaku Lab © 2026

About

Wikipedia link-clicking race game — real-time PvP, NLP-powered CPU AI, genre voting. Built with Flask, Socket.IO, and Capacitor.

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors