Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Awaitables and timeline thinking

What is awaitable

Awaitable is a point, where logic intentionally stops until X happen. It’s a contract with the engine that tells: “wake me up when requirement for progression gets fulfilled”.

Take a look at this coroutine:

#![allow(unused)]
fn main() {
async {
    wait_for_input_action("interact").await;
    wait_for_seconds(0.1).await;
    wait_for_animation_to_finish("punch").await;
}
}

Every .await in there is that point, which suspends coroutine until future we await is complete.

It’s important to remember: awaiting doesn’t mean sleeping - it means yielding control.

When we await at wait_for_seconds(), we are yielding control to other coroutines running in the background, until timer hits the elapsed time we requested, and only then coroutine moves forward to the next awaiting point.

We are suspending coroutine, no code is executed at all until coroutine gets told that whatever we wait for is done and available, and only then execution does continue further - nothing happen until then, other coroutines can do their work at the same time until they hit await point.

This distinction is important, as async might at first feel like a magic, and it might bring up wrong intuition for gamedevs that are used to just spawn task in a thread and sleep() in it to wait for some time, or in case of something like wait_for_input_action(), that it just blocking-loop constantly asking if input action did triggered - that’s wrong way to look at it, it just pauses and lets game do other work while it waits.

What are action timelines

Action timeline is a concept, where you lay out a sequence of state changes spread in time.

In traditional way, you can see action timeline defined within our NPC fight pattern as:

use crossterm::{
    event::{Event, KeyCode},
    style::Stylize,
};
use moirai_book_samples::{
    game::{Game, GameState, GameStateChange},
    terminal::Terminal,
    utils::is_key_pressed,
};
use std::time::Duration;

fn main() {
    Game::new(Example::default()).run_blocking();
}

const TIME_SCALE: f32 = 5.0;
const DAMAGE_STAMINA_MIN_REQUIREMENT: f32 = 5.0;
const LOG_CAPACITY: usize = 5;

#[derive(Default)]
struct Example {
    terminal: Terminal,
    player: Character,
    enemy: Npc,
    log: Vec<String>,
}

impl GameState for Example {
    fn frame(&mut self, delta_time: Duration) -> GameStateChange {
        // Collect application input events that happened since last frame.
        let events = Terminal::events().collect::<Vec<_>>();

        // Run game systems
        if self.player.is_alive() && self.enemy.character.is_alive() {
            update_player_system(
                &events,
                &mut self.player,
                &mut self.enemy.character,
                &mut self.log,
                delta_time.as_secs_f32(),
            );
            update_enemy_system(
                &mut self.enemy,
                &mut self.player,
                &mut self.log,
                delta_time.as_secs_f32(),
            );
        }
        draw_system(
            &mut self.terminal,
            &self.player,
            &self.enemy.character,
            &self.log,
        );

        if is_key_pressed(&events, KeyCode::Esc) {
            GameStateChange::Quit
        } else {
            GameStateChange::None
        }
    }
}

struct Character {
    health: f32,
    stamina: f32,
    blocking_cooldown: f32,
}

impl Default for Character {
    fn default() -> Self {
        Self {
            health: 100.0,
            stamina: 0.0,
            blocking_cooldown: 0.0,
        }
    }
}

impl Character {
    fn is_alive(&self) -> bool {
        self.health > 0.0
    }

    fn is_blocking(&self) -> bool {
        self.blocking_cooldown > f32::EPSILON
    }

    fn update(&mut self, delta_time: f32) {
        if self.is_blocking() {
            self.blocking_cooldown = (self.blocking_cooldown - delta_time * TIME_SCALE).max(0.0);
        } else {
            self.stamina = (self.stamina + delta_time * TIME_SCALE).min(25.0);
        }
    }

    fn deal_damage(&mut self, target: &mut Character) -> bool {
        if !target.is_blocking() && self.stamina >= DAMAGE_STAMINA_MIN_REQUIREMENT {
            target.health = (target.health - self.stamina).max(0.0);
            self.stamina = 0.0;
            true
        } else {
            false
        }
    }

    fn block(&mut self) {
        self.blocking_cooldown = self.stamina;
        self.stamina = 0.0;
    }
}

#[derive(Default)]
struct Npc {
    character: Character,
    fight_phase_index: usize,
}

enum NpcFightState {
    Charge { target_stamina: f32 },
    Attack,
    Block,
}

const NPC_FIGHT_PATTERN: &[NpcFightState] = &[
    NpcFightState::Charge {
        target_stamina: 10.0,
    },
    NpcFightState::Attack,
    NpcFightState::Charge {
        target_stamina: 10.0,
    },
    NpcFightState::Block,
    NpcFightState::Charge {
        target_stamina: 20.0,
    },
    NpcFightState::Attack,
];

fn update_player_system(
    events: &[Event],
    player: &mut Character,
    enemy: &mut Character,
    log: &mut Vec<String>,
    delta_time: f32,
) {
    // Update player character state, such as stamina regeneration and blocking cooldown.
    player.update(delta_time);

    // Perform actions based on inputs.
    if is_key_pressed(events, KeyCode::Enter) {
        if player.deal_damage(enemy) {
            log.push("Player attacking!".to_string());
        }
    } else if is_key_pressed(events, KeyCode::Char(' ')) {
        player.block();
        log.push("Player blocking!".to_string());
    }
}

fn update_enemy_system(
    enemy: &mut Npc,
    player: &mut Character,
    log: &mut Vec<String>,
    delta_time: f32,
) {
    // Update NPC character state, such as stamina regeneration and blocking cooldown.
    enemy.character.update(delta_time);

    // Get current NPC state or reset if out of bounds, to loop through pattern.
    let Some(state) = NPC_FIGHT_PATTERN.get(enemy.fight_phase_index) else {
        enemy.fight_phase_index = 0;
        return;
    };

    // Perform an action of currently active state.
    match state {
        // Stay idle while recharching stamina.
        NpcFightState::Charge { target_stamina } => {
            if enemy.character.stamina >= *target_stamina {
                enemy.fight_phase_index += 1;
            }
        }
        // Attack the player with accumulated stamina.
        NpcFightState::Attack => {
            if enemy.character.deal_damage(player) {
                log.push("Enemy attacking!".to_string());
            }
            enemy.fight_phase_index += 1;
        }
        // Block the player's incoming attack for time of accumulated stamina.
        NpcFightState::Block => {
            enemy.character.block();
            log.push("Enemy blocking!".to_string());
            enemy.fight_phase_index += 1;
        }
    }
}

fn draw_system(terminal: &mut Terminal, player: &Character, enemy: &Character, log: &[String]) {
    terminal.begin_draw(true);

    match (player.is_alive(), enemy.is_alive()) {
        (true, true) => {
            terminal.display([1, 1], "Player:".green().bold());
            terminal.display([2, 2], format!("Health: {:.1}", player.health));
            terminal.display([2, 3], format!("Stamina: {:.1}", player.stamina));
            terminal.display([2, 4], format!("Cooldown: {:.1}", player.blocking_cooldown));

            terminal.display([1, 6], "Enemy:".red().bold());
            terminal.display([2, 7], format!("Health: {:.1}", enemy.health));
            terminal.display([2, 8], format!("Stamina: {:.1}", enemy.stamina));
            terminal.display([2, 9], format!("Cooldown: {:.1}", enemy.blocking_cooldown));

            terminal.display([20, 1], "Log:".underlined());
            for (i, log) in log.iter().rev().take(LOG_CAPACITY).enumerate() {
                terminal.display([21, 2 + i as u16], log);
            }
        }
        (false, false) => {
            terminal.display([1, 1], "It's a draw!".yellow().bold());
        }
        (true, false) => {
            terminal.display([1, 1], "You are victorious!".green().bold());
            terminal.display([2, 2], format!("Health: {:.1}", player.health));
        }
        (false, true) => {
            terminal.display([1, 1], "You have been defeated!".red().bold());
            terminal.display([2, 2], format!("Health: {:.1}", enemy.health));
        }
    }

    terminal.end_draw();
}

In traditional way this is how we tell the manually driven state machine what is expected chain of actions, and state machine takes care of switching to the next action, when current action completes. In there we declare it in single place, so it could help us keep mental picture of the logic flow.

You can also think of action timelines as scripting a sequence of steps, where all steps doesn’t happen now, but each does sometime in the future, if that helps you.

And similarily to traditional way, we can declare such timeline of actions as a coroutine, in form of a procedurally looking code, denoting with await points where the transition happen, explicitly telling we are awaiting for each step completion.

use crossterm::{
    event::{Event, KeyCode},
    style::Stylize,
};
use moirai::{
    coroutine::{CompletionImportance, meta, with_importance},
    job::{JobHandle, JobLocation},
    third_party::intuicio_data::{managed::DynamicManagedLazy, shared::AsyncShared},
};
use moirai_book_samples::{
    coroutines::{Coroutines, next_frame},
    game::{Game, GameState, GameStateChange},
    terminal::Terminal,
    utils::is_key_pressed,
};
use std::time::Duration;

fn main() {
    Game::new(Example::default()).run_blocking();
}

const TIME_SCALE: f32 = 5.0;
const DAMAGE_STAMINA_MIN_REQUIREMENT: f32 = 5.0;
const LOG_CAPACITY: usize = 5;
const EVENTS_META: &str = "~events~";
const DELTA_TIME_META: &str = "~delta-time~";
const LOG_META: &str = "~log~";

struct Example {
    terminal: Terminal,
    coroutines: Coroutines,
    player: AsyncShared<Character>,
    enemy: AsyncShared<Character>,
    log: Vec<String>,
}

impl Default for Example {
    fn default() -> Self {
        let coroutines = Coroutines::default();

        let player = AsyncShared::new(Character::new("Player"));
        let enemy = AsyncShared::new(Character::new("Enemy"));

        let player_shared = player.clone();
        let enemy_shared = enemy.clone();
        player.write().unwrap().timeline(&coroutines, async move {
            loop {
                let events = events().await;

                if is_key_pressed(&events, KeyCode::Enter) {
                    attack(&player_shared, &enemy_shared).await;
                } else if is_key_pressed(&events, KeyCode::Char(' ')) {
                    block(player_shared.clone()).await;
                } else {
                    update(&player_shared).await;
                }

                next_frame().await;
            }
        });

        let player_shared = player.clone();
        let enemy_shared = enemy.clone();
        enemy.write().unwrap().timeline(&coroutines, async move {
            loop {
                charge(&enemy_shared, 10.0).await;
                attack(&enemy_shared, &player_shared).await;
                charge(&enemy_shared, 10.0).await;
                block(enemy_shared.clone()).await;
                charge(&enemy_shared, 20.0).await;
                attack(&enemy_shared, &player_shared).await;
            }
        });

        Self {
            terminal: Terminal::default(),
            coroutines,
            player,
            enemy,
            log: Vec::new(),
        }
    }
}

impl GameState for Example {
    fn frame(&mut self, mut delta_time: Duration) -> GameStateChange {
        let mut events = Terminal::events().collect::<Vec<_>>();

        draw_system(&mut self.terminal, &self.player, &self.enemy, &self.log);

        {
            let (events_lazy, _events_lifemtime) = DynamicManagedLazy::make(&mut events);
            let (dt_lazy, _dt_lifemtime) = DynamicManagedLazy::make(&mut delta_time);
            let (log_lazy, _log_lifetime) = DynamicManagedLazy::make(&mut self.log);
            self.coroutines.run_frame(
                [
                    (EVENTS_META.into(), events_lazy.into()),
                    (DELTA_TIME_META.into(), dt_lazy.into()),
                    (LOG_META.into(), log_lazy.into()),
                ]
                .into_iter()
                .collect(),
            );
        }

        {
            let player = self.player.write().unwrap();
            if !player.is_alive() {
                player.timeline.cancel();
            }
            let enemy = self.enemy.write().unwrap();
            if !enemy.is_alive() {
                enemy.timeline.cancel();
            }
        }

        if is_key_pressed(&events, KeyCode::Esc) {
            GameStateChange::Quit
        } else {
            GameStateChange::None
        }
    }
}

struct Character {
    name: String,
    health: f32,
    stamina: f32,
    timeline: JobHandle<()>,
}

impl Character {
    fn new(name: impl ToString) -> Self {
        Self {
            name: name.to_string(),
            health: 100.0,
            stamina: 0.0,
            timeline: Default::default(),
        }
    }

    fn timeline(
        &mut self,
        coroutines: &Coroutines,
        timeline: impl Future<Output = ()> + Send + Sync + 'static,
    ) {
        self.timeline = coroutines
            .queue()
            .spawn(JobLocation::Local, timeline)
            .cancel_on_drop();
    }

    fn is_alive(&self) -> bool {
        self.health > 0.0
    }
}

async fn update(this: &AsyncShared<Character>) {
    let dt = delta_time().await.as_secs_f32() * TIME_SCALE;
    let mut this = this.write().unwrap();
    this.stamina = (this.stamina + dt).min(100.0);
}

async fn attack(this: &AsyncShared<Character>, target: &AsyncShared<Character>) {
    log(format!(
        "{} attacks {} with {:.02} hitpoints",
        this.read().unwrap().name,
        target.read().unwrap().name,
        this.read().unwrap().stamina
    ))
    .await;

    let mut this = this.write().unwrap();
    let mut target = target.write().unwrap();

    if this.stamina >= DAMAGE_STAMINA_MIN_REQUIREMENT {
        target.health = (target.health - this.stamina).max(0.0);
        this.stamina = 0.0;
    }
}

async fn charge(this: &AsyncShared<Character>, target_stamina: f32) {
    loop {
        let dt = delta_time().await.as_secs_f32() * TIME_SCALE;
        {
            let mut this = this.write().unwrap();
            this.stamina = (this.stamina + dt).min(100.0);
            if this.stamina >= target_stamina {
                break;
            }
        }

        next_frame().await;
    }
}

async fn block(this: AsyncShared<Character>) {
    log(format!(
        "{} blocks with {:.02} cooldown",
        this.read().unwrap().name,
        this.read().unwrap().stamina
    ))
    .await;

    let health = this.read().unwrap().health;

    loop {
        let dt = delta_time().await.as_secs_f32() * TIME_SCALE;
        {
            let mut this = this.write().unwrap();
            this.health = health;
            this.stamina = (this.stamina - dt).min(100.0);
            if this.stamina <= 0.0 {
                break;
            }
        }

        next_frame().await;
    }

    log(format!("{} stops blocking", this.read().unwrap().name)).await;
}

async fn parry(this: AsyncShared<Character>, target: AsyncShared<Character>, mut cooldown: f32) {
    let origin_health = this.read().unwrap().health;
    let damage = loop {
        let dt = delta_time().await.as_secs_f32() * TIME_SCALE;
        cooldown -= dt;
        if cooldown <= 0.0 {
            return;
        }

        let current_health = this.read().unwrap().health;
        if current_health < origin_health {
            break origin_health - current_health;
        }

        next_frame().await;
    };

    log(format!(
        "{} parries {} with {:.02} hitpoints",
        this.read().unwrap().name,
        target.read().unwrap().name,
        damage
    ))
    .await;

    let mut this = this.write().unwrap();
    let mut target = target.write().unwrap();

    target.health = (target.health - damage).max(0.0);
    this.stamina = 0.0;
    this.health = (this.health + damage).min(100.0);
}

fn draw_system(
    terminal: &mut Terminal,
    player: &AsyncShared<Character>,
    enemy: &AsyncShared<Character>,
    log: &[String],
) {
    terminal.begin_draw(true);

    let player = player.read().unwrap();
    let enemy = enemy.read().unwrap();

    match (player.is_alive(), enemy.is_alive()) {
        (true, true) => {
            terminal.display([1, 1], "Player:".green().bold());
            terminal.display([2, 2], format!("Health: {:.1}", player.health));
            terminal.display([2, 3], format!("Stamina: {:.1}", player.stamina));

            terminal.display([1, 6], "Enemy:".red().bold());
            terminal.display([2, 7], format!("Health: {:.1}", enemy.health));
            terminal.display([2, 8], format!("Stamina: {:.1}", enemy.stamina));

            terminal.display([20, 1], "Log:".underlined());
            for (i, log) in log.iter().rev().take(LOG_CAPACITY).enumerate() {
                terminal.display([21, 2 + i as u16], log);
            }
        }
        (false, false) => {
            terminal.display([1, 1], "It's a draw!".yellow().bold());
        }
        (true, false) => {
            terminal.display([1, 1], "You are victorious!".green().bold());
            terminal.display([2, 2], format!("Health: {:.1}", player.health));
        }
        (false, true) => {
            terminal.display([1, 1], "You have been defeated!".red().bold());
            terminal.display([2, 2], format!("Health: {:.1}", enemy.health));
        }
    }

    terminal.end_draw();
}

async fn delta_time() -> Duration {
    *meta::<Duration>(DELTA_TIME_META)
        .await
        .unwrap()
        .read()
        .unwrap()
}

async fn events() -> Vec<Event> {
    meta::<Vec<Event>>(EVENTS_META)
        .await
        .unwrap()
        .read()
        .unwrap()
        .clone()
}

async fn log(content: impl ToString) {
    meta::<Vec<String>>(LOG_META)
        .await
        .unwrap()
        .write()
        .unwrap()
        .push(content.to_string());
}

// ===

#[allow(dead_code)]
async fn example_with_parry(
    enemy_shared: AsyncShared<Character>,
    player_shared: AsyncShared<Character>,
) {
    loop {
        charge(&enemy_shared, 10.0).await;
        attack(&enemy_shared, &player_shared).await;
        charge(&enemy_shared, 10.0).await;

        let parry = parry(enemy_shared.clone(), player_shared.clone(), 10.0);
        let block = block(enemy_shared.clone());

        with_importance(vec![
            CompletionImportance::ignored(parry),
            CompletionImportance::required(block),
        ])
        .await;

        charge(&enemy_shared, 20.0).await;
        attack(&enemy_shared, &player_shared).await;
    }
}

#[allow(dead_code)]
async fn example_with_waiting_for_input(
    player: &AsyncShared<Character>,
    enemy: &AsyncShared<Character>,
) {
    loop {
        let events = events().await;

        if is_key_pressed(&events, KeyCode::Enter) {
            attack(player, enemy).await;
        } else if is_key_pressed(&events, KeyCode::Char(' ')) {
            block(player.clone()).await;
        } else {
            update(player).await;
        }
    }
}

With difference that we don’t need to care about any state machine running the states and transitions, as it’s built for us by the compiler - we get a coroutine with action timeline, send it for execution and do something useful with its result when it’s done.

Key points on more complex example

To summarize, looking at this code, slightly more complex than what we have shown so far:

use crossterm::{
    event::{Event, KeyCode},
    style::Stylize,
};
use moirai::{
    coroutine::{CompletionImportance, meta, with_importance},
    job::{JobHandle, JobLocation},
    third_party::intuicio_data::{managed::DynamicManagedLazy, shared::AsyncShared},
};
use moirai_book_samples::{
    coroutines::{Coroutines, next_frame},
    game::{Game, GameState, GameStateChange},
    terminal::Terminal,
    utils::is_key_pressed,
};
use std::time::Duration;

fn main() {
    Game::new(Example::default()).run_blocking();
}

const TIME_SCALE: f32 = 5.0;
const DAMAGE_STAMINA_MIN_REQUIREMENT: f32 = 5.0;
const LOG_CAPACITY: usize = 5;
const EVENTS_META: &str = "~events~";
const DELTA_TIME_META: &str = "~delta-time~";
const LOG_META: &str = "~log~";

struct Example {
    terminal: Terminal,
    coroutines: Coroutines,
    player: AsyncShared<Character>,
    enemy: AsyncShared<Character>,
    log: Vec<String>,
}

impl Default for Example {
    fn default() -> Self {
        let coroutines = Coroutines::default();

        let player = AsyncShared::new(Character::new("Player"));
        let enemy = AsyncShared::new(Character::new("Enemy"));

        let player_shared = player.clone();
        let enemy_shared = enemy.clone();
        player.write().unwrap().timeline(&coroutines, async move {
            loop {
                let events = events().await;

                if is_key_pressed(&events, KeyCode::Enter) {
                    attack(&player_shared, &enemy_shared).await;
                } else if is_key_pressed(&events, KeyCode::Char(' ')) {
                    block(player_shared.clone()).await;
                } else {
                    update(&player_shared).await;
                }

                next_frame().await;
            }
        });

        let player_shared = player.clone();
        let enemy_shared = enemy.clone();
        enemy.write().unwrap().timeline(&coroutines, async move {
            loop {
                charge(&enemy_shared, 10.0).await;
                attack(&enemy_shared, &player_shared).await;
                charge(&enemy_shared, 10.0).await;
                block(enemy_shared.clone()).await;
                charge(&enemy_shared, 20.0).await;
                attack(&enemy_shared, &player_shared).await;
            }
        });

        Self {
            terminal: Terminal::default(),
            coroutines,
            player,
            enemy,
            log: Vec::new(),
        }
    }
}

impl GameState for Example {
    fn frame(&mut self, mut delta_time: Duration) -> GameStateChange {
        let mut events = Terminal::events().collect::<Vec<_>>();

        draw_system(&mut self.terminal, &self.player, &self.enemy, &self.log);

        {
            let (events_lazy, _events_lifemtime) = DynamicManagedLazy::make(&mut events);
            let (dt_lazy, _dt_lifemtime) = DynamicManagedLazy::make(&mut delta_time);
            let (log_lazy, _log_lifetime) = DynamicManagedLazy::make(&mut self.log);
            self.coroutines.run_frame(
                [
                    (EVENTS_META.into(), events_lazy.into()),
                    (DELTA_TIME_META.into(), dt_lazy.into()),
                    (LOG_META.into(), log_lazy.into()),
                ]
                .into_iter()
                .collect(),
            );
        }

        {
            let player = self.player.write().unwrap();
            if !player.is_alive() {
                player.timeline.cancel();
            }
            let enemy = self.enemy.write().unwrap();
            if !enemy.is_alive() {
                enemy.timeline.cancel();
            }
        }

        if is_key_pressed(&events, KeyCode::Esc) {
            GameStateChange::Quit
        } else {
            GameStateChange::None
        }
    }
}

struct Character {
    name: String,
    health: f32,
    stamina: f32,
    timeline: JobHandle<()>,
}

impl Character {
    fn new(name: impl ToString) -> Self {
        Self {
            name: name.to_string(),
            health: 100.0,
            stamina: 0.0,
            timeline: Default::default(),
        }
    }

    fn timeline(
        &mut self,
        coroutines: &Coroutines,
        timeline: impl Future<Output = ()> + Send + Sync + 'static,
    ) {
        self.timeline = coroutines
            .queue()
            .spawn(JobLocation::Local, timeline)
            .cancel_on_drop();
    }

    fn is_alive(&self) -> bool {
        self.health > 0.0
    }
}

async fn update(this: &AsyncShared<Character>) {
    let dt = delta_time().await.as_secs_f32() * TIME_SCALE;
    let mut this = this.write().unwrap();
    this.stamina = (this.stamina + dt).min(100.0);
}

async fn attack(this: &AsyncShared<Character>, target: &AsyncShared<Character>) {
    log(format!(
        "{} attacks {} with {:.02} hitpoints",
        this.read().unwrap().name,
        target.read().unwrap().name,
        this.read().unwrap().stamina
    ))
    .await;

    let mut this = this.write().unwrap();
    let mut target = target.write().unwrap();

    if this.stamina >= DAMAGE_STAMINA_MIN_REQUIREMENT {
        target.health = (target.health - this.stamina).max(0.0);
        this.stamina = 0.0;
    }
}

async fn charge(this: &AsyncShared<Character>, target_stamina: f32) {
    loop {
        let dt = delta_time().await.as_secs_f32() * TIME_SCALE;
        {
            let mut this = this.write().unwrap();
            this.stamina = (this.stamina + dt).min(100.0);
            if this.stamina >= target_stamina {
                break;
            }
        }

        next_frame().await;
    }
}

async fn block(this: AsyncShared<Character>) {
    log(format!(
        "{} blocks with {:.02} cooldown",
        this.read().unwrap().name,
        this.read().unwrap().stamina
    ))
    .await;

    let health = this.read().unwrap().health;

    loop {
        let dt = delta_time().await.as_secs_f32() * TIME_SCALE;
        {
            let mut this = this.write().unwrap();
            this.health = health;
            this.stamina = (this.stamina - dt).min(100.0);
            if this.stamina <= 0.0 {
                break;
            }
        }

        next_frame().await;
    }

    log(format!("{} stops blocking", this.read().unwrap().name)).await;
}

async fn parry(this: AsyncShared<Character>, target: AsyncShared<Character>, mut cooldown: f32) {
    let origin_health = this.read().unwrap().health;
    let damage = loop {
        let dt = delta_time().await.as_secs_f32() * TIME_SCALE;
        cooldown -= dt;
        if cooldown <= 0.0 {
            return;
        }

        let current_health = this.read().unwrap().health;
        if current_health < origin_health {
            break origin_health - current_health;
        }

        next_frame().await;
    };

    log(format!(
        "{} parries {} with {:.02} hitpoints",
        this.read().unwrap().name,
        target.read().unwrap().name,
        damage
    ))
    .await;

    let mut this = this.write().unwrap();
    let mut target = target.write().unwrap();

    target.health = (target.health - damage).max(0.0);
    this.stamina = 0.0;
    this.health = (this.health + damage).min(100.0);
}

fn draw_system(
    terminal: &mut Terminal,
    player: &AsyncShared<Character>,
    enemy: &AsyncShared<Character>,
    log: &[String],
) {
    terminal.begin_draw(true);

    let player = player.read().unwrap();
    let enemy = enemy.read().unwrap();

    match (player.is_alive(), enemy.is_alive()) {
        (true, true) => {
            terminal.display([1, 1], "Player:".green().bold());
            terminal.display([2, 2], format!("Health: {:.1}", player.health));
            terminal.display([2, 3], format!("Stamina: {:.1}", player.stamina));

            terminal.display([1, 6], "Enemy:".red().bold());
            terminal.display([2, 7], format!("Health: {:.1}", enemy.health));
            terminal.display([2, 8], format!("Stamina: {:.1}", enemy.stamina));

            terminal.display([20, 1], "Log:".underlined());
            for (i, log) in log.iter().rev().take(LOG_CAPACITY).enumerate() {
                terminal.display([21, 2 + i as u16], log);
            }
        }
        (false, false) => {
            terminal.display([1, 1], "It's a draw!".yellow().bold());
        }
        (true, false) => {
            terminal.display([1, 1], "You are victorious!".green().bold());
            terminal.display([2, 2], format!("Health: {:.1}", player.health));
        }
        (false, true) => {
            terminal.display([1, 1], "You have been defeated!".red().bold());
            terminal.display([2, 2], format!("Health: {:.1}", enemy.health));
        }
    }

    terminal.end_draw();
}

async fn delta_time() -> Duration {
    *meta::<Duration>(DELTA_TIME_META)
        .await
        .unwrap()
        .read()
        .unwrap()
}

async fn events() -> Vec<Event> {
    meta::<Vec<Event>>(EVENTS_META)
        .await
        .unwrap()
        .read()
        .unwrap()
        .clone()
}

async fn log(content: impl ToString) {
    meta::<Vec<String>>(LOG_META)
        .await
        .unwrap()
        .write()
        .unwrap()
        .push(content.to_string());
}

// ===

#[allow(dead_code)]
async fn example_with_parry(
    enemy_shared: AsyncShared<Character>,
    player_shared: AsyncShared<Character>,
) {
    loop {
        charge(&enemy_shared, 10.0).await;
        attack(&enemy_shared, &player_shared).await;
        charge(&enemy_shared, 10.0).await;

        let parry = parry(enemy_shared.clone(), player_shared.clone(), 10.0);
        let block = block(enemy_shared.clone());

        with_importance(vec![
            CompletionImportance::ignored(parry),
            CompletionImportance::required(block),
        ])
        .await;

        charge(&enemy_shared, 20.0).await;
        attack(&enemy_shared, &player_shared).await;
    }
}

#[allow(dead_code)]
async fn example_with_waiting_for_input(
    player: &AsyncShared<Character>,
    enemy: &AsyncShared<Character>,
) {
    loop {
        let events = events().await;

        if is_key_pressed(&events, KeyCode::Enter) {
            attack(player, enemy).await;
        } else if is_key_pressed(&events, KeyCode::Char(' ')) {
            block(player.clone()).await;
        } else {
            update(player).await;
        }
    }
}
  1. At every .await point, this coroutine suspends - it does not block current thread, does not sleep. It just suspends execution entirely, until requested resource is available (like new batch of events, that might be available immediatelly or soon).
  2. Each branch of if statement represents a selection of action sub-timeline, meaning whether we choose to do attack(), block() or update() state, we will suspend entire coroutine, loop itself, until selected state completes, then after resuming we might then wait for next game frame to occur and repeat.
  3. Every loop iteration here represents a game frame or more (if we are currently executing any of above states; explained why later).

I get that this specific coroutine might look weird, as we see a loop waiting for next frame at the end. It might give us thinking that single iteration is single game frame, but then we suddenly await inside that iteration? This is confusing!

It might not be intuitive at first, i really get you. But bare with me for a second.

What if that next_frame() await in loop is actually the single point of confusion? Here, look at this:

use crossterm::{
    event::{Event, KeyCode},
    style::Stylize,
};
use moirai::{
    coroutine::{CompletionImportance, meta, with_importance},
    job::{JobHandle, JobLocation},
    third_party::intuicio_data::{managed::DynamicManagedLazy, shared::AsyncShared},
};
use moirai_book_samples::{
    coroutines::{Coroutines, next_frame},
    game::{Game, GameState, GameStateChange},
    terminal::Terminal,
    utils::is_key_pressed,
};
use std::time::Duration;

fn main() {
    Game::new(Example::default()).run_blocking();
}

const TIME_SCALE: f32 = 5.0;
const DAMAGE_STAMINA_MIN_REQUIREMENT: f32 = 5.0;
const LOG_CAPACITY: usize = 5;
const EVENTS_META: &str = "~events~";
const DELTA_TIME_META: &str = "~delta-time~";
const LOG_META: &str = "~log~";

struct Example {
    terminal: Terminal,
    coroutines: Coroutines,
    player: AsyncShared<Character>,
    enemy: AsyncShared<Character>,
    log: Vec<String>,
}

impl Default for Example {
    fn default() -> Self {
        let coroutines = Coroutines::default();

        let player = AsyncShared::new(Character::new("Player"));
        let enemy = AsyncShared::new(Character::new("Enemy"));

        let player_shared = player.clone();
        let enemy_shared = enemy.clone();
        player.write().unwrap().timeline(&coroutines, async move {
            loop {
                let events = events().await;

                if is_key_pressed(&events, KeyCode::Enter) {
                    attack(&player_shared, &enemy_shared).await;
                } else if is_key_pressed(&events, KeyCode::Char(' ')) {
                    block(player_shared.clone()).await;
                } else {
                    update(&player_shared).await;
                }

                next_frame().await;
            }
        });

        let player_shared = player.clone();
        let enemy_shared = enemy.clone();
        enemy.write().unwrap().timeline(&coroutines, async move {
            loop {
                charge(&enemy_shared, 10.0).await;
                attack(&enemy_shared, &player_shared).await;
                charge(&enemy_shared, 10.0).await;
                block(enemy_shared.clone()).await;
                charge(&enemy_shared, 20.0).await;
                attack(&enemy_shared, &player_shared).await;
            }
        });

        Self {
            terminal: Terminal::default(),
            coroutines,
            player,
            enemy,
            log: Vec::new(),
        }
    }
}

impl GameState for Example {
    fn frame(&mut self, mut delta_time: Duration) -> GameStateChange {
        let mut events = Terminal::events().collect::<Vec<_>>();

        draw_system(&mut self.terminal, &self.player, &self.enemy, &self.log);

        {
            let (events_lazy, _events_lifemtime) = DynamicManagedLazy::make(&mut events);
            let (dt_lazy, _dt_lifemtime) = DynamicManagedLazy::make(&mut delta_time);
            let (log_lazy, _log_lifetime) = DynamicManagedLazy::make(&mut self.log);
            self.coroutines.run_frame(
                [
                    (EVENTS_META.into(), events_lazy.into()),
                    (DELTA_TIME_META.into(), dt_lazy.into()),
                    (LOG_META.into(), log_lazy.into()),
                ]
                .into_iter()
                .collect(),
            );
        }

        {
            let player = self.player.write().unwrap();
            if !player.is_alive() {
                player.timeline.cancel();
            }
            let enemy = self.enemy.write().unwrap();
            if !enemy.is_alive() {
                enemy.timeline.cancel();
            }
        }

        if is_key_pressed(&events, KeyCode::Esc) {
            GameStateChange::Quit
        } else {
            GameStateChange::None
        }
    }
}

struct Character {
    name: String,
    health: f32,
    stamina: f32,
    timeline: JobHandle<()>,
}

impl Character {
    fn new(name: impl ToString) -> Self {
        Self {
            name: name.to_string(),
            health: 100.0,
            stamina: 0.0,
            timeline: Default::default(),
        }
    }

    fn timeline(
        &mut self,
        coroutines: &Coroutines,
        timeline: impl Future<Output = ()> + Send + Sync + 'static,
    ) {
        self.timeline = coroutines
            .queue()
            .spawn(JobLocation::Local, timeline)
            .cancel_on_drop();
    }

    fn is_alive(&self) -> bool {
        self.health > 0.0
    }
}

async fn update(this: &AsyncShared<Character>) {
    let dt = delta_time().await.as_secs_f32() * TIME_SCALE;
    let mut this = this.write().unwrap();
    this.stamina = (this.stamina + dt).min(100.0);
}

async fn attack(this: &AsyncShared<Character>, target: &AsyncShared<Character>) {
    log(format!(
        "{} attacks {} with {:.02} hitpoints",
        this.read().unwrap().name,
        target.read().unwrap().name,
        this.read().unwrap().stamina
    ))
    .await;

    let mut this = this.write().unwrap();
    let mut target = target.write().unwrap();

    if this.stamina >= DAMAGE_STAMINA_MIN_REQUIREMENT {
        target.health = (target.health - this.stamina).max(0.0);
        this.stamina = 0.0;
    }
}

async fn charge(this: &AsyncShared<Character>, target_stamina: f32) {
    loop {
        let dt = delta_time().await.as_secs_f32() * TIME_SCALE;
        {
            let mut this = this.write().unwrap();
            this.stamina = (this.stamina + dt).min(100.0);
            if this.stamina >= target_stamina {
                break;
            }
        }

        next_frame().await;
    }
}

async fn block(this: AsyncShared<Character>) {
    log(format!(
        "{} blocks with {:.02} cooldown",
        this.read().unwrap().name,
        this.read().unwrap().stamina
    ))
    .await;

    let health = this.read().unwrap().health;

    loop {
        let dt = delta_time().await.as_secs_f32() * TIME_SCALE;
        {
            let mut this = this.write().unwrap();
            this.health = health;
            this.stamina = (this.stamina - dt).min(100.0);
            if this.stamina <= 0.0 {
                break;
            }
        }

        next_frame().await;
    }

    log(format!("{} stops blocking", this.read().unwrap().name)).await;
}

async fn parry(this: AsyncShared<Character>, target: AsyncShared<Character>, mut cooldown: f32) {
    let origin_health = this.read().unwrap().health;
    let damage = loop {
        let dt = delta_time().await.as_secs_f32() * TIME_SCALE;
        cooldown -= dt;
        if cooldown <= 0.0 {
            return;
        }

        let current_health = this.read().unwrap().health;
        if current_health < origin_health {
            break origin_health - current_health;
        }

        next_frame().await;
    };

    log(format!(
        "{} parries {} with {:.02} hitpoints",
        this.read().unwrap().name,
        target.read().unwrap().name,
        damage
    ))
    .await;

    let mut this = this.write().unwrap();
    let mut target = target.write().unwrap();

    target.health = (target.health - damage).max(0.0);
    this.stamina = 0.0;
    this.health = (this.health + damage).min(100.0);
}

fn draw_system(
    terminal: &mut Terminal,
    player: &AsyncShared<Character>,
    enemy: &AsyncShared<Character>,
    log: &[String],
) {
    terminal.begin_draw(true);

    let player = player.read().unwrap();
    let enemy = enemy.read().unwrap();

    match (player.is_alive(), enemy.is_alive()) {
        (true, true) => {
            terminal.display([1, 1], "Player:".green().bold());
            terminal.display([2, 2], format!("Health: {:.1}", player.health));
            terminal.display([2, 3], format!("Stamina: {:.1}", player.stamina));

            terminal.display([1, 6], "Enemy:".red().bold());
            terminal.display([2, 7], format!("Health: {:.1}", enemy.health));
            terminal.display([2, 8], format!("Stamina: {:.1}", enemy.stamina));

            terminal.display([20, 1], "Log:".underlined());
            for (i, log) in log.iter().rev().take(LOG_CAPACITY).enumerate() {
                terminal.display([21, 2 + i as u16], log);
            }
        }
        (false, false) => {
            terminal.display([1, 1], "It's a draw!".yellow().bold());
        }
        (true, false) => {
            terminal.display([1, 1], "You are victorious!".green().bold());
            terminal.display([2, 2], format!("Health: {:.1}", player.health));
        }
        (false, true) => {
            terminal.display([1, 1], "You have been defeated!".red().bold());
            terminal.display([2, 2], format!("Health: {:.1}", enemy.health));
        }
    }

    terminal.end_draw();
}

async fn delta_time() -> Duration {
    *meta::<Duration>(DELTA_TIME_META)
        .await
        .unwrap()
        .read()
        .unwrap()
}

async fn events() -> Vec<Event> {
    meta::<Vec<Event>>(EVENTS_META)
        .await
        .unwrap()
        .read()
        .unwrap()
        .clone()
}

async fn log(content: impl ToString) {
    meta::<Vec<String>>(LOG_META)
        .await
        .unwrap()
        .write()
        .unwrap()
        .push(content.to_string());
}

// ===

#[allow(dead_code)]
async fn example_with_parry(
    enemy_shared: AsyncShared<Character>,
    player_shared: AsyncShared<Character>,
) {
    loop {
        charge(&enemy_shared, 10.0).await;
        attack(&enemy_shared, &player_shared).await;
        charge(&enemy_shared, 10.0).await;

        let parry = parry(enemy_shared.clone(), player_shared.clone(), 10.0);
        let block = block(enemy_shared.clone());

        with_importance(vec![
            CompletionImportance::ignored(parry),
            CompletionImportance::required(block),
        ])
        .await;

        charge(&enemy_shared, 20.0).await;
        attack(&enemy_shared, &player_shared).await;
    }
}

#[allow(dead_code)]
async fn example_with_waiting_for_input(
    player: &AsyncShared<Character>,
    enemy: &AsyncShared<Character>,
) {
    loop {
        let events = events().await;

        if is_key_pressed(&events, KeyCode::Enter) {
            attack(player, enemy).await;
        } else if is_key_pressed(&events, KeyCode::Char(' ')) {
            block(player.clone()).await;
        } else {
            update(player).await;
        }
    }
}

If we remove it, then this loop reads:

Acquire events -> Decide on which state user wants to be next -> Do selected state -> Repeat

So why we have put that next_frame() in the first place?

Well, because what if all async functions called in there would complete immediatelly? It can happen, if given function depends only on resource availability, and it happen that resource is available upfront.

In that case we don’t want to loop for ever, blocking entire game until some of async functions won’t complete immediatelly, so we just expect given iteration to wait for next frame for good measure.