├── .gitignore ├── .vscode └── settings.json ├── Cargo.lock ├── Cargo.toml ├── Gemfile ├── Gemfile.lock ├── README.md ├── lib ├── adventure.rb ├── game.rb ├── inventory_item.rb ├── location.rb ├── main.rb ├── map.rb ├── npc.rb ├── player.rb └── room.rb ├── spec ├── inventory_item_spec.rb ├── location_spec.rb ├── map_spec.rb ├── room_spec.rb └── spec_helper.rb └── src ├── game.rs ├── inventory_item.rs ├── location.rs ├── main.rs ├── main.rs.bk ├── map.rs ├── npc.rs ├── player.rs └── room.rs /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | // Place your settings in this file to overwrite default and user settings. 2 | { 3 | "rust.checkWith": "clippy", 4 | "rust.formatOnSave": true, 5 | "rust.checkOnSave": true, 6 | "rust.rustLangSrcPath": "/Users/engineering/Code/rust/src", 7 | "rust.useNewErrorFormat": true 8 | } -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | [root] 2 | name = "adventure_game" 3 | version = "0.1.0" 4 | dependencies = [ 5 | "regex 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", 6 | ] 7 | 8 | [[package]] 9 | name = "aho-corasick" 10 | version = "0.6.3" 11 | source = "registry+https://github.com/rust-lang/crates.io-index" 12 | dependencies = [ 13 | "memchr 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", 14 | ] 15 | 16 | [[package]] 17 | name = "kernel32-sys" 18 | version = "0.2.2" 19 | source = "registry+https://github.com/rust-lang/crates.io-index" 20 | dependencies = [ 21 | "winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", 22 | "winapi-build 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", 23 | ] 24 | 25 | [[package]] 26 | name = "libc" 27 | version = "0.2.22" 28 | source = "registry+https://github.com/rust-lang/crates.io-index" 29 | 30 | [[package]] 31 | name = "memchr" 32 | version = "1.0.1" 33 | source = "registry+https://github.com/rust-lang/crates.io-index" 34 | dependencies = [ 35 | "libc 0.2.22 (registry+https://github.com/rust-lang/crates.io-index)", 36 | ] 37 | 38 | [[package]] 39 | name = "regex" 40 | version = "0.2.1" 41 | source = "registry+https://github.com/rust-lang/crates.io-index" 42 | dependencies = [ 43 | "aho-corasick 0.6.3 (registry+https://github.com/rust-lang/crates.io-index)", 44 | "memchr 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", 45 | "regex-syntax 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", 46 | "thread_local 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)", 47 | "utf8-ranges 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", 48 | ] 49 | 50 | [[package]] 51 | name = "regex-syntax" 52 | version = "0.4.0" 53 | source = "registry+https://github.com/rust-lang/crates.io-index" 54 | 55 | [[package]] 56 | name = "thread-id" 57 | version = "3.0.0" 58 | source = "registry+https://github.com/rust-lang/crates.io-index" 59 | dependencies = [ 60 | "kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", 61 | "libc 0.2.22 (registry+https://github.com/rust-lang/crates.io-index)", 62 | ] 63 | 64 | [[package]] 65 | name = "thread_local" 66 | version = "0.3.3" 67 | source = "registry+https://github.com/rust-lang/crates.io-index" 68 | dependencies = [ 69 | "thread-id 3.0.0 (registry+https://github.com/rust-lang/crates.io-index)", 70 | "unreachable 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", 71 | ] 72 | 73 | [[package]] 74 | name = "unreachable" 75 | version = "0.1.1" 76 | source = "registry+https://github.com/rust-lang/crates.io-index" 77 | dependencies = [ 78 | "void 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", 79 | ] 80 | 81 | [[package]] 82 | name = "utf8-ranges" 83 | version = "1.0.0" 84 | source = "registry+https://github.com/rust-lang/crates.io-index" 85 | 86 | [[package]] 87 | name = "void" 88 | version = "1.0.2" 89 | source = "registry+https://github.com/rust-lang/crates.io-index" 90 | 91 | [[package]] 92 | name = "winapi" 93 | version = "0.2.8" 94 | source = "registry+https://github.com/rust-lang/crates.io-index" 95 | 96 | [[package]] 97 | name = "winapi-build" 98 | version = "0.1.1" 99 | source = "registry+https://github.com/rust-lang/crates.io-index" 100 | 101 | [metadata] 102 | "checksum aho-corasick 0.6.3 (registry+https://github.com/rust-lang/crates.io-index)" = "500909c4f87a9e52355b26626d890833e9e1d53ac566db76c36faa984b889699" 103 | "checksum kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "7507624b29483431c0ba2d82aece8ca6cdba9382bff4ddd0f7490560c056098d" 104 | "checksum libc 0.2.22 (registry+https://github.com/rust-lang/crates.io-index)" = "babb8281da88cba992fa1f4ddec7d63ed96280a1a53ec9b919fd37b53d71e502" 105 | "checksum memchr 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "1dbccc0e46f1ea47b9f17e6d67c5a96bd27030519c519c9c91327e31275a47b4" 106 | "checksum regex 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "4278c17d0f6d62dfef0ab00028feb45bd7d2102843f80763474eeb1be8a10c01" 107 | "checksum regex-syntax 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "2f9191b1f57603095f105d317e375d19b1c9c5c3185ea9633a99a6dcbed04457" 108 | "checksum thread-id 3.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "4437c97558c70d129e40629a5b385b3fb1ffac301e63941335e4d354081ec14a" 109 | "checksum thread_local 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "c85048c6260d17cf486ceae3282d9fb6b90be220bf5b28c400f5485ffc29f0c7" 110 | "checksum unreachable 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "1f2ae5ddb18e1c92664717616dd9549dde73f539f01bd7b77c2edb2446bdff91" 111 | "checksum utf8-ranges 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "662fab6525a98beff2921d7f61a39e7d59e0b425ebc7d0d9e66d316e55124122" 112 | "checksum void 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d" 113 | "checksum winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)" = "167dc9d6949a9b857f3451275e911c3f44255842c1f7a76f33c55103a909087a" 114 | "checksum winapi-build 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "2d315eee3b34aca4797b2da6b13ed88266e6d612562a0c46390af8299fc699bc" 115 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "adventure_game" 3 | version = "0.1.0" 4 | authors = ["Liz Baillie and Yehuda Katz "] 5 | 6 | [dependencies] 7 | regex = "0.2" -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # A sample Gemfile 2 | source "https://rubygems.org" 3 | 4 | # gem "rails" 5 | gem "rspec" 6 | gem "pry" -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | coderay (1.1.1) 5 | diff-lcs (1.2.5) 6 | method_source (0.8.2) 7 | pry (0.10.3) 8 | coderay (~> 1.1.0) 9 | method_source (~> 0.8.1) 10 | slop (~> 3.4) 11 | rspec (3.4.0) 12 | rspec-core (~> 3.4.0) 13 | rspec-expectations (~> 3.4.0) 14 | rspec-mocks (~> 3.4.0) 15 | rspec-core (3.4.4) 16 | rspec-support (~> 3.4.0) 17 | rspec-expectations (3.4.0) 18 | diff-lcs (>= 1.2.0, < 2.0) 19 | rspec-support (~> 3.4.0) 20 | rspec-mocks (3.4.1) 21 | diff-lcs (>= 1.2.0, < 2.0) 22 | rspec-support (~> 3.4.0) 23 | rspec-support (3.4.1) 24 | slop (3.6.0) 25 | 26 | PLATFORMS 27 | ruby 28 | 29 | DEPENDENCIES 30 | pry 31 | rspec 32 | 33 | BUNDLED WITH 34 | 1.11.2 35 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Learning Rust via Ruby 2 | 3 | ### A text-based adventure game being built in Rust and Ruby (simultaneously) 4 | 5 | If you want to play it: 6 | 7 | 1. clone this repository 8 | 2. cd into it (i.e. enter `cd learning-rust` in your terminal) 9 | 3. for the Ruby version: enter `ruby lib/main.rb` in your terminal and hit return. For the Rust version: enter `cargo run` in your terminal and hit return 10 | 4. play :) 11 | 12 | The actual narrative still needs to be ported over & some prettifying still needs to happen, but you can play a super simplistic placeholder game right now. Both games should basically work at this point, but please submit an issue if you notice something's broken! The Ruby side seems to be especially fragile. 13 | 14 | Built mostly by @lbaillie with help from @wycats 15 | 16 | ## Contributing 17 | 18 | Contributions are welcome! Check out the issues for what's currently needed and comment on any issue you might be interested in. I'm happy to help guide you if you need help :D 19 | -------------------------------------------------------------------------------- /lib/adventure.rb: -------------------------------------------------------------------------------- 1 | require_relative 'game' 2 | require_relative 'inventory_item' 3 | require_relative 'location' 4 | require_relative 'map' 5 | require_relative 'npc' 6 | require_relative 'player' 7 | require_relative 'room' 8 | -------------------------------------------------------------------------------- /lib/game.rb: -------------------------------------------------------------------------------- 1 | class Game 2 | def self.play 3 | Game.new.tap(&:play) 4 | end 5 | 6 | def initialize 7 | @valid_choices = [ 8 | :help, 9 | :exit, 10 | :display_map, 11 | :look_around, 12 | :pick_up, 13 | :use, 14 | :print_inventory, 15 | :talk, 16 | :take, 17 | :move 18 | ] 19 | 20 | @player = Player.new 21 | @playing = true 22 | @map = Map.new("Great Ruby Adventure", rooms, @player) 23 | end 24 | 25 | def play 26 | puts "Welcome to #{@map.title}" 27 | puts "What would you like to do? (Enter 'help' to see a list of commands)" 28 | parse_choice(gets.chomp) 29 | while @playing 30 | break if !@playing 31 | puts "What now?" 32 | choice = gets.chomp 33 | parse_choice(choice) 34 | end 35 | end 36 | 37 | private 38 | 39 | # take item from an NPC 40 | def take(item_name) 41 | if current_room.npc && current_room.npc.has_item(item_name) 42 | npc_item = item_to_take(item_name) 43 | @player.add_to_inventory(npc_item) 44 | # this just doesn't seem to remove the item from NPC inventory no matter what I do :( 45 | current_room.npc.remove_from_inventory(npc_item) 46 | else 47 | puts "Sorry, that item isn't here." 48 | end 49 | end 50 | 51 | def item_to_take(item_name) 52 | if current_room.npc.inventory.map(&:name).include?(item_name) 53 | current_room.npc.inventory.select { |i| i.name == item_name }.first 54 | else 55 | nil 56 | end 57 | end 58 | 59 | # take item from a Room 60 | def pick_up(item_name) 61 | if current_room.has_item(item_name) 62 | item = current_room.items.find { |thing| thing.name == item_name } 63 | @player.add_to_inventory(item) 64 | current_room.remove_one(item) 65 | else 66 | puts "Sorry, that item is not in this room. Try again." 67 | end 68 | end 69 | 70 | # use an item from your inventory 71 | def use(item_name) 72 | # TODO: figure out how to pass around entire item object to access effects anywhere 73 | if @player.has_item(item_name) && current_room.npc && current_room.npc.has_item(item_name) 74 | effect = current_room.npc.inventory.select { |i| i.name == item_name }.first.effects 75 | puts effect 76 | @player.remove_from_inventory(item_name) 77 | # TODO: eventually remove from NPC inventory & change ownership of item 78 | elsif @player.has_item(item_name) && current_room.has_item(item_name) 79 | effect = current_room.items.find { |i| i.name == item_name }.effects 80 | puts effect 81 | @player.remove_from_inventory(item_name) 82 | # TODO: eventually remove from Room inventory & change ownership of item 83 | else 84 | puts "Sorry, that item is not in your inventory. Did you pick it up or try taking it from someone?" 85 | end 86 | end 87 | 88 | # talk to an NPC 89 | def talk 90 | if current_room.npc 91 | puts current_room.npc.dialogue[:default] 92 | else 93 | puts "Sorry, no one else is in here!" 94 | end 95 | end 96 | 97 | # print your inventory to the console 98 | def print_inventory 99 | @player.print_inventory 100 | end 101 | 102 | #TODO: clean this method up like whoa 103 | def parse_choice(choice) 104 | new_choice = choice.split.join("_") 105 | valid_choice = @valid_choices.select { |entry| entry == new_choice.to_sym }.first 106 | if choice.include?('move') && valid_move(choice) 107 | move(choice.split(' ').last.to_sym) 108 | elsif choice.include?('pick up') 109 | item = choice.split.select{ |item| item != 'pick' && item != 'up' }.join(' ') 110 | pick_up(item) 111 | elsif choice.include?('take') 112 | item = choice.split.select{ |item| item != 'take' }.join(' ') 113 | take(item) 114 | elsif choice.include?('use') 115 | item = choice.split.select{ |item| item != 'use' }.join(' ') 116 | use(item) 117 | elsif valid_choice != nil && check_validity(new_choice.to_sym) 118 | self.send valid_choice 119 | else 120 | puts "That is not a valid choice, try again." 121 | end 122 | end 123 | 124 | # check that move is in a valid direction 125 | def valid_move(user_input) 126 | user_input.split(' ').first == 'move' && 127 | ['north','south', 'east', 'west'].include?(user_input.split(' ').last) 128 | end 129 | 130 | # see all the rooms 131 | def display_map 132 | @map.display_map 133 | end 134 | 135 | def current_room 136 | rooms.select { |room| room[0] == @player.location }.first 137 | end 138 | 139 | def look_around 140 | puts current_room.description 141 | if current_room.has_items 142 | puts "This room contains #{current_room.item_list}" 143 | end 144 | puts current_room.npc.name + " is here too." if current_room.npc 145 | end 146 | 147 | def move(direction) 148 | if @map.valid_directions(@player.location)[direction] 149 | case direction 150 | when :north 151 | @player.location.y += 1 152 | puts "You have moved north! Your new location is #{@player.location.x}, #{@player.location.y}" 153 | when :south 154 | @player.location.y -= 1 155 | puts "You have moved south! Your new location is #{@player.location.x}, #{@player.location.y}" 156 | when :west 157 | @player.location.x -= 1 158 | puts "You have moved west! Your new location is #{@player.location.x}, #{@player.location.y}" 159 | when :east 160 | @player.location.x += 1 161 | puts "You have moved east! Your new location is #{@player.location.x}, #{@player.location.y}" 162 | else 163 | puts "Sorry, you can't move there!" 164 | end 165 | else 166 | puts "Sorry, you can't move there!" 167 | end 168 | end 169 | 170 | def help 171 | puts <<-HEREDOC 172 | exit: exit the game 173 | move north, south, east, west: move in this direction 174 | look around: see a description of the current room 175 | pick up _item_: add the item to your inventory 176 | take _item_: take an item from an NPC 177 | use _item_: use an item in your inventory 178 | talk: talk to an NPC 179 | display map: look at map 180 | print inventory: show current player inventory 181 | HEREDOC 182 | end 183 | 184 | def exit 185 | puts "Bye, #{@player.name}! Thanks for playing!" 186 | abort 187 | end 188 | 189 | def rooms 190 | [ 191 | Room.new( 192 | 0, 0, 193 | "Unicorn Room", 194 | "This room contains a rare and glorious unicorn. It's amazing.", 195 | [InventoryItem.new("a jar of unicorn farts", 1, nil, "BOOM. Unicorn farts are powerful! You are now very sparkly.")], 196 | NPC.new( 197 | "Unicorn Doctor", 198 | [InventoryItem.new("vial of unicorn blood", 1, self, "The amazing unicorn blood has made you INVINCIBLE!")], 199 | { 200 | default: "Hi I'm a unicorn doctor. It's pretty cool. I have a vial of unicorn blood, do you want it?" 201 | } 202 | ), 203 | ), 204 | Room.new( 205 | 0, 1, 206 | "Bear Room", 207 | "HOLY CRAP THERE'S A BEAR IN THIS ROOM.", 208 | [InventoryItem.new("a canister of bear repellant", 1, nil, "You are now safe from the bear! Stop stressin'")], 209 | nil 210 | ), 211 | Room.new( 212 | 0, 2, 213 | "Cool Stuff Room", 214 | "This room's pretty cool, nbd", 215 | [nil], 216 | nil 217 | ), 218 | Room.new( 219 | 1, 0, 220 | "Crappy Stuff Room", 221 | "Everything in this room stinks like garbage.", 222 | [InventoryItem.new("a garbage bomb", 3, nil, "Bad move, now you stink like garbage. But so does everything else.")], 223 | nil 224 | ), 225 | Room.new( 226 | 1, 1, 227 | "Starting Out Room", 228 | "This is a room to start out in. Nothing to see here.", 229 | [nil], 230 | nil 231 | ), 232 | Room.new( 233 | 1, 2, 234 | "Cute Puppy Room", 235 | "OMG this room is FULL. OF. PUPPIES. So many puppies!", 236 | [InventoryItem.new("a puppy", 10, nil, "You pet the heck out of the puppy. Look at his waggy tail! LOOK AT IT!! You are hypnotized.")], 237 | nil 238 | ), 239 | Room.new( 240 | 2, 0, 241 | "Sandwich and Chips Room", 242 | "Yum, there's a sandwich and some chips in here!", 243 | [ 244 | InventoryItem.new("a sandwich", 1, nil, "Yum, that was a good sandwich. It was made of whatever your favorite sandwich is."), 245 | InventoryItem.new("a bag of chips", 1, nil, "That was a delightful bag of chips! Crunchy as heck with perfect salt.") 246 | ], 247 | nil 248 | ), 249 | Room.new( 250 | 2, 1, 251 | "Home Alone Room", 252 | "This room's got nothing in it. You're allll aloooone.", 253 | [nil], 254 | nil 255 | ), 256 | Room.new( 257 | 2, 2, 258 | "Dank Meme Room", 259 | "This room is nothing but sweet memes.", 260 | [ 261 | InventoryItem.new("a pic of Hillary Clinton texting", 1, nil, "You laugh yourself to sleep because memes are so funny right"), 262 | InventoryItem.new("a pic of a dog getting hit in the face with a frisbee", 1, nil, "Man that frisbee dog is hilarious, isn't he? Memes are the best.") 263 | ], 264 | nil 265 | ) 266 | ] 267 | end 268 | 269 | # checks that Player input is valid 270 | def check_validity(choice) 271 | valid_options = ['pick up', 'use', 'display inventory', 'move'] 272 | @valid_choices.include?(choice.to_sym) || valid_options.include?(choice) 273 | end 274 | end 275 | -------------------------------------------------------------------------------- /lib/inventory_item.rb: -------------------------------------------------------------------------------- 1 | class InventoryItem 2 | attr_accessor :count, :owner 3 | attr_reader :name, :effects 4 | 5 | def initialize(name, count, owner, effects) 6 | @name = name 7 | @count = count 8 | @owner = owner # owner can only be Player or Room 9 | @effects = effects 10 | end 11 | end -------------------------------------------------------------------------------- /lib/location.rb: -------------------------------------------------------------------------------- 1 | # A Location is a simple object that has an `x` and 2 | # `y` coordinate. Both are numbers. 3 | class Location < Struct.new(:x, :y) 4 | end -------------------------------------------------------------------------------- /lib/main.rb: -------------------------------------------------------------------------------- 1 | require_relative 'adventure' 2 | 3 | Game.play 4 | -------------------------------------------------------------------------------- /lib/map.rb: -------------------------------------------------------------------------------- 1 | # A Map is an object that has a `title` as a string, 2 | # a Hash of rooms (where the keys are Locations), 3 | # a max_x, which is the east-most room, and max_y, 4 | # which is the north-most room. 5 | class Map < Struct.new(:title, :rooms, :max_x, :max_y) 6 | # A Map is initialized with a title and room list, 7 | # which is an array of rooms, and the room list is 8 | # converted into the expected Hash and max_x/max_y 9 | # values. 10 | def initialize(title, room_list, player) 11 | @player = player 12 | # Construct an empty Hash and initialize max_x and max_y 13 | rooms = {} 14 | max_x = 0 15 | max_y = 0 16 | 17 | # Iterate over the room_list 18 | room_list.each do |room| 19 | # extract x and y from the room's location 20 | x = room.location.x 21 | y = room.location.y 22 | 23 | #update max_x and max_y if necessary 24 | max_x = [max_x, x].max 25 | max_y = [max_y, y].max 26 | 27 | #insert the room into the rooms Hash with its location 28 | rooms[room.location] = room 29 | end 30 | 31 | # construct a new Map with title, rooms, max_x and max_y 32 | super title, rooms, max_x, max_y 33 | end 34 | 35 | def display_map 36 | puts title 37 | title.length.times do 38 | print "=" 39 | end 40 | puts "\n" 41 | rooms.each_with_index do |room, index| 42 | if room[0] == @player.location 43 | puts "#{index}. #{room[1].name}. You are here." 44 | else 45 | puts "#{index}. #{room[1].name}" 46 | end 47 | end 48 | end 49 | 50 | def [](location) 51 | self.rooms[location] 52 | end 53 | 54 | # valid_directions takes a Location contained in this 55 | # Map and answers which directions a player can go. 56 | # Specifically, a player is not allowed to move off 57 | # the edge of the map. 58 | def valid_directions(location) 59 | { 60 | # a player can go north if they are not already 61 | # at the max_y 62 | north: location.y < self.max_y, 63 | # a player can go south if they are not at the 64 | # bottom edge 65 | south: location.y > 0, 66 | # a player can go east if they are not already 67 | # at the max_x 68 | east: location.x < self.max_x, 69 | # a player can go west if they are not at the 70 | # left edge 71 | west: location.x > 0 72 | } 73 | end 74 | end -------------------------------------------------------------------------------- /lib/npc.rb: -------------------------------------------------------------------------------- 1 | require 'pry' 2 | # NPC = Non Player Character 3 | class NPC 4 | attr_reader :name, :dialogue 5 | attr_accessor :inventory 6 | 7 | def initialize(name, inventory, dialogue) 8 | @name = name 9 | @inventory = inventory 10 | @dialogue = dialogue 11 | end 12 | 13 | def has_item(item) 14 | @inventory.map(&:name).include?(item) 15 | end 16 | 17 | def remove_from_inventory(item) 18 | @inventory.delete_if { |i| item.name == i.name } 19 | puts "#{item.name} has been removed from #{self.name}'s inventory." 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/player.rb: -------------------------------------------------------------------------------- 1 | class Player 2 | attr_accessor :name, :inventory, :location 3 | 4 | def initialize 5 | @location = Location.new(0,0) 6 | puts "What is your name?" 7 | @name = gets.chomp 8 | @inventory = [] 9 | end 10 | 11 | def has_item(item_name) 12 | self.inventory.map(&:name).include?(item_name) 13 | end 14 | 15 | def add_to_inventory(item) 16 | self.inventory.push(item) 17 | puts "#{item.name.capitalize} has been added to your inventory." 18 | end 19 | 20 | def remove_from_inventory(item_name) 21 | if @inventory.delete_if { |i| i.name == item_name } 22 | puts "#{item_name} has been removed from your inventory" 23 | else 24 | puts "Sorry, you don't have #{item_name}" 25 | end 26 | end 27 | 28 | def print_inventory 29 | if @inventory.length > 0 30 | puts "\n" 31 | puts "INVENTORY" 32 | puts "=========" 33 | @inventory.each do |item| 34 | puts "* #{item.name}: #{item.count}" 35 | end 36 | puts "\n" 37 | else 38 | puts "Sorry, your inventory is empty!" 39 | puts "Why not look around and see if you can find something to pick up!" 40 | end 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /lib/room.rb: -------------------------------------------------------------------------------- 1 | # A Room is a simple object that has a `location`, 2 | # (which is a Location object) and a description 3 | # (which is a string). 4 | class Room < Struct.new(:location, :name, :description, :items, :npc) 5 | # `items` is the Room's inventory, which is empty 6 | # unless populated on Room initialization 7 | @items = [] 8 | # The Room is constructed with an x and y, which 9 | # are passed to Location.new, so they should be 10 | # numbers. 11 | def self.new(x, y, name, description, items, npc) 12 | super Location.new(x, y), name, description, items, npc 13 | end 14 | 15 | # to_s prints out the x and y coordinates and 16 | # the name. 17 | def to_s 18 | "#{location.x}, #{location.y}, #{name}" 19 | end 20 | 21 | def has_items 22 | self.items.any? 23 | end 24 | 25 | # check to see if this Room has this item 26 | def has_item(item) 27 | item_names = [] 28 | self.items.compact.each { |item| item_names << item.name } 29 | item_names.include? item 30 | end 31 | 32 | def remove_one(item) 33 | room_item = self.items.find { |i| i == item } 34 | # can't remove an item that's not there 35 | return if !room_item 36 | index = self.items.index(room_item) 37 | self.items.delete_at(index) 38 | end 39 | 40 | # puts-able list of items in Room 41 | def item_list 42 | if self.items.compact.count == 1 43 | self.items.compact.first.name 44 | else 45 | item_names = [] 46 | self.items.compact.each { |item| item_names << item.name } 47 | item_names.join(", ") 48 | end 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /spec/inventory_item_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe InventoryItem do 4 | let(:subject) { described_class.new(name,count,owner,effects) } 5 | 6 | let(:name) { 'Candlestick' } 7 | let(:count) { 2 } 8 | let(:owner) { double('Player') } 9 | let(:effects) { 'Lighting the candlestick illuminates the room and possibly a dangerous weapon in the right hands.' } 10 | 11 | 12 | it 'has a name' do 13 | expect(subject.name).to eq name 14 | end 15 | 16 | it 'has a count' do 17 | expect(subject.count).to eq count 18 | end 19 | 20 | it 'can have the count increased' do 21 | subject.count += 1 22 | expect(subject.count).to eq(count + 1) 23 | end 24 | 25 | it 'can have the count decreased' do 26 | subject.count -= 1 27 | expect(subject.count).to eq(count - 1) 28 | end 29 | 30 | it 'has an owner' do 31 | expect(subject.owner).to eq owner 32 | end 33 | 34 | it 'has effects' do 35 | expect(subject.effects).to eq effects 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /spec/location_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Location do 4 | let(:subject) { described_class.new(1,2) } 5 | 6 | it 'has an x' do 7 | expect(subject.x).to eq 1 8 | end 9 | 10 | it 'has a y' do 11 | expect(subject.y).to eq 2 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /spec/map_spec.rb: -------------------------------------------------------------------------------- 1 | # describe Map do 2 | # let(:subject) { described_class.new(title,rooms,max_x,max_y) } 3 | # 4 | # let(:title) { 'Test Map' } 5 | # let(:rooms) { [ ] } 6 | # let(:max_x) { 4 } 7 | # let(:max_y) { 8 } 8 | # 9 | # it 'has a title' do 10 | # expect(subject.title).to title 11 | # end 12 | # end 13 | -------------------------------------------------------------------------------- /spec/room_spec.rb: -------------------------------------------------------------------------------- 1 | # describe Room do 2 | # let(:subject) { described_class.new(x,y,name,description,items,npc) } 3 | # end 4 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'adventure' 2 | 3 | # This file was generated by the `rspec --init` command. Conventionally, all 4 | # specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`. 5 | # The generated `.rspec` file contains `--require spec_helper` which will cause 6 | # this file to always be loaded, without a need to explicitly require it in any 7 | # files. 8 | # 9 | # Given that it is always loaded, you are encouraged to keep this file as 10 | # light-weight as possible. Requiring heavyweight dependencies from this file 11 | # will add to the boot time of your test suite on EVERY test run, even for an 12 | # individual file that may not need all of that loaded. Instead, consider making 13 | # a separate helper file that requires the additional dependencies and performs 14 | # the additional setup, and require it from the spec files that actually need 15 | # it. 16 | # 17 | # The `.rspec` file also contains a few flags that are not defaults but that 18 | # users commonly want. 19 | # 20 | # See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration 21 | RSpec.configure do |config| 22 | # rspec-expectations config goes here. You can use an alternate 23 | # assertion/expectation library such as wrong or the stdlib/minitest 24 | # assertions if you prefer. 25 | config.expect_with :rspec do |expectations| 26 | # This option will default to `true` in RSpec 4. It makes the `description` 27 | # and `failure_message` of custom matchers include text for helper methods 28 | # defined using `chain`, e.g.: 29 | # be_bigger_than(2).and_smaller_than(4).description 30 | # # => "be bigger than 2 and smaller than 4" 31 | # ...rather than: 32 | # # => "be bigger than 2" 33 | expectations.include_chain_clauses_in_custom_matcher_descriptions = true 34 | end 35 | 36 | # rspec-mocks config goes here. You can use an alternate test double 37 | # library (such as bogus or mocha) by changing the `mock_with` option here. 38 | config.mock_with :rspec do |mocks| 39 | # Prevents you from mocking or stubbing a method that does not exist on 40 | # a real object. This is generally recommended, and will default to 41 | # `true` in RSpec 4. 42 | mocks.verify_partial_doubles = true 43 | end 44 | 45 | # The settings below are suggested to provide a good initial experience 46 | # with RSpec, but feel free to customize to your heart's content. 47 | =begin 48 | # These two settings work together to allow you to limit a spec run 49 | # to individual examples or groups you care about by tagging them with 50 | # `:focus` metadata. When nothing is tagged with `:focus`, all examples 51 | # get run. 52 | config.filter_run :focus 53 | config.run_all_when_everything_filtered = true 54 | 55 | # Allows RSpec to persist some state between runs in order to support 56 | # the `--only-failures` and `--next-failure` CLI options. We recommend 57 | # you configure your source control system to ignore this file. 58 | config.example_status_persistence_file_path = "spec/examples.txt" 59 | 60 | # Limits the available syntax to the non-monkey patched syntax that is 61 | # recommended. For more details, see: 62 | # - http://rspec.info/blog/2012/06/rspecs-new-expectation-syntax/ 63 | # - http://www.teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/ 64 | # - http://rspec.info/blog/2014/05/notable-changes-in-rspec-3/#zero-monkey-patching-mode 65 | config.disable_monkey_patching! 66 | 67 | # This setting enables warnings. It's recommended, but in some cases may 68 | # be too noisy due to issues in dependencies. 69 | config.warnings = true 70 | 71 | # Many RSpec users commonly either run the entire suite or an individual 72 | # file, and it's useful to allow more verbose output when running an 73 | # individual spec file. 74 | if config.files_to_run.one? 75 | # Use the documentation formatter for detailed output, 76 | # unless a formatter has already been configured 77 | # (e.g. via a command-line flag). 78 | config.default_formatter = 'doc' 79 | end 80 | 81 | # Print the 10 slowest examples and example groups at the 82 | # end of the spec run, to help surface which specs are running 83 | # particularly slow. 84 | config.profile_examples = 10 85 | 86 | # Run specs in random order to surface order dependencies. If you find an 87 | # order dependency and want to debug it, you can fix the order by providing 88 | # the seed, which is printed after each run. 89 | # --seed 1234 90 | config.order = :random 91 | 92 | # Seed global randomization in this process using the `--seed` CLI option. 93 | # Setting this allows you to use `--seed` to deterministically reproduce 94 | # test failures related to randomization by passing the same `--seed` value 95 | # as the one that triggered the failure. 96 | Kernel.srand config.seed 97 | =end 98 | end 99 | -------------------------------------------------------------------------------- /src/game.rs: -------------------------------------------------------------------------------- 1 | extern crate regex; 2 | 3 | use player::Player; 4 | use map::Map; 5 | use room::Room; 6 | use inventory_item::InventoryItem; 7 | use std::io; 8 | 9 | // where most player console interactions and game loop will be defined 10 | #[derive(Debug, Eq, PartialEq)] 11 | pub struct Game { 12 | player: Player, 13 | map: Map, 14 | pub playing: bool, 15 | } 16 | 17 | impl Game { 18 | fn start(player: Player, map: Map, playing: bool) { 19 | let mut game = Game::new(player, map, playing); 20 | game.play(); 21 | } 22 | 23 | pub fn new(player: Player, map: Map, playing: bool) -> Game { 24 | println!("Hi {}Welcome to {}", player.name, map.title); 25 | 26 | Game { 27 | player: player, 28 | map: map, 29 | playing: playing, 30 | } 31 | } 32 | 33 | pub fn play(&mut self) { 34 | println!("What would you like to do? (enter 'help' to see a list of commands)"); 35 | self.parse_choice(); 36 | } 37 | 38 | fn current_room(&self) -> &Room { 39 | self.map 40 | .rooms 41 | .get(&self.player.location) 42 | .expect("BUG: The player's location must exist in the map") 43 | } 44 | 45 | fn current_room_mut(&mut self) -> &mut Room { 46 | self.map 47 | .rooms 48 | .get_mut(&self.player.location) 49 | .expect("BUG: The player's location must exist in the map") 50 | } 51 | 52 | fn player(&self) -> &Player { 53 | &self.player 54 | } 55 | 56 | fn player_mut(&mut self) -> &mut Player { 57 | &mut self.player 58 | } 59 | 60 | fn parse_choice(&mut self) { 61 | let mut user_input = String::new(); 62 | 63 | io::stdin() 64 | .read_line(&mut user_input) 65 | .expect("Could not read line"); 66 | 67 | let user_input = user_input.trim(); 68 | 69 | if user_input == "look around" { 70 | self.look_around(); 71 | } else if user_input == "talk" { 72 | self.talk(); 73 | } else if user_input == "display map" { 74 | self.map.display_map(); 75 | } else if user_input == "print inventory" { 76 | self.player.print_inventory(); 77 | } else if user_input == "exit" { 78 | println!("Thanks for playing!"); 79 | self.playing = false; 80 | } else if user_input == "help" { 81 | println!("exit: exit the game"); 82 | println!("move north, south, east, west: move in this direction"); 83 | println!("look around: see a description of the current room"); 84 | println!("pick up _item_: add the item to your inventory"); 85 | println!("take _item_: take an item from an NPC"); 86 | println!("use _item_: use an item in your inventory"); 87 | println!("talk: talk to an NPC"); 88 | println!("display map: look at map"); 89 | println!("print inventory: show current player inventory"); 90 | } else if let Some(captures) = regex("(?i)^pick up (?P.*)").captures(user_input) { 91 | self.pick_up(captures.name("thing").expect("unexpected optional capture").as_str()); 92 | } else if let Some(captures) = regex("(?i)^take (?P.*)").captures(user_input) { 93 | self.take(captures.name("thing").expect("unexpected optional capture").as_str()); 94 | } else if let Some(captures) = regex("(?i)^use (?P.*)").captures(user_input) { 95 | self.use_item(captures.name("thing").expect("unexpected optional capture").as_str()); 96 | } else if let Some(captures) = regex("(?i)^move (?P.*)").captures(user_input) { 97 | self.change_location(captures.name("direction") 98 | .expect("unexpected optional capture") 99 | .as_str()); 100 | } else { 101 | println!("No such command: {:?}. Sorry!", user_input); 102 | } 103 | } 104 | 105 | // MOVES // 106 | 107 | fn string_to_inventory_item(&mut self, item: &str) -> Option { 108 | let items = self.current_room_mut().items_mut(); 109 | 110 | items.iter() 111 | .position(|thing| item == thing.name) 112 | .map(|i| items.remove(i)) 113 | } 114 | 115 | fn string_to_npc_item(&mut self, item_name: &str) -> Option { 116 | let npc_inventory = self.current_room_mut().npc_mut().inventory_mut(); 117 | 118 | npc_inventory.iter() 119 | .position(|thing| item_name == thing.name) 120 | .map(|i| npc_inventory.remove(i)) 121 | } 122 | 123 | fn string_to_player_item(&mut self, item_name: &str) -> Option { 124 | let player_inventory = self.player_mut().inventory_mut(); 125 | 126 | player_inventory.iter() 127 | .position(|thing| item_name == thing.name) 128 | .map(|i| player_inventory.remove(i)) 129 | } 130 | 131 | fn pick_up(&mut self, item_name: &str) { 132 | match self.string_to_inventory_item(item_name) { 133 | Some(item) => { 134 | self.player.add_to_inventory(item); 135 | } 136 | None => println!("Sorry, {} wasn't found in the current room", item_name), 137 | }; 138 | } 139 | 140 | fn take(&mut self, item_name: &str) { 141 | match self.string_to_npc_item(item_name) { 142 | Some(item) => { 143 | self.player.add_to_inventory(item); 144 | } 145 | None => { 146 | println!("Sorry, {} doesn't have {}.", 147 | self.current_room().npc().name, 148 | item_name) 149 | } 150 | } 151 | } 152 | 153 | fn use_item(&mut self, item_name: &str) { 154 | match self.string_to_player_item(item_name) { 155 | Some(item) => println!("{}", item.effects), 156 | None => println!("Sorry, you don't have {} in your inventory.", item_name), 157 | } 158 | } 159 | 160 | fn change_location(&mut self, direction: &str) { 161 | let valid_directions = self.map.valid_directions(&self.player().location()); 162 | 163 | if direction == "north" { 164 | if valid_directions.north { 165 | self.player_mut().location_mut().y += 1; 166 | println!("You have moved north."); 167 | } else { 168 | println!("You can not go north. Try a different direction."); 169 | } 170 | } else if direction == "south" { 171 | if valid_directions.south { 172 | self.player_mut().location_mut().y -= 1; 173 | println!("You have moved south."); 174 | } else { 175 | println!("You can not go south. Try a different direction."); 176 | } 177 | } else if direction == "west" { 178 | if valid_directions.west { 179 | println!("You have moved west."); 180 | self.player_mut().location_mut().x -= 1; 181 | } else { 182 | println!("You can not go west. Try a different direction."); 183 | } 184 | } else if direction == "east" { 185 | if valid_directions.east { 186 | println!("You have moved east."); 187 | self.player_mut().location_mut().x += 1; 188 | } else { 189 | println!("You can not go north. Try a different direction."); 190 | } 191 | } else { 192 | println!("That is not a valid direction. Try north, south, east, or west."); 193 | } 194 | } 195 | 196 | fn look_around(&self) { 197 | // display the current room's description 198 | println!("{}", &self.current_room().description); 199 | // if the room has any items, display information about them 200 | if !&self.current_room().items.is_empty() { 201 | println!("This room contains: {:?}", &self.current_room().items); 202 | } 203 | // display information about the room's NPC 204 | println!("{} is here too!", &self.current_room().npc.name); 205 | 206 | if !&self.current_room().npc().inventory.is_empty() { 207 | println!("{} has {:?}.", 208 | &self.current_room().npc.name, 209 | &self.current_room().npc.inventory) 210 | } 211 | } 212 | 213 | fn talk(&self) { 214 | println!("{}", &self.current_room().npc.dialogue); 215 | } 216 | } 217 | 218 | fn regex(s: &str) -> regex::Regex { 219 | regex::Regex::new(s).unwrap() 220 | } 221 | -------------------------------------------------------------------------------- /src/inventory_item.rs: -------------------------------------------------------------------------------- 1 | #[derive(Debug, Eq, PartialEq)] 2 | pub struct InventoryItem { 3 | count: u64, 4 | pub name: String, 5 | pub effects: String, 6 | } 7 | 8 | impl InventoryItem { 9 | pub fn new(count: u64, name: String, effects: String) -> InventoryItem { 10 | InventoryItem { 11 | count: count, 12 | name: name, 13 | effects: effects, 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/location.rs: -------------------------------------------------------------------------------- 1 | // We want to be able to debug Location, `==` it (which requires 2 | // Eq and PartialEq), use it as a HashMap key, and clone it. 3 | #[derive(Debug, Eq, PartialEq, Hash, Clone)] 4 | pub struct Location { 5 | pub x: u64, // it has an x and y, both of which are unsigned numbers 6 | pub y: u64, // that means they cannot be negative (and we'll get an 7 | } // error if we try to underflow) 8 | 9 | // The implementation is separate from the struct definition, which 10 | // just describes the data structure. You can have as many `impl` 11 | // blocks as you want, and they get merged together. 12 | impl Location { 13 | // This provides Location::new(), which takes two `u64`s and 14 | // produces a `Location` object. 15 | pub fn new(x: u64, y: u64) -> Location { 16 | // Note that the lack of a `self` parameter is what makes 17 | // this a "class method", which means it is invoked as 18 | // Location::new(x, y). 19 | Location { x: x, y: y } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | // #![feature(question_mark)] 3 | 4 | pub mod player; 5 | pub mod inventory_item; 6 | pub mod location; 7 | pub mod room; 8 | pub mod npc; 9 | pub mod game; 10 | pub mod map; 11 | 12 | pub type StringLiteral = &'static str; 13 | 14 | // A helper function for easily contructing a room to pass to Map::new. 15 | // Used below in tests. 16 | pub fn room(x: u64, 17 | y: u64, 18 | name: StringLiteral, 19 | desc: StringLiteral, 20 | items: Vec, 21 | npc: npc::NPC) 22 | -> room::Room { 23 | // In Rust, string literals are "slices", which means they are 24 | // shared, but we want an owned String. We can use to_string() 25 | // to copy the StringLiteral into something we can own. 26 | room::Room::new(x, y, name.to_string(), desc.to_string(), items, npc) 27 | } 28 | 29 | fn main() { 30 | // vec![] is the Array literal syntax in Rust. 31 | let rooms = 32 | vec![room(0, 33 | 2, 34 | "top left", 35 | "this is room one", 36 | vec![inventory_item::InventoryItem::new(1, 37 | "cool potion".to_string(), 38 | "this potion has turned you into a \ 39 | C00L d00d!" 40 | .to_string())], 41 | npc::NPC::new("George".to_string(), vec![], "hi I'm George".to_string())), 42 | room(1, 43 | 2, 44 | "top center", 45 | "this is room two", 46 | vec![inventory_item::InventoryItem::new(1, 47 | "dumb potion".to_string(), 48 | "this potion has turned you into a \ 49 | dumbb d00d!" 50 | .to_string())], 51 | npc::NPC::new("Mike".to_string(), vec![], "hi I'm Mike".to_string())), 52 | room(2, 53 | 2, 54 | "top right", 55 | "this is room three", 56 | vec![inventory_item::InventoryItem::new(1, 57 | "stinky potion".to_string(), 58 | "this potion has turned you into a \ 59 | stinky d00d!" 60 | .to_string())], 61 | npc::NPC::new("Helen".to_string(), vec![], "hi I'm Helen".to_string())), 62 | room(0, 63 | 1, 64 | "middle left", 65 | "this is room four", 66 | vec![inventory_item::InventoryItem::new(1, 67 | "charming potion".to_string(), 68 | "this potion has turned you into a \ 69 | charming d00d!" 70 | .to_string())], 71 | npc::NPC::new("Linda".to_string(), vec![], "hi I'm Linda".to_string())), 72 | room(1, 73 | 1, 74 | "middle center", 75 | "this is room five", 76 | vec![inventory_item::InventoryItem::new(1, 77 | "dog potion".to_string(), 78 | "this potion has turned you into a \ 79 | C00L d0g!" 80 | .to_string())], 81 | npc::NPC::new("Prudence".to_string(), 82 | vec![inventory_item::InventoryItem::new(1, 83 | "potato chip potion" 84 | .to_string(), 85 | "this potion has \ 86 | given you potato \ 87 | chips. You can't \ 88 | eat them, but \ 89 | they're there. \ 90 | LOOKING AT YOU." 91 | .to_string())], 92 | "hi I'm Prudence".to_string())), 93 | room(2, 94 | 1, 95 | "middle right", 96 | "this is room six", 97 | vec![inventory_item::InventoryItem::new(1, 98 | "barfing potion".to_string(), 99 | "this potion has turned you into a \ 100 | barfing d00d!" 101 | .to_string())], 102 | npc::NPC::new("Fred".to_string(), vec![], "hi I'm Fred".to_string())), 103 | room(0, 104 | 0, 105 | "bottom left", 106 | "this is room seven", 107 | vec![inventory_item::InventoryItem::new(1, 108 | "hungry potion".to_string(), 109 | "this potion has turned you into a \ 110 | hungry d00d!" 111 | .to_string())], 112 | npc::NPC::new("Crocodile Man".to_string(), 113 | vec![], 114 | "hi I'm Crocodile Man".to_string())), 115 | room(1, 116 | 0, 117 | "bottom center", 118 | "this is room eight", 119 | vec![inventory_item::InventoryItem::new(1, 120 | "cute potion".to_string(), 121 | "this potion has turned you into a \ 122 | cute d00d!" 123 | .to_string())], 124 | npc::NPC::new("Crocodile Woman".to_string(), 125 | vec![], 126 | "hi I'm Crocodile Woman".to_string())), 127 | room(2, 128 | 0, 129 | "bottom right", 130 | "this is room nine", 131 | vec![inventory_item::InventoryItem::new(1, 132 | "tall potion".to_string(), 133 | "this potion has turned you into a \ 134 | tall d00d!" 135 | .to_string())], 136 | npc::NPC::new("Cool Unicorn".to_string(), 137 | vec![], 138 | "hi I'm Cool Unicorn".to_string()))]; 139 | 140 | let player = player::Player::new(vec![], 1, 1); 141 | let map = map::Map::new("Great Rust Adventure", rooms); 142 | let mut game = game::Game::new(player, map, true); 143 | 144 | while game.playing { 145 | game.play(); 146 | } 147 | } 148 | 149 | // cfg(test) means only include this code when compiling for test mode 150 | #[cfg(test)] 151 | mod tests { 152 | // this is a nested module 153 | use super::*; // include all the public items from the parent module 154 | 155 | // helper function for constructing a 3x3 list of rooms for testing 156 | fn rooms() -> Vec { 157 | vec![room(0, 2, "top left"), 158 | room(1, 2, "top center"), 159 | room(2, 2, "top right"), 160 | room(0, 1, "middle left"), 161 | room(1, 1, "middle center"), 162 | room(2, 1, "middle right"), 163 | room(0, 0, "bottom left"), 164 | room(1, 0, "bottom center"), 165 | room(2, 0, "bottom right")] 166 | } 167 | 168 | // helper function for constructing a room whose description is 169 | // just its `x, y` coordinates. 170 | fn simple_room(x: u64, y: u64) -> Room { 171 | Room::new(x, y, format!("{}, {}", x, y)) 172 | } 173 | 174 | // helper function for constructing a 4x4 list of rooms. 175 | fn big_rooms() -> Vec { 176 | vec![simple_room(0, 3), 177 | simple_room(1, 3), 178 | simple_room(2, 3), 179 | simple_room(3, 3), 180 | simple_room(0, 2), 181 | simple_room(1, 2), 182 | simple_room(2, 2), 183 | simple_room(3, 2), 184 | simple_room(0, 1), 185 | simple_room(1, 1), 186 | simple_room(2, 1), 187 | simple_room(3, 1), 188 | simple_room(0, 0), 189 | simple_room(1, 0), 190 | simple_room(2, 0), 191 | simple_room(3, 0)] 192 | } 193 | 194 | fn map() -> Map { 195 | Map::new("Liz's Great Adventure", rooms()) 196 | } 197 | 198 | fn big_map() -> Map { 199 | Map::new("Liz's Great Adventure", big_rooms()) 200 | } 201 | 202 | #[test] // tell the test runner that this is a test function 203 | fn valid_directions_for_bottom_left() { 204 | let map = map(); 205 | let location = Location { x: 0, y: 0 }; 206 | let valid_directions = map.valid_directions(&location); 207 | 208 | // If the two sides are not equal, panic and fail the 209 | // test. This assumes both sides are Eq and PartialEq. 210 | assert_eq!(valid_directions, 211 | ValidDirection { 212 | north: true, 213 | south: false, 214 | east: true, 215 | west: false, 216 | }); 217 | } 218 | 219 | #[test] 220 | fn looks_up_rooms_by_coordinates() { 221 | let map = map(); 222 | // Indexing a HashMap does not take ownership of the value 223 | // passed to `[]`, so we lend the Location we create here 224 | // by using `&`. 225 | assert_eq!(map.rooms[&Location::new(0, 2)], rooms()[0]); 226 | } 227 | 228 | #[test] 229 | fn valid_directions_for_top_right() { 230 | let map = map(); 231 | let location = Location::new(2, 2); 232 | let valid_directions = map.valid_directions(&location); 233 | 234 | assert_eq!(valid_directions, 235 | ValidDirection { 236 | north: false, 237 | south: true, 238 | east: false, 239 | west: true, 240 | }); 241 | } 242 | 243 | #[test] 244 | fn valid_directions_for_top_right_in_big_map() { 245 | let map = big_map(); 246 | let location = Location::new(3, 3); 247 | let valid_directions = map.valid_directions(&location); 248 | 249 | assert_eq!(valid_directions, 250 | ValidDirection { 251 | north: false, 252 | south: true, 253 | east: false, 254 | west: true, 255 | }); 256 | } 257 | } 258 | 259 | 260 | 261 | trait Chomp { 262 | fn chomp(&self) -> &str; 263 | } 264 | 265 | impl Chomp for str { 266 | fn chomp(&self) -> &str { 267 | if self.ends_with('\n') { 268 | &self[0..self.len() - 1] 269 | } else { 270 | self 271 | } 272 | } 273 | } 274 | 275 | impl Chomp for String { 276 | fn chomp(&self) -> &str { 277 | self[..].chomp() 278 | } 279 | } 280 | -------------------------------------------------------------------------------- /src/main.rs.bk: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | #![feature(question_mark)] 3 | 4 | #[derive(Debug)] 5 | struct Location { 6 | x: u64, 7 | y: u64, 8 | } 9 | 10 | 11 | #[derive(Debug)] 12 | pub struct Room { 13 | location: Location, 14 | description: String, 15 | } 16 | 17 | type StringLiteral = &'static str; 18 | 19 | impl Room { 20 | fn new(x: u64, y: u64, description: StringLiteral) -> Room { 21 | Room { 22 | location: Location { x: x, y: y }, 23 | description: description.to_string(), 24 | } 25 | } 26 | } 27 | 28 | #[derive(Debug)] 29 | struct Map { 30 | title: String, 31 | rooms: Vec>, 32 | } 33 | 34 | use std::fmt; 35 | 36 | impl fmt::Display for Map { 37 | fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { 38 | writeln!(f, "{}", self.title)?; 39 | 40 | for _ in self.title.chars() { 41 | write!(f, "=")?; 42 | } 43 | 44 | write!(f, "\n\n")?; 45 | 46 | for rooms in &self.rooms { 47 | for room in rooms { 48 | write!(f, "{:20} ", room.description)?; 49 | } 50 | 51 | write!(f, "\n")?; 52 | 53 | for room in rooms { 54 | write!(f, "{:20} ", format!("{}, {}", room.location.x, room.location.y))?; 55 | } 56 | 57 | write!(f, "\n")?; 58 | } 59 | 60 | Ok(()) 61 | } 62 | } 63 | 64 | fn write(f: &mut fmt::Formatter) -> fmt::Result { 65 | write!(f, "hello")?; 66 | write!(f, "world") 67 | } 68 | 69 | impl Map { 70 | fn new(title: StringLiteral, rooms: Vec>) -> Map { 71 | Map { 72 | title: title.to_string(), 73 | rooms: rooms, 74 | } 75 | } 76 | } 77 | 78 | fn main() { 79 | let rooms = vec![ 80 | vec![Room::new(0, 0, "top left"), Room::new(0, 1, "top center"), Room::new(0, 2, "top right")], 81 | vec![Room::new(1, 0, "middle left"), Room::new(1, 1, "middle center"), Room::new(1, 2, "middle right")], 82 | vec![Room::new(2, 0, "bottom left"), Room::new(2, 1, "bottom center"), Room::new(2, 2, "bottom right")], 83 | ]; 84 | 85 | let map = Map::new("Liz's Great Adventure", rooms); 86 | 87 | println!("{}", map); 88 | } -------------------------------------------------------------------------------- /src/map.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use std::cmp::max; 3 | use std::fmt; 4 | use location::Location; 5 | use room::Room; 6 | 7 | pub type StringLiteral = &'static str; 8 | 9 | // We want to be able to debug Room and `==` it. 10 | #[derive(Debug, Eq, PartialEq)] 11 | pub struct Map { 12 | pub title: String, 13 | pub rooms: HashMap, 14 | max_x: u64, // east-most room 15 | max_y: u64, // north-most room 16 | } 17 | 18 | #[derive(Debug, Eq, PartialEq)] 19 | pub struct ValidDirection { 20 | pub north: bool, 21 | pub south: bool, 22 | pub east: bool, 23 | pub west: bool, 24 | } 25 | 26 | impl Map { 27 | pub fn new(title: StringLiteral, room_list: Vec) -> Map { 28 | // Make a new mutable HashMap 29 | let mut rooms = HashMap::new(); 30 | 31 | // Make new mutable max_x and max_y values, initialized to 0 32 | let mut max_x = 0; 33 | let mut max_y = 0; 34 | 35 | // Iterate over the room_list 36 | for room in room_list { 37 | // extract x and y from the room's location 38 | let x = room.location.x; 39 | let y = room.location.y; 40 | 41 | // update max_x and max_y if necessary 42 | max_x = max(max_x, x); 43 | max_y = max(max_y, y); 44 | 45 | // insert the room into the rooms Hash with its location 46 | // as a key. clone the location because removing the 47 | // location from the room to use as a key prevents use 48 | // from using the room as a value. 49 | rooms.insert(room.location.clone(), room); 50 | } 51 | 52 | // construct a new Map 53 | Map { 54 | title: title.to_string(), 55 | rooms: rooms, 56 | max_x: max_x, 57 | max_y: max_y, 58 | } 59 | } 60 | 61 | // valid_directions takes a Location contained in this 62 | // Map and answers which directions a player can go. 63 | // Specifically, a player is not allowed to move off 64 | // the edge of the map. 65 | pub fn valid_directions(&self, l: &Location) -> ValidDirection { 66 | ValidDirection { 67 | // identical to the Ruby code 68 | north: l.y < self.max_y, 69 | south: l.y > 0, 70 | east: l.x < self.max_x, 71 | west: l.x > 0, 72 | } 73 | } 74 | 75 | pub fn display_map(&self) { 76 | println!("Possible Destinations for {}", self.title); 77 | println!("{:=<1$}", "", self.title.len() + 26); 78 | for room_info in &self.rooms { 79 | println!("{}", room_info.1.name); 80 | } 81 | } 82 | } 83 | 84 | impl fmt::Display for Map { 85 | fn fmt(&self, _f: &mut fmt::Formatter) -> Result<(), fmt::Error> { 86 | // writeln!(_f, "{}", self.title)?; 87 | 88 | // for _ in self.title.chars() { 89 | // write!(_f, "=")?; 90 | // } 91 | 92 | // write!(_f, "\n\n")?; 93 | 94 | // for row in &self.rooms { 95 | // for room in row { 96 | // write!(_f, "{:20} ", room.description)?; 97 | // } 98 | 99 | // write!(_f, "\n")?; 100 | 101 | // for room in row { 102 | // write!(_f, "{:20} ", 103 | // format!("{}, {}", room.location.x, room.location.y))?; 104 | // } 105 | 106 | // write!(_f, "\n")?; 107 | // } 108 | 109 | Ok(()) 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/npc.rs: -------------------------------------------------------------------------------- 1 | use inventory_item::InventoryItem; 2 | 3 | // a non-player character 4 | #[derive(Debug, Eq, PartialEq)] 5 | pub struct NPC { 6 | pub name: String, 7 | pub inventory: Vec, 8 | pub dialogue: String, 9 | } 10 | 11 | impl NPC { 12 | pub fn new(name: String, inventory: Vec, dialogue: String) -> NPC { 13 | NPC { 14 | name: name, 15 | inventory: inventory, 16 | dialogue: dialogue, 17 | } 18 | } 19 | 20 | fn inventory(&self) -> &[InventoryItem] { 21 | &self.inventory[..] 22 | } 23 | 24 | pub fn inventory_mut(&mut self) -> &mut Vec { 25 | &mut self.inventory 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/player.rs: -------------------------------------------------------------------------------- 1 | use std::io; 2 | use inventory_item::InventoryItem; 3 | use location::Location; 4 | 5 | #[derive(Debug, Eq, PartialEq)] 6 | pub struct Player { 7 | pub name: String, 8 | inventory: Vec, 9 | pub location: Location, 10 | } 11 | 12 | impl Player { 13 | pub fn new(inventory: Vec, x: u64, y: u64) -> Player { 14 | // get user input an assign the input to `name` 15 | println!("What's your name?"); 16 | 17 | let mut name = String::new(); 18 | 19 | io::stdin() 20 | .read_line(&mut name) 21 | .expect("Could not read line"); 22 | 23 | Player { 24 | name: name, 25 | inventory: inventory, 26 | location: Location { x: x, y: y }, 27 | } 28 | } 29 | 30 | pub fn print_inventory(&self) { 31 | if self.inventory.is_empty() { 32 | println!("Oops! You don't have any items. Why not take a look around?"); 33 | } else { 34 | println!("{:?}", self.inventory); 35 | } 36 | } 37 | 38 | pub fn add_to_inventory(&mut self, item: InventoryItem) { 39 | println!("{} has been added to your inventory!", item.name); 40 | self.inventory.push(item); 41 | } 42 | 43 | pub fn inventory(&self) -> &[InventoryItem] { 44 | &self.inventory[..] 45 | } 46 | 47 | pub fn inventory_mut(&mut self) -> &mut Vec { 48 | &mut self.inventory 49 | } 50 | 51 | pub fn location(&self) -> &Location { 52 | &self.location 53 | } 54 | 55 | pub fn location_mut(&mut self) -> &mut Location { 56 | &mut self.location 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/room.rs: -------------------------------------------------------------------------------- 1 | use location::Location; 2 | use inventory_item::InventoryItem; 3 | use npc::NPC; 4 | 5 | // We want to be able to debug Room and `==` it. 6 | #[derive(Debug, Eq, PartialEq)] 7 | pub struct Room { 8 | pub location: Location, // it has a Location 9 | pub name: String, // it has a name 10 | pub description: String, // it has a description 11 | pub items: Vec, // it has items/inventory (may be empty) 12 | pub npc: NPC, // it may have an NPC / non-player character 13 | } 14 | 15 | impl Room { 16 | // As in Ruby, we make Room::new take an x, y and description, 17 | // and construct a Location using the x and y. 18 | pub fn new(x: u64, 19 | y: u64, 20 | name: String, 21 | description: String, 22 | items: Vec, 23 | npc: NPC) 24 | -> Room { 25 | // returns a Room 26 | Room { 27 | // Construct a Room 28 | location: Location::new(x, y), // construct a Location 29 | name: name, // save the name 30 | description: description, // save the description 31 | items: items, // save the inventory 32 | npc: npc, // save the NPC 33 | } 34 | } 35 | 36 | pub fn items(&self) -> &[InventoryItem] { 37 | &self.items[..] 38 | } 39 | 40 | pub fn items_mut(&mut self) -> &mut Vec { 41 | &mut self.items 42 | } 43 | 44 | pub fn npc(&self) -> &NPC { 45 | &self.npc 46 | } 47 | 48 | pub fn npc_mut(&mut self) -> &mut NPC { 49 | &mut self.npc 50 | } 51 | } 52 | --------------------------------------------------------------------------------