1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
|
//! 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::data::{Inventory, Item, Location, Mood, Speaker};
use super::dsl::{FreeSausageRoll, exposition, give_player_options, present_location, say_dialogue_line};
//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, Inventory::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.is_empty() => {
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 {
FreeSausageRoll::pure(c)
} else {
//If this were an actual game, we could put a save-point here. At this location the next room to handle is just determined by room and inventory.
handle_rooms(c.0, c.1)
}
}
}
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_shelves(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_deli(inventory.clone())
}
},
3 => {
run!{
i <= talk_to_deli_lady(inventory.clone());
handle_deli(i)
}
},
_ => 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) => {
run!{
exposition("You leave the supermarket. Your partner is already waiting outside.");
FreeSausageRoll::pure((Location::Entrance, inventory.clone()))
}
},
Err(inventory) => handle_checkout(inventory),
}
}
},
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)).copied().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()))
}
|