1

Okay, I have Combatants which battle on a Battlefield. My intuition for which things go on which place is a little off. It's pretty close to being a game, but I am now stuck.

I want the Battlefield to have a tick() function, which allows all Combatants to take a decision, like attacking another of the opposite team or moving closing to one if no one is in range. I'm having issues making the borrow checker happy in doing this.

Here's a minimal version which has all the problems of my code.

struct Combatant{
    current_health: i16,
    max_health: i16
}

struct Battlefield{
    combatants: Vec<Combatant>
}

impl Combatant {
    fn attack(&self, other: &mut Combatant) {
        other.current_health -= 3;
    }
}

impl Battlefield {

    fn tick(&mut self) {
        let target = &mut self.combatants[0];
        for combatant in &self.combatants {
            combatant.attack(target);
        }
    }
}

cargo check returns

error[E0502]: cannot borrow `self.combatants` as immutable because it is also borrowed as mutable
  --> src/main.rs:20:26
   |
19 |         let target = &mut self.combatants[0];
   |                           --------------- mutable borrow occurs here
20 |         for combatant in &self.combatants {
   |                          ^^^^^^^^^^^^^^^^ immutable borrow occurs here
21 |             combatant.attack(target);
   |                              ------ mutable borrow later used here

How can I design this function (or more like, this whole scenario, heh) to make it work in Rust?

user93114
  • 61
  • 5
  • Take a look at ECS libraries like hecs or specs, it's usually much easier to build game-alike app's logic on top of these. – ozkriff Aug 25 '21 at 12:34
  • Yeah, I have used Bevy a little prior. I am doing this to challenge myself, and maybe getting something fun out of it as a side effect. – user93114 Aug 25 '21 at 19:43

3 Answers3

1

The problem is this: When you iterate over the combatants, that requires an immutable borrow of all the combatants in that Vec. However, one of them, combatants[0] is already borrowed, and it's a mutable borrow.

You cannot, at the same time, have a mutable and an immutable borrow to the same thing.

This prevents a lot of logic errors. For example, in your code, if the borrow was actually allowed, you'd actually have combatants[0] attack itself!

So what to do? In the specific example above, one thing you could do is use the split_first_mut method of vec, https://doc.rust-lang.org/std/vec/struct.Vec.html#method.split_first_mut

let (first, rest) = self.combatants.split_first_mut();
if let Some(first) = first {
  if let Some(rest) = rest {
    for combatant in rest {
      combatant.attack(first);
    }
  }
}
cadolphs
  • 9,014
  • 1
  • 24
  • 41
1

Since in your scenario you need to simultaneously have a mutable reference and an immutable reference on two elements in the same container, I think you need the help of interior mutability.

This will check at run-time that the same element is not accessed simultaneously through a mutable (.borrow_mut()) and immutable (.borrow()) reference (otherwise it panics). Obviously, you have to ensure that by yourself (which is quite ugly since we have to compare pointers!).

Apparently, it is necessary to reach pointers because references cannot be compared (the self argument of std::cmp::PartialEq::eq() will be dereferenced). The documentation of std::ptr::eq() (which should probably be used here) shows the difference between comparing references and comparing pointers.

struct Combatant {
    current_health: i16,
    max_health: i16,
}

struct Battlefield {
    combatants: Vec<std::cell::RefCell<Combatant>>,
}

impl Combatant {
    fn attack(
        &self,
        other: &mut Combatant,
    ) {
        other.current_health -= 3;
    }
}

impl Battlefield {
    fn tick(&mut self) {
        let target_cell = &self.combatants[0];
        let target = &*target_cell.borrow();
        for combatant_cell in &self.combatants {
            let combatant = &*combatant_cell.borrow();
            // if combatant as *const _ != target as *const _ {
            if !std::ptr::eq(combatant, target) {
                let target_mut = &mut *target_cell.borrow_mut();
                combatant.attack(target_mut);
            }
        }
    }
}

Note that this interior mutability was bothering me at first, and seemed like « bending the rules » because I was reasoning essentially in terms of « immutable becoming suddenly mutable » (like const-casting in C++), and the shared/exclusive aspect was only the consequence of that. But the answer to the linked question explains that we should think at first in terms of shared/exclusive access to our data, and then the immutable/mutable aspect is just the consequence.

Back to your example, the shared access to the Combatants in the vector seems essential because, at any time, any of them could access any other. Because the consequence of this choice (shared aspect) is that mutable accesses become almost impossible, we need the help of interior mutability.

This is not « bending the rules » because strict checking is done on .borrow()/.borrow_mut() (small overhead at this moment) and the obtained Ref/RefMut have lifetimes allowing usual (static) borrow-checking in the portion of code where they appear. It is much safer than free immutable/mutable accesses we could perform with other programming languages. For example, even in C++ where we could consider target as const (reference/pointer to const) while iterating on the non-const combatants vector, one iteration can accidentally mutate the target that we consider as const (reference/pointer to const means « I won't mutate it », not « it cannot be mutated by anyone »), which could be misleading. And with other languages where const/mut do not even exist, anything can be mutated at any time (except for objects which are strictly immutable, like str in Python, but it becomes difficult to manage objects with states that could change over time, like current_health in your example).

prog-fh
  • 13,492
  • 1
  • 15
  • 30
  • Maybe iterating over `&self.combatants[1..]` would prevent the first combatant attacking itself without the complicated pointer-based identity checks? – user4815162342 Aug 24 '21 at 20:44
  • @user4815162342 Certainly, but I guess index 0 here is just a specific simple case for a minimal reproducible example; probably in the real application the target could be any element of the vector. – prog-fh Aug 24 '21 at 20:46
  • Good point. I think I'd rather have a `target_index` and use that for comparison, but some complication is still there - no silver bullet. – user4815162342 Aug 24 '21 at 20:48
  • It can certainly be any element of the vector. It looks like we need to bend the rules significantly to get this done here, even though the effect is quite simple - is there a different way of going about it that I'm missing? – user93114 Aug 25 '21 at 03:57
  • @user93114 I added a paragraph to explain this choice (and why I had the same opinion as yours at first). – prog-fh Aug 25 '21 at 08:18
  • Fantastic, thank you. The only thing which I don't quite grasp at the moment is casting `combatant` and `target` to `* const _`. Is this necessary? Will the equality not work on the existing types? – user93114 Aug 26 '21 at 05:41
  • @user93114 I added a few words about pointer comparison. – prog-fh Aug 26 '21 at 07:19
0

You can also use split_at_mut to only iterate on the other elements:

fn tick(&mut self) {
    let idx = 0;
    let (before, after) = self.combatants.split_at_mut (idx);
    let (target, after) = after.split_at_mut (1);
    let target = &mut target[0];
    for combatant in before {
        combatant.attack(target);
    }
    for combatant in after {
        combatant.attack(target);
    }
}

Playground

Note that this will panic if idx >= len (self.combatants).

Jmb
  • 18,893
  • 2
  • 28
  • 55