I would include std::fmt::Display here just as well. You're already importing quite a few things: might throw in the formatting as well. Unless you want to do without it at all, more on that later.
The compiler is smart enough to infer the types it's working on, making most of the type annotations in let <var>: <type> somewhat redundant. Except for the one in the .parse(). That one is much preferred to an otherwise quite verbose "turbofish" syntax: .parse::<i32>().
.unwrap_or(-1) there, on the other hand, is a bit of a tricky choice. Its main purpose is to fall back on a reasonable "default" value in case None or an Err in question isn't a deal-breaker. Which isn't quite the case for you here: if the player types in "five" or "a watermelon" they would likely appreciate at least some hint that their input doesn't match the expected constraints. I would:
Summary
fn prompt_number(message: &str) -> i32 {
loop {
let input_str = input(message);
if let Ok(number) = player_input.parse() {
return number;
}
eprintln!("Not a [whole] number! Let's try again.");
// continue looping until you get a valid number as an input
}
}
Generating a random number after requesting the input from the user seems like a bit of an odd choice as well here. I'd expect a basic algorithm of this sort to follow the logic of:
Generate a number
Get a number from the user
Check the generated number against the input
a. if true: congratulate and exit
b. if false: repeat from 2
Anyone (including you) who might stumble upon your code in the future would be very likely to ask themselves why would a user be prompted for input before any random number is even generated. Try to avoid confusing anyone (including yourself) with less-than-intuitive logic. The simpler and the easier it is to reason about, the better. Same goes for any clever "tricks" you ever come up with.
A bit of a confusing flow again here. If you're only using the_string in the winning else branch: why compute it outside of that particular branch? Keep an uninitialized variable (the_string) outside of the block (if { ... } and else { ... } here) is best for very few occasions. This one seems just a bit unnecessary. Many conditions (||) aren't very readable, either. Instead:
Summary
let guess = prompt_number("Take a guess: ");
let is_valid = guess >= 0 && guess <= 10;
if !is_valid {
eprintln!("Sorry, I can only count from 0 to 10!");
return;
}
// for readability purposes
let is_correct = guess == random;
let winner = if is_correct { "You" } else { "I" };
print!("Your guess was {guess}. My guess was {random}. {winner} win!");
Generics a bit more advanced of a topic, yet generally - if you can do without them, you probably should. It'll speed up your compilation time and make the whole thing a bit easier to reason about.
Over here, it'd be trivial to keep the whole function a simple fn input(prompt: &str). Any valid container for a piece of text / UTF-8: including String among other types, could easily be coerced into the &str slice by a simple & reference. Check the chapter on slices in the book for details.
@Jesper's reply has some more ideas with regards to the extensibility and variation with regards to the range of accepted numbers. Note that the !(MIN..=MAX).contains(x) is somewhat sub-optimal there, since it requires iterating through the whole range just to confirm whether the x lies within its bounds or not: a simple x >= MIN && X <= MAX check would suffice, instead.