Goldmoon - The 3rd Level Magician
It’s Friday night. You and a group of friends are playing the role-playing game, Dungeons & Dragons. Your character, Goldmoon, is a 3rd level Magic User. Goldmoon, like any magician, has the power to cast magic spells.
Limitations
The number of spells that she can memorize is limited. The power of her spells is also constrained by her level (she has attained 3rd level magic user). Higher level, more powerful, spells demand more preparation and memory than lower level spells.
According to the official Dungeons & Dragons rulebook, Goldmoon, at her level (3rd) can memorize the following number of spells per level:
- 1st-level spells: 4
- 2nd-level spells: 2
This means that Goldmoon cannot memorize and cast —, a 3rd-level spell. She’s simply not powerful enough, yet.
Once Goldmoon reaches the 4th level, her spell casting abilities will grow to look like this:
- 1st-level spells: 4
- 2nd-level spells: 2
enum FirstLevelSpells {
CharmPerson,
DetectMagic,
FloatingDisc,
HoldPortal,
Light,
MagicMissile,
ProtectionFromEvil,
ReadLanguages,
ReadMagic,
Shield,
}
enum SecondLevelSpells {
ContinualLight,
DetectEvil,
DetectInvisible,
ESP,
Invisibility,
Knock,
Levitate,
LocateObject,
MirrorImage,
PhantasmalForce,
Web,
WizardLock,
}
struct SpellBook {
spells: Vec<[FirstLevel; const N: usize], [SecondLevel; const N: usize]>
}
fn main() {
let gooldmoon
let mut memorize_spells = Vec<[FirstLevel; 4], [SecondLevel, 2]>
}
To cast a 1st-level spell like Magic Missile, the wizard uses one 1st-level slot. For a 2nd-level spell like Mirror Image, a 2nd-level slot is used.
Array of Pain
Handling arrays in Rust can be somewhat painful. The reason is:
- First, arrays are fixed-length: once declared, the length of an array cannot be changed.
- Second, arrays are typed based on their length and the type of their elements (which can only be of one type).
Arrays were only considered to be of the same type if they had both the same size and the same type of element.
Before const generics, working with arrays in Rust could be painful.
Const Generics to the rescue
Const Generics, which dropped in Stable version 1.83, enable us to define a generic over a constant value, like the length of an array. This is useful because quivers are fixed-size arrays.
There are three types of generics in π¦ Rust:
LifetimeParam,TypeParam, andConstParam.
let quiver = [WoodenArrow; 10];
let quiver = [WoodenArrow; 11];
let quiver = [SilverTipArrow; 10];
Here’s our first problem.
But like any container, the capacity (size) of the quiver can vary.
We want to create a generic trait that will apply to any arrow quiver, regardless of its capacity.
// Only array_a and array_d are of the same type.
struct ElfArcher {
array_a: [u8; 5],
array_b: [u8; 6], // larger than array_a, so different type
array_c: [i8; 5], // elements of different type, so different type
array_d: [u8; 5], // same type as array_a
}
// The keyword `const` below is what we use to inform the Rust compiler that we
// want to use a const generic for N (which is of type usize).
// Note! We have to use usize because Rust uses it to index arrays.
struct Warrior<T, const N: usize> {
array_a: [T; N],
array_b: [T; N],
}
Trait Pains
What if we want to implement a trait for these arrays? With const generics, we can now implement traits that work across arrays of different sizes, as long as they contain the same element type.
const genericscan be used for other things but solving the previous pain of working with arrays was the main contribution.