0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【個人開発】Discord×OpenAIでGM不要のTRPGを作りました

Last updated at Posted at 2025-07-25

記事の概要

私が作成したDiscordBotの概要です。
一般向けリリースはまだですが、基本機能は実装完了したので下記にまとめます。

背景

① 学習目的

Pythonの言語理解を深めると同時に、AWS上でのサーバレス環境構築の学習も兼ねて開発を進めました。
本プロジェクトでは以下のAWSサービスを採用しています:

  • DynamoDB:ユーザ情報・ゲームデータの保存
  • AWS Lambda:バックエンド処理
  • API Gateway:BotとLambda間の連携
  • S3:静的Webホスティング

②アプリケーション概要

現在、ゲームバランスの調整・バグ修正・ドキュメント整備中のため、一般公開前の状態ですが、
ホスティングすれば問題なく稼働する段階まで完成しています。

DiscordBotでできること(VC移動・チャンネル制御など)とTRPGを組み合わせたら面白いのでは?
という発想から開発に着手しました。

また、TRPGを遊ぶためには

ツールの使い方を覚えて、GM(ゲームマスター)を用意して、どのシナリオで遊ぶか選定して...ルールブックも買って...

と、カジュアル層からは敷居が高く思えます。

そこで今回のBot開発では、次の3点を意識しました:

  • Discordで遊べる(アカウントさえあれば導入不要)
  • ルールブック不要(シンプルな内部ロジック)
  • GM不要(Botがダイスで判定を自動処理/生成AIを用いた舞台演出)

何度も遊んでもらうために、人狼ゲームをベースに設計することでリプレイ性を確保しつつ、
没入感と物語性のある体験を生成AIを活用して提供することを目指しています。

スペック

項目 内容
言語 Python 3.11
DBMS Amazon DynamoDB
フロントエンド Discord Bot UI(スラッシュコマンド、ボタン、モーダル)
構成管理 YAML
外部API OpenAI GPT-4o API
バックエンド AWS Lambda(Python)+ API Gateway
ストレージ S3

特に注力した機能や処理

  • ①プレイヤーの参加 / ロール割り当て / ステータス管理
    Discordサーバー上にマップを作成
    Animation.gif
     
    このとき、DynamoDB上にゲームのマップデータが作成され、マス毎に設定されている初期イベントと地形情報が登録されます。
    全ての初期データはyamlに定義しており、キャッシュ化して読み取ることで軽量化を図っています。

image.png

また、マス毎のテキストチャンネルと地形毎のVCを作成します。
プレイヤーの現在地の情報に基づいて適宜権限を与え、そこでコマンドを実行します。VCは地形情報に基づいて自動的に移動されます。

image.png

マップ作成後、プレイヤー登録
ステータス割り振りもここでできます。
Animation2.gif

このタイミングでマップデータとは別のテーブルにプレイヤーデータが作成されます。

PlayerStatus(一部抜粋)
{
  "user_id": {
    "S": "1085146937965158431#1034070510184714330"
  },
  "dexterity": {
    "N": "9"
  },
  "display_name": {
    "S": "TEST"
  },
  "guild_id": {
    "S": "1034070510184714330"
  },
  "hp": {
    "N": "150"
  },
  "intellect": {
    "N": "5"
  },
  "max_hp": {
    "N": "150"
  },
  "max_stamina": {
    "N": "30"
  },
  "role": {
    "S": "森の住人"
  },

ステータス管理はこのテーブルを使用します。
ゲームという特性上機能追加のたびに柔軟に項目を増やせるよう、RDBではなくDynamoDBを選択しました。
また、user_idguild_idを識別キーとしており、異なるサーバー間で処理を分けられるよう設計しています。


  • ②マップ移動・座標管理・地形に応じたVC&テキスト制御
    ゲームを開始するとランダムに現在地のマップデータがPlayerStatusテーブルに登録されます。
    PlayerStatusテーブルのプレイヤーの現在地を参照し、Botがユーザーにchannel権限を与えます。
    このとき権限が与えられたテキストchannelがコマンドを実行する場所となります。
    本当はVCもマス毎に作成しようと思ったのですが、
    VCの移動が多くなりBotに負荷がかかる点
    全25マスあるので、他のプレイヤーと会話しないでゲームが終わる可能性がある
    等の理由から、地形毎に変更しました。
    これにより、
     ■近くのプレイヤー同士でのみ会話できる
     ■同じマスにいるプレイヤーには行動がバレる
    という要素を実現しています。

Animation3.gif

image.png

移動時などプレイヤーが行動した場合、Botから情景描写が返ってきます。
地形毎の情報をyamlに定義しており、それをプロンプトとしてOpenAIに渡しています。

image.png


  • ③ターン制進行(全員の行動完了後に自動でターンが切り替わる)
    プレイヤー全員の行動完了を監視し、ターンの進行やリマインド、未行動者の自動処理を行う非同期ループ処理です。
    各ターンにおいて、全プレイヤーが行動を完了したかを継続的に確認
    一定時間行動しなかったプレイヤーに個別リマインド
    複数回リマインドしても行動がなければ、自動的に "何もしない" 処理でターンを進行

詳細について以下に記載していきます。

1.プレイヤーが行動したとき、テーブル側のプレイヤーデータを行動済にすると共に、サーバー固有のguildIDを辞書に登録します。

turn_check_queues
turn_check_queues = {
    "123456789012345678": True,   # ギルドID: 監視中フラグ
    "987654321098765432": False
}

2.↑をトリガーとして以降の処理を動かしています。

turn_check_queues
while turn_check_queues.get(guild_id, False):

このフラグを挟むことで無限ループを防ぎ、ゲーム進行中にイレギュラーが発生したときにもゲームを止めたり、対応しやすくしました。

3.アクティブなプレイヤーの取得

turn_handler.py
participants = await get_all_active_participants(guild)
unacted = [(member, pdata) for member, pdata in participants if pdata.get("turn_status") == "ready"]

 ⇒ターン中で「まだ行動していないプレイヤー(turn_status が ready)」を抽出します。

4.全員が行動済みの場合はターンを進行

turn_handler.py
if not unacted:
    await update_game_flag(...)
    await check_and_advance_turn(...)

 ⇒状態フラグを更新し、ターン処理を実行。

5.リマインド処理(待機時間が一定を超えた場合)

turn_handler.py
if elapsed >= reminder_threshold and reminder_count < max_reminders:
    await private_channel.send(embed=build_embed(...))

 ⇒elapsed が閾値を超えた場合に、未行動者へ個別通知します。

6.自動スキップ処理(リマインドを3回送っても反応がない場合)

turn_handler.py
if reminder_count >= max_reminders:
    await create_private_text_channel(...)
    await perform_do_nothing_action(...)

 ⇒戦闘中・イベント中でなければ、「何もしない」という行動を自動実行。


  • ④イベント / アイテム / 戦闘 / 儀式などのTRPG要素を全自動処理
    TRPGなので、演出にプレイヤーが干渉できる要素を設けました。
    例えば攻撃時には何の武器で、誰を、どのように攻撃するか?を求められます。
    kougeki_Animation.gif

裏で攻撃の成功・失敗も判定しています。
それら諸々の情報をJSON形式でAWS lambdaへ渡し、lambda上でaction_type毎に分けたフォーマットに当てはめてプロンプトにしています。

AWS lambdaに渡す情報
{
  "action_type": "attack",
  "context": {
    "attacker_name": "TEST",
    "target_name": "Aether",
    "str": 5,
    "power": 30,
    "weapon_name": "森の石の斧",
    "weapon_description": "丈夫な石の斧。大きな木でも切り倒してしまえそう。武器にもなりそう。",
    "attack_rp": "斧を思いっきり振り下ろす",
    "result": "TESTは的確に攻撃を繰り出すことができる。"
  }
}

攻撃を受けた側がまだ行動を行っていない場合は、抵抗や回避を選択することができます。
抵抗や回避をした場合はターン消費扱いとなり、判定やプロンプトも変化します。
teikou_Animation.gif

リアルタイムでターン進行する都合上、行動済・行動可能の不具合を潰していくのが非常に大変でした。
最終的にはプレイヤーに「今何をしているか」のデータをテーブルに持たせて、攻撃やイベントの処理中であればターン処理やコマンド入力などを拒否する形式を採用しています。
この形式だと特に、例外が発生したときに初期化を忘れてゲームが進行できないということになりかねないので、これからさらに例外が起きそうな箇所を潰しておく必要があります。


フォルダ構成

project-root/
├── bot_main.py                   # Discord Botエントリーポイント
└── bot/
    ├── commands/                 # /コマンド群(移動・攻撃など)
    ├── services/                 # 各種ビジネスロジック
    ├── ui/                       # Discord UI(ボタン、モーダル)
    ├── config/                   # 地形・アイテム・パラメータ設定(YAML)
    ├── trpg_utils.py             # 共通ユーティリティ関数
    └── .env                      # トークン・APIキーなどの機密情報を記載

ざっくり↑のようにまとめていますが、まだまだ散らかっているのでこれから整理していく必要があります。


工夫した点

  • OpenAIによるナラティブ演出プロンプトの整形と最適化
  • リアルタイムターン制御:行動完了者が出るたびにキュー処理→一定時間後に自動ターン進行
  • guild_idごとの非同期進行処理(マルチサーバ同時進行対応)
  • OpenAI API + DynamoDB を組み合わせたイベント処理構成
  • 非公開チャンネルによるロールプレイの秘匿性確保
  • UXを重視し、プレイヤーが迷わず行動できる直感的設計

まとめ

本記事では、DiscordとAWSを活用したDiscord上で遊べる非同期ターン制TRPG「RoD」の開発概要について紹介しました。
本当はイベントの設定とかアイテム使用効果はそれぞれ設定する必要があり、最も時間がかかったところなのですが、すべて書くとキリがないため主要機能のみピックアップして取り上げました。

今後も引き続き本番リリースに向けた改善を進めていく予定です。
ユーザー側でシナリオのカスタマイズとかできると面白そうだなと考えているので、安定稼働できればそのあたりにも着手していこうと考えています。

関連リンク

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?