Треки
/
Cairo
Cairo
/
Вправи
/
Рольова гра
Рольова гра

Рольова гра

Навчальна вправа

Вступ

Нульові посилання

Якщо нам уже доводилося працювати з іншою мовою програмування (C/C++, Python, Java, Ruby, Lisp тощо), то ми, найімовірніше, вже стикалися з null або nil. За допомогою null або nil ці мови позначають, що певна змінна не має значення. Однак через це випадкове використання змінної зі значенням null стає легкою (і частою) помилкою. Як неважко уявити, спроба викликати функцію, якої немає, або отримати доступ до значення, якого не існує, може призвести до найрізноманітніших багів і збоїв. Творець null навіть назвав його своєю «помилкою на мільярд доларів».

Тип Option

Щоб уникнути цих проблем, змінні в Cairo можуть використовувати enum Option, аби безпечно позначити відсутність значення. Цей enum має два варіанти: None - аналог null у Cairo; і Some(T), де T - значення будь-якого типу.

Він має такий вигляд:

enum Option<T> {
    None,
    Some(T),
}

Option можна уявити як прошарок безпеки між нами і проблемами, які можуть спричинити нульові посилання, і який водночас зберігає їхню концептуальну користь.

Використання Option

Присвоїти змінній None досить просто:

let nothing: Option<u32> = Option::None;  // Variable nothing is set to None

Однак, якщо ми хочемо, щоб тип Option ніс у собі значення, присвоїти це значення напряму не вийде. Змінна типу Option і, скажімо, змінна типу i32 не є еквівалентними. Потрібно використати Some:

let wrong_way: Option<i32> = -4; // This will not work

let right_way: Option<i32> = Option::Some(-4); // This will work
let another_right_way = Option::Some(-4); // Compiler infers that this is Option<i32>

З тієї ж причини не спрацює й таке:

let number = 47;
let option_number = Option::Some(15);

let compile_error = number + option_number; // Cannot add an i32 and an Option<i32> - they are of different types

Щоб отримати значення, яке містить Some, спочатку потрібно перевірити, чи воно там є:

let mut some_words = Option::Some("choose something to say"); // some_words set to something

match some_words {
    Option::Some(str) => println!("Here, we will {}", str),
    Option::None => println!("I've got nothing to say"),
} // Prints, "Here, we will choose something to say"

some_words = Option::None; // some_words now set to None

// Exactly the same match block as above
match some_words {
    Option::Some(str) => println!("Here, we will {}", str),
    Option::None => println!("I've got nothing to say"),
} // Prints, "I've got nothing to say"

У Cairo є й інші інструменти, крім match, для перевірки та доступу до значень, що містяться в Option, але match уже має бути нам знайомим.

Нехай це буде демонстрацією того, чому Cairo використовує Option замість нульового посилання. Річ у тім, що ми мусимо перевірити, чи змінна Option дорівнює Some (і тоді можна сміливо дістати й використати значення, яке в ній міститься), чи None. Інакше наша програма не скомпілюється; компілятор захищає нас від null.

Вказівки

Ми працюємо над реалізацією рольової гри. Персонажа гравця представлено так:

pub struct Player {
    health: u32,
    mana: Option<u32>,
    level: u32,
}

Гравці в цій грі мають досягти 10 рівня, перш ніж відкриють запас мани, який можна використовувати для чаклування. До цього моменту мана гравця дорівнює Option::None.

Ми працюємо над двома частинами функціональності цієї гри: механікою відродження та механікою чаклування.

Метод revive має перевірити, що гравець справді мертвий (його здоровʼя досягло 0), і якщо це так, метод має повернути новий екземпляр Player зі здоровʼям 100. Метод revive має зберегти рівень гравця. Якщо рівень гравця 10 або вище, його слід відродити зі 100 мани. Якщо рівень гравця нижче 10, його мана має бути Option::None.

let dead_player = Player { health: 0, mana: Option::None, level: 2 };
dead_player.revive()
// Returns Player { health: 100, mana: Option::None, level: 2 }

Якщо метод revive викликати для гравця, здоровʼя якого 1 або більше, то метод має повернути Option::None.

let alive_player = Player { health: 1, mana: Option::Some(15), level: 11 };
alive_player.revive()
// Returns Option::None

Метод cast_spell приймає змінне посилання на Player, а також параметр mana_cost, який указує, скільки мани коштує заклинання. Він повертає кількість шкоди, яку завдає застосоване заклинання.

  • Якщо гравець не має доступу до запасу мани, спроба застосувати заклинання має зменшити його здоровʼя на вартість заклинання в мани. Повернена шкода має дорівнювати 0.

    let not_a_wizard_yet = Player { health: 79, mana: Option::None, level: 9 };
    assert_eq!(not_a_wizard_yet.cast_spell(5), 0)
    assert_eq!(not_a_wizard_yet.health, 74);
    assert_eq!(not_a_wizard_yet.mana, Option::None);
    
  • Якщо гравець має запас мани, але мани недостатньо, метод не повинен впливати на запас. Повернена шкода має дорівнювати 0.

    let low_mana_wizard = Player { health: 93, mana: Option::Some(3), level: 12 };
    assert_eq!(low_mana_wizard.cast_spell(10), 0);
    assert_eq!(low_mana_wizard.health, 93);
    assert_eq!(low_mana_wizard.mana, Option::Some(3));
    
  • В іншому разі заклинання застосовується успішно. mana_cost має бути віднято від запасу мани гравця. Повернена шкода має дорівнювати подвоєній вартості заклинання в мани.

    let wizard = Player { health: 123, mana: Option::Some(30), level: 18 };
    assert_eq!(wizard.cast_spell(10), 20);
    assert_eq!(wizard.health, 123);
    assert_eq!(wizard.mana, Option::Some(20));
    

Гарної гри!

Редагувати через GitHub Посилання відкривається в новому вікні або вкладці
Cairo Exercism

Час розпочати Рольова гра?

Зареєструйтеся на Exercism, щоб вивчати й опановувати Cairo, а також 25 концепцій68 вправ та справжнє наставництво від людей, і все це безкоштовно.