Externalize card database and fusion recipes to JSON #1

Closed
opened 2026-05-11 13:11:31 +02:00 by sleepy · 0 comments
Owner

Problem

Card data (~50 cards) and fusion recipes (~40) are currently hardcoded as Rust struct literals in source files:

  • crates/card-data/src/card_db.rs (494 lines of inline MonsterCard { ... } and SpellCard { ... } declarations)
  • crates/fusion-engine/src/fusion_table.rs (555 lines of inline FusionRecipe { ... } declarations)

This makes it impossible to expand the dataset (target: 722 cards, ~700 fusion recipes) without recompiling the entire Rust codebase. Designers cannot add cards without touching Rust code.

Goal

Move 100% of card and fusion data out of .rs files into external JSON files that are loaded at runtime. The Rust code should only contain loading logic, not data.

Files to Create

assets/cards.json

Top-level structure:

{
  "cards": [
    {
      "type": "Monster",
      "name": "Blue-Eyes White Dragon",
      "atk": 3000,
      "def": 2500,
      "level": 8,
      "monster_type": "Dragon",
      "guardian_stars": ["Sun", "Mars"]
    },
    {
      "type": "Spell",
      "name": "Dark Hole",
      "spell_type": "Normal",
      "effect_id": "destroy_all_monsters"
    },
    {
      "type": "Trap",
      "name": "Trap Hole",
      "effect_id": "destroy_summoned_monster"
    }
  ]
}

Monster types must match MonsterType enum variants exactly (Dragon, Warrior, Machine, Zombie, Thunder, Aqua, Fiend, Spellcaster, Beast, BeastWarrior, Insect, Plant, Dinosaur, Rock, Fish, SeaSerpent, Pyro, Fairy, Reptile, WingedBeast).

Spell types must match SpellType enum variants exactly (Normal, Field, Equip, Ritual).

Guardian stars must match GuardianStar enum variants exactly (Sun, Mercury, Venus, Moon, Mars, Jupiter, Saturn, Uranus, Neptune, Pluto).

assets/fusions.json

Top-level structure:

{
  "recipes": [
    {
      "materials": [
        {"Type": "Dragon"},
        {"Type": "Thunder"}
      ],
      "result": {
        "Monster": {
          "name": "Twin-Headed Thunder Dragon",
          "atk": 2800,
          "def": 2100,
          "level": 7,
          "monster_type": "Thunder",
          "guardian_stars": ["Pluto", "Saturn"]
        }
      }
    },
    {
      "materials": [
        {"Specific": "Blue-Eyes White Dragon"},
        {"Specific": "Blue-Eyes White Dragon"}
      ],
      "result": {
        "Spell": {
          "name": "Yami",
          "spell_type": "Field",
          "effect_id": "terrain_yami"
        }
      }
    }
  ]
}

FusionMaterial uses externally tagged enum representation:

  • {"Type": "Dragon"} for type-based material
  • {"Specific": "Blue-Eyes White Dragon"} for specific card material

FusionResult uses externally tagged enum representation:

  • {"Monster": {...}} for monster result
  • {"Spell": {...}} for spell result

Files to Modify

crates/card-data/Cargo.toml

Add serde_json = "1.0" dependency.

crates/card-data/src/card_db.rs

  • Remove the vec![...] inline data from new()
  • Add pub fn from_file(path: &str) -> Result<Self, CardDbError>
  • Add pub fn from_str(json: &str) -> Result<Self, CardDbError>
  • Keep a minimal pub fn test_data() -> Self with ~10 cards for unit tests (or load from test fixture)
  • new() should attempt to load from assets/cards.json at runtime, returning empty on error (or panicking with a clear message)

crates/fusion-engine/Cargo.toml

Add serde_json = "1.0" dependency.

crates/fusion-engine/src/recipe.rs

  • Add Serialize, Deserialize derives to FusionMaterial, FusionResult, and FusionRecipe
  • Note: FusionMaterial and FusionResult currently only derive Clone, Debug, PartialEq, Eq

crates/fusion-engine/src/fusion_table.rs

  • Remove the vec![...] inline recipes from new()
  • Add pub fn from_file(path: &str) -> Result<Self, FusionError>
  • Add pub fn from_str(json: &str) -> Result<Self, FusionError>
  • Keep a minimal test dataset helper
  • new() should attempt to load from assets/fusions.json

crates/cli/src/main.rs and/or crates/cli/src/game.rs

  • Ensure CLI loads data from assets/ directory at startup
  • The executable should look for assets/cards.json and assets/fusions.json relative to the working directory

crates/duel-engine/src/duel.rs (tests only)

  • Update any tests that construct FusionTable::new() directly to use a test helper

Error Types

Use thiserror to define:

  • CardDbError in card-data (IO, Parse, InvalidMonsterType, etc.)
  • FusionError in fusion-engine (IO, Parse, etc.)

Data Preservation

Every single card and fusion recipe currently in the codebase must be preserved in the JSON files. Do not drop any data.

Testing Strategy

  1. Unit tests in card-data should use CardDatabase::test_data() (small embedded set)
  2. Unit tests in fusion-engine should use FusionTable::test_data() (small embedded set)
  3. Add integration tests that verify from_file loads the full assets/ correctly
  4. All existing 81 workspace tests must continue to pass

Acceptance Criteria

  • assets/cards.json exists with all 50+ current cards exactly preserved
  • assets/fusions.json exists with all ~40 current recipes exactly preserved
  • CardDatabase::from_file("assets/cards.json") loads successfully
  • FusionTable::from_file("assets/fusions.json") loads successfully
  • card-data and fusion-engine have serde_json in Cargo.toml
  • FusionMaterial, FusionResult, FusionRecipe derive Deserialize
  • CLI loads data from assets/ at startup and works
  • All 81 workspace tests pass (cargo test --workspace)
  • cargo clippy --workspace clean (zero warnings)
  • cargo check --workspace clean
## Problem Card data (~50 cards) and fusion recipes (~40) are currently hardcoded as Rust struct literals in source files: - `crates/card-data/src/card_db.rs` (494 lines of inline `MonsterCard { ... }` and `SpellCard { ... }` declarations) - `crates/fusion-engine/src/fusion_table.rs` (555 lines of inline `FusionRecipe { ... }` declarations) This makes it impossible to expand the dataset (target: 722 cards, ~700 fusion recipes) without recompiling the entire Rust codebase. Designers cannot add cards without touching Rust code. ## Goal Move 100% of card and fusion data out of `.rs` files into external JSON files that are loaded at runtime. The Rust code should only contain loading logic, not data. ## Files to Create ### `assets/cards.json` Top-level structure: ```json { "cards": [ { "type": "Monster", "name": "Blue-Eyes White Dragon", "atk": 3000, "def": 2500, "level": 8, "monster_type": "Dragon", "guardian_stars": ["Sun", "Mars"] }, { "type": "Spell", "name": "Dark Hole", "spell_type": "Normal", "effect_id": "destroy_all_monsters" }, { "type": "Trap", "name": "Trap Hole", "effect_id": "destroy_summoned_monster" } ] } ``` Monster types must match `MonsterType` enum variants exactly (`Dragon`, `Warrior`, `Machine`, `Zombie`, `Thunder`, `Aqua`, `Fiend`, `Spellcaster`, `Beast`, `BeastWarrior`, `Insect`, `Plant`, `Dinosaur`, `Rock`, `Fish`, `SeaSerpent`, `Pyro`, `Fairy`, `Reptile`, `WingedBeast`). Spell types must match `SpellType` enum variants exactly (`Normal`, `Field`, `Equip`, `Ritual`). Guardian stars must match `GuardianStar` enum variants exactly (`Sun`, `Mercury`, `Venus`, `Moon`, `Mars`, `Jupiter`, `Saturn`, `Uranus`, `Neptune`, `Pluto`). ### `assets/fusions.json` Top-level structure: ```json { "recipes": [ { "materials": [ {"Type": "Dragon"}, {"Type": "Thunder"} ], "result": { "Monster": { "name": "Twin-Headed Thunder Dragon", "atk": 2800, "def": 2100, "level": 7, "monster_type": "Thunder", "guardian_stars": ["Pluto", "Saturn"] } } }, { "materials": [ {"Specific": "Blue-Eyes White Dragon"}, {"Specific": "Blue-Eyes White Dragon"} ], "result": { "Spell": { "name": "Yami", "spell_type": "Field", "effect_id": "terrain_yami" } } } ] } ``` `FusionMaterial` uses externally tagged enum representation: - `{"Type": "Dragon"}` for type-based material - `{"Specific": "Blue-Eyes White Dragon"}` for specific card material `FusionResult` uses externally tagged enum representation: - `{"Monster": {...}}` for monster result - `{"Spell": {...}}` for spell result ## Files to Modify ### `crates/card-data/Cargo.toml` Add `serde_json = "1.0"` dependency. ### `crates/card-data/src/card_db.rs` - **Remove** the `vec![...]` inline data from `new()` - **Add** `pub fn from_file(path: &str) -> Result<Self, CardDbError>` - **Add** `pub fn from_str(json: &str) -> Result<Self, CardDbError>` - Keep a minimal `pub fn test_data() -> Self` with ~10 cards for unit tests (or load from test fixture) - `new()` should attempt to load from `assets/cards.json` at runtime, returning empty on error (or panicking with a clear message) ### `crates/fusion-engine/Cargo.toml` Add `serde_json = "1.0"` dependency. ### `crates/fusion-engine/src/recipe.rs` - Add `Serialize, Deserialize` derives to `FusionMaterial`, `FusionResult`, and `FusionRecipe` - Note: `FusionMaterial` and `FusionResult` currently only derive `Clone, Debug, PartialEq, Eq` ### `crates/fusion-engine/src/fusion_table.rs` - **Remove** the `vec![...]` inline recipes from `new()` - **Add** `pub fn from_file(path: &str) -> Result<Self, FusionError>` - **Add** `pub fn from_str(json: &str) -> Result<Self, FusionError>` - Keep a minimal test dataset helper - `new()` should attempt to load from `assets/fusions.json` ### `crates/cli/src/main.rs` and/or `crates/cli/src/game.rs` - Ensure CLI loads data from `assets/` directory at startup - The executable should look for `assets/cards.json` and `assets/fusions.json` relative to the working directory ### `crates/duel-engine/src/duel.rs` (tests only) - Update any tests that construct `FusionTable::new()` directly to use a test helper ## Error Types Use `thiserror` to define: - `CardDbError` in `card-data` (IO, Parse, InvalidMonsterType, etc.) - `FusionError` in `fusion-engine` (IO, Parse, etc.) ## Data Preservation Every single card and fusion recipe currently in the codebase must be preserved in the JSON files. Do not drop any data. ## Testing Strategy 1. Unit tests in `card-data` should use `CardDatabase::test_data()` (small embedded set) 2. Unit tests in `fusion-engine` should use `FusionTable::test_data()` (small embedded set) 3. Add integration tests that verify `from_file` loads the full `assets/` correctly 4. All existing 81 workspace tests must continue to pass ## Acceptance Criteria - [ ] `assets/cards.json` exists with all 50+ current cards exactly preserved - [ ] `assets/fusions.json` exists with all ~40 current recipes exactly preserved - [ ] `CardDatabase::from_file("assets/cards.json")` loads successfully - [ ] `FusionTable::from_file("assets/fusions.json")` loads successfully - [ ] `card-data` and `fusion-engine` have `serde_json` in Cargo.toml - [ ] `FusionMaterial`, `FusionResult`, `FusionRecipe` derive `Deserialize` - [ ] CLI loads data from `assets/` at startup and works - [ ] All 81 workspace tests pass (`cargo test --workspace`) - [ ] `cargo clippy --workspace` clean (zero warnings) - [ ] `cargo check --workspace` clean
Sign in to join this conversation.
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
sleepy/duelist-kingdom#1
No description provided.