diff options
| author | Andreas Grois <andi@grois.info> | 2023-04-02 12:06:05 +0200 |
|---|---|---|
| committer | Andreas Grois <andi@grois.info> | 2023-04-02 13:27:12 +0200 |
| commit | c9880dd12df2c614360cbc85609d859f3652507f (patch) | |
| tree | 15b7004d5ee69cdf1c79af0aa7155c41c207c6f5 /examples/text-adventure | |
| parent | ea1814025f3bbab7a0aac640e69c56d983a18308 (diff) | |
Example: Initial draft of the story
Diffstat (limited to 'examples/text-adventure')
| -rw-r--r-- | examples/text-adventure/dsl.rs | 205 | ||||
| -rw-r--r-- | examples/text-adventure/logic.rs | 417 | ||||
| -rw-r--r-- | examples/text-adventure/main.rs | 11 | ||||
| -rw-r--r-- | examples/text-adventure/side_effects.rs | 1 |
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 |
