見出し画像

Claude Fable 5でマスターデータエディタを作ってみた。一気通貫で...できたじゃねぇか

今回はFable 5で、ゲームで使うマスターデータを編集するマスターデータエディタを作ってみました。

ここでは、マスターデータを以下のように定義しています。

マスターデータとは アイテムや武器のパラメータや、ストーリーやイベント、開催期間などのスタティックなデータがそれぞれ個別にまとめられたデータ群のこと 大体Excelでやっていることが多い
イメージとしては いわゆるデータベースのテーブル定義にデータが入っていて、それぞれ外部参照などでリレーションしている

マスターデータとは

このように定義した上で、これらのデータを作ったり、編集したりするWindows用のツールを作ります。

ちなみにマスターデータの作業風景はこんな感じ。

前回Fable 5で作った時のように、事前に仕様を詰めて実装しました。

今回の所要時間は、
仕様詰めに10分程度、実装に26分くらいで、5時間のリミット内で収まり、一気通貫で実装できました(速)

プランとバージョンは以下の通りです。

Claude Pro
Claude Code 2.1.215
Fable 5

プランとバージョン

それでは実装してきます。

実装

そもそも、
マスターデータはExcelを使うことが多いけど、gitで管理する際にコンフリクトが面倒なのと、差分がわかりにくい(もちろん費用も)
結局ゲームで使うのはテキストもしくは形のわかっているバイナリなので、それを最初からゲームに沿った入力ツールがあればそれでいいのでは、
というところがスタートです。

まず、こんな感じで話題を挙げて詰めていきました。

ゲームのマスターデータエディタを作ろうと思うので概要まとめて、質問はまとめて答えるから

まず マスターデータとは アイテムや武器のパラメータや、ストーリーやイベント、開催期間などのスタティックなデータがそれぞれ個別にまとめられたデータ群のこと
大体Excelでやっていることが多い

イメージとしては いわゆるデータベースのテーブル定義にデータが入っていて、それぞれ外部参照などでリレーションしている

今回作るツールは Windows用アプリケーション c#で実装したexe
Excelではなくcsvでデータを管理し
ツール上でデータ定義と入力と保存

入力はExcelのような表画面 ヘッダー行があり各列にはそれぞれのマスターに応じた項目がある
列は名称、型、ユニークキーであるか、カラム名、他のマスターとリレーションしているならマスターと列名の情報を持つ
行はマスターに応じた項目が入っている
入力確定でバリデーションする
列の追加や削除ができる 大量のデータを入れる時があるので、マスターに対して

ツール画面は2ペインあり 左側に、
マスターデータのリスト 右側に、選んだマスターデータの表画面を表示
上部のWindowsメニューから マスターデータの新規追加や、ツールの設定を行うダイアログを表示する
設定類はこれから決める

全マスターはオンメモリで処理する
変更したら都度メモリを更新しcsvファイルも保存する

クライアント用に
c#で読めるクラスで定義を出力 データはシリアライズしてバイナリファイルとして出力
サーバ用に
データベーステーブル定義とinsert文出力 c#でアクセスクラスも出力
データベースはmysql8.x系

enum的なデータも作りたい
これはマスター定義に種別を持たせよう
今は通常、enumの二つ
enumは、数値、定義名、コメントの3カラム

バイナリはマスター毎1ファイルでいい
マスター毎に1ファイル

あと、レコード単位で、項目とは別に出力する/しないをチェックできるようにして
要はデータ入力しておいて出力には含めないという機能
ツール上だけで使う
出力はid歯抜けになってもいい

ざっと書きました

仕上がりのイメージは、
Windows用のアプリで、
表計算のようなUIでデータを入力して、
C#のクラスとシリアライズされたデータ、
データベースのcreate文、insert文を出力する
というツールの想定です。

割と作りたいイメージが最初からしっかりあったので、仕様書つくりは10分程度でした。

CLAUDE.mdがこちらです。

# CLAUDE.md

ゲーム用マスターデータエディタ。C# / WinForms製のWindowsデスクトップアプリ。
マスターデータのスキーマ定義・入力・バリデーションと、クライアント(Unity)/サーバ(MySQL, C#)向けの出力を行う。

## ドキュメント(必読)

- `docs/master-data-editor-spec.md` — 機能仕様。「何を作るか」の正
- `docs/technical-spec.md` — 技術仕様。構成・ファイルフォーマット・生成物の形の正

実装と仕様が食い違ったら仕様を正とする。仕様自体を変える場合は、先にドキュメントを更新してから実装する。

## コマンド

Windows上で実行すること(WinFormsのためLinux/WSLではビルド不可)。

```
dotnet build src/MasterDataEditor          # ビルド
dotnet run --project src/MasterDataEditor  # 起動(--data <path> でデータフォルダ指定可)
dotnet test                                # テスト
```

## プロジェクト構成

```
src/MasterDataEditor/
  Models/      メモリ上のスキーマ・データ表現(他層に依存しない)
  Io/          schema.json / CSV / settings.json の読み書き
  Validation/  検証・ユニークキー辞書・逆参照インデックス
  Export/      バイナリ・C#・SQLの生成
  Ui/          WinForms(MainForm・各ダイアログ)
tests/MasterDataEditor.Tests/
data/          実データ(schema.json / *.csv / settings.json)
```

依存方向は `Ui → Io / Validation / Export → Models`。逆方向・層間の横依存は禁止。

## 実装ルール

- 実行時のNuGet依存は追加しない(テストのxUnit系のみ可)。必要になったら実装前に提案する
- ツール本体は最新のC#でよい。**Exportが生成するコードのみC# 9以下の文法に限定**(Unity互換)
- WinFormsデザイナは使わない。UIはコードで構築する
- CSV/JSONの入出力はIo層に集約。行順・列順をツール都合で並べ替えない
- ファイル保存は必ずIoのアトミック置換ヘルパーを経由する
- 数値・日時の変換はInvariantCulture固定
- 識別子の `_` 始まりはツール予約。生成物・データに使わない
- `data/` 配下や出力物のフォーマットを勝手に変えない(変えるなら技術仕様を先に更新)

## テスト

- Io / Validation / Export はユニットテスト必須。UI層は対象外
- テストはグリーンを維持し、フォーマット変更時はテストも同時に更新する

今回CLAUDE.mdはシンプルというか短めでした。
詳しくは仕様書にという感じですね。

そしてこちらが仕様書

# ゲームマスターデータエディタ 仕様書(v0.3)

## 1. 目的・概要

ゲームのマスターデータ(アイテム・武器のパラメータ、ストーリー、イベント、開催期間などの静的データ)を、Excelに代わって管理する専用エディタ。
スキーマ定義・データ入力・バリデーション・クライアント/サーバ向け出力を1つのツールで完結させる。

## 2. 技術構成

| 項目 | 内容 |
|---|---|
| 形態 | Windows用デスクトップアプリ(exe) |
| 言語 / UI | C# / WinForms + DataGridView |
| ランタイム | .NET 10 (LTS) |
| 共有 | Gitリポジトリに入れて共有(配布形態は考慮しない) |
| データ永続化 | CSV(UTF-8・BOM無し) |
| 対象DB | MySQL 8.0 |
| クライアント | Unity(生成コードはUnityで動作する範囲のC#に限定) |

## 3. マスターの構成とデータ管理

- **データフォルダ**: exeと同一フォルダ内の `data/` 配下に schema.json / CSV / settings.json を置く
- **想定規模**: 1マスターにつき最大1,000行程度 × 100マスター程度(DataGridViewは標準バインディングで足りる規模。VirtualMode不要)
- マスターは2種別
  - **通常**: 任意の列定義を持つ
  - **enum**: 固定3列(数値・定義名・コメント)。C#のenumの定数として出力するための定義用マスター
- 1マスター = スキーマ定義ファイル + データCSV のペア(例: `Item.schema.json` + `Item.csv`)
- 全マスターを起動時にオンメモリへロード。編集確定のたびにメモリを更新し、CSVも即時保存する
- 行順は入力順を保持し、自動ソートは行わない(基本は末尾追加、手動での並び替えは可能)
- **出力フラグ**: 各レコードが「出力する/しない」のチェックを持つ(ツール内専用の機能)
  - CSVでは先頭の予約列 `_out`(1/0)として保存
  - OFFの行はすべての出力(バイナリ・INSERT等)から除外する。出力結果のIDが歯抜けになっても構わない
  - CSVインポート時に `_out` 列が無い場合は全行ON扱い

## 4. スキーマ(列定義)

各列は以下の属性を持つ。

| 属性 | 説明 |
|---|---|
| 表示名 | グリッドのヘッダーに表示する名称 |
| カラム名 | 物理名。C#プロパティ名・DBカラム名に使用 |
| 型 | 5章の型一覧から選択(enum型は参照先enumマスターを指定) |
| ユニークキー | キー列か否か。複数列指定で複合キーを構成可能 |
| リレーション | 任意。参照先のマスター名+列名。**参照先は単一列ユニークキーの列に限定** |

- 列の追加・削除が可能
- 全列NOT NULL前提(必要になったらnullableフラグを追加)
- enumマスターの列定義は固定(数値: 整数 / 定義名: 文字列 / コメント: 文字列)のため編集不可

## 5. 型マッピング(MySQL 8.0 ⇔ C#)

| MySQL | C# |
|---|---|
| TINYINT / SMALLINT / INT / BIGINT | sbyte / short / int / long |
| 同UNSIGNED | byte / ushort / uint / ulong |
| FLOAT / DOUBLE / DECIMAL | float / double / decimal |
| BOOL(TINYINT(1)) | bool |
| CHAR / VARCHAR / TEXT | string |
| DATE / DATETIME | DateTime |
| TIME | TimeSpan |

- **enum型**(ツール独自型): 参照先enumマスターを指定。C#出力は生成されたenum型、DB出力はINT。データ上は数値で保持する
- 対象外: BLOB、MEDIUMINT、TIMESTAMP(DATETIMEに統一)、SET / JSON / BIT / YEAR(MySQLのENUM型も使わない)
- Unity互換のため DateOnly / TimeOnly は使用しない

## 6. スキーマ定義ファイル(schema.json)

通常マスターの例:

```json
{
  "name": "Item",
  "displayName": "アイテム",
  "kind": "normal",
  "columns": [
    { "name": "Id", "displayName": "ID",
      "type": "INT", "unsigned": true, "key": true },
    { "name": "Name", "displayName": "名称",
      "type": "VARCHAR", "length": 64 },
    { "name": "ItemType", "displayName": "種別",
      "type": "ENUM", "enumMaster": "ItemType" },
    { "name": "RarityId", "displayName": "レア度",
      "type": "INT",
      "relation": { "master": "Rarity", "column": "Id" } }
  ]
}
```

- VARCHAR / CHAR は `length` 必須、DECIMAL は `precision` / `scale` を持つ
- enumマスターの schema.json は `"kind": "enum"` と `name` / `displayName` のみ(列は固定のため)

## 7. 画面構成・設定

- 2ペイン構成
  - 左: マスターデータ一覧
  - 右: 選択中マスターのグリッド(Excelライクな表編集)
- グリッド左端に出力フラグのチェックボックス列を固定表示
- 上部メニュー
  - マスター新規作成(種別: 通常 / enum を選択)
  - 設定ダイアログ
- 設定ダイアログの項目
  - コンバート出力先パス(`data/` からの相対パス推奨)
  - 難読化用XORキー
  - 禁止ワードのユーザー追加分の管理
- 設定は `data/settings.json` に保存(git共有前提)

## 8. 入力・バリデーション

- 入力確定時に実行
  - 型チェック
  - ユニークキー(複合含む)の重複チェック
  - リレーション列の参照先存在チェック
  - enum列: 参照先enumマスターに存在しない数値を弾く
  - enumマスターの定義名: C#の識別子として妥当かチェック
- **禁止ワードチェック**: マスター名・カラム名・enum定義名に適用
  - 組み込みの予約語リスト(MySQL 8.0 / C#)は常時禁止
  - ユーザーが設定画面から追加した禁止ワードも対象
  - `_` 始まりの名前はツール予約のため禁止(`_out` 等との衝突防止)
- リレーション列・enum列の入力方式
  - インクリメンタル検索(ID/名称で候補を絞り込み)+直接入力の併用
  - セル表示はID+名称の併記(例: `101 (スライム)`)
- 逆参照チェック: 他マスターから参照されている行を削除しようとした場合は警告し、参照元を提示する(削除自体は可能)
- CSVインポート: マスター単位で一括投入。上記バリデーションを通す
- 出力実行時チェック: 出力ONの行が出力OFFの行を参照している場合は警告する

## 9. 出力

すべてメニューから手動実行する。出力先は設定ダイアログで指定したパス。出力フラグOFFの行は全出力から除外する。

### 9.1 クライアント向け(Unity)

- 各マスターのC#クラス定義(BinaryWriter / BinaryReader による読み書きメソッドを同梱。BinaryFormatterは不使用)
- enumマスターはC#のenum型として生成し、コメントはXMLドキュメントコメントに出力
- データはマスター毎に1バイナリファイル(例: `Item.bytes`。UnityのTextAssetでそのまま読める)
- XORキーのクライアント側への受け渡しはツールのスコープ外(git等で別途共有する)

バイナリフォーマット:

```
magic          4B    "MST1"(ファイル種別の誤り検知)
formatVersion  int   バイナリ形式自体のバージョン
schemaHash     uint  schema.jsonから算出。ロード時に照合
recordCount    int   レコード数
records        ...   レコード×N(列順にシリアライズ)
```

- string: 長さプレフィックス付きUTF-8(BinaryWriter標準) / DateTime・TimeSpan: Ticksをlongで / bool: 1バイト / リトルエンディアン固定
- 難読化: records部のみXOR(キー文字列のUTF-8バイト列を循環適用)。ヘッダーは平文のまま残し、形式チェックを先に通せるようにする
- 圧縮: なし(必要になったらヘッダーにフラグを追加)
- formatVersion / schemaHash 不一致時はロードエラー

### 9.2 サーバ向け

- CREATE TABLE 文(MySQL 8.0)
- INSERT 文
- アクセスクラス(C#)
- enumマスター: DBテーブルは出力せず、C#はenum型クラスとして出力

## 10. 実装メモ

- CSVはRFC 4180準拠で実装(セル内改行・カンマ・ダブルクォートに対応。ストーリー系テキストで必ず出る)
- 数値・日時のパース/出力はInvariantCulture固定。floatはラウンドトリップ書式、日時は `yyyy-MM-dd HH:mm:ss` 固定(タイムゾーン情報なし)
- CSV保存はテンポラリに書いてから `File.Replace` でアトミック置換(クラッシュ時の破損防止)
- ロード時にユニークキー辞書と逆参照インデックスを構築し、編集時に差分維持する(参照チェックを都度全走査にしない)
- 生成コードはC# 9以下の文法に限定(Unity互換)
- WinFormsデザイナ非依存でコードを直書きし、.NET SDKのみでビルド可能にする

## 11. 将来拡張の候補

- 圧縮(GZip等)対応
- バイナリの一括パック形式
- nullable列 / 配列型
- enum以外のマスター種別

ページの都合で全部掲載は難しいのですが、それぞれ細かく書いてあります。

そして実装は、

CLAUDE.mdを読んで、実装して

これで
実装開始です

今回の実装はゲームとは違って、淡々と実装していく感じだったので出力は割愛しました(笑)

実装時間は、25分56秒でした(スゴ)

コンテキストウインドウの使用状況はこんな感じでした。

コンテキストウインドウはこのような感じ

セッションもリセットからスタートして85%で収まった感じです。

トークン消費量

ツール画面

ベーシックなWindowsアプリのインターフェースにしました。
というのも、いろいろモジュールとか入れずにどこでも動くようにと。
今風じゃないのはまぁそのうちにでも(笑)

通常系のマスター

これがマスターデータエディタ画面です。
シンプルですね(笑)
ここには左のペインで選んだマスターデータの内容を表示します。
カラム変更、データの追加や編集をします。

enum系

enum系もこのような形の画面で編集します。

新規作成

これはマスターデータの新規作成です。
名前とタイプを決めます。

新しく作るマスターの構造

名前とタイプが決まったら、カラムを追加してマスターデータの形を決めます。

設定

設定画面です。
ここにはいろいろツールの設定を詰めて行きます。
xorで簡単な符号化をしたりとか、使ってはいけないカラム名などを定義したりします。

出力

エクスポートです。
これで実際のゲームで使えるデータにする、という感じですね。

エクスポートしたソース類

MasterBinary.cs

using System;
using System.IO;
using System.Text;

namespace MasterData
{
    /// <summary>マスターバイナリ共通ヘルパ(ヘッダー検証・XOR復号)</summary>
    public static class MasterBinary
    {
        public const int FormatVersion = 1;
        private const int HeaderSize = 16;

        /// <summary>
        /// ヘッダーを検証し、records部を復号したBinaryReaderを返す。
        /// magic / formatVersion / schemaHash 不一致は InvalidDataException。
        /// </summary>
        public static BinaryReader OpenRecords(byte[] data, byte[] xorKey, uint schemaHash, out int recordCount)
        {
            if (data == null || data.Length < HeaderSize)
            {
                throw new InvalidDataException("master binary: data too short");
            }
            if (data[0] != (byte)'M' || data[1] != (byte)'S' || data[2] != (byte)'T' || data[3] != (byte)'1')
            {
                throw new InvalidDataException("master binary: bad magic");
            }
            int formatVersion = BitConverter.ToInt32(data, 4);
            if (formatVersion != FormatVersion)
            {
                throw new InvalidDataException("master binary: format version mismatch: " + formatVersion);
            }
            uint hash = BitConverter.ToUInt32(data, 8);
            if (hash != schemaHash)
            {
                throw new InvalidDataException("master binary: schema hash mismatch");
            }
            recordCount = BitConverter.ToInt32(data, 12);

            byte[] records = new byte[data.Length - HeaderSize];
            Buffer.BlockCopy(data, HeaderSize, records, 0, records.Length);
            if (xorKey != null && xorKey.Length > 0)
            {
                for (int i = 0; i < records.Length; i++)
                {
                    records[i] ^= xorKey[i % xorKey.Length];
                }
            }
            return new BinaryReader(new MemoryStream(records), Encoding.UTF8);
        }
    }
}

Item.cs

using System;
using System.Collections.Generic;
using System.IO;

namespace MasterData
{
    /// <summary>アイテム</summary>
    public sealed class Item
    {
        public uint Id;
        public string Name;
        public ItemType ItemType;
        public int RarityId;

        public const uint SchemaHash = 0x672AD2E8u;

        public static List<Item> Load(byte[] data, byte[] xorKey)
        {
            int recordCount;
            using (BinaryReader reader = MasterBinary.OpenRecords(data, xorKey, SchemaHash, out recordCount))
            {
                List<Item> list = new List<Item>(recordCount);
                for (int i = 0; i < recordCount; i++)
                {
                    Item record = new Item();
                    record.Id = reader.ReadUInt32();
                    record.Name = reader.ReadString();
                    record.ItemType = (ItemType)reader.ReadInt32();
                    record.RarityId = reader.ReadInt32();
                    list.Add(record);
                }
                return list;
            }
        }
    }
}

Rarity.cs

using System;
using System.Collections.Generic;
using System.IO;

namespace MasterData
{
    /// <summary>レア度</summary>
    public sealed class Rarity
    {
        public uint Id;
        public string Name;

        public const uint SchemaHash = 0x608F3511u;

        public static List<Rarity> Load(byte[] data, byte[] xorKey)
        {
            int recordCount;
            using (BinaryReader reader = MasterBinary.OpenRecords(data, xorKey, SchemaHash, out recordCount))
            {
                List<Rarity> list = new List<Rarity>(recordCount);
                for (int i = 0; i < recordCount; i++)
                {
                    Rarity record = new Rarity();
                    record.Id = reader.ReadUInt32();
                    record.Name = reader.ReadString();
                    list.Add(record);
                }
                return list;
            }
        }
    }
}

Item.sql

DROP TABLE IF EXISTS `Item`;

CREATE TABLE `Item` (
  `Id` INT UNSIGNED NOT NULL,
  `Name` VARCHAR(64) NOT NULL,
  `ItemType` INT NOT NULL,
  `RarityId` INT NOT NULL,
  PRIMARY KEY (`Id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

INSERT INTO `Item` (`Id`, `Name`, `ItemType`, `RarityId`) VALUES
(1, 'ロングソード', 1, 2),
(2, 'ウッドシールド', 2, 1),
(3, 'ポーション', 3, 1);

Rarity.sql

DROP TABLE IF EXISTS `Rarity`;

CREATE TABLE `Rarity` (
  `Id` INT UNSIGNED NOT NULL,
  `Name` VARCHAR(32) NOT NULL,
  PRIMARY KEY (`Id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

INSERT INTO `Rarity` (`Id`, `Name`) VALUES
(1, 'ノーマル'),
(2, 'レア'),
(3, 'スーパーレア');

想定通りの内容で出力できているようです。

おわりに

今回はマスターデータエディタを作ってみました。
いいですねこれ。
ルールは簡単なので、AI的にも理解と実装しやすいものだろうと思いました。

実装もFable 5で一気通貫で終わりました。
もちろん事前に仕様詰めて、25分56秒という速さで完成です(スゴ)

Windows用ということで、
実装中にWindows SDKや.Net SDKの存在確認していたようです。
その辺りが入っていないPCで実装指示を出すと、これらのモジュールのインストールで実装時間が伸びると思います。
あらかじめインストールしておくと良さそうです。

Visual Studioなどインストールしてある環境だと導入済みかもです。
Visual Studio Installerあたりから確認できると思います。

とりあえず、やって欲しい機能は一通り入りました。
あとはデータベースのmigrationやクライアント側への導入や更新などのフローに合わせて、追加実装や調整ですね。

今日はツールでも作る日にしようかなと思って仕様考えつつ実装案を練ろうかというくらいの時間でできちゃう時代になってしまいました(笑)

【最後まで読んでいただきありがとうございます!】
面白かったと感じた方は、ぜひ「スキ」と「フォロー」をお願いします!