【第6回】モンスター育成ゲーム用APIをPythonのFastAPIで作ってみたい…けど大丈夫かな?

「本ページはプロモーションが含まれています」

こんにちは。ここまでの連載では

  • 第1回:モンスター育成ゲームAPIの概要
  • 第2回:シーケンス図(ガチャやフロー)をMermaidで可視化
  • 第3回:DB構造をMermaidのER図で考案(マイグレーションなし)
  • 第4回:FastAPIの環境構築とフォルダ構成
  • 第5回:ユーザー管理APIを実装し、CRUD操作の基本を学習

…という流れで準備してきました。

今回はモンスター管理にチャレンジします。ゲームっぽい要素が入ってくるので、ワクワクする一方で「何をどう管理すればいいんだっけ?」と戸惑いがちですが、、、

まずは“モンスターの種類”を登録・取得・削除できるAPIを作り、そのあとに“ユーザーが所有するモンスター”も追加してみる流れを。。。

1. モンスターに関するテーブルを振り返る

第3回のDB設計で書いたMermaid ER図を思い出しながら、以下のテーブルを用意しておく想定です。

  1. monsters: ゲーム内で登場するモンスター種(名前、レア度、ステータスなど)
  2. user_monsters: ユーザーが所有しているモンスター個体(user_id、monster_id、レベル、経験値…など)

まずは“monsters”テーブルのCRUDを実装し、それが問題なく動くようになってから“user_monsters”をやります。 最終的には「ユーザーがガチャでモンスターをゲットする」APIを作りたいのですが、それにはmonstersテーブルがきちんと動いている前提が必要なので、順番に進めます。

2. モンスター種モデルとDBテーブル

2.1 モデル定義(app/models.py)

以下のように、monsters テーブルを追加します。

# app/models.py (抜粋)
from sqlalchemy import Column, Integer, String, DateTime, func, ForeignKey
from sqlalchemy.ext.declarative import declarative_base

Base = declarative_base()

class User(Base):
    __tablename__ = "users"
    # ...(ユーザー定義は第5回で紹介した通り)

class Monster(Base):
    __tablename__ = "monsters"

    id = Column(Integer, primary_key=True, index=True)
    name = Column(String, nullable=False)
    rarity = Column(String, default="common")
    base_hp = Column(Integer, default=10)
    base_attack = Column(Integer, default=5)
    base_defense = Column(Integer, default=5)
    created_at = Column(DateTime, server_default=func.now())

class UserMonster(Base):
    __tablename__ = "user_monsters"

    id = Column(Integer, primary_key=True, index=True)
    user_id = Column(Integer, ForeignKey("users.id"), nullable=False)
    monster_id = Column(Integer, ForeignKey("monsters.id"), nullable=False)
    level = Column(Integer, default=1)
    exp = Column(Integer, default=0)
    obtained_at = Column(DateTime, server_default=func.now())

Monster: モンスター種を管理。rarity は "common" / "rare" / "legendary" など想定。 ★とかでもそのまま返却できるかな?それとも、intで管理とか?まぁ、一覧見たとき、一発でわかる文字列にしておきます。

UserMonster: ユーザーが所有している個体情報(今回は作りませんが、、、)

2.2 DBテーブルの作成

Base.metadata.create_all(bind=engine) を呼び出せば、monsters と user_monsters テーブルが作られるはずです。 (すでに第5回でユーザーテーブルを作成済み、その流れと同様に呼び出す)

3. MonstersのCRUDエンドポイントを実装

3.1 app/routers/monsters.py

以下の例では、モンスターの登録・取得・削除を実装しています。上級的にはステータス更新などもあり得ますが、まずは最低限の形で。。。

from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.orm import Session
from pydantic import BaseModel
from typing import Optional

from app.database import get_db
from app.models import Monster

router = APIRouter()

# Pydanticスキーマ
class MonsterCreate(BaseModel):
    name: str
    rarity: Optional[str] = "common"
    base_hp: Optional[int] = 10
    base_attack: Optional[int] = 5
    base_defense: Optional[int] = 5

class MonsterRead(BaseModel):
    id: int
    name: str
    rarity: str
    base_hp: int
    base_attack: int
    base_defense: int

    class Config:
        orm_mode = True

# CREATE (モンスター種の登録)
@router.post("/", response_model=MonsterRead)
def create_monster(monster_data: MonsterCreate, db: Session = Depends(get_db)):
    new_monster = Monster(
        name=monster_data.name,
        rarity=monster_data.rarity,
        base_hp=monster_data.base_hp,
        base_attack=monster_data.base_attack,
        base_defense=monster_data.base_defense,
    )
    db.add(new_monster)
    db.commit()
    db.refresh(new_monster)
    return new_monster

# READ (全モンスター種の一覧取得)
@router.get("/", response_model=list[MonsterRead])
def get_all_monsters(db: Session = Depends(get_db)):
    monsters = db.query(Monster).all()
    return monsters

# READ by ID
@router.get("/{monster_id}", response_model=MonsterRead)
def get_monster_by_id(monster_id: int, db: Session = Depends(get_db)):
    monster = db.query(Monster).filter(Monster.id == monster_id).first()
    if not monster:
        raise HTTPException(status_code=404, detail="Monster not found")
    return monster

# DELETE
@router.delete("/{monster_id}")
def delete_monster(monster_id: int, db: Session = Depends(get_db)):
    monster = db.query(Monster).filter(Monster.id == monster_id).first()
    if not monster:
        raise HTTPException(status_code=404, detail="Monster not found")
    db.delete(monster)
    db.commit()
    return {"detail": "Monster deleted"}
  • POST /monsters:モンスター種の新規登録
  • GET /monsters:全モンスター種を一覧取得
  • GET /monsters/{id}:個別取得
  • DELETE /monsters/{id}:削除

3.2 main.py へのルーター追加

app.include_router(monsters.router, prefix="/monsters", tags=["monsters"])

実装後のSwagger

4. 動作確認

  1. POST /monsters に下記のようなJSONを送信
{
  "name": "Slime",
  "rarity": "common",
  "base_hp": 10,
  "base_attack": 5,
  "base_defense": 5
}

実行

  1. GET /monsters で一覧が見られるか確認

GET ALL

  1. GET /monsters/1 で特定IDを取得

GET id

  1. DELETE /monsters/1 で削除テスト

delete 1

5. 今後の展望:ユーザーが所有するモンスター

「monster種」の管理ができたところで、次の一歩としてuser_monstersを操作するAPIを検討します。

  • POST /users/{user_id}/owned-monsters で所有モンスターを追加
  • GET /users/{user_id}/owned-monsters でユーザーが持っている個体一覧を取得
  • PUT /users/{user_id}/owned-monsters/{owned_monster_id} でレベルアップ・経験値加算などの更新

…など

ここを実装すると、やっとゲームらしい「育成」要素が少しずつ形にできそうです。 また、ガチャでランダムにモンスターを引く仕組みを作るなら、monstersテーブルの中から確率に応じて1体を選出してuser_monstersに追加する、というロジックが必要になってきます。

6. まとめ

  • モンスター種の管理を実装し、ゲームらしいデータ構造が見えてきた
  • 新しいテーブル (monsters) を追加して、FastAPIのAPIRouterでCRUDを定義する手順はユーザーAPIと同様
  • 今後はuser_monstersやガチャ機能などを作って、育成ゲームに近い形へ発展させる予定

これで第6回「モンスター管理APIの実装」が完了です。 次回は「ユーザーが所有する個体」をどう扱うか、UserMonsterテーブルを軸にしてAPIを充実させたいですね。あるいはガチャ機能を先にやってみても面白そうです。少しずつ手を動かしながら進めます。

本来は、UnitTestを行う予定でしたが、ガチャ実装を次回にします。