Prompts
Prompts are kinds of coroutines where we have to await on specific user input.
Let’s remind ourselves the player turn:
use crossterm::{
event::{Event, KeyCode},
style::Stylize,
terminal::size,
};
use moirai::{
coroutine::{meta, spawn, wait_time, yield_now},
job::JobLocation,
jobs::Jobs,
third_party::intuicio_data::{managed::DynamicManagedLazy, shared::AsyncShared},
};
use moirai_book_samples::{
game::{Game, GameState, GameStateChange},
terminal::Terminal,
utils::{is_key_pressed, text_size, text_wrap},
};
use std::time::Duration;
fn main() {
Game::new(Jrpg::default()).run_blocking();
}
const EVENTS_META: &str = "~events~";
const TURN_INDEX_META: &str = "~turn-index~";
const SELECTED_ACTION_META: &str = "~selected-action~";
const UI_SCREEN: &str = "~ui-screen~";
const LOG_META: &str = "~log~";
const STEP_DELAY: Duration = Duration::from_secs(1);
const HEALTH_CAPACITY: usize = 100;
const MANA_CAPACITY: usize = 100;
struct Jrpg {
terminal: Terminal,
jobs: Jobs,
turn_index: usize,
ui_screen: Box<dyn UiScreen>,
selected_action: Option<Action>,
log: Vec<String>,
}
impl Default for Jrpg {
fn default() -> Self {
let jobs = Jobs::default();
let player = AsyncShared::new(Character::new("Player"));
let enemy = AsyncShared::new(Character::new("Enemy"));
jobs.queue()
.spawn(JobLocation::Local, battle(player, enemy));
Self {
terminal: Terminal::default(),
jobs,
turn_index: 0,
ui_screen: Box::new(LogsScreen),
selected_action: None,
log: Default::default(),
}
}
}
impl GameState for Jrpg {
fn frame(&mut self, _delta_time: Duration) -> GameStateChange {
let mut events = Terminal::events().collect::<Vec<_>>();
self.ui_screen.update(&events, &mut self.selected_action);
self.render_system();
self.coroutines_system(&mut events);
if is_key_pressed(&events, KeyCode::Esc) {
GameStateChange::Quit
} else {
GameStateChange::None
}
}
}
impl Jrpg {
fn render_system(&mut self) {
self.terminal.begin_draw(true);
self.ui_screen.render(&mut self.terminal, &self.log);
self.terminal.end_draw();
}
fn coroutines_system(&mut self, events: &mut Vec<Event>) {
let (events_lazy, _events_lifetime) = DynamicManagedLazy::make(events);
let (turn_lazy, _turn_lifetime) = DynamicManagedLazy::make(&mut self.turn_index);
let (action_lazy, _action_lifetime) = DynamicManagedLazy::make(&mut self.selected_action);
let (ui_screen_lazy, _ui_screen_lifetime) = DynamicManagedLazy::make(&mut self.ui_screen);
let (log_lazy, _log_lifetime) = DynamicManagedLazy::make(&mut self.log);
self.jobs.run_local_with_meta(
[
(EVENTS_META.into(), events_lazy.into()),
(TURN_INDEX_META.into(), turn_lazy.into()),
(SELECTED_ACTION_META.into(), action_lazy.into()),
(UI_SCREEN.into(), ui_screen_lazy.into()),
(LOG_META.into(), log_lazy.into()),
]
.into_iter()
.collect(),
);
}
}
async fn battle(player: AsyncShared<Character>, enemy: AsyncShared<Character>) {
log("Battle starts!").await;
// Battle turns loop.
loop {
if win_conditions(player.clone(), enemy.clone()).await {
break;
}
present_information(player.clone(), enemy.clone()).await;
player_turn(player.clone(), enemy.clone()).await;
enemy_turn(enemy.clone(), player.clone()).await;
go_to_next_turn().await;
}
log("Hit ESC to exit the game!").await;
}
async fn win_conditions(player: AsyncShared<Character>, enemy: AsyncShared<Character>) -> bool {
if enemy.read().unwrap().health == 0 {
log("Enemy has been defeated! You win!").await;
true
} else if player.read().unwrap().health == 0 {
log("Player has been defeated! Game over.").await;
true
} else {
false
}
}
async fn present_information(player: AsyncShared<Character>, enemy: AsyncShared<Character>) {
log(format!(
"* New turn #{}\n\
- Player health: {}\n\
- Player mana: {}\n\
- Enemy health: {}",
turn_index().await + 1,
player.read().unwrap().health,
player.read().unwrap().mana,
enemy.read().unwrap().health,
))
.await;
wait_time(STEP_DELAY).await;
}
async fn player_turn(player: AsyncShared<Character>, enemy: AsyncShared<Character>) {
log("* Player turn.\nHit ENTER to take an action!").await;
wait_for_key(KeyCode::Enter).await;
loop {
match selected_action().await {
Action::SkipTurn => {
wait_time(STEP_DELAY).await;
return;
}
Action::Forfeit => {
player.write().unwrap().health = 0;
yield_now().await;
return;
}
Action::SwordSwing => {
sword_swing(player.clone(), enemy.clone()).await;
return;
}
Action::PoisonSpell => {
if poison_spell(player.clone(), enemy.clone()).await {
return;
}
}
Action::FireSpell => {
if fire_spell(player.clone(), enemy.clone()).await {
return;
}
}
Action::CureSpell => {
if cure_spell(player.clone()).await {
return;
}
}
}
yield_now().await;
}
}
async fn enemy_turn(enemy: AsyncShared<Character>, player: AsyncShared<Character>) {
log("* Enemy turn").await;
wait_time(STEP_DELAY).await;
let action_probabilities = [
(4, Action::SwordSwing),
(2, Action::PoisonSpell),
(1, Action::FireSpell),
(
if enemy.read().unwrap().health < 50 {
4
} else {
1
},
Action::CureSpell,
),
];
loop {
match select_random_action(&action_probabilities) {
Action::SwordSwing => {
sword_swing(enemy.clone(), player.clone()).await;
return;
}
Action::PoisonSpell => {
if poison_spell(enemy.clone(), player.clone()).await {
return;
}
}
Action::FireSpell => {
if fire_spell(enemy.clone(), player.clone()).await {
return;
}
}
Action::CureSpell => {
if cure_spell(enemy.clone()).await {
return;
}
}
_ => {
wait_time(STEP_DELAY).await;
return;
}
}
yield_now().await;
}
}
fn select_random_action(action_probabilities: &[(usize, Action)]) -> Action {
let total_weight = action_probabilities
.iter()
.map(|(weight, _)| *weight)
.sum::<usize>();
let mut choice = rand::random_range(0..total_weight) % total_weight;
for (weight, action) in action_probabilities.iter().copied() {
if choice < weight {
return action;
} else {
choice -= weight;
}
}
Action::SkipTurn
}
async fn sword_swing(this: AsyncShared<Character>, target: AsyncShared<Character>) {
{
let mut target = target.write().unwrap();
target.health = target.health.saturating_sub(10).max(0);
}
log(format!(
"{} swings sword at {}",
this.read().unwrap().name,
target.read().unwrap().name
))
.await;
wait_time(STEP_DELAY).await;
}
async fn poison_spell(this: AsyncShared<Character>, target: AsyncShared<Character>) -> bool {
let cast = {
let mut this = this.write().unwrap();
if let Some(mana) = this.mana.checked_sub(30) {
this.mana = mana;
true
} else {
false
}
};
if cast {
spawn(JobLocation::Local, poison_spell_effect(target.clone())).await;
log(format!(
"{} casts poison spell at {}",
this.read().unwrap().name,
target.read().unwrap().name
))
.await;
wait_time(STEP_DELAY).await;
}
cast
}
async fn poison_spell_effect(target: AsyncShared<Character>) {
for index in 0..6 {
{
let mut target = target.write().unwrap();
target.health = target.health.saturating_sub(5).max(0);
}
log(format!(
"Poison status damage dealt to {} ({}/6)",
target.read().unwrap().name,
index + 1,
))
.await;
next_turn().await;
}
}
async fn fire_spell(this: AsyncShared<Character>, target: AsyncShared<Character>) -> bool {
// Attempt to cast the spell by checking and deducting mana.
let cast = {
let mut this = this.write().unwrap();
if let Some(mana) = this.mana.checked_sub(40) {
this.mana = mana;
true
} else {
false
}
};
if cast {
// Spawn the fire spell effect as a concurrent job to be ran in the
// background, alongside of battle loop job, and await until it completes.
spawn(JobLocation::Local, fire_spell_effect(target.clone())).await;
log(format!(
"{} casts fire spell at {}",
this.read().unwrap().name,
target.read().unwrap().name
))
.await;
wait_time(STEP_DELAY).await;
}
cast
}
async fn fire_spell_effect(target: AsyncShared<Character>) {
// Run this effect for 2 battle turns.
for index in 0..2 {
{
// Deal high damage to target.
let mut target = target.write().unwrap();
target.health = target.health.saturating_sub(20).max(0);
}
log(format!(
"Fire status damage dealt to {} ({}/2)",
target.read().unwrap().name,
index + 1,
))
.await;
// Wait until next turn to apply next tick of damage.
next_turn().await;
}
}
async fn cure_spell(target: AsyncShared<Character>) -> bool {
let cast = {
let mut target = target.write().unwrap();
if let Some(mana) = target.mana.checked_sub(25) {
target.mana = mana;
target.health = (target.health + 25).min(HEALTH_CAPACITY);
true
} else {
false
}
};
if cast {
log(format!(
"{} casts cure spell on itself",
target.read().unwrap().name
))
.await;
wait_time(STEP_DELAY).await;
}
cast
}
const ACTIONS: [Action; 6] = [
Action::SkipTurn,
Action::Forfeit,
Action::SwordSwing,
Action::PoisonSpell,
Action::FireSpell,
Action::CureSpell,
];
#[derive(Clone, Copy, PartialEq, Eq)]
enum Action {
SkipTurn,
Forfeit,
SwordSwing,
PoisonSpell,
FireSpell,
CureSpell,
}
impl std::fmt::Display for Action {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let text = match self {
Action::SkipTurn => "Skip Turn",
Action::Forfeit => "Forfeit",
Action::SwordSwing => "Sword Swing",
Action::PoisonSpell => "Poison Spell",
Action::FireSpell => "Fire Spell",
Action::CureSpell => "Cure Spell",
};
write!(f, "{}", text)
}
}
trait UiScreen {
#[allow(unused_variables)]
fn update(&mut self, events: &[Event], selected_action: &mut Option<Action>) {}
#[allow(unused_variables)]
fn render(&self, terminal: &mut Terminal, log: &[String]) {}
}
struct LogsScreen;
impl UiScreen for LogsScreen {
fn render(&self, terminal: &mut Terminal, log: &[String]) {
let (w, h) = size().unwrap();
let mut y = 0;
for (index, text) in log.iter().rev().enumerate() {
let text = text_wrap(text, w as usize);
let size = text_size(&text);
if y + size.y > h as usize {
break;
}
let text = if index == 0 {
text.white().bold()
} else {
text.white().italic()
};
terminal.display([0, y as _], text);
y += size.y + 1;
}
}
}
#[derive(Default)]
struct TakeActionScreen {
selected_index: usize,
}
impl UiScreen for TakeActionScreen {
fn update(&mut self, events: &[Event], selected_action: &mut Option<Action>) {
if is_key_pressed(events, KeyCode::Up) {
self.selected_index = (self.selected_index + ACTIONS.len() - 1) % ACTIONS.len();
} else if is_key_pressed(events, KeyCode::Down) {
self.selected_index = (self.selected_index + 1) % ACTIONS.len();
} else if is_key_pressed(events, KeyCode::Enter) {
*selected_action = Some(ACTIONS[self.selected_index]);
}
}
fn render(&self, terminal: &mut Terminal, _log: &[String]) {
let (w, _) = size().unwrap();
let mut y = 0;
let text = text_wrap("Your turn, take an action:", w as usize);
let size = text_size(&text);
terminal.display([0, y as _], text.green());
y += size.y + 1;
for (index, action) in ACTIONS.iter().enumerate() {
let text = if index == self.selected_index {
format!("> {}", action)
} else {
format!(" {}", action)
};
let text = text_wrap(&text, w as usize);
let size = text_size(&text);
let text = if index == self.selected_index {
text.black().on_white().bold()
} else {
text.white().on_dark_grey().italic()
};
terminal.display([0, y as _], text);
y += size.y + 1;
}
}
}
struct Character {
name: String,
health: usize,
mana: usize,
}
impl Character {
fn new(name: impl ToString) -> Self {
Self {
name: name.to_string(),
health: HEALTH_CAPACITY,
mana: MANA_CAPACITY,
}
}
}
async fn go_to_next_turn() {
*meta::<usize>(TURN_INDEX_META)
.await
.unwrap()
.write()
.unwrap() += 1;
yield_now().await;
}
async fn turn_index() -> usize {
*meta::<usize>(TURN_INDEX_META)
.await
.unwrap()
.read()
.unwrap()
}
async fn next_turn() {
let index = turn_index().await;
while turn_index().await <= index {
yield_now().await;
}
}
async fn events() -> Vec<Event> {
meta::<Vec<Event>>(EVENTS_META)
.await
.unwrap()
.read()
.unwrap()
.clone()
}
async fn wait_for_key(key: KeyCode) {
loop {
let events = events().await;
if is_key_pressed(&events, key) {
return;
}
// Yield to allow other coroutines to run (we suspend this coroutine).
// Without suspending, this coroutine will block entirely and prevent
// other coroutines from running in local thread (and all our coroutines
// are running in local thread, so deadlock guaranteed).
yield_now().await;
}
}
async fn selected_action() -> Action {
// Change UI to take action screen.
// We hold current UI screen reference available in meta storage to be able
// to access it at any time during coroutine work.
*meta::<Box<dyn UiScreen>>(UI_SCREEN)
.await
.unwrap()
.write()
.unwrap() = Box::new(TakeActionScreen::default());
loop {
// Check if an action has been selected from the list.
// Currently selected action by the user is stored in meta storage.
let action = meta::<Option<Action>>(SELECTED_ACTION_META)
.await
.unwrap()
.write()
.unwrap()
.take();
if let Some(action) = action {
// Change UI back to logs screen.
*meta::<Box<dyn UiScreen>>(UI_SCREEN)
.await
.unwrap()
.write()
.unwrap() = Box::new(LogsScreen);
// Return the selected action.
return action;
}
yield_now().await;
}
}
async fn log(content: impl ToString) {
meta::<Vec<String>>(LOG_META)
.await
.unwrap()
.write()
.unwrap()
.push(content.to_string());
yield_now().await;
}
There are two things interesting to us:
wait_for_key()selected_action()
Waiting for key requires us to check for events if there is a specific key pressed event.
use crossterm::{
event::{Event, KeyCode},
style::Stylize,
terminal::size,
};
use moirai::{
coroutine::{meta, spawn, wait_time, yield_now},
job::JobLocation,
jobs::Jobs,
third_party::intuicio_data::{managed::DynamicManagedLazy, shared::AsyncShared},
};
use moirai_book_samples::{
game::{Game, GameState, GameStateChange},
terminal::Terminal,
utils::{is_key_pressed, text_size, text_wrap},
};
use std::time::Duration;
fn main() {
Game::new(Jrpg::default()).run_blocking();
}
const EVENTS_META: &str = "~events~";
const TURN_INDEX_META: &str = "~turn-index~";
const SELECTED_ACTION_META: &str = "~selected-action~";
const UI_SCREEN: &str = "~ui-screen~";
const LOG_META: &str = "~log~";
const STEP_DELAY: Duration = Duration::from_secs(1);
const HEALTH_CAPACITY: usize = 100;
const MANA_CAPACITY: usize = 100;
struct Jrpg {
terminal: Terminal,
jobs: Jobs,
turn_index: usize,
ui_screen: Box<dyn UiScreen>,
selected_action: Option<Action>,
log: Vec<String>,
}
impl Default for Jrpg {
fn default() -> Self {
let jobs = Jobs::default();
let player = AsyncShared::new(Character::new("Player"));
let enemy = AsyncShared::new(Character::new("Enemy"));
jobs.queue()
.spawn(JobLocation::Local, battle(player, enemy));
Self {
terminal: Terminal::default(),
jobs,
turn_index: 0,
ui_screen: Box::new(LogsScreen),
selected_action: None,
log: Default::default(),
}
}
}
impl GameState for Jrpg {
fn frame(&mut self, _delta_time: Duration) -> GameStateChange {
let mut events = Terminal::events().collect::<Vec<_>>();
self.ui_screen.update(&events, &mut self.selected_action);
self.render_system();
self.coroutines_system(&mut events);
if is_key_pressed(&events, KeyCode::Esc) {
GameStateChange::Quit
} else {
GameStateChange::None
}
}
}
impl Jrpg {
fn render_system(&mut self) {
self.terminal.begin_draw(true);
self.ui_screen.render(&mut self.terminal, &self.log);
self.terminal.end_draw();
}
fn coroutines_system(&mut self, events: &mut Vec<Event>) {
let (events_lazy, _events_lifetime) = DynamicManagedLazy::make(events);
let (turn_lazy, _turn_lifetime) = DynamicManagedLazy::make(&mut self.turn_index);
let (action_lazy, _action_lifetime) = DynamicManagedLazy::make(&mut self.selected_action);
let (ui_screen_lazy, _ui_screen_lifetime) = DynamicManagedLazy::make(&mut self.ui_screen);
let (log_lazy, _log_lifetime) = DynamicManagedLazy::make(&mut self.log);
self.jobs.run_local_with_meta(
[
(EVENTS_META.into(), events_lazy.into()),
(TURN_INDEX_META.into(), turn_lazy.into()),
(SELECTED_ACTION_META.into(), action_lazy.into()),
(UI_SCREEN.into(), ui_screen_lazy.into()),
(LOG_META.into(), log_lazy.into()),
]
.into_iter()
.collect(),
);
}
}
async fn battle(player: AsyncShared<Character>, enemy: AsyncShared<Character>) {
log("Battle starts!").await;
// Battle turns loop.
loop {
if win_conditions(player.clone(), enemy.clone()).await {
break;
}
present_information(player.clone(), enemy.clone()).await;
player_turn(player.clone(), enemy.clone()).await;
enemy_turn(enemy.clone(), player.clone()).await;
go_to_next_turn().await;
}
log("Hit ESC to exit the game!").await;
}
async fn win_conditions(player: AsyncShared<Character>, enemy: AsyncShared<Character>) -> bool {
if enemy.read().unwrap().health == 0 {
log("Enemy has been defeated! You win!").await;
true
} else if player.read().unwrap().health == 0 {
log("Player has been defeated! Game over.").await;
true
} else {
false
}
}
async fn present_information(player: AsyncShared<Character>, enemy: AsyncShared<Character>) {
log(format!(
"* New turn #{}\n\
- Player health: {}\n\
- Player mana: {}\n\
- Enemy health: {}",
turn_index().await + 1,
player.read().unwrap().health,
player.read().unwrap().mana,
enemy.read().unwrap().health,
))
.await;
wait_time(STEP_DELAY).await;
}
async fn player_turn(player: AsyncShared<Character>, enemy: AsyncShared<Character>) {
log("* Player turn.\nHit ENTER to take an action!").await;
wait_for_key(KeyCode::Enter).await;
loop {
match selected_action().await {
Action::SkipTurn => {
wait_time(STEP_DELAY).await;
return;
}
Action::Forfeit => {
player.write().unwrap().health = 0;
yield_now().await;
return;
}
Action::SwordSwing => {
sword_swing(player.clone(), enemy.clone()).await;
return;
}
Action::PoisonSpell => {
if poison_spell(player.clone(), enemy.clone()).await {
return;
}
}
Action::FireSpell => {
if fire_spell(player.clone(), enemy.clone()).await {
return;
}
}
Action::CureSpell => {
if cure_spell(player.clone()).await {
return;
}
}
}
yield_now().await;
}
}
async fn enemy_turn(enemy: AsyncShared<Character>, player: AsyncShared<Character>) {
log("* Enemy turn").await;
wait_time(STEP_DELAY).await;
let action_probabilities = [
(4, Action::SwordSwing),
(2, Action::PoisonSpell),
(1, Action::FireSpell),
(
if enemy.read().unwrap().health < 50 {
4
} else {
1
},
Action::CureSpell,
),
];
loop {
match select_random_action(&action_probabilities) {
Action::SwordSwing => {
sword_swing(enemy.clone(), player.clone()).await;
return;
}
Action::PoisonSpell => {
if poison_spell(enemy.clone(), player.clone()).await {
return;
}
}
Action::FireSpell => {
if fire_spell(enemy.clone(), player.clone()).await {
return;
}
}
Action::CureSpell => {
if cure_spell(enemy.clone()).await {
return;
}
}
_ => {
wait_time(STEP_DELAY).await;
return;
}
}
yield_now().await;
}
}
fn select_random_action(action_probabilities: &[(usize, Action)]) -> Action {
let total_weight = action_probabilities
.iter()
.map(|(weight, _)| *weight)
.sum::<usize>();
let mut choice = rand::random_range(0..total_weight) % total_weight;
for (weight, action) in action_probabilities.iter().copied() {
if choice < weight {
return action;
} else {
choice -= weight;
}
}
Action::SkipTurn
}
async fn sword_swing(this: AsyncShared<Character>, target: AsyncShared<Character>) {
{
let mut target = target.write().unwrap();
target.health = target.health.saturating_sub(10).max(0);
}
log(format!(
"{} swings sword at {}",
this.read().unwrap().name,
target.read().unwrap().name
))
.await;
wait_time(STEP_DELAY).await;
}
async fn poison_spell(this: AsyncShared<Character>, target: AsyncShared<Character>) -> bool {
let cast = {
let mut this = this.write().unwrap();
if let Some(mana) = this.mana.checked_sub(30) {
this.mana = mana;
true
} else {
false
}
};
if cast {
spawn(JobLocation::Local, poison_spell_effect(target.clone())).await;
log(format!(
"{} casts poison spell at {}",
this.read().unwrap().name,
target.read().unwrap().name
))
.await;
wait_time(STEP_DELAY).await;
}
cast
}
async fn poison_spell_effect(target: AsyncShared<Character>) {
for index in 0..6 {
{
let mut target = target.write().unwrap();
target.health = target.health.saturating_sub(5).max(0);
}
log(format!(
"Poison status damage dealt to {} ({}/6)",
target.read().unwrap().name,
index + 1,
))
.await;
next_turn().await;
}
}
async fn fire_spell(this: AsyncShared<Character>, target: AsyncShared<Character>) -> bool {
// Attempt to cast the spell by checking and deducting mana.
let cast = {
let mut this = this.write().unwrap();
if let Some(mana) = this.mana.checked_sub(40) {
this.mana = mana;
true
} else {
false
}
};
if cast {
// Spawn the fire spell effect as a concurrent job to be ran in the
// background, alongside of battle loop job, and await until it completes.
spawn(JobLocation::Local, fire_spell_effect(target.clone())).await;
log(format!(
"{} casts fire spell at {}",
this.read().unwrap().name,
target.read().unwrap().name
))
.await;
wait_time(STEP_DELAY).await;
}
cast
}
async fn fire_spell_effect(target: AsyncShared<Character>) {
// Run this effect for 2 battle turns.
for index in 0..2 {
{
// Deal high damage to target.
let mut target = target.write().unwrap();
target.health = target.health.saturating_sub(20).max(0);
}
log(format!(
"Fire status damage dealt to {} ({}/2)",
target.read().unwrap().name,
index + 1,
))
.await;
// Wait until next turn to apply next tick of damage.
next_turn().await;
}
}
async fn cure_spell(target: AsyncShared<Character>) -> bool {
let cast = {
let mut target = target.write().unwrap();
if let Some(mana) = target.mana.checked_sub(25) {
target.mana = mana;
target.health = (target.health + 25).min(HEALTH_CAPACITY);
true
} else {
false
}
};
if cast {
log(format!(
"{} casts cure spell on itself",
target.read().unwrap().name
))
.await;
wait_time(STEP_DELAY).await;
}
cast
}
const ACTIONS: [Action; 6] = [
Action::SkipTurn,
Action::Forfeit,
Action::SwordSwing,
Action::PoisonSpell,
Action::FireSpell,
Action::CureSpell,
];
#[derive(Clone, Copy, PartialEq, Eq)]
enum Action {
SkipTurn,
Forfeit,
SwordSwing,
PoisonSpell,
FireSpell,
CureSpell,
}
impl std::fmt::Display for Action {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let text = match self {
Action::SkipTurn => "Skip Turn",
Action::Forfeit => "Forfeit",
Action::SwordSwing => "Sword Swing",
Action::PoisonSpell => "Poison Spell",
Action::FireSpell => "Fire Spell",
Action::CureSpell => "Cure Spell",
};
write!(f, "{}", text)
}
}
trait UiScreen {
#[allow(unused_variables)]
fn update(&mut self, events: &[Event], selected_action: &mut Option<Action>) {}
#[allow(unused_variables)]
fn render(&self, terminal: &mut Terminal, log: &[String]) {}
}
struct LogsScreen;
impl UiScreen for LogsScreen {
fn render(&self, terminal: &mut Terminal, log: &[String]) {
let (w, h) = size().unwrap();
let mut y = 0;
for (index, text) in log.iter().rev().enumerate() {
let text = text_wrap(text, w as usize);
let size = text_size(&text);
if y + size.y > h as usize {
break;
}
let text = if index == 0 {
text.white().bold()
} else {
text.white().italic()
};
terminal.display([0, y as _], text);
y += size.y + 1;
}
}
}
#[derive(Default)]
struct TakeActionScreen {
selected_index: usize,
}
impl UiScreen for TakeActionScreen {
fn update(&mut self, events: &[Event], selected_action: &mut Option<Action>) {
if is_key_pressed(events, KeyCode::Up) {
self.selected_index = (self.selected_index + ACTIONS.len() - 1) % ACTIONS.len();
} else if is_key_pressed(events, KeyCode::Down) {
self.selected_index = (self.selected_index + 1) % ACTIONS.len();
} else if is_key_pressed(events, KeyCode::Enter) {
*selected_action = Some(ACTIONS[self.selected_index]);
}
}
fn render(&self, terminal: &mut Terminal, _log: &[String]) {
let (w, _) = size().unwrap();
let mut y = 0;
let text = text_wrap("Your turn, take an action:", w as usize);
let size = text_size(&text);
terminal.display([0, y as _], text.green());
y += size.y + 1;
for (index, action) in ACTIONS.iter().enumerate() {
let text = if index == self.selected_index {
format!("> {}", action)
} else {
format!(" {}", action)
};
let text = text_wrap(&text, w as usize);
let size = text_size(&text);
let text = if index == self.selected_index {
text.black().on_white().bold()
} else {
text.white().on_dark_grey().italic()
};
terminal.display([0, y as _], text);
y += size.y + 1;
}
}
}
struct Character {
name: String,
health: usize,
mana: usize,
}
impl Character {
fn new(name: impl ToString) -> Self {
Self {
name: name.to_string(),
health: HEALTH_CAPACITY,
mana: MANA_CAPACITY,
}
}
}
async fn go_to_next_turn() {
*meta::<usize>(TURN_INDEX_META)
.await
.unwrap()
.write()
.unwrap() += 1;
yield_now().await;
}
async fn turn_index() -> usize {
*meta::<usize>(TURN_INDEX_META)
.await
.unwrap()
.read()
.unwrap()
}
async fn next_turn() {
let index = turn_index().await;
while turn_index().await <= index {
yield_now().await;
}
}
async fn events() -> Vec<Event> {
meta::<Vec<Event>>(EVENTS_META)
.await
.unwrap()
.read()
.unwrap()
.clone()
}
async fn wait_for_key(key: KeyCode) {
loop {
let events = events().await;
if is_key_pressed(&events, key) {
return;
}
// Yield to allow other coroutines to run (we suspend this coroutine).
// Without suspending, this coroutine will block entirely and prevent
// other coroutines from running in local thread (and all our coroutines
// are running in local thread, so deadlock guaranteed).
yield_now().await;
}
}
async fn selected_action() -> Action {
// Change UI to take action screen.
// We hold current UI screen reference available in meta storage to be able
// to access it at any time during coroutine work.
*meta::<Box<dyn UiScreen>>(UI_SCREEN)
.await
.unwrap()
.write()
.unwrap() = Box::new(TakeActionScreen::default());
loop {
// Check if an action has been selected from the list.
// Currently selected action by the user is stored in meta storage.
let action = meta::<Option<Action>>(SELECTED_ACTION_META)
.await
.unwrap()
.write()
.unwrap()
.take();
if let Some(action) = action {
// Change UI back to logs screen.
*meta::<Box<dyn UiScreen>>(UI_SCREEN)
.await
.unwrap()
.write()
.unwrap() = Box::new(LogsScreen);
// Return the selected action.
return action;
}
yield_now().await;
}
}
async fn log(content: impl ToString) {
meta::<Vec<String>>(LOG_META)
.await
.unwrap()
.write()
.unwrap()
.push(content.to_string());
yield_now().await;
}
While awaiting for user selected action is bit more complex, as it interacts with the user input and UI widget to fetch data from the UI options list.
use crossterm::{
event::{Event, KeyCode},
style::Stylize,
terminal::size,
};
use moirai::{
coroutine::{meta, spawn, wait_time, yield_now},
job::JobLocation,
jobs::Jobs,
third_party::intuicio_data::{managed::DynamicManagedLazy, shared::AsyncShared},
};
use moirai_book_samples::{
game::{Game, GameState, GameStateChange},
terminal::Terminal,
utils::{is_key_pressed, text_size, text_wrap},
};
use std::time::Duration;
fn main() {
Game::new(Jrpg::default()).run_blocking();
}
const EVENTS_META: &str = "~events~";
const TURN_INDEX_META: &str = "~turn-index~";
const SELECTED_ACTION_META: &str = "~selected-action~";
const UI_SCREEN: &str = "~ui-screen~";
const LOG_META: &str = "~log~";
const STEP_DELAY: Duration = Duration::from_secs(1);
const HEALTH_CAPACITY: usize = 100;
const MANA_CAPACITY: usize = 100;
struct Jrpg {
terminal: Terminal,
jobs: Jobs,
turn_index: usize,
ui_screen: Box<dyn UiScreen>,
selected_action: Option<Action>,
log: Vec<String>,
}
impl Default for Jrpg {
fn default() -> Self {
let jobs = Jobs::default();
let player = AsyncShared::new(Character::new("Player"));
let enemy = AsyncShared::new(Character::new("Enemy"));
jobs.queue()
.spawn(JobLocation::Local, battle(player, enemy));
Self {
terminal: Terminal::default(),
jobs,
turn_index: 0,
ui_screen: Box::new(LogsScreen),
selected_action: None,
log: Default::default(),
}
}
}
impl GameState for Jrpg {
fn frame(&mut self, _delta_time: Duration) -> GameStateChange {
let mut events = Terminal::events().collect::<Vec<_>>();
self.ui_screen.update(&events, &mut self.selected_action);
self.render_system();
self.coroutines_system(&mut events);
if is_key_pressed(&events, KeyCode::Esc) {
GameStateChange::Quit
} else {
GameStateChange::None
}
}
}
impl Jrpg {
fn render_system(&mut self) {
self.terminal.begin_draw(true);
self.ui_screen.render(&mut self.terminal, &self.log);
self.terminal.end_draw();
}
fn coroutines_system(&mut self, events: &mut Vec<Event>) {
let (events_lazy, _events_lifetime) = DynamicManagedLazy::make(events);
let (turn_lazy, _turn_lifetime) = DynamicManagedLazy::make(&mut self.turn_index);
let (action_lazy, _action_lifetime) = DynamicManagedLazy::make(&mut self.selected_action);
let (ui_screen_lazy, _ui_screen_lifetime) = DynamicManagedLazy::make(&mut self.ui_screen);
let (log_lazy, _log_lifetime) = DynamicManagedLazy::make(&mut self.log);
self.jobs.run_local_with_meta(
[
(EVENTS_META.into(), events_lazy.into()),
(TURN_INDEX_META.into(), turn_lazy.into()),
(SELECTED_ACTION_META.into(), action_lazy.into()),
(UI_SCREEN.into(), ui_screen_lazy.into()),
(LOG_META.into(), log_lazy.into()),
]
.into_iter()
.collect(),
);
}
}
async fn battle(player: AsyncShared<Character>, enemy: AsyncShared<Character>) {
log("Battle starts!").await;
// Battle turns loop.
loop {
if win_conditions(player.clone(), enemy.clone()).await {
break;
}
present_information(player.clone(), enemy.clone()).await;
player_turn(player.clone(), enemy.clone()).await;
enemy_turn(enemy.clone(), player.clone()).await;
go_to_next_turn().await;
}
log("Hit ESC to exit the game!").await;
}
async fn win_conditions(player: AsyncShared<Character>, enemy: AsyncShared<Character>) -> bool {
if enemy.read().unwrap().health == 0 {
log("Enemy has been defeated! You win!").await;
true
} else if player.read().unwrap().health == 0 {
log("Player has been defeated! Game over.").await;
true
} else {
false
}
}
async fn present_information(player: AsyncShared<Character>, enemy: AsyncShared<Character>) {
log(format!(
"* New turn #{}\n\
- Player health: {}\n\
- Player mana: {}\n\
- Enemy health: {}",
turn_index().await + 1,
player.read().unwrap().health,
player.read().unwrap().mana,
enemy.read().unwrap().health,
))
.await;
wait_time(STEP_DELAY).await;
}
async fn player_turn(player: AsyncShared<Character>, enemy: AsyncShared<Character>) {
log("* Player turn.\nHit ENTER to take an action!").await;
wait_for_key(KeyCode::Enter).await;
loop {
match selected_action().await {
Action::SkipTurn => {
wait_time(STEP_DELAY).await;
return;
}
Action::Forfeit => {
player.write().unwrap().health = 0;
yield_now().await;
return;
}
Action::SwordSwing => {
sword_swing(player.clone(), enemy.clone()).await;
return;
}
Action::PoisonSpell => {
if poison_spell(player.clone(), enemy.clone()).await {
return;
}
}
Action::FireSpell => {
if fire_spell(player.clone(), enemy.clone()).await {
return;
}
}
Action::CureSpell => {
if cure_spell(player.clone()).await {
return;
}
}
}
yield_now().await;
}
}
async fn enemy_turn(enemy: AsyncShared<Character>, player: AsyncShared<Character>) {
log("* Enemy turn").await;
wait_time(STEP_DELAY).await;
let action_probabilities = [
(4, Action::SwordSwing),
(2, Action::PoisonSpell),
(1, Action::FireSpell),
(
if enemy.read().unwrap().health < 50 {
4
} else {
1
},
Action::CureSpell,
),
];
loop {
match select_random_action(&action_probabilities) {
Action::SwordSwing => {
sword_swing(enemy.clone(), player.clone()).await;
return;
}
Action::PoisonSpell => {
if poison_spell(enemy.clone(), player.clone()).await {
return;
}
}
Action::FireSpell => {
if fire_spell(enemy.clone(), player.clone()).await {
return;
}
}
Action::CureSpell => {
if cure_spell(enemy.clone()).await {
return;
}
}
_ => {
wait_time(STEP_DELAY).await;
return;
}
}
yield_now().await;
}
}
fn select_random_action(action_probabilities: &[(usize, Action)]) -> Action {
let total_weight = action_probabilities
.iter()
.map(|(weight, _)| *weight)
.sum::<usize>();
let mut choice = rand::random_range(0..total_weight) % total_weight;
for (weight, action) in action_probabilities.iter().copied() {
if choice < weight {
return action;
} else {
choice -= weight;
}
}
Action::SkipTurn
}
async fn sword_swing(this: AsyncShared<Character>, target: AsyncShared<Character>) {
{
let mut target = target.write().unwrap();
target.health = target.health.saturating_sub(10).max(0);
}
log(format!(
"{} swings sword at {}",
this.read().unwrap().name,
target.read().unwrap().name
))
.await;
wait_time(STEP_DELAY).await;
}
async fn poison_spell(this: AsyncShared<Character>, target: AsyncShared<Character>) -> bool {
let cast = {
let mut this = this.write().unwrap();
if let Some(mana) = this.mana.checked_sub(30) {
this.mana = mana;
true
} else {
false
}
};
if cast {
spawn(JobLocation::Local, poison_spell_effect(target.clone())).await;
log(format!(
"{} casts poison spell at {}",
this.read().unwrap().name,
target.read().unwrap().name
))
.await;
wait_time(STEP_DELAY).await;
}
cast
}
async fn poison_spell_effect(target: AsyncShared<Character>) {
for index in 0..6 {
{
let mut target = target.write().unwrap();
target.health = target.health.saturating_sub(5).max(0);
}
log(format!(
"Poison status damage dealt to {} ({}/6)",
target.read().unwrap().name,
index + 1,
))
.await;
next_turn().await;
}
}
async fn fire_spell(this: AsyncShared<Character>, target: AsyncShared<Character>) -> bool {
// Attempt to cast the spell by checking and deducting mana.
let cast = {
let mut this = this.write().unwrap();
if let Some(mana) = this.mana.checked_sub(40) {
this.mana = mana;
true
} else {
false
}
};
if cast {
// Spawn the fire spell effect as a concurrent job to be ran in the
// background, alongside of battle loop job, and await until it completes.
spawn(JobLocation::Local, fire_spell_effect(target.clone())).await;
log(format!(
"{} casts fire spell at {}",
this.read().unwrap().name,
target.read().unwrap().name
))
.await;
wait_time(STEP_DELAY).await;
}
cast
}
async fn fire_spell_effect(target: AsyncShared<Character>) {
// Run this effect for 2 battle turns.
for index in 0..2 {
{
// Deal high damage to target.
let mut target = target.write().unwrap();
target.health = target.health.saturating_sub(20).max(0);
}
log(format!(
"Fire status damage dealt to {} ({}/2)",
target.read().unwrap().name,
index + 1,
))
.await;
// Wait until next turn to apply next tick of damage.
next_turn().await;
}
}
async fn cure_spell(target: AsyncShared<Character>) -> bool {
let cast = {
let mut target = target.write().unwrap();
if let Some(mana) = target.mana.checked_sub(25) {
target.mana = mana;
target.health = (target.health + 25).min(HEALTH_CAPACITY);
true
} else {
false
}
};
if cast {
log(format!(
"{} casts cure spell on itself",
target.read().unwrap().name
))
.await;
wait_time(STEP_DELAY).await;
}
cast
}
const ACTIONS: [Action; 6] = [
Action::SkipTurn,
Action::Forfeit,
Action::SwordSwing,
Action::PoisonSpell,
Action::FireSpell,
Action::CureSpell,
];
#[derive(Clone, Copy, PartialEq, Eq)]
enum Action {
SkipTurn,
Forfeit,
SwordSwing,
PoisonSpell,
FireSpell,
CureSpell,
}
impl std::fmt::Display for Action {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let text = match self {
Action::SkipTurn => "Skip Turn",
Action::Forfeit => "Forfeit",
Action::SwordSwing => "Sword Swing",
Action::PoisonSpell => "Poison Spell",
Action::FireSpell => "Fire Spell",
Action::CureSpell => "Cure Spell",
};
write!(f, "{}", text)
}
}
trait UiScreen {
#[allow(unused_variables)]
fn update(&mut self, events: &[Event], selected_action: &mut Option<Action>) {}
#[allow(unused_variables)]
fn render(&self, terminal: &mut Terminal, log: &[String]) {}
}
struct LogsScreen;
impl UiScreen for LogsScreen {
fn render(&self, terminal: &mut Terminal, log: &[String]) {
let (w, h) = size().unwrap();
let mut y = 0;
for (index, text) in log.iter().rev().enumerate() {
let text = text_wrap(text, w as usize);
let size = text_size(&text);
if y + size.y > h as usize {
break;
}
let text = if index == 0 {
text.white().bold()
} else {
text.white().italic()
};
terminal.display([0, y as _], text);
y += size.y + 1;
}
}
}
#[derive(Default)]
struct TakeActionScreen {
selected_index: usize,
}
impl UiScreen for TakeActionScreen {
fn update(&mut self, events: &[Event], selected_action: &mut Option<Action>) {
if is_key_pressed(events, KeyCode::Up) {
self.selected_index = (self.selected_index + ACTIONS.len() - 1) % ACTIONS.len();
} else if is_key_pressed(events, KeyCode::Down) {
self.selected_index = (self.selected_index + 1) % ACTIONS.len();
} else if is_key_pressed(events, KeyCode::Enter) {
*selected_action = Some(ACTIONS[self.selected_index]);
}
}
fn render(&self, terminal: &mut Terminal, _log: &[String]) {
let (w, _) = size().unwrap();
let mut y = 0;
let text = text_wrap("Your turn, take an action:", w as usize);
let size = text_size(&text);
terminal.display([0, y as _], text.green());
y += size.y + 1;
for (index, action) in ACTIONS.iter().enumerate() {
let text = if index == self.selected_index {
format!("> {}", action)
} else {
format!(" {}", action)
};
let text = text_wrap(&text, w as usize);
let size = text_size(&text);
let text = if index == self.selected_index {
text.black().on_white().bold()
} else {
text.white().on_dark_grey().italic()
};
terminal.display([0, y as _], text);
y += size.y + 1;
}
}
}
struct Character {
name: String,
health: usize,
mana: usize,
}
impl Character {
fn new(name: impl ToString) -> Self {
Self {
name: name.to_string(),
health: HEALTH_CAPACITY,
mana: MANA_CAPACITY,
}
}
}
async fn go_to_next_turn() {
*meta::<usize>(TURN_INDEX_META)
.await
.unwrap()
.write()
.unwrap() += 1;
yield_now().await;
}
async fn turn_index() -> usize {
*meta::<usize>(TURN_INDEX_META)
.await
.unwrap()
.read()
.unwrap()
}
async fn next_turn() {
let index = turn_index().await;
while turn_index().await <= index {
yield_now().await;
}
}
async fn events() -> Vec<Event> {
meta::<Vec<Event>>(EVENTS_META)
.await
.unwrap()
.read()
.unwrap()
.clone()
}
async fn wait_for_key(key: KeyCode) {
loop {
let events = events().await;
if is_key_pressed(&events, key) {
return;
}
// Yield to allow other coroutines to run (we suspend this coroutine).
// Without suspending, this coroutine will block entirely and prevent
// other coroutines from running in local thread (and all our coroutines
// are running in local thread, so deadlock guaranteed).
yield_now().await;
}
}
async fn selected_action() -> Action {
// Change UI to take action screen.
// We hold current UI screen reference available in meta storage to be able
// to access it at any time during coroutine work.
*meta::<Box<dyn UiScreen>>(UI_SCREEN)
.await
.unwrap()
.write()
.unwrap() = Box::new(TakeActionScreen::default());
loop {
// Check if an action has been selected from the list.
// Currently selected action by the user is stored in meta storage.
let action = meta::<Option<Action>>(SELECTED_ACTION_META)
.await
.unwrap()
.write()
.unwrap()
.take();
if let Some(action) = action {
// Change UI back to logs screen.
*meta::<Box<dyn UiScreen>>(UI_SCREEN)
.await
.unwrap()
.write()
.unwrap() = Box::new(LogsScreen);
// Return the selected action.
return action;
}
yield_now().await;
}
}
async fn log(content: impl ToString) {
meta::<Vec<String>>(LOG_META)
.await
.unwrap()
.write()
.unwrap()
.push(content.to_string());
yield_now().await;
}
And this is how our synchronous side manages user-selected action provided to running coroutines:
use crossterm::{
event::{Event, KeyCode},
style::Stylize,
terminal::size,
};
use moirai::{
coroutine::{meta, spawn, wait_time, yield_now},
job::JobLocation,
jobs::Jobs,
third_party::intuicio_data::{managed::DynamicManagedLazy, shared::AsyncShared},
};
use moirai_book_samples::{
game::{Game, GameState, GameStateChange},
terminal::Terminal,
utils::{is_key_pressed, text_size, text_wrap},
};
use std::time::Duration;
fn main() {
Game::new(Jrpg::default()).run_blocking();
}
const EVENTS_META: &str = "~events~";
const TURN_INDEX_META: &str = "~turn-index~";
const SELECTED_ACTION_META: &str = "~selected-action~";
const UI_SCREEN: &str = "~ui-screen~";
const LOG_META: &str = "~log~";
const STEP_DELAY: Duration = Duration::from_secs(1);
const HEALTH_CAPACITY: usize = 100;
const MANA_CAPACITY: usize = 100;
struct Jrpg {
terminal: Terminal,
jobs: Jobs,
turn_index: usize,
ui_screen: Box<dyn UiScreen>,
selected_action: Option<Action>,
log: Vec<String>,
}
impl Default for Jrpg {
fn default() -> Self {
let jobs = Jobs::default();
let player = AsyncShared::new(Character::new("Player"));
let enemy = AsyncShared::new(Character::new("Enemy"));
jobs.queue()
.spawn(JobLocation::Local, battle(player, enemy));
Self {
terminal: Terminal::default(),
jobs,
turn_index: 0,
ui_screen: Box::new(LogsScreen),
selected_action: None,
log: Default::default(),
}
}
}
impl GameState for Jrpg {
fn frame(&mut self, _delta_time: Duration) -> GameStateChange {
let mut events = Terminal::events().collect::<Vec<_>>();
self.ui_screen.update(&events, &mut self.selected_action);
self.render_system();
self.coroutines_system(&mut events);
if is_key_pressed(&events, KeyCode::Esc) {
GameStateChange::Quit
} else {
GameStateChange::None
}
}
}
impl Jrpg {
fn render_system(&mut self) {
self.terminal.begin_draw(true);
self.ui_screen.render(&mut self.terminal, &self.log);
self.terminal.end_draw();
}
fn coroutines_system(&mut self, events: &mut Vec<Event>) {
let (events_lazy, _events_lifetime) = DynamicManagedLazy::make(events);
let (turn_lazy, _turn_lifetime) = DynamicManagedLazy::make(&mut self.turn_index);
let (action_lazy, _action_lifetime) = DynamicManagedLazy::make(&mut self.selected_action);
let (ui_screen_lazy, _ui_screen_lifetime) = DynamicManagedLazy::make(&mut self.ui_screen);
let (log_lazy, _log_lifetime) = DynamicManagedLazy::make(&mut self.log);
self.jobs.run_local_with_meta(
[
(EVENTS_META.into(), events_lazy.into()),
(TURN_INDEX_META.into(), turn_lazy.into()),
(SELECTED_ACTION_META.into(), action_lazy.into()),
(UI_SCREEN.into(), ui_screen_lazy.into()),
(LOG_META.into(), log_lazy.into()),
]
.into_iter()
.collect(),
);
}
}
async fn battle(player: AsyncShared<Character>, enemy: AsyncShared<Character>) {
log("Battle starts!").await;
// Battle turns loop.
loop {
if win_conditions(player.clone(), enemy.clone()).await {
break;
}
present_information(player.clone(), enemy.clone()).await;
player_turn(player.clone(), enemy.clone()).await;
enemy_turn(enemy.clone(), player.clone()).await;
go_to_next_turn().await;
}
log("Hit ESC to exit the game!").await;
}
async fn win_conditions(player: AsyncShared<Character>, enemy: AsyncShared<Character>) -> bool {
if enemy.read().unwrap().health == 0 {
log("Enemy has been defeated! You win!").await;
true
} else if player.read().unwrap().health == 0 {
log("Player has been defeated! Game over.").await;
true
} else {
false
}
}
async fn present_information(player: AsyncShared<Character>, enemy: AsyncShared<Character>) {
log(format!(
"* New turn #{}\n\
- Player health: {}\n\
- Player mana: {}\n\
- Enemy health: {}",
turn_index().await + 1,
player.read().unwrap().health,
player.read().unwrap().mana,
enemy.read().unwrap().health,
))
.await;
wait_time(STEP_DELAY).await;
}
async fn player_turn(player: AsyncShared<Character>, enemy: AsyncShared<Character>) {
log("* Player turn.\nHit ENTER to take an action!").await;
wait_for_key(KeyCode::Enter).await;
loop {
match selected_action().await {
Action::SkipTurn => {
wait_time(STEP_DELAY).await;
return;
}
Action::Forfeit => {
player.write().unwrap().health = 0;
yield_now().await;
return;
}
Action::SwordSwing => {
sword_swing(player.clone(), enemy.clone()).await;
return;
}
Action::PoisonSpell => {
if poison_spell(player.clone(), enemy.clone()).await {
return;
}
}
Action::FireSpell => {
if fire_spell(player.clone(), enemy.clone()).await {
return;
}
}
Action::CureSpell => {
if cure_spell(player.clone()).await {
return;
}
}
}
yield_now().await;
}
}
async fn enemy_turn(enemy: AsyncShared<Character>, player: AsyncShared<Character>) {
log("* Enemy turn").await;
wait_time(STEP_DELAY).await;
let action_probabilities = [
(4, Action::SwordSwing),
(2, Action::PoisonSpell),
(1, Action::FireSpell),
(
if enemy.read().unwrap().health < 50 {
4
} else {
1
},
Action::CureSpell,
),
];
loop {
match select_random_action(&action_probabilities) {
Action::SwordSwing => {
sword_swing(enemy.clone(), player.clone()).await;
return;
}
Action::PoisonSpell => {
if poison_spell(enemy.clone(), player.clone()).await {
return;
}
}
Action::FireSpell => {
if fire_spell(enemy.clone(), player.clone()).await {
return;
}
}
Action::CureSpell => {
if cure_spell(enemy.clone()).await {
return;
}
}
_ => {
wait_time(STEP_DELAY).await;
return;
}
}
yield_now().await;
}
}
fn select_random_action(action_probabilities: &[(usize, Action)]) -> Action {
let total_weight = action_probabilities
.iter()
.map(|(weight, _)| *weight)
.sum::<usize>();
let mut choice = rand::random_range(0..total_weight) % total_weight;
for (weight, action) in action_probabilities.iter().copied() {
if choice < weight {
return action;
} else {
choice -= weight;
}
}
Action::SkipTurn
}
async fn sword_swing(this: AsyncShared<Character>, target: AsyncShared<Character>) {
{
let mut target = target.write().unwrap();
target.health = target.health.saturating_sub(10).max(0);
}
log(format!(
"{} swings sword at {}",
this.read().unwrap().name,
target.read().unwrap().name
))
.await;
wait_time(STEP_DELAY).await;
}
async fn poison_spell(this: AsyncShared<Character>, target: AsyncShared<Character>) -> bool {
let cast = {
let mut this = this.write().unwrap();
if let Some(mana) = this.mana.checked_sub(30) {
this.mana = mana;
true
} else {
false
}
};
if cast {
spawn(JobLocation::Local, poison_spell_effect(target.clone())).await;
log(format!(
"{} casts poison spell at {}",
this.read().unwrap().name,
target.read().unwrap().name
))
.await;
wait_time(STEP_DELAY).await;
}
cast
}
async fn poison_spell_effect(target: AsyncShared<Character>) {
for index in 0..6 {
{
let mut target = target.write().unwrap();
target.health = target.health.saturating_sub(5).max(0);
}
log(format!(
"Poison status damage dealt to {} ({}/6)",
target.read().unwrap().name,
index + 1,
))
.await;
next_turn().await;
}
}
async fn fire_spell(this: AsyncShared<Character>, target: AsyncShared<Character>) -> bool {
// Attempt to cast the spell by checking and deducting mana.
let cast = {
let mut this = this.write().unwrap();
if let Some(mana) = this.mana.checked_sub(40) {
this.mana = mana;
true
} else {
false
}
};
if cast {
// Spawn the fire spell effect as a concurrent job to be ran in the
// background, alongside of battle loop job, and await until it completes.
spawn(JobLocation::Local, fire_spell_effect(target.clone())).await;
log(format!(
"{} casts fire spell at {}",
this.read().unwrap().name,
target.read().unwrap().name
))
.await;
wait_time(STEP_DELAY).await;
}
cast
}
async fn fire_spell_effect(target: AsyncShared<Character>) {
// Run this effect for 2 battle turns.
for index in 0..2 {
{
// Deal high damage to target.
let mut target = target.write().unwrap();
target.health = target.health.saturating_sub(20).max(0);
}
log(format!(
"Fire status damage dealt to {} ({}/2)",
target.read().unwrap().name,
index + 1,
))
.await;
// Wait until next turn to apply next tick of damage.
next_turn().await;
}
}
async fn cure_spell(target: AsyncShared<Character>) -> bool {
let cast = {
let mut target = target.write().unwrap();
if let Some(mana) = target.mana.checked_sub(25) {
target.mana = mana;
target.health = (target.health + 25).min(HEALTH_CAPACITY);
true
} else {
false
}
};
if cast {
log(format!(
"{} casts cure spell on itself",
target.read().unwrap().name
))
.await;
wait_time(STEP_DELAY).await;
}
cast
}
const ACTIONS: [Action; 6] = [
Action::SkipTurn,
Action::Forfeit,
Action::SwordSwing,
Action::PoisonSpell,
Action::FireSpell,
Action::CureSpell,
];
#[derive(Clone, Copy, PartialEq, Eq)]
enum Action {
SkipTurn,
Forfeit,
SwordSwing,
PoisonSpell,
FireSpell,
CureSpell,
}
impl std::fmt::Display for Action {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let text = match self {
Action::SkipTurn => "Skip Turn",
Action::Forfeit => "Forfeit",
Action::SwordSwing => "Sword Swing",
Action::PoisonSpell => "Poison Spell",
Action::FireSpell => "Fire Spell",
Action::CureSpell => "Cure Spell",
};
write!(f, "{}", text)
}
}
trait UiScreen {
#[allow(unused_variables)]
fn update(&mut self, events: &[Event], selected_action: &mut Option<Action>) {}
#[allow(unused_variables)]
fn render(&self, terminal: &mut Terminal, log: &[String]) {}
}
struct LogsScreen;
impl UiScreen for LogsScreen {
fn render(&self, terminal: &mut Terminal, log: &[String]) {
let (w, h) = size().unwrap();
let mut y = 0;
for (index, text) in log.iter().rev().enumerate() {
let text = text_wrap(text, w as usize);
let size = text_size(&text);
if y + size.y > h as usize {
break;
}
let text = if index == 0 {
text.white().bold()
} else {
text.white().italic()
};
terminal.display([0, y as _], text);
y += size.y + 1;
}
}
}
#[derive(Default)]
struct TakeActionScreen {
selected_index: usize,
}
impl UiScreen for TakeActionScreen {
fn update(&mut self, events: &[Event], selected_action: &mut Option<Action>) {
if is_key_pressed(events, KeyCode::Up) {
self.selected_index = (self.selected_index + ACTIONS.len() - 1) % ACTIONS.len();
} else if is_key_pressed(events, KeyCode::Down) {
self.selected_index = (self.selected_index + 1) % ACTIONS.len();
} else if is_key_pressed(events, KeyCode::Enter) {
*selected_action = Some(ACTIONS[self.selected_index]);
}
}
fn render(&self, terminal: &mut Terminal, _log: &[String]) {
let (w, _) = size().unwrap();
let mut y = 0;
let text = text_wrap("Your turn, take an action:", w as usize);
let size = text_size(&text);
terminal.display([0, y as _], text.green());
y += size.y + 1;
for (index, action) in ACTIONS.iter().enumerate() {
let text = if index == self.selected_index {
format!("> {}", action)
} else {
format!(" {}", action)
};
let text = text_wrap(&text, w as usize);
let size = text_size(&text);
let text = if index == self.selected_index {
text.black().on_white().bold()
} else {
text.white().on_dark_grey().italic()
};
terminal.display([0, y as _], text);
y += size.y + 1;
}
}
}
struct Character {
name: String,
health: usize,
mana: usize,
}
impl Character {
fn new(name: impl ToString) -> Self {
Self {
name: name.to_string(),
health: HEALTH_CAPACITY,
mana: MANA_CAPACITY,
}
}
}
async fn go_to_next_turn() {
*meta::<usize>(TURN_INDEX_META)
.await
.unwrap()
.write()
.unwrap() += 1;
yield_now().await;
}
async fn turn_index() -> usize {
*meta::<usize>(TURN_INDEX_META)
.await
.unwrap()
.read()
.unwrap()
}
async fn next_turn() {
let index = turn_index().await;
while turn_index().await <= index {
yield_now().await;
}
}
async fn events() -> Vec<Event> {
meta::<Vec<Event>>(EVENTS_META)
.await
.unwrap()
.read()
.unwrap()
.clone()
}
async fn wait_for_key(key: KeyCode) {
loop {
let events = events().await;
if is_key_pressed(&events, key) {
return;
}
// Yield to allow other coroutines to run (we suspend this coroutine).
// Without suspending, this coroutine will block entirely and prevent
// other coroutines from running in local thread (and all our coroutines
// are running in local thread, so deadlock guaranteed).
yield_now().await;
}
}
async fn selected_action() -> Action {
// Change UI to take action screen.
// We hold current UI screen reference available in meta storage to be able
// to access it at any time during coroutine work.
*meta::<Box<dyn UiScreen>>(UI_SCREEN)
.await
.unwrap()
.write()
.unwrap() = Box::new(TakeActionScreen::default());
loop {
// Check if an action has been selected from the list.
// Currently selected action by the user is stored in meta storage.
let action = meta::<Option<Action>>(SELECTED_ACTION_META)
.await
.unwrap()
.write()
.unwrap()
.take();
if let Some(action) = action {
// Change UI back to logs screen.
*meta::<Box<dyn UiScreen>>(UI_SCREEN)
.await
.unwrap()
.write()
.unwrap() = Box::new(LogsScreen);
// Return the selected action.
return action;
}
yield_now().await;
}
}
async fn log(content: impl ToString) {
meta::<Vec<String>>(LOG_META)
.await
.unwrap()
.write()
.unwrap()
.push(content.to_string());
yield_now().await;
}
This method of a TakeActionScreen is then called every game frame.
Effectively, we have showed shared state communication between traditional game logic and running coroutines.