演習: 魔法使いのインベントリ

この演習では、借用と所有権について学んだことを使って、魔法使いのインベントリを管理します。

  • 魔法使いは呪文のコレクションを持っています。インベントリに呪文を追加する関数と、その中の呪文を唱える関数を実装する必要があります。

  • 呪文には使用回数の上限があります。呪文の残り使用回数がなくなったら、それを魔法使いのインベントリから削除しなければなりません。

// Copyright 2023 Google LLC
// SPDX-License-Identifier: Apache-2.0

struct Spell {
    name: String,
    cost: u32,
    uses: u32,
}

struct Wizard {
    spells: Vec<Spell>,
    mana: u32,
}

impl Wizard {
    fn new(mana: u32) -> Self {
        Wizard { spells: vec![], mana }
    }

    // TODO: `add_spell` を実装して、呪文の所有権を受け取り、それを
    // 魔法使いのインベントリに追加する。
    fn add_spell(..., spell: ...) {
        todo!()
    }

    // TODO: インベントリから呪文を借用して唱えるように
    // `cast_spell` を実装する。魔法使いのマナは呪文のコスト分だけ減り、
    // 呪文の使用回数は 1 減少するべきです。
    //
    // 魔法使いのマナが足りない場合、呪文は失敗するべきです。
    // 呪文の残り使用回数がない場合は、インベントリから削除されます。
    fn cast_spell(..., name: ...) {
        todo!()
    }
}

fn main() {
    let mut merlin = Wizard::new(100);
    let fireball = Spell { name: String::from("Fireball"), cost: 10, uses: 2 };
    let ice_blast = Spell { name: String::from("Ice Blast"), cost: 15, uses: 1 };

    merlin.add_spell(fireball);
    merlin.add_spell(ice_blast);

    merlin.cast_spell("Fireball"); // Casts successfully
    merlin.cast_spell("Ice Blast"); // Casts successfully, then removed
    merlin.cast_spell("Ice Blast"); // Fails (not found)
    merlin.cast_spell("Fireball"); // Casts successfully, then removed
    merlin.cast_spell("Fireball"); // Fails (not found)
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_add_spell() {
        let mut wizard = Wizard::new(10);
        let spell = Spell { name: String::from("Fireball"), cost: 5, uses: 3 };
        wizard.add_spell(spell);
        assert_eq!(wizard.spells.len(), 1);
    }

    #[test]
    fn test_cast_spell() {
        let mut wizard = Wizard::new(10);
        let spell = Spell { name: String::from("Fireball"), cost: 5, uses: 3 };
        wizard.add_spell(spell);

        wizard.cast_spell("Fireball");
        assert_eq!(wizard.mana, 5);
        assert_eq!(wizard.spells.len(), 1);
        assert_eq!(wizard.spells[0].uses, 2);
    }

    #[test]
    fn test_cast_spell_insufficient_mana() {
        let mut wizard = Wizard::new(10);
        let spell = Spell { name: String::from("Fireball"), cost: 15, uses: 3 };
        wizard.add_spell(spell);

        wizard.cast_spell("Fireball");
        assert_eq!(wizard.mana, 10);
        assert_eq!(wizard.spells.len(), 1);
        assert_eq!(wizard.spells[0].uses, 3);
    }

    #[test]
    fn test_cast_spell_not_found() {
        let mut wizard = Wizard::new(10);
        wizard.cast_spell("Fireball");
        assert_eq!(wizard.mana, 10);
    }

    #[test]
    fn test_cast_spell_removal() {
        let mut wizard = Wizard::new(10);
        let spell = Spell { name: String::from("Fireball"), cost: 5, uses: 1 };
        wizard.add_spell(spell);

        wizard.cast_spell("Fireball");
        assert_eq!(wizard.mana, 5);
        assert_eq!(wizard.spells.len(), 0);
    }
}
  • この演習の目標は、所有権と借用の中核概念、特にコレクションの要素の 1 つへの参照を保持している間はそのコレクションを変更できないというルールを練習することです。
  • add_spellSpell の所有権を受け取り、それを Wizard のインベントリへムーブするべきです。
  • cast_spell はこの演習の中核です。これには次が必要です:
    1. 呪文を見つける(インデックスまたは参照で)。
    2. マナを確認して減らす。
    3. 呪文の uses を減らす。
    4. uses == 0 の場合は呪文を削除する。
  • 借用チェッカーの競合: 学習者が呪文への参照(例: let spell = &mut self.spells[i])を保持したまま、その参照が同じスコープ内でまだ「生きている」間に self.spells.remove(i) を呼び出そうとすると、借用チェッカーがエラーを報告します。これは、借用チェッカーを満たすようにコードを構成する方法(たとえばインデックスを使う、または変更の前に借用が終わるようにすること)を示す絶好の機会です。