## Predeclarations role Thing { ... } role Container { ... } role Darkness { ... } role Heavy { ... } role Implicit { ... } role Openable { ... } role Platform { ... } role Readable { ... } role Showable { ... } role Takable { ... } class Basket { ... } class Brook { ... } class Bushes { ... } class Butterfly { ... } class Car { ... } class Disk { ... } class Doom { ... } class Door { ... } class Fire { ... } class Flashlight { ... } class Grass { ... } class Helmet { ... } class Inventory { ... } class Leaves { ... } class Pedestal { ... } class Rope { ... } class Sarcophagi { ... } class Sign { ... } class Trees { ... } class Walls { ... } class Water { ... } role Room { ... } class Cave { ... } class Crypt { ... } class Hall { ... } class Hill { ... } ## Global variables my %descriptions; for slurp("descriptions").split(/\n\n/) { /^^ '== ' (\N+) \n (.*)/ or die "Could not parse 'descriptions' file: $_"; %descriptions{$0} = ~$1; } my $room; my $inventory = Inventory.new(); my $used_abbreviated_syntax = False; my %things = car => Car.new(:name, :contents, :herephrase("Your %s is parked here.")), flashlight => Flashlight.new(:name), rope => Rope.new(:name), grass => Grass.new(:name), bushes => Bushes.new(:name), door => Door.new(:name), trees => Trees.new(:name), leaves => Leaves.new(:name, :containphrase("69,105 %s.")), brook => Brook.new(:name, :herephrase("A small brook runs through the forest.")), water => Water.new(:name, :containphrase("Some %s.")), sign => Sign.new(:name), basket => Basket.new(:name), "tiny disk" => Disk.new(:name("tiny disk")), "small disk" => Disk.new(:name("small disk")), "medium disk" => Disk.new(:name("medium disk")), "large disk" => Disk.new(:name("large disk")), "huge disk" => Disk.new(:name("huge disk")), fire => Fire.new(:name), helmet => Helmet.new(:name), pedestal => Pedestal.new(:name, :supports), butterfly => Butterfly.new(:name), doom => Doom.new(), sarcophagi => Sarcophagi.new(:name), walls => Walls.new(:name), ; my %rooms = clearing => Room.new( :name, :contents ), hill => Hill.new( :name, :contents, :in ), chamber => Room.new( :name(), :contents, :out ), hall => Hall.new( :name(), :contents(, map { "$_ disk" }, )), cave => Cave.new( :name(), :contents ), crypt => Crypt.new( :name(), :contents ), ; %things.push(%rooms); %rooms.connect('east', %rooms); %rooms.connect('northwest', %rooms); my @base_verbs = ; my %verb_synonyms = "x" => "examine", "look" => "examine", "move" => "put", "place" => "put", "pick" => "take", "pick up" => "take", "get" => "take", "retrieve" => "take", "retreive" => "take", # might as well "turn on" => "use", "switch on" => "use", ; my @verbs = @base_verbs, %verb_synonyms.keys; ## Utility subroutines sub exclude(@l, $e) { grep { $_ !=== $e }, @l } sub inverse_index(@array, $value) { my $index = @array.keys.first({ @array[$_] eq $value }); return $index; } sub current_container_of(Str $name) { return $room if $name eq $room.name.lc; return $room if $name eq any $room.contents; return $inventory if $name eq any $inventory.contents; for %things{$room.contents, $inventory.contents} -> $thing { return $thing if $name eq any $thing.?contents; return $thing if $name eq any $thing.?supports; } return Nil; } sub room_contains(Str $name) { return current_container_of($name).?name eq any $room.name, $room.contents; } sub inventory_contains(Str $name) { return True if $name eq any $inventory.contents; return True if $name eq any map { .contents.flat }, grep { player_can_see_inside($_) }, %things{$inventory.contents}; return False; } sub player_can_see(Thing $thing) { my $thing_is_visible = $thing !~~ Showable || $thing.is_visible; return False unless $thing_is_visible; return False unless room_contains($thing.name) || inventory_contains($thing.name); return True; } sub player_can_see_inside(Thing $thing) { my $thing_is_open = $thing ~~ Container && ($thing !~~ Openable || $thing.is_open); return False unless $thing_is_open; return False unless player_can_see($thing); return True; } sub there_is_light() { my $there_is_sun = $room !~~ Darkness; return True if $there_is_sun; my $flashlight = %things; my $flashlight_is_here = player_can_see($flashlight); return True if $flashlight_is_here && $flashlight.is_on; my $fire = %things; my $fire_is_here = player_can_see($fire); return True if $fire_is_here; return False; } ## Roles for things and rooms role Thing { has Str $.name; has Str $!description = %descriptions{$.name}; has Str $.herephrase; has Str $.containphrase; method examine { if there_is_light() { say $!description; self.?on_examine; if self !~~ Room && player_can_see_inside(self) && self.contents { say "The ", self.name, " contains:"; self.list_container_contents("A %s"); } } else { say "You can't see anything, because it's pitch black."; } } } role Container does Thing { has Thing @.contents is rw; method add(Str $name) { @.contents.push($name); } method remove(Str $name) { @.contents = exclude(@.contents, $name); } method list_contents($herephrase) { for %things{@.contents} -> Thing $thing { if player_can_see($thing) { next if $thing ~~ Implicit; say sprintf $thing.herephrase // $herephrase, $thing.name; if player_can_see_inside($thing) && $thing.contents { say "The $thing.name() contains:"; $thing.list_container_contents("A %s."); } if $thing ~~ Platform && $thing.supports { say "On the $thing.name() you see:"; $thing.list_platform_supports("A %s."); } } } } method list_container_contents($containphrase, $indent = 1) { for %things{@.contents} -> Thing $thing { say ' ' x $indent, sprintf $thing.containphrase // $containphrase, $thing.name; if player_can_see_inside($thing) && $thing.contents { say ' ' x $indent, "The $thing.name() contains:"; $thing.list_container_contents("A %s.", $indent + 1); } } } method on_open { if @.contents { say "Opening the $.name reveals a {join " and a ", @.contents}."; } } } role Darkness does Room { } role Heavy does Thing { method on_remove_from($_) { when Platform { unless grep Heavy, %things.supports { say "An alarm starts sounding in the whole cavern."; %things.activate; } } } method on_put($_) { when Platform { say "The alarm stops."; %things.inactivate; } } } role Implicit does Thing { } role Openable does Thing { has Bool $.is_open; method open { if $.is_open { say "The $.name is open."; return; } say "You open the $.name."; $!is_open = True; self.?on_open; } method close { unless $.is_open { say "The $.name is closed."; return; } say "You close the $.name."; $!is_open = False; self.?on_close; } } role Platform does Thing { has Thing @.supports is rw; method add(Str $name) { @.supports.push($name); } method remove(Str $name) { @.supports = exclude(@.supports, $name); } method list_platform_supports($containphrase, $indent = 1) { for %things{@.supports} -> Thing $thing { say ' ' x $indent, sprintf $thing.containphrase // $containphrase, $thing.name; if player_can_see_inside($thing) && $thing.contents { say ' ' x $indent, "The $thing.name() contains:"; $thing.list_container_contents("A %s.", $indent + 1); } } } } role Readable does Thing { method read { self.examine; } } role Showable does Thing { has Bool $.is_visible = False; method show { unless $.is_visible { $!is_visible = True; self.?on_show; } } } role Takable does Thing { method put($new_receiver) { my $old_receiver = current_container_of($.name); $old_receiver.remove($.name); self.?on_remove_from($old_receiver); $new_receiver.add($.name); self.?on_put($new_receiver); } method take { if inventory_contains($.name) { say "You are already holding the $.name"; return; } say "You take the $.name."; self.put($inventory); } method drop { unless inventory_contains($.name) { say "You are not holding the $.name"; return; } say "You drop the $.name on the ground."; self.put($room); } } ## Things class Basket does Container { } class Brook does Container { } class Bushes does Implicit { method on_examine { %things.show; } } class Butterfly does Takable does Heavy { } class Car does Openable does Container { method go { say "You get in the car, but then remember that you haven't found"; say "the treasure yet, so you get out again."; } } class Disk does Takable does Heavy { } class Doom { has Bool $.activated = False; has Int $!time_left; method activate { $!activated = True; $!time_left = 4; } method inactivate { $!activated = False; } method tick { if $!activated { $!time_left--; unless $!time_left { say "The alarm starts sounding louder."; say "The whole cavern shakes, and falls in on itself."; say "You die."; last; } } } } class Door does Showable does Openable { method on_show { say "You discover a door in the hill, under the thick grass!"; } method on_open { say "You can see into the hill now!"; %rooms.connect('south', %rooms); } method on_close { %rooms.disconnect('south'); } } class Fire does Container does Platform { method add(Str $name) { self.Container::add($name); } method remove(Str $name) { self.Container::remove($name); } } class Flashlight does Takable { has Bool $.is_on = False; method use { if $!is_on { say "It's already switched on."; } my $was_dark = !there_is_light; $!is_on = True; say "You switch on the flashlight."; if $was_dark { say ""; $room.look; } } method examine { self.Thing::examine; say ""; say "The $.name is switched {$.is_on ?? "on" !! "off"}."; } } class Grass does Implicit { method on_examine { %things.show; } } class Helmet does Container does Takable { method on_remove_from(Container $_) { when Brook { %things.put(self); } } } class Inventory does Container { } class Leaves does Implicit does Takable { method on_put(Container $_) { when Car { say "Great. Now your car is full of leaves."; } when Basket { say "The ground rumbles and shakes a bit."; say "A passageway opens up to the south, into the caverns."; %rooms.connect('south', %rooms); } when Fire { say "The leaves burn up within seconds."; } } } class Pedestal does Platform { } class Rope does Takable { } class Sarcophagi does Implicit { } class Sign does Readable { } class Trees does Implicit { } class Walls does Implicit does Readable { method examine { say %descriptions{"walls:$room.name()"}.lines.pick; self.?on_examine; } } class Water does Implicit does Takable { method use { if player_can_see(%things) { say "You put the water on the fire."; self.put(%things); } else { say "Use the $.name for what?"; } } method on_put($_) { when Inventory { say "Your bare hands aren't very good at carrying water."; self.drop; } when Fire { say "The fire wanes and dies."; $room.remove("fire"); if !there_is_light() { $room.look; } } } } ## Directions my @directions = < north south east west northeast northwest southeast southwest up down in out >; my %abbr_directions = < n north s south e east w west ne northeast nw northwest se southeast sw southwest u up d down >; # RAKUDO: Have to repeat the list here because of a scoping bug [perl #95500] subset Direction of Str where any(< north south east west northeast northwest southeast southwest up down in out >); sub opposite_direction(Direction $d) { my %opposites = 'north' => 'south', 'east' => 'west', 'northeast' => 'southwest', 'northwest' => 'southeast', 'up' => 'down', 'in' => 'out', ; %opposites.push( %opposites.invert ); %opposites{$d}; } ## Rooms role Room does Container { has Direction %.exits is rw; has Direction $.in; has Direction $.out; has Bool $!visited = False; method connect(Direction $direction, Room $other_room) { my $opposite = opposite_direction($direction); self.exits{$direction} = $other_room; $other_room.exits{$opposite} = self; } method disconnect(Direction $direction) { my $opposite = opposite_direction($direction); my $other_room = self.exits.delete($direction); if $other_room { $other_room.exits.delete($opposite); } } method list_exits { given %.exits { when 0 { say "There are no obvious exits from here."; } when 1 { say "You can go {.keys}."; } when 2 { say "You can go {.keys.join(" and ")}."; } default { say "You can go {.keys[0..*-2].join(", ")} and {.keys[*-1]}."; } } } method look { if there_is_light() { self.examine; self.list_contents("There is a %s here."); self.list_exits; } else { say "It is pitch black."; } } method enter { say $!name.ucfirst; $room = self; unless $!visited { say ""; self.look; } $!visited = True; self.?on_enter; if %things.activated { say "An alarm is sounding."; } } } class Hill does Room { method on_enter { if inventory_contains 'butterfly' { say "Congratulations! You found the treasure and got out with it ", "alive!"; last; } } } class Cave does Room does Darkness { method on_try_exit($direction) { if $direction eq "northwest" && player_can_see(%things) { say "You try to walk past the fire, but it's too hot!"; return False; } return True; } } class Crypt does Room does Darkness { } class Hall does Room does Darkness { has @!disks = [5, 4, 3, 2, 1], [], [], ; has $!moves_made = 0; my @sizes = <. tiny small medium large huge>; my @rods = ; method list_contents($herephrase) { say "There are rods stuck in the floor with disks on them, ", "like this:"; self.show_disks; say ""; for %things{@.contents} -> Thing $thing { next if $thing ~~ Disk; next if $thing ~~ Implicit; if player_can_see($thing) { say sprintf $thing.herephrase // $herephrase, $thing.name; } } } method show_disks { say ""; my $indent = " "; my @c = " | ", " === ", " ===== ", " ======= ", " ========= ", "===========", ; for reverse 0..5 -> $row { say $indent, join " ", map { @c[@!disks[$_][$row] // 0] }, 0..2; } say $indent, "-" x 37; say $indent, join " ", map { " $_ " }, ; } method take_disk(Str $adjective) { my $size = inverse_index(@sizes, $adjective); my $old_rod = first { $size == any @!disks[$_].list }, 0..2; if defined $old_rod { if @!disks[$old_rod][*-1] != $size { say "You can't take the $adjective disk, because it is under ", (join " and ", map { "the @sizes[$_] disk" }, grep { $_ < $size }, @!disks[$old_rod].list), "."; return; } } unless $adjective eq "tiny" { say "The $adjective disk is too heavy to carry!"; return; } %things{"$adjective disk"}.take; if defined $old_rod { pop @!disks[$old_rod]; self.?on_move_disk($old_rod); } } method move_disk_to_rod(Str $adjective, Str $position) { my $size = inverse_index(@sizes, $adjective); my $old_rod = first { $size == any @!disks[$_].list }, 0..2; if defined $old_rod && @!disks[$old_rod][*-1] != $size { say "You can't take the $adjective disk, because it is under ", (join " and ", map { "the @sizes[$_] disk" }, grep { $_ < $size }, @!disks[$old_rod].list), "."; return; } my $new_rod = inverse_index(@rods, $position); if @!disks[$new_rod] { if @!disks[$new_rod][*-1] == $size { say "The $adjective disk is already on the $position rod."; return; } elsif @!disks[$new_rod][*-1] < $size { say "A sense of dread fills you as you attempt to put a ", "bigger disk on a smaller one."; return; } } if defined $old_rod { pop @!disks[$old_rod]; } %things{"$adjective disk"}.put($room); push @!disks[$new_rod], $size; say "You put the $adjective disk on the $position rod."; self.show_disks; if defined $old_rod { $!moves_made++; self.suggest_short_syntax($old_rod, $new_rod); } self.?on_move_disk($old_rod); } method move_rod_to_rod(Int $old_rod, Int $new_rod) { unless @!disks[$old_rod] { say "The {@rods[$old_rod]} rod is empty."; return; } my $size = @!disks[$old_rod][*-1]; if @!disks[$new_rod] { if @!disks[$new_rod][*-1] < $size { say "A sense of dread fills you as you attempt to put a ", "bigger disk on a smaller one."; return; } } pop @!disks[$old_rod]; push @!disks[$new_rod], $size; my $adjective = @sizes[$size]; my $position = @rods[$new_rod]; say "You put the $adjective disk on the $position rod."; self.show_disks; $!moves_made++; self.suggest_short_syntax($old_rod, $new_rod); self.?on_move_disk($old_rod); } method suggest_short_syntax($old_rod, $new_rod) { if 3 <= $!moves_made < 5 && !$used_abbreviated_syntax { my $abbr = chr(ord("A") + $old_rod) ~ chr(ord("A") + $new_rod); say "(You can also write this move as $abbr)"; } } method on_move_disk($old_rod) { sub hole_is_revealed { %rooms.exits.exists("down") } if @!disks[2] == 5 { say "The whole floor tips, and reveals a hole beneath the wall."; %rooms.connect('down', %rooms); } if defined $old_rod && $old_rod == 2 && @!disks[2] == 3 && hole_is_revealed() { say "The whole floor tips back, hiding the hole again."; %rooms.disconnect('down'); } } } ## The game itself say "CRYPT"; say "====="; say ""; say "You've heard there's supposed to be an ancient hidden crypt in these"; say "woods. One containing a priceless treasure. Well, there's only one way"; say "to find out..."; say ""; %rooms.enter; loop { say ""; my $command = prompt("> "); given $command { when !.defined || .lc eq "q" | "quit" { say ""; my $really = prompt "Really quit (y/N)? "; if !defined $really || "y"|"yes" eq lc $really { last; } } $command .= trim; $command .= lc; when "" { succeed; } when /^help>>/|"h"|"?" { say "Here are some (made-up) examples of commands you can use:"; say ""; say "look (l) | ", "take banana"; say "examine banana (x banana) | ", "drop banana"; say "[go] north/south/east/west (n/s/e/w) | ", "put banana in bag"; say "open bag | ", "close bag"; } when /^ :s go (\w+) $/ && $0 eq any @directions, %abbr_directions.keys, { $command = ~$0; proceed; } when any(%abbr_directions.keys) { $command = %abbr_directions{$command}; proceed; } when 'in' { if $room.in -> $real_direction { $command = $real_direction; } proceed; } when 'out' { if $room.out -> $real_direction { $command = $real_direction; } proceed; } when Direction { my $direction = $command; if $room.exits{$direction} -> $new_room { my $succeeded = $room.?on_try_exit($direction) // True; if $succeeded { $new_room.enter; } %things.tick; } else { say "Sorry, you can't go $direction from here."; } } when "look"|"l" { $room.look; } when /^ :s look in $=[\w+] $/ { my $thing = %things{$}; unless $thing { say "I am unfamiliar with the noun '$'."; succeed; } unless player_can_see($thing) { say "You see no $ here."; succeed; } unless player_can_see_inside($thing) { say "You can't see inside the $."; succeed; } say "The $thing.name() contains:"; $thing.list_container_contents("A %s."); } when "inventory"|"i" { if $inventory.contents { say "You are carrying:"; $inventory.list_container_contents("A %s."); } else { say "You are empty-handed."; } } when /^ :s $=[\w+[ \w+]?] eq any(@verbs) }> $/ { say "What do you want to $?"; } # RAKUDO: Due to [perl #95504], we have to do the checking like # this instead of just $=@verbs when /^ :s $=[\w+[ \w+]?] eq any(@verbs) }> [the]? $=[\w+] $/ { my $verb = $; if %verb_synonyms{$verb} -> $synonym { $verb = $synonym; } my $thing = %things{$}; unless $thing { say "I am unfamiliar with the noun '$'."; succeed; } unless player_can_see($thing) { say "You see no $ here."; succeed; } unless $thing.can($verb) { say "You can't $ the $."; succeed; } if $verb eq "put" { say "You want to $ the $ in what or on what?"; succeed; } $thing."$verb"(); %things.tick; } # RAKUDO: Due to [perl #95504], we have to do the checking like # this instead of just $=@verbs when /^ :s $=[\w+[ \w+]?] eq any(@verbs) }> [the]? $=[\w+] $=[in||on] [the]? $=[\w+] $/ { my $verb = $; if %verb_synonyms{$verb} -> $synonym { $verb = $synonym; } unless $verb eq 'put' { say "Sorry, I did not understand that."; say "Type 'help' for suggestions."; succeed; } if $ eq "disk" && $room ~~ Hall { say "Which disk do you mean; the tiny disk, the small disk, ", "the medium disk,"; say "the large disk, or the huge disk?"; succeed; } my $noun1 = $; if $ eq "disk" && $room ~~ Crypt { $noun1 = "tiny disk"; } my $thing = %things{$noun1}; unless $thing { say "I am unfamiliar with the noun '$noun1'."; succeed; } unless player_can_see($thing) { say "You see no $ here."; succeed; } my $receiver = %things{$}; unless $receiver { say "I am unfamiliar with the noun '$'."; succeed; } unless player_can_see($receiver) { say "You see no $ here."; succeed; } unless $thing ~~ Takable { say "You can't move the $noun1."; succeed; } if $ eq "in" { unless $receiver ~~ Container { say "You can't put things in the $."; succeed; } unless player_can_see_inside($receiver) { $receiver.open; } say "You put the $noun1 in the $."; } elsif $ eq "on" { unless $receiver ~~ Platform { say "You can't put things on the $."; succeed; } say "You put the $noun1 on the $."; } $thing.put($receiver); } when /^ :s [move|put|take] [the]? disk / { say "Which disk do you mean; the tiny disk, the small disk, ", "the medium disk,"; say "the large disk, or the huge disk?"; } when /^ :s take [the]? $=[tiny||small||medium||large||huge] disk / { unless player_can_see(%things{"$ disk"}) { say "You see no $ disk here."; succeed; } if $room ~~ Hall { $room.take_disk(~$); } else { %things{"$ disk"}.take; } } when /^ :s [move|put] [the]? $=[tiny||small||medium||large||huge] disk [on|to] [the]? $=[left||middle||right] rod $/ { unless player_can_see(%things{"$ disk"}) { say "You see no $ disk here."; succeed; } unless $room ~~ Hall { say "You see no rod here."; succeed; } $room.move_disk_to_rod(~$, ~$); } when /^ :s [move|put] [the]? $=[left||middle||right] disk [on|to] [the]? $=[left||middle||right] rod $/ { unless $room ~~ Hall { say "You see no rod here."; succeed; } my $old_rod = inverse_index , $; my $new_rod = inverse_index , $; if $old_rod == $new_rod { succeed; } $room.move_rod_to_rod($old_rod, $new_rod); } when /^ :s (<[abc]>)(<[abc]>) $/ { $used_abbreviated_syntax = True; unless $room ~~ Hall { say "That command only works in the Hall."; succeed; } my $old_rod = inverse_index , $0; my $new_rod = inverse_index , $1; if $old_rod == $new_rod { succeed; } $room.move_rod_to_rod($old_rod, $new_rod); } default { say "Sorry, I did not understand that."; say "Type 'help' for suggestions."; } } } say "Thanks for playing.";