├── .gitignore
├── LICENSE.txt
├── README.md
├── Rakefile
├── TODO
├── bin
└── game_player.rb
├── data
└── epic_adventure
│ ├── items.yml
│ ├── locations.yml
│ └── messages.yml
├── lib
├── avatar.rb
├── bootstrap.rb
├── engine.rb
├── game.rb
├── game_data_loader.rb
├── input_controller.rb
└── room.rb
├── play.sh
└── tests
├── avatar_test.rb
└── room_test.rb
/.gitignore:
--------------------------------------------------------------------------------
1 | .swp
2 | .DS_Store
3 |
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
1 | Text Adventure - a simple text game
2 |
3 | Copyright (C) 2015 Sebastian Wittenkamp
4 |
5 | This program is free software: you can redistribute it and/or modify
6 | it under the terms of the GNU General Public License as published by
7 | the Free Software Foundation, either version 3 of the License, or
8 | (at your option) any later version.
9 |
10 | This program is distributed in the hope that it will be useful,
11 | but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | GNU General Public License for more details.
14 |
15 | You should have received a copy of the GNU General Public License
16 | long with this program. If not, see .
17 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Text Adventures!
2 |
3 | ## About
4 |
5 | This is a text game and engine that I am writing just for fun. And to learn more about the basics of game programming. I loved
6 | playing games like Kings Quest when I was a kid and text adventures harken back to those days for me.
7 |
8 | Text games also remind me of a book I borrowed when I was a kid. It was a thick book full of BASIC code that you would type
9 | in to the computer and then run it. When you ran the code, you had a full game! That was REAL open source because you had
10 | to make the effort to type all 25 or 50 pages of code in, while the editor was hot. Makes you grateful for how far
11 | we've come since then. :)
12 |
13 | I'm going to continue updating the data files for the "Epic Adventure" so that the story gets bigger and bigger. But if
14 | you play the game, happen to like it, and have an idea, please feel free to contribute it.
15 |
16 | This project is all about fun so I'm always open to new ideas.
17 |
18 |
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
1 | require 'rake/testtask'
2 |
3 | Rake::TestTask.new do |t|
4 | t.pattern = "tests/*_test.rb"
5 | end
6 |
7 | task :default => [:test]
8 |
--------------------------------------------------------------------------------
/TODO:
--------------------------------------------------------------------------------
1 | * add an inventory in the game.
2 | * add a real grammar for parsing commands
3 | * state machine for NPC interaction
4 | * level editor
5 |
--------------------------------------------------------------------------------
/bin/game_player.rb:
--------------------------------------------------------------------------------
1 | GAME_ROOT = File.expand_path(File.join(File.dirname(__FILE__), ".."))
2 |
3 | require File.join(GAME_ROOT, 'lib', 'bootstrap')
4 | require File.join(GAME_ROOT, 'lib', 'game')
5 |
6 | def lookup_file_from(path)
7 | File.absolute_path(File.join(GAME_ROOT, path))
8 | end
9 |
10 | # TODO: this could be a little nicer.
11 | location_data_file = lookup_file_from "#{ARGV[0]}"
12 | message_data_file = lookup_file_from "#{ARGV[1]}"
13 |
14 | # main
15 | bootstrap = Bootstrap.new(location_data_file, message_data_file)
16 | game = Game.new(bootstrap)
17 | game.play
--------------------------------------------------------------------------------
/data/epic_adventure/items.yml:
--------------------------------------------------------------------------------
1 | -
2 | handle: crystal
3 | name: Glowing Crystal
4 | description: This crystal glows with an eerie light.
5 | -
6 |
--------------------------------------------------------------------------------
/data/epic_adventure/locations.yml:
--------------------------------------------------------------------------------
1 | -
2 | starting_location: true
3 | handle: lake
4 | desc: A beautiful lake surrounded by trees.
5 | info: A trail runs to the north from here.
6 | rooms:
7 | north: forest
8 | -
9 | handle: forest
10 | desc: A forest full of lush trees.
11 | info: The path splits. To the west lies your home. To the east are the mountains. Back to the south is a lake.
12 | rooms:
13 | west: home
14 | east: mountains
15 | south: lake
16 | -
17 | handle: home
18 | desc: Your home. It is a happy house.
19 | info: Your house is here. It is a quiet place. To the east is the path in the forest. If you go west you will enter your house.
20 | rooms:
21 | east: forest
22 | west: home_main
23 | -
24 | handle: home_main
25 | desc: You are standing in the main room of your house.
26 | info: The inside of your house is simple but clean and quiet. You have spent many happy hours here. The study is in the north room.
27 | rooms:
28 | north: home_study
29 | east: home
30 | -
31 | handle: home_study
32 | desc: You are in your study. It is a clean room with a desk and windows facing outside.
33 | info: Your study is where your work gets done. There is a letter on your desk.
34 | rooms:
35 | south: home_main
36 | -
37 | handle: mountains
38 | desc: These are the mountains. From up here you can see the valley in which you live.
39 | info: Further to the east is the pass that leads to the ocean. Back west is the forest.
40 | rooms:
41 | west: forest
42 | east: pass
43 | -
44 | handle: pass
45 | desc: This is the pass that leads to the ocean. It is rocky terrain with huge stone walls all around.
46 | info: To the east is the ocean. Back west lies the mountains. You can smell the ocean.
47 | rooms:
48 | west: mountains
49 | east: ocean
50 | -
51 | handle: ocean
52 | desc: You are standing by the ocean. It is overcast and cloudy here. Seagulls circle overhead.
53 | info: Further east along the beach you can see some buildings. Back to the west lies the pass.
54 | rooms:
55 | west: pass
56 | east: village
57 | -
58 | handle: village
59 | desc: You are standing among the buildings of an abandoned village.
60 | info: There is a strange glowing crystal lying on the ground.
61 | items: crystal
62 | rooms:
63 | west: ocean
64 |
65 |
--------------------------------------------------------------------------------
/data/epic_adventure/messages.yml:
--------------------------------------------------------------------------------
1 | splash: An Epic Adventure awaits you. Type 'help' for instructions.
2 | help: Type 'look' for more information. You can 'go ' to move around. Type 'exit' or 'quit' to stop the game.
3 |
--------------------------------------------------------------------------------
/lib/avatar.rb:
--------------------------------------------------------------------------------
1 | class Avatar
2 |
3 | def location
4 | @current_room
5 | end
6 |
7 | def initialize(starting_location)
8 | @current_room = starting_location
9 | end
10 |
11 | def can_move?(direction)
12 | @current_room.has_room_to_the?(direction)
13 | end
14 |
15 | def move(direction)
16 | if can_move?(direction)
17 | new_room = @current_room.rooms[direction]
18 | @current_room = new_room
19 | true
20 | else
21 | false
22 | end
23 | end
24 | end
25 |
--------------------------------------------------------------------------------
/lib/bootstrap.rb:
--------------------------------------------------------------------------------
1 | lib_dir = File.expand_path(File.dirname(__FILE__))
2 |
3 | require File.join(lib_dir, 'avatar')
4 | require File.join(lib_dir, 'input_controller')
5 | require File.join(lib_dir, 'game_data_loader')
6 |
7 | class Bootstrap
8 | def initialize(location_data_file, message_data_file)
9 | @locations = loader.load_location_data(location_data_file)
10 | @messages = loader.load_message_data(message_data_file)
11 | end
12 |
13 | def starting_location
14 | @locations.find {|location| location.starting_location?}
15 | end
16 |
17 | def avatar
18 | Avatar.new(starting_location)
19 | end
20 |
21 | def loader
22 | @loader ||= GameDataLoader.new
23 | end
24 |
25 | def controller
26 | ctl = InputController.new
27 | ctl.messages = @messages
28 | ctl.avatar = avatar
29 | ctl.initialize_message
30 | ctl
31 | end
32 |
33 | def splash_message
34 | @messages["splash"]
35 | end
36 | end
--------------------------------------------------------------------------------
/lib/engine.rb:
--------------------------------------------------------------------------------
1 | require 'readline'
2 |
3 | class Engine
4 | attr_accessor :splash_message
5 |
6 | def initialize(controller)
7 | @ctl = controller
8 | end
9 |
10 | def repl
11 | puts @ctl.current_message
12 | puts
13 | input = read_line
14 | @ctl.evaluate(input)
15 | repl
16 | end
17 |
18 | def read_line
19 | Readline.readline('> ', true)
20 | end
21 |
22 | def start
23 | # Print splash message
24 | puts @splash_message
25 | # Start the game loop
26 | repl
27 | end
28 |
29 | end
--------------------------------------------------------------------------------
/lib/game.rb:
--------------------------------------------------------------------------------
1 | require File.join(File.expand_path(File.dirname(__FILE__)), 'engine')
2 |
3 | class Game
4 | def initialize(bootstrap)
5 | @engine = engine.new(bootstrap.controller)
6 | @engine.splash_message = bootstrap.splash_message
7 | end
8 |
9 | def play
10 | @engine.start
11 | end
12 |
13 | def engine
14 | Engine
15 | end
16 | end
--------------------------------------------------------------------------------
/lib/game_data_loader.rb:
--------------------------------------------------------------------------------
1 | require File.join(File.dirname(__FILE__), 'room')
2 | require 'yaml'
3 |
4 | class GameDataLoader
5 |
6 | def load_location_data(file)
7 | data = load_data_from(file)
8 | rooms = load_initial_state(data)
9 | establish_relationships(rooms)
10 | rooms
11 | end
12 |
13 | def load_message_data(file)
14 | load_data_from(file)
15 | end
16 |
17 | def load_initial_state(data)
18 | rooms = []
19 | data.each {|room_data| rooms << build_room(room_data)}
20 | rooms
21 | end
22 |
23 | def establish_relationships(all_rooms)
24 | all_rooms.each do |room|
25 | room.rooms.each do |direction, handle|
26 | room.rooms[direction] = all_rooms.find {|r| r.handle == handle}
27 | end
28 | end
29 | end
30 |
31 | def build_room(room_data)
32 | room = get_room
33 | room.handle = room_data["handle"]
34 | room.description = room_data["desc"]
35 | room.info = room_data["info"]
36 | room.rooms = room_data["rooms"]
37 | room.starting_location = room_data["starting_location"]
38 | room
39 | end
40 |
41 | private
42 | def get_room
43 | Room.new
44 | end
45 |
46 | def load_data_from(file)
47 | YAML.load_file(file)
48 | end
49 |
50 | end
51 |
--------------------------------------------------------------------------------
/lib/input_controller.rb:
--------------------------------------------------------------------------------
1 | class InputController
2 | attr_reader :avatar, :current_message
3 |
4 | def avatar=(avatar)
5 | @avatar = avatar
6 | end
7 |
8 | def messages=(messages)
9 | @messages = messages
10 | end
11 |
12 | def initialize_message
13 | @current_message = avatar.location.description
14 | end
15 |
16 | def evaluate(input)
17 | tokens = input.split
18 | unless valid?(input)
19 | @current_message = "Sorry, that is not a valid command."
20 | return
21 | end
22 |
23 | command = tokens.first
24 |
25 | if command == "go"
26 | direction = tokens.last
27 | if avatar.can_move?(direction)
28 | avatar.move(direction)
29 | @current_message = avatar.location.description
30 | else
31 | @current_message = "Sorry, you cannot go #{direction} from here."
32 | end
33 | end
34 |
35 | if command == "look"
36 | @current_message = avatar.location.info
37 | end
38 |
39 | if command == "help"
40 | @current_message = @messages["help"]
41 | end
42 |
43 | if command == "exit" || command == "quit"
44 | puts "Thank you for playing!"
45 | exit(0)
46 | end
47 | end
48 |
49 | def valid?(input)
50 | tokens = input.split
51 | result = false
52 | if valid_commands.include?(tokens.first) && tokens.size == 1
53 | result = true
54 | elsif tokens.size == 2
55 | result = true
56 | end
57 | result
58 | end
59 |
60 | def valid_commands
61 | @commands ||= %w(look exit quit help)
62 | end
63 |
64 | end
65 |
--------------------------------------------------------------------------------
/lib/room.rb:
--------------------------------------------------------------------------------
1 | class Room
2 | attr_accessor :description, :rooms, :items, :info, :handle
3 | attr_writer :starting_location
4 |
5 | def has_room_to_the?(direction)
6 | rooms.key?(direction)
7 | end
8 |
9 | def starting_location?
10 | @starting_location
11 | end
12 | end
13 |
--------------------------------------------------------------------------------
/play.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | ruby bin/game_player.rb data/epic_adventure/locations.yml data/epic_adventure/messages.yml
3 |
--------------------------------------------------------------------------------
/tests/avatar_test.rb:
--------------------------------------------------------------------------------
1 | require 'minitest/autorun'
2 | require File.join(File.expand_path(File.dirname(__FILE__)), '..', 'lib', 'avatar')
3 |
4 | class AvatarTest < MiniTest::Unit::TestCase
5 |
6 | def setup
7 | @starting_location = MiniTest::Mock.new
8 | @avatar = Avatar.new(@starting_location)
9 | end
10 |
11 | def teardown
12 | @starting_location.verify
13 | end
14 |
15 | def test_avatar_has_location
16 | assert_equal @starting_location, @avatar.location
17 | end
18 |
19 | def test_avatar_knows_if_can_move_in_a_direction
20 | @starting_location.expect(:has_room_to_the?, true, ["north"])
21 | assert @avatar.can_move?("north")
22 |
23 | @starting_location.expect(:has_room_to_the?, false, ["east"])
24 | refute @avatar.can_move?("east")
25 | end
26 |
27 | def test_avatar_can_move
28 | @starting_location.expect(:has_room_to_the?, true, ["east"])
29 | rooms = MiniTest::Mock.new
30 | room = MiniTest::Mock.new
31 | rooms.expect(:[], room, ["east"])
32 | @starting_location.expect(:rooms, rooms)
33 | assert @avatar.move("east")
34 | assert_equal room, @avatar.location
35 | end
36 |
37 | end
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
--------------------------------------------------------------------------------
/tests/room_test.rb:
--------------------------------------------------------------------------------
1 | require 'minitest/autorun'
2 | require File.join(File.expand_path(File.dirname(__FILE__)), '..', 'lib', 'room')
3 |
4 | class RoomTest < MiniTest::Unit::TestCase
5 |
6 | def setup
7 | @room = Room.new
8 | @room.description = "A room."
9 | @room.rooms = []
10 | @room.items = []
11 | @room.info = "Info about the room."
12 | @room.handle = "Test room"
13 | end
14 |
15 | def test_room_has_properties
16 | assert_equal("A room.", @room.description)
17 | assert_equal([], @room.rooms)
18 | assert_equal([], @room.items)
19 | assert_equal("Info about the room.", @room.info)
20 | assert_equal("Test room", @room.handle)
21 | end
22 |
23 | def test_room_knows_if_it_is_starting_location
24 | @room.starting_location = false
25 | refute(@room.starting_location?)
26 | @room.starting_location = true
27 | assert(@room.starting_location?)
28 | end
29 |
30 | def test_rooms_knows_adjacent_rooms
31 | @room.rooms = {
32 | "north" => "room_a",
33 | "south" => "room_b"
34 | }
35 | assert(@room.has_room_to_the?("north"))
36 | assert(@room.has_room_to_the?("south"))
37 | refute(@room.has_room_to_the?("west"))
38 | refute(@room.has_room_to_the?("east"))
39 | end
40 |
41 | end
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
--------------------------------------------------------------------------------