diff --git a/Cargo.toml b/Cargo.toml index 0c6ed81..08d4347 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,5 +17,6 @@ bevy_utils = { version = "0.12" } [dev-dependencies] bevy = { version = "0.12" } +bevy_ascii_terminal = { version = "0.14.0" } [features] \ No newline at end of file diff --git a/README.md b/README.md index 730e04f..4c1cc2a 100644 --- a/README.md +++ b/README.md @@ -26,3 +26,6 @@ how I can improve it with time and usage. ## Examples Check out [this example](examples/simple.rs) demonstrating a simple Petri net in action, as well as [the tests here](src/net.rs) for more Petri net instances. + +### More examples: +- [Dining philosophers](examples/dining_philosophers.rs) (demonstrates competing for shared resources, and a deadlock situation) diff --git a/examples/dining_philosophers.rs b/examples/dining_philosophers.rs new file mode 100644 index 0000000..a7917a4 --- /dev/null +++ b/examples/dining_philosophers.rs @@ -0,0 +1,423 @@ +use bevy::input::common_conditions::input_just_pressed; +use bevy::prelude::*; +use bevy::window::WindowResolution; +use bevy_ascii_terminal::{ + AutoCamera, Border, Terminal, TerminalBundle, TerminalPlugin, TileFormatter, +}; + +use petnat::{NetId, PetriNet, PetriNetBuilder, PetriNetPlugin, Place, Token, Trans, W}; + +enum DiningPhils {} + +enum ForkFree {} +enum ForkTaken {} +enum ForkUsed {} +enum Eating {} +enum Thinking {} + +enum Take {} +enum Return {} +enum Eat {} +enum Finish {} + +impl NetId for DiningPhils {} + +impl Place for ForkFree {} +impl Place for ForkTaken {} +impl Place for ForkUsed {} +impl Place for Eating {} +impl Place for Thinking {} + +impl Trans for Take {} +impl Trans for Return {} +impl Trans for Eat {} +impl Trans for Finish {} + +fn add_philosopher( + builder: PetriNetBuilder, +) -> PetriNetBuilder { + builder + .add_place::>() + .add_place::>() + .add_place::>() + .add_place::>() + .add_place::>() + .add_place::>() + .add_trans::, (ForkFree, W<1>), (ForkTaken, W<1>)>() + .add_trans::, (ForkFree, W<1>), (ForkTaken, W<1>)>() + .add_trans::, (ForkUsed, W<1>), (ForkFree, W<1>)>() + .add_trans::, (ForkUsed, W<1>), (ForkFree, W<1>)>() + .add_trans::, ( + (Thinking, W<1>), + (ForkTaken, W<1>), + (ForkTaken, W<1>), + ), (Eating, W<1>)>() + .add_trans::, (Eating, W<1>), ( + (Thinking, W<1>), + (ForkUsed, W<1>), + (ForkUsed, W<1>), + )>() +} + +fn main() { + App::new() + .add_plugins(DefaultPlugins.set(WindowPlugin { + primary_window: Some(Window { + resolution: WindowResolution::new(640.0, 640.0), + title: "Dining Philosophers".to_string(), + resizable: true, + ..default() + }), + ..default() + })) + .add_plugins(TerminalPlugin) + .insert_resource(ClearColor(Color::BLACK)) + .add_plugins(PetriNetPlugin:: { + build: |builder| { + let builder = builder + .add_place::>() + .add_place::>(); + let builder = add_philosopher::<0>(builder); + add_philosopher::<1>(builder) + }, + }) + .add_systems(Startup, spawn_terminal) + .add_systems( + PostStartup, + ( + ( + spawn_token, + apply_deferred, + (init_forks, init_philosopher::<0>, init_philosopher::<1>), + ) + .chain(), + (draw_manual, draw_net), + ), + ) + .add_systems( + PreUpdate, + ( + despawn_token, + spawn_token, + apply_deferred, + (init_forks, init_philosopher::<0>, init_philosopher::<1>), + ) + .chain() + .run_if(input_just_pressed(KeyCode::G)), + ) + .add_systems( + Update, + ( + take_fork::.run_if(input_just_pressed(KeyCode::Q)), + take_fork::.run_if(input_just_pressed(KeyCode::E)), + return_fork::.run_if(input_just_pressed(KeyCode::A)), + return_fork::.run_if(input_just_pressed(KeyCode::D)), + eat::<0>.run_if(input_just_pressed(KeyCode::W)), + finish::<0>.run_if(input_just_pressed(KeyCode::S)), + take_fork::.run_if(input_just_pressed(KeyCode::U)), + take_fork::.run_if(input_just_pressed(KeyCode::O)), + return_fork::.run_if(input_just_pressed(KeyCode::J)), + return_fork::.run_if(input_just_pressed(KeyCode::L)), + eat::<1>.run_if(input_just_pressed(KeyCode::I)), + finish::<1>.run_if(input_just_pressed(KeyCode::K)), + ) + .chain(), + ) + .add_systems( + PostUpdate, + ( + |tokens: Query<(), Changed>>| { + if tokens.get_single().is_ok() { + info!("=== STATE ==="); + } + }, + show_free_fork::, + show_free_fork::, + show_taken_fork::, + show_taken_fork::, + show_taken_fork::, + show_taken_fork::, + show_philosopher::<0>, + show_philosopher::<1>, + |tokens: Query<(), Changed>>| { + if tokens.get_single().is_ok() { + info!("============="); + } + }, + ) + .chain(), + ) + .run(); +} + +const TERM_SIZE: [usize; 2] = [50, 50]; + +fn spawn_terminal(mut commands: Commands) { + let term = Terminal::new(TERM_SIZE).with_border(Border::single_line()); + commands.spawn((TerminalBundle::from(term), AutoCamera)); +} + +fn spawn_token(mut commands: Commands, net: Res>) { + // A token can represent the current state of some game object, + // or some sort of resource + let token = net.spawn_token(); + commands.spawn(token); + info!("Spawning a token..."); +} + +fn init_forks(net: Res>, mut tokens: Query<&mut Token>) { + let mut token = tokens.single_mut(); + net.mark::>(&mut token, 1); + net.mark::>(&mut token, 1); +} + +fn init_philosopher( + net: Res>, + mut tokens: Query<&mut Token>, +) { + let mut token = tokens.single_mut(); + net.mark::>(&mut token, 1); +} + +fn take_fork( + net: Res>, + mut tokens: Query<&mut Token>, +) { + let mut token = tokens.single_mut(); + if let Some(()) = net.fire::>(token.bypass_change_detection()) { + token.set_changed(); + info!("Philosopher {N} took the {} fork.", side::()); + } else { + warn!("Philosopher {N} cannot take the {} fork.", side::()); + } +} + +fn return_fork( + net: Res>, + mut tokens: Query<&mut Token>, +) { + let mut token = tokens.single_mut(); + if let Some(()) = net.fire::>(token.bypass_change_detection()) { + token.set_changed(); + info!("Philosopher {N} returned the {} fork.", side::()); + } else { + warn!("Philosopher {N} cannot return the {} fork.", side::()); + } +} + +fn eat( + net: Res>, + mut tokens: Query<&mut Token>, +) { + let mut token = tokens.single_mut(); + if let Some(()) = net.fire::>(token.bypass_change_detection()) { + token.set_changed(); + info!("Philosopher {N} started eating."); + } else if token.marks::>() > 0 { + warn!("Philosopher {N} is already eating."); + } else { + warn!("Philosopher {N} cannot eat without both forks."); + } +} + +fn finish( + net: Res>, + mut tokens: Query<&mut Token>, +) { + let mut token = tokens.single_mut(); + if let Some(()) = net.fire::>(token.bypass_change_detection()) { + token.set_changed(); + info!("Philosopher {N} finished eating."); + } else { + warn!("Philosopher {N} cannot finish eating."); + } +} + +fn despawn_token(mut commands: Commands, tokens: Query>>) { + let token = tokens.single(); + commands.entity(token).despawn(); + info!("Despawning the token..."); +} + +#[rustfmt::skip] +fn draw_manual(mut terms: Query<&mut Terminal>) { + let mut term = terms.single_mut(); + let ys = &mut (0..TERM_SIZE[1]).rev().take(19); + let mut y = || ys.next().unwrap(); + term.put_string([0, y()], " DINING PHILOSOPHERS "); + term.put_string([0, y()], " "); + term.put_string([0, y()], " Controls "); + term.put_string([0, y()], " "); + term.put_string([0, y()], " Philosopher 0 Philosopher 1 "); + term.put_string([0, y()], " "); + term.put_string([0, y()], " take left fork take left fork "); + term.put_string([0, y()], " "); + term.put_string([0, y()], " take right fork take right fork "); + term.put_string([0, y()], " "); + term.put_string([0, y()], " start eating start eating "); + term.put_string([0, y()], " "); + term.put_string([0, y()], " finish eating finish eating "); + term.put_string([0, y()], " "); + term.put_string([0, y()], " return left fork return left fork "); + term.put_string([0, y()], " "); + term.put_string([0, y()], " return right fork return right fork "); + term.put_string([0, y()], " "); + term.put_string([0, y()], "--------------------------------------------------"); +} + +#[rustfmt::skip] +fn draw_net(mut terms: Query<&mut Terminal>) { + let mut term = terms.single_mut(); + let ys = &mut (0..TERM_SIZE[1]).rev().skip(20); + let mut y = || ys.next().unwrap(); + term.put_string([0, y()], r" RET FINISH RET "); + term.put_string([0, y()], r" "); + term.put_string([0, y()], r" ----[ ]<---( )<-----[ ]----->( )--->[ ]---- "); + term.put_string([0, y()], r" | used ^ \ used | "); + term.put_string([0, y()], r" | / \ | "); + term.put_string([0, y()], r" | / v | "); + term.put_string([0, y()], r" | ( ) 0 ( ) | "); + term.put_string([0, y()], r" | eating ^ / thinking | "); + term.put_string([0, y()], r" | TAKE \ / TAKE | "); + term.put_string([0, y()], r" | \ v | "); + term.put_string([0, y()], r" | [ ]--->( )----->[ ]<-----( )<---[ ] | "); + term.put_string([0, y()], r" | ^ taken taken ^ | "); + term.put_string([0, y()], r" | / EAT \ | "); + term.put_string([0, y()], r" V/ \V "); + term.put_string([0, y()], r"L ( ) ( ) R"); + term.put_string([0, y()], r" ^\ /^ "); + term.put_string([0, y()], r" | \ EAT / | "); + term.put_string([0, y()], r" | v v | "); + term.put_string([0, y()], r" | [ ]<---( )<-----[ ]----->( )--->[ ] | "); + term.put_string([0, y()], r" | taken / ^ taken | "); + term.put_string([0, y()], r" | TAKE / \ TAKE | "); + term.put_string([0, y()], r" | v \ | "); + term.put_string([0, y()], r" | ( ) 1 ( ) | "); + term.put_string([0, y()], r" | eating \ ^ thinking | "); + term.put_string([0, y()], r" | \ / | "); + term.put_string([0, y()], r" | v / | "); + term.put_string([0, y()], r" ----[ ]<---( )<-----[ ]----->( )--->[ ]---- "); + term.put_string([0, y()], r" "); + term.put_string([0, y()], r" RET FINISH RET "); +} + +fn show_free_fork( + tokens: Query<&Token, Changed>>, + mut terms: Query<&mut Terminal>, +) { + let mut term = terms.single_mut(); + let coord = if LR { + [3, TERM_SIZE[1] - 20 - 15] + } else { + [TERM_SIZE[0] - 5, TERM_SIZE[1] - 20 - 15] + }; + if let Ok(token) = tokens.get_single() { + if token.marks::>() > 0 { + info!("{} fork: on the table", side::()); + term.put_char(coord, '*'.fg(Color::WHITE)); + } else { + term.clear_box(coord, [1, 1]); + } + } +} + +fn show_taken_fork( + net: Res>, + tokens: Query<&Token, Changed>>, + mut terms: Query<&mut Terminal>, +) { + let mut term = terms.single_mut(); + let x = if LR { 15 } else { TERM_SIZE[0] - 17 }; + let y_taken = match N { + 0 => TERM_SIZE[1] - 20 - 11, + 1 => TERM_SIZE[1] - 20 - 19, + _ => unreachable!(), + }; + let y_used = match N { + 0 => TERM_SIZE[1] - 20 - 3, + 1 => TERM_SIZE[1] - 20 - 27, + _ => unreachable!(), + }; + if let Ok(token) = tokens.get_single() { + if token.marks::>() > 0 { + info!("{} fork: taken by Philosopher {N}", side::()); + term.put_char([x, y_taken], '*'.fg(Color::WHITE)); + } else { + term.clear_box([x, y_taken], [1, 1]); + } + if token.marks::>() > 0 { + info!("{} fork: used by Philosopher {N}", side::()); + term.put_char([x, y_used], '*'.fg(Color::WHITE)); + } else { + term.clear_box([x, y_used], [1, 1]); + } + let x = if LR { 8 } else { TERM_SIZE[0] - 10 }; + if net.enabled::>(token) { + term.put_char([x, y_taken], '='.fg(Color::GREEN).bg(Color::GREEN)); + } else { + term.clear_box([x, y_taken], [1, 1]); + } + if net.enabled::>(token) { + term.put_char([x, y_used], '='.fg(Color::GREEN).bg(Color::GREEN)); + } else { + term.clear_box([x, y_used], [1, 1]); + } + } +} + +fn show_philosopher( + net: Res>, + tokens: Query<&Token, Changed>>, + mut terms: Query<&mut Terminal>, +) { + let mut term = terms.single_mut(); + let x_eating = 19; + let x_thinking = 29; + let y = match N { + 0 => TERM_SIZE[1] - 20 - 7, + 1 => TERM_SIZE[1] - 20 - 23, + _ => unreachable!(), + }; + if let Ok(token) = tokens.get_single() { + if token.marks::>() > 0 { + info!("Philosopher {N}: eating"); + term.put_char([x_eating, y], '*'.fg(Color::WHITE)); + } else { + term.clear_box([x_eating, y], [1, 1]); + } + if token.marks::>() > 0 { + info!("Philosopher {N}: thinking"); + term.put_char([x_thinking, y], '*'.fg(Color::WHITE)); + } else { + term.clear_box([x_thinking, y], [1, 1]); + } + let x = 24; + let y_eat = match N { + 0 => TERM_SIZE[1] - 20 - 11, + 1 => TERM_SIZE[1] - 20 - 19, + _ => unreachable!(), + }; + if net.enabled::>(token) { + term.put_char([x, y_eat], '='.fg(Color::GREEN).bg(Color::GREEN)); + } else { + term.clear_box([x, y_eat], [1, 1]); + } + let y_think = match N { + 0 => TERM_SIZE[1] - 20 - 3, + 1 => TERM_SIZE[1] - 20 - 27, + _ => unreachable!(), + }; + if net.enabled::>(token) { + term.put_char([x, y_think], '='.fg(Color::GREEN).bg(Color::GREEN)); + } else { + term.clear_box([x, y_think], [1, 1]); + } + } +} + +const fn side() -> &'static str { + match LR { + true => "left", + false => "right", + } +} diff --git a/src/net.rs b/src/net.rs index 43baa93..826100d 100644 --- a/src/net.rs +++ b/src/net.rs @@ -56,6 +56,7 @@ impl PetriNet { } /// Fires all transitions once. + /// todo: prevent firing if transition is involved in non-determinism pub fn fire_all(&self, token: &mut Token) -> Vec> { self.transitions .keys()