aboutsummaryrefslogtreecommitdiff
path: root/examples
diff options
context:
space:
mode:
Diffstat (limited to 'examples')
-rw-r--r--examples/text-adventure/dsl.rs205
-rw-r--r--examples/text-adventure/logic.rs417
-rw-r--r--examples/text-adventure/main.rs11
-rw-r--r--examples/text-adventure/side_effects.rs1
4 files changed, 634 insertions, 0 deletions
diff --git a/examples/text-adventure/dsl.rs b/examples/text-adventure/dsl.rs
new file mode 100644
index 0000000..394a5c6
--- /dev/null
+++ b/examples/text-adventure/dsl.rs
@@ -0,0 +1,205 @@
+//This module defines the domain specific language.
+
+use std::{rc::Rc, borrow::Cow};
+
+use higher_free_macro::free;
+use std::convert::identity;
+use higher::Functor;
+
+#[derive(Clone)]
+pub enum Speaker{
+ Partner,
+ DeliLady,
+ Cashier,
+}
+
+#[derive(Clone)]
+pub enum Mood{
+ Friendly,
+ Confused,
+ Happy,
+ Amused,
+ Annoyed,
+ Apologetic,
+}
+
+#[derive(Clone, Copy, PartialEq, Eq)]
+pub enum Location{
+ Entrance,
+ Deli,
+ Checkout,
+ Refrigerators,
+ Shelves,
+}
+
+#[derive(Clone)]
+pub enum SausageRoll<'a, 's,A>{
+ SayDialogueLine{
+ speaker: Speaker,
+ text : Cow<'s, str>, //In a real project I would just make this a String. Here it's a reference to show off lifetime support.
+ mood : Mood,
+ next : A
+ },
+ GivePlayerOptions{
+ options : Vec<&'s str>,
+ next : Rc<dyn Fn(usize)-> A + 'a> //let's just assume that the interpreter validates input.
+ },
+ PresentLocation{
+ location : Location,
+ next : A,
+ },
+ Exposition{
+ text : &'s str,
+ next : A,
+ }
+}
+#[derive(Copy,Clone,PartialEq,Eq)]
+pub enum Item{
+ //refrigerators
+ Milk,
+ Yoghurt,
+ Cheese,
+ //shelves
+ Pickles,
+ CatFood,
+ Beer,
+ ToiletPaper,
+ //deli
+ SausageRoll,
+ FishSandwich,
+ //cashier
+ ChewingGum,
+ Shots,
+ Pulp,
+}
+
+impl Item {
+ fn price(self) -> usize {
+ match self {
+ Item::SausageRoll => 300,
+ Item::Pickles => 250,
+ Item::Milk => 125,
+ Item::Yoghurt => 125,
+ Item::Cheese => 750,
+ Item::CatFood => 2500,
+ Item::Beer => 125,
+ Item::ToiletPaper => 500,
+ Item::FishSandwich => 300,
+ Item::ChewingGum => 100,
+ Item::Shots => 300,
+ Item::Pulp => 250,
+
+ }
+ }
+ pub fn description(self) -> &'static str {
+ match self {
+ Item::SausageRoll => "A sausage roll, costing €3.00.",
+ Item::Pickles => "A glass of pickles, costing €2.50",
+ Item::Milk => "A bottle of milk, costing €1.25",
+ Item::Yoghurt => "A cup of yoghurt, costing €1.25",
+ Item::Cheese => "A block of expensive grey cheese, costing €7.50",
+ Item::CatFood => "A bag of cat food, costing €25.00",
+ Item::Beer => "A bottle of beer, for €1.25",
+ Item::ToiletPaper => "A package of toilet paper, costing €5.00",
+ Item::FishSandwich => "A fish sandwich, emitting a tasty smell, costing €3.00",
+ Item::ChewingGum => "A pack of chewing gum, costing €1.00",
+ Item::Shots => "A shot of a sad excuse for whisky, costing €3.00",
+ Item::Pulp => "A pulp novel called \"Aliens ate my trashbin\", which should not cost the €2.50 it does",
+
+ }
+ }
+}
+
+impl Location{
+ pub fn items(self) -> Vec<Item> {
+ match self {
+ Location::Entrance => Vec::default(),
+ Location::Deli => vec![], //must talk to deli lady to get the sausage roll. This also means it cannot be returned.
+ Location::Checkout => vec![Item::ChewingGum, Item::Shots, Item::Pulp],
+ Location::Refrigerators => vec![Item::Milk, Item::Yoghurt, Item::Cheese],
+ Location::Shelves => vec![Item::Pickles, Item::CatFood, Item::Beer, Item::ToiletPaper],
+ }
+ }
+}
+
+//In a real project I would probably aim to make this Copy as well, especially if it's as small as this.
+//I left it as Clone intentionally, to illustrate how one can work around the limitation of it not being Copy.
+#[derive(Clone)]
+pub struct Inventory {
+ pub items : Vec<Item>,
+}
+
+impl Default for Inventory{
+ fn default() -> Self {
+ Self { items: Default::default() }
+ }
+}
+
+impl Inventory{
+ pub fn has_item_from_room(&self, room : Location) -> bool {
+ let items_from_room = room.items();
+ self.items.iter().any(|i| items_from_room.contains(i))
+ }
+ pub fn try_add(self, item : Item) -> Result<Self, Self>{
+ if self.items.len() < 3 {
+ let mut items = self.items;
+ items.push(item); //am I the only one that hates that push doesn't return the updated vec?
+ Ok(Inventory{items, ..self})
+ } else {
+ Err(self)
+ }
+ }
+ pub fn try_remove(mut self, item : Item) -> Result<Self, Self>{
+ let idx = self.items.iter().position(|i| *i == item);
+ match idx {
+ Some(idx) => {
+ self.items.swap_remove(idx);
+ Ok(self)
+ },
+ None => Err(self),
+ }
+ }
+ pub fn get_money() -> &'static str{
+ "€10.00" //It doesn't change. Can as well be a constant.
+ }
+ pub fn total_price(&self) -> usize {
+ self.items.iter().cloned().map(Item::price).sum::<usize>()
+ }
+ pub fn can_afford(&self) -> bool {
+ self.total_price() <= 1000_usize
+ }
+}
+
+impl<'a,'s, A : 'a> Functor<'a,A> for SausageRoll<'a,'s,A>{
+ type Target<T> = SausageRoll<'a,'s,T>;
+
+ fn fmap<B, F>(self, f: F) -> Self::Target<B>
+ where
+ F: Fn(A) -> B + 'a {
+ match self {
+ SausageRoll::SayDialogueLine { speaker, text, mood, next } => SausageRoll::SayDialogueLine { speaker, text, mood, next: f(next) },
+ SausageRoll::GivePlayerOptions { options, next } => SausageRoll::GivePlayerOptions { options, next: Rc::new(move |x| f(next(x))) },
+ SausageRoll::PresentLocation { location, next } => SausageRoll::PresentLocation { location, next: f(next) },
+ SausageRoll::Exposition { text, next } => SausageRoll::Exposition { text, next: f(next) },
+ }
+ }
+}
+
+free!(<'a>, pub FreeSausageRoll<'a,'s,A>, SausageRoll<'a, 's, FreeSausageRoll<'a,'s,A>>);
+
+
+pub fn say_dialogue_line<'a,'s:'a>(speaker : Speaker, text : Cow<'s,str>, mood : Mood) -> FreeSausageRoll<'a, 's, ()>{
+ FreeSausageRoll::lift_f(SausageRoll::SayDialogueLine { speaker, text, mood, next: () })
+}
+
+pub fn give_player_options<'a,'s:'a>(options : Vec<&'s str>) -> FreeSausageRoll<'a,'s, usize>{
+ FreeSausageRoll::lift_f(SausageRoll::GivePlayerOptions { options, next: Rc::new(identity) })
+}
+
+pub fn present_location<'a, 's:'a>(location : Location) -> FreeSausageRoll<'a,'s, ()>{
+ FreeSausageRoll::lift_f(SausageRoll::PresentLocation { location, next: () })
+}
+
+pub fn exposition<'a,'s:'a>(text : &'s str) -> FreeSausageRoll<'a,'s,()>{
+ FreeSausageRoll::lift_f(SausageRoll::Exposition { text, next: () })
+}
diff --git a/examples/text-adventure/logic.rs b/examples/text-adventure/logic.rs
new file mode 100644
index 0000000..5df5f6e
--- /dev/null
+++ b/examples/text-adventure/logic.rs
@@ -0,0 +1,417 @@
+//! This module does nothing. It just creates the game's high level flow encoded as Free Monad.
+
+
+use std::borrow::Cow;
+
+use higher::{run, Functor, Pure, Bind};
+use super::dsl::*;
+
+//Haskell has a when function, and it's nice. Sooo, copy that.
+macro_rules! when {
+ {$a:expr => {$($b:tt)*}} => {
+ if($a){
+ run!{$($b)*}
+ } else {
+ run!{ yield () }
+ }
+ };
+}
+
+
+pub fn game<'a,'s : 'a>() -> FreeSausageRoll<'a, 's, ()>{
+ run!{
+ c <= intro();
+ when!{c => {
+ //handle_rooms is the main game loop: Go from room to room.
+ c <= handle_rooms(Location::Refrigerators, Default::default()); //can't destructure in assignment in higher-0.2. Maybe later.
+ //if we ended up here, we left the supermarket.
+ ending(c.1)
+ }}
+ }
+}
+
+fn intro<'a,'s:'a>() -> FreeSausageRoll<'a, 's, bool>{
+ run!{
+ present_location(Location::Entrance);
+ say_dialogue_line(Speaker::Partner, Cow::from("Would you be so kind as to quickly grab me a sausage roll from the supermarket? With pickle if possible?"), Mood::Friendly);
+ say_dialogue_line(Speaker::Partner, Cow::from("I'd meanwhile go over to the pharmacy, and buy some pills against headache."), Mood::Friendly);
+ c <= give_player_options(vec!["Say yes and enter the supermarket.", "Say no."]);
+ if c == 0 {
+ say_dialogue_line(Speaker::Partner, Cow::from("Thanks! We'll meet here in a couple of minutes then."), Mood::Friendly)
+ }
+ else { //as already stated: Let's just assume the interpreter validates input.
+ say_dialogue_line(Speaker::Partner, Cow::from("Well, I won't force you. But if I get hangry, it's going to be your problem."), Mood::Annoyed)
+ };
+ yield c == 0
+ }
+}
+
+fn ending<'a,'s:'a>(inventory : Inventory) -> FreeSausageRoll<'a, 's, ()>{
+ if inventory.items.contains(&Item::SausageRoll) {
+ if inventory.items.contains(&Item::Pickles){
+ run!{
+ say_dialogue_line(Speaker::Partner, Cow::from("Wait, seriously? You bought a glass of pickles and a sausage roll without pickle?"), Mood::Confused);
+ exposition("You explain that the deli counter had run out of pickles.");
+ say_dialogue_line(Speaker::Partner, Cow::from("Well, that's a creative solution."), Mood::Amused);
+ say_dialogue_line(Speaker::Partner, Cow::from("Thanks a lot, let's move on."), Mood::Happy)
+ }
+ } else {
+ run!{
+ say_dialogue_line(Speaker::Partner, Cow::from("Thanks for the sausage roll, but there are no pickles in it?"), Mood::Annoyed);
+ exposition("You explain that the deli counter had run out of pickles.");
+ say_dialogue_line(Speaker::Partner, Cow::from("Well, that can't be helped then. Thanks a lot, let's move on."), Mood::Happy)
+ }
+ }
+ } else {
+ run!{
+ say_dialogue_line(Speaker::Partner, Cow::from("What did you do in there? I asked you to bring me a sausage roll..."), Mood::Annoyed);
+ when!{inventory.items.len() > 0 => {
+ say_dialogue_line(Speaker::Partner, Cow::from("Also, why did you buy all that other stuff?"), Mood::Annoyed)
+ }};
+ say_dialogue_line(Speaker::Partner, Cow::from("Well, let's move on, but don't complain if I get hangry on the way."), Mood::Annoyed)
+ }
+ }
+}
+
+fn handle_rooms<'a,'s:'a>(room: Location, inventory : Inventory) -> FreeSausageRoll<'a, 's, (Location, Inventory)>{
+ run!{
+ c <= handle_room(room, inventory);
+ if c.0 != Location::Entrance {
+ handle_rooms(c.0, c.1)
+ } else {
+ run!{
+ yield c
+ }
+ }
+ }
+}
+
+fn handle_room<'a,'s:'a>(room : Location, inventory : Inventory) -> FreeSausageRoll<'a, 's, (Location, Inventory)> {
+ run!{
+ present_location(room);
+ match room {
+ Location::Refrigerators => handle_refrigerators(inventory.clone()),
+ Location::Shelves => {handle_shelves(inventory.clone())},
+ Location::Deli => {handle_deli(inventory.clone())},
+ Location::Checkout => {handle_checkout(inventory.clone())},
+ Location::Entrance => unreachable!(), //if we are at the entrance, we won.
+ }
+ }
+}
+
+fn handle_refrigerators<'a, 's: 'a>(inventory : Inventory) -> FreeSausageRoll<'a, 's, (Location, Inventory)>{
+ run!{
+ c <= {
+ let options = if inventory.has_item_from_room(Location::Refrigerators) {
+ vec!["Move on to the Shelves.", "Move to the deli counter.", "Check Inventory", "Take an item", "Return an item"]
+ } else {
+ vec!["Move on to the Shelves.", "Move to the deli counter.", "Check Inventory", "Take an item"]
+ };
+ give_player_options(options)
+ };
+ match c {
+ 0 => { FreeSausageRoll::pure((Location::Shelves, inventory.clone())) },
+ 1 => { FreeSausageRoll::pure((Location::Deli, inventory.clone()))},
+ 2 => {
+ let inventory = inventory.clone();
+ run!{
+ check_inventory(inventory.clone());
+ handle_refrigerators(inventory.clone())
+ }
+ }
+ 3 => { run!{
+ i <= try_take_item(inventory.clone(), Location::Refrigerators.items());
+ handle_refrigerators(i)
+ } },
+ 4 => { run!{
+ i <= return_item(inventory.clone(), Location::Refrigerators);
+ handle_refrigerators(i)
+ } },
+ _ => unreachable!()
+ }
+ }
+}
+
+fn handle_shelves<'a, 's: 'a>(inventory : Inventory) -> FreeSausageRoll<'a, 's, (Location, Inventory)>{
+ //this is rather similar to refrigerators. Just different items.
+ run!{
+ c <= {
+ let options = if inventory.has_item_from_room(Location::Shelves) {
+ vec!["Move on to the Refrigerators.", "Move to the deli counter.", "Move to the checkout.", "Check Inventory","Take an item", "Return an item"]
+ } else {
+ vec!["Move on to the Shelves.", "Move to the deli counter.", "Move to the checkout.", "Check Inventory","Take an item"]
+ };
+ give_player_options(options)
+ };
+ match c {
+ 0 => { FreeSausageRoll::pure((Location::Refrigerators, inventory.clone())) },
+ 1 => { FreeSausageRoll::pure((Location::Deli, inventory.clone()))},
+ 2 => { FreeSausageRoll::pure((Location::Checkout, inventory.clone()))}
+ 3 => {
+ let inventory = inventory.clone();
+ run!{
+ check_inventory(inventory.clone());
+ handle_refrigerators(inventory.clone())
+ }
+ }
+ 4 => { run!{
+ i <= try_take_item(inventory.clone(), Location::Shelves.items());
+ handle_shelves(i)
+ } },
+ 5 => { run!{
+ i <= return_item(inventory.clone(), Location::Shelves);
+ handle_shelves(i)
+ } },
+ _ => unreachable!()
+ }
+ }
+}
+
+fn handle_deli<'a,'s:'a>(inventory : Inventory) -> FreeSausageRoll<'a, 's, (Location, Inventory)>{
+ run!{
+ c <= give_player_options(vec!["Move on to refrigerators.", "Move on to shelves.", "Check Inventory", "Talk to the lady behind the counter"]);
+ match c{
+ 0 => FreeSausageRoll::pure((Location::Refrigerators, inventory.clone())),
+ 1 => FreeSausageRoll::pure((Location::Shelves, inventory.clone())),
+ 2 => {
+ let inventory = inventory.clone();
+ run!{
+ check_inventory(inventory.clone());
+ handle_refrigerators(inventory.clone())
+ }
+ },
+ 3 => {
+ run!{
+ i <= talk_to_deli_lady(inventory.clone());
+ handle_deli(i.clone())
+ }
+ },
+ _ => unreachable!()
+ }
+ }
+}
+
+fn handle_checkout<'a,'s:'a>(inventory : Inventory) -> FreeSausageRoll<'a, 's, (Location, Inventory)>{
+ run!{
+ c <= {
+ let options = if inventory.has_item_from_room(Location::Checkout) {
+ vec!["Move back to the shelves.", "Pay for your stuff and leave.", "Check Inventory", "Take an item", "Return an item"]
+ } else {
+ vec!["Move back to the shelves.", "Pay for your stuff and leave.", "Check Inventory", "Take an item"]
+ };
+ give_player_options(options)
+ };
+ match c {
+ 0 => { FreeSausageRoll::pure((Location::Shelves, inventory.clone())) },
+ 1 => {
+ run!{
+ r <= try_pay(inventory.clone());
+ match r {
+ Ok(inventory) => FreeSausageRoll::pure((Location::Entrance, inventory.clone())),
+ Err(inventory) => handle_checkout(inventory.clone()),
+ }
+ }
+ },
+ 2 => {
+ let inventory = inventory.clone();
+ run!{
+ check_inventory(inventory.clone());
+ handle_checkout(inventory.clone())
+ }
+ }
+ 3 => { run!{
+ i <= try_take_item(inventory.clone(), Location::Checkout.items());
+ handle_checkout(i)
+ } },
+ 4 => { run!{
+ i <= return_item(inventory.clone(), Location::Shelves);
+ handle_checkout(i)
+ } },
+ _ => unreachable!()
+ }
+ }
+}
+
+fn try_take_item<'a, 's: 'a>(inventory : Inventory, options : Vec<Item>) -> FreeSausageRoll<'a, 's,Inventory>{
+ //here we run into an "interesting" issue with Rust's ownership and do-notation.
+ //We would like to capture inventory and use it in bind-notation, but that doesn't work (except in the first 2 lines), because Inventory isn't Copy.
+ //This leaves us with a couple of options: We can either pass it through by repeated cloning (done here), or leave do-notation before capturing it.
+ run!{
+ i <= exposition("You look around and these items nearby catch your attention.").fmap(move |_| (inventory.clone(), options.clone()));
+ o <= give_player_options(i.1.iter().map(|o| o.description()).chain(std::iter::once("Cancel")).collect()).fmap(move |c| (c, i.0.clone(), i.1.clone()));
+ {
+ let aborted = o.2.len() <= o.0;
+ let updated_inventory = o.2.get(o.0).ok_or_else(|| o.1.clone()).and_then(|c| o.1.clone().try_add(*c));
+ match updated_inventory {
+ Ok(i) => {
+ run! {
+ exposition("You take the item.");
+ yield (i.clone())
+ }
+ },
+ Err(i) => {
+ run! {
+ if aborted {
+ exposition("You changed your mind, and didn't take an item.")
+ } else {
+ exposition("You try to pick up the item, but your hands are full.")
+ };
+ yield i.clone()
+ }
+ }
+ }
+ }
+ }
+}
+
+fn return_item<'a,'s:'a>(inventory : Inventory, room : Location) -> FreeSausageRoll<'a,'s, Inventory>{
+ //similar to try_take_item here the inventory can't easily be captured. For illustration purposes, here we
+ //don't pass it along as clones, but rather return from do-notation to capture it.
+ let items_in_room = room.items();
+ let carried_items_from_here = inventory.items.iter().filter(|i| items_in_room.contains(i)).cloned().collect::<Vec<_>>();
+ //ownership shmownership
+ let carried_items_from_here2 = carried_items_from_here.clone();
+ let chosen = run! {
+ exposition("You check which items you can return here. You find places where you can return the following:");
+ give_player_options(carried_items_from_here.iter().map(|o| o.description()).chain(std::iter::once("Cancel")).collect())
+ };
+ chosen.bind(move |c| {
+ match carried_items_from_here2.get(c).ok_or_else(|| inventory.clone()).and_then(|i| inventory.clone().try_remove(*i)) {
+ Ok(i) => {
+ run!{
+ exposition("You put back the item.");
+ yield i.clone()
+ }
+ },
+ Err(i) => {
+ run! {
+ exposition("You decided to not return an item."); //good enough. We filtered for valid items beforehand.
+ yield i.clone()
+ }
+ },
+ }
+ })
+}
+
+fn check_inventory<'a, 's:'a>(inventory : Inventory) -> FreeSausageRoll<'a,'s, ()>{
+ let c = inventory.items.len();
+ run!{
+ exposition("You look at the items you carry. You are holding:");
+ list_inventory_items(inventory.clone(),0);
+ if c < 2 {
+ run!{
+ exposition("You check your pocket to see how much money you have.");
+ exposition(Inventory::get_money())
+ }
+ } else {
+ exposition("You would like to check how much money you have on you, but you need both hands to carry all the stuff you gathered.")
+ }
+ }
+}
+
+fn list_inventory_items<'a,'s:'a>(inventory : Inventory, index : usize) -> FreeSausageRoll<'a,'s, ()>{
+ if index < inventory.items.len() {
+ run!{
+ exposition(inventory.items[index].description());
+ list_inventory_items(inventory.clone(), index+1)
+ }
+ } else {
+ FreeSausageRoll::pure(())
+ }
+}
+
+fn talk_to_deli_lady<'a,'s:'a>(inventory : Inventory) -> FreeSausageRoll<'a,'s,Inventory>{
+ run!{
+ exposition("You greet the lady at the deli counter.");
+ say_dialogue_line(Speaker::DeliLady, Cow::from("Hi! How can I help you, dear?"), Mood::Friendly);
+ say_dialogue_line(Speaker::DeliLady, Cow::from("We have the most awesome fish sandwiches today. Would you like one?"), Mood::Friendly)
+ }.bind(move |_| deli_lady_loop(inventory.clone()))
+}
+
+fn deli_lady_loop<'a, 's: 'a>(inventory : Inventory) -> FreeSausageRoll<'a,'s,Inventory>{
+ let has_deli_item = inventory.has_item_from_room(Location::Deli);
+ let c = run! {
+ if has_deli_item {
+ give_player_options(vec!["Yes, please!", "No, thanks. I'd rather buy a sausage roll with pickle.", "Nothing, thanks.", "Do you take stuff from the deli back?"])
+ } else {
+ give_player_options(vec!["Yes, please!", "No, thanks. I'd rather buy a sausage roll with pickle.", "Nothing, thanks."])
+ }
+ };
+ c.bind(move |c| {
+ match c {
+ 0 => {
+ let inventory = inventory.clone().try_add(Item::FishSandwich);
+ match inventory{
+ Ok(inventory) => {
+ run!{
+ say_dialogue_line(Speaker::DeliLady, Cow::from("Here you go! Is there anything else I can help you with? Maybe another fish sandwich?"), Mood::Happy);
+ deli_lady_loop(inventory.clone())
+ }
+ },
+ Err(inventory) => {
+ run!{
+ say_dialogue_line(Speaker::DeliLady, Cow::from("I would love to hand it to you, but your hands seem kinda full. Please come back later, when you can actually carry the food I sell."), Mood::Annoyed);
+ yield inventory.clone()
+ }
+ },
+ }
+ },
+ 1 => {
+ let inventory = inventory.clone().try_add(Item::SausageRoll);
+ match inventory{
+ Ok(inventory) => {
+ let d = run!{
+ say_dialogue_line(Speaker::DeliLady, Cow::from("I'm sorry, but I don't have any pickles here right now. But you can take a glass from the shelf over there."), Mood::Apologetic);
+ say_dialogue_line(Speaker::DeliLady, Cow::from("I'll put in extra sausage to make up for it."), Mood::Apologetic);
+ say_dialogue_line(Speaker::DeliLady, Cow::from("Here you go! Is there anything else I can help you with? Maybe a fish sandwich?"), Mood::Happy)
+ };
+ d.bind(move |_| deli_lady_loop(inventory.clone()))
+ },
+ Err(inventory) => {
+ run!{
+ say_dialogue_line(Speaker::DeliLady, Cow::from("I would love to hand it to you, but your hands seem kinda full. Please come back later, when you can actually carry the food I sell."), Mood::Annoyed);
+ yield inventory.clone()
+ }
+ },
+ }
+ },
+ 2 => {
+ let inventory = inventory.clone();
+ say_dialogue_line(Speaker::DeliLady, Cow::from("So, you are just here to steal my time? I've got other customers to serve."), Mood::Annoyed)
+ .bind(move |_| FreeSausageRoll::pure(inventory.clone()))
+ },
+ 3 => {
+ let inventory = inventory.clone();
+ say_dialogue_line(Speaker::DeliLady, Cow::from("No, that would be gross. Would you buy a sandwich handed back by some other random customer?"), Mood::Confused)
+ .bind(move |_| FreeSausageRoll::pure(inventory.clone()))
+ }
+ _ => unreachable!()
+ }
+ })
+}
+
+fn try_pay<'a, 's:'a>(inventory : Inventory) -> FreeSausageRoll<'a,'s,Result<Inventory,Inventory>>{
+ let total_price = inventory.total_price();
+ let can_afford = inventory.can_afford();
+ run!{
+ exposition("You put your items onto the conveyor and wait until the cashier scans them.");
+ {
+ let line = format!("That would be €{}.{}, please.", total_price/100usize, total_price%100usize);
+ say_dialogue_line(Speaker::Cashier, Cow::from(line), Mood::Friendly)
+ };
+ if can_afford {
+ run!{
+ exposition("You hand the cashier the required amount of money.");
+ say_dialogue_line(Speaker::Cashier, Cow::from("Thank you very much, have a nice day!"), Mood::Friendly);
+ yield Ok(())
+ }
+ } else {
+ run!{
+ exposition("When you hear the total amount you need to pay, you blush.");
+ say_dialogue_line(Speaker::Cashier, Cow::from("I know that face. You haven't got enough money on you, right?"), Mood::Annoyed);
+ say_dialogue_line(Speaker::Cashier, Cow::from("Please bring back some items to where you took them from, and come back when you can actually pay the stuff you want to buy."), Mood::Annoyed);
+ yield Err(())
+ }
+ }
+ }.fmap(move |e| e.map(|_| inventory.clone()).map_err(|_| inventory.clone()))
+} \ No newline at end of file
diff --git a/examples/text-adventure/main.rs b/examples/text-adventure/main.rs
new file mode 100644
index 0000000..788c3d5
--- /dev/null
+++ b/examples/text-adventure/main.rs
@@ -0,0 +1,11 @@
+//! A small example text adventure, the logic of which is implemented as a Free Monad based eDSL.
+//!
+//! The goal of this game is to buy a sausage roll.
+
+mod dsl;
+mod logic;
+mod side_effects;
+
+fn main() {
+ let game = logic::game();
+} \ No newline at end of file
diff --git a/examples/text-adventure/side_effects.rs b/examples/text-adventure/side_effects.rs
new file mode 100644
index 0000000..51f1f4c
--- /dev/null
+++ b/examples/text-adventure/side_effects.rs
@@ -0,0 +1 @@
+//this module interprets the domain specific language. \ No newline at end of file