diff --git a/Gemfile b/Gemfile index c317d9a..51c4a5a 100644 --- a/Gemfile +++ b/Gemfile @@ -2,4 +2,4 @@ source 'http://rubygems.org' gem 'rspec' gem 'ruby-debug19' -gem 'rawline' \ No newline at end of file +gem 'rawline' diff --git a/lib/game/engine.rb b/lib/game/engine.rb index e949584..829351a 100644 --- a/lib/game/engine.rb +++ b/lib/game/engine.rb @@ -21,10 +21,22 @@ def map end def take_turn + activate_objects + active_objects = Game::Object.engine_objects(map.name)[Game::Object::ACTIVE] active_objects.each {|obj| obj.active_turn } end + def activate_objects + close_tiles = Game::Player.instance.tile.elements_within(2) + tile = Game::Player.instance.tile + close_tiles.each do |tile| + tile.objects.each do |obj| + obj.activate if obj.respond_to?(:activate) + end + end + end + def ended? !!@ended end diff --git a/lib/game/input.rb b/lib/game/input.rb index 36fd08b..6b9ff88 100644 --- a/lib/game/input.rb +++ b/lib/game/input.rb @@ -25,7 +25,7 @@ def bind_keys bind_key(:' ') { @player.take_action } - bind_key(:'1') { skip_to('level5') } + bind_key(:'1') { skip_to('level9') } bind_key(:'2') { skip_to('level6') } bind_key(:'3') { skip_to('level7_a') } bind_key(:'4') { skip_to('level8') } diff --git a/lib/game/map.rb b/lib/game/map.rb index 9fb2e4d..f52c4cf 100644 --- a/lib/game/map.rb +++ b/lib/game/map.rb @@ -1,9 +1,11 @@ class Game class Map - NORTH = :north - SOUTH = :south - EAST = :east - WEST = :west + DIRECTIONS = [ + NORTH = :north, + SOUTH = :south, + EAST = :east, + WEST = :west + ] def self.load_map(filename) new(filename) diff --git a/lib/game/modules.rb b/lib/game/modules.rb index ec8236e..77a47f2 100644 --- a/lib/game/modules.rb +++ b/lib/game/modules.rb @@ -1,4 +1,5 @@ require 'lib/game/modules/object_management' require 'lib/game/modules/try_helper' +require 'lib/game/modules/array_sum' require 'lib/game/modules/instance_setter' require 'lib/game/modules/impassible' \ No newline at end of file diff --git a/lib/game/modules/array_sum.rb b/lib/game/modules/array_sum.rb new file mode 100644 index 0000000..6af4375 --- /dev/null +++ b/lib/game/modules/array_sum.rb @@ -0,0 +1,13 @@ +class Game + module Modules + module ArraySum + def sum(&block) + self.inject(0) do |res, value| + res + block.call(value) + end + end + end + end +end + +Array.send :include, Game::Modules::ArraySum diff --git a/lib/game/modules/impassible.rb b/lib/game/modules/impassible.rb index 2cf1499..a566b17 100644 --- a/lib/game/modules/impassible.rb +++ b/lib/game/modules/impassible.rb @@ -4,6 +4,8 @@ module Impassible def passible?(player_objects=[]) false end + + alias :activatable? :passible? end end end diff --git a/lib/game/object/status.rb b/lib/game/object/status.rb index e3a7c3a..9ef2ed2 100644 --- a/lib/game/object/status.rb +++ b/lib/game/object/status.rb @@ -12,12 +12,14 @@ def expire end def activate + return if @status == Game::Object::ACTIVE Game::Object.remove(Game::Engine.instance.map.name, status, self) @status = Game::Object::ACTIVE Game::Object.add(Game::Engine.instance.map.name, status, self) end def deactivate + return if @status == Game::Object::IDLE Game::Object.remove(Game::Engine.instance.map.name, status, self) @status = Game::Object::IDLE Game::Object.add(Game::Engine.instance.map.name, status, self) diff --git a/lib/game/player.rb b/lib/game/player.rb index 1e4ac71..7d118b2 100644 --- a/lib/game/player.rb +++ b/lib/game/player.rb @@ -1,4 +1,5 @@ require 'lib/game/player/actions' +require 'lib/game/player/equipment' require 'lib/game/player/life_force' require 'lib/game/player/movements' @@ -6,12 +7,9 @@ class Game class Player include Game::Modules::ObjectManagement include Game::Player::Actions + include Game::Player::Equipment include Game::Player::Movements include Game::Player::LifeForce include Game::Modules::InstanceSetter - - attr_accessor :tile - attr_reader :hp - end end \ No newline at end of file diff --git a/lib/game/player/equipment.rb b/lib/game/player/equipment.rb new file mode 100644 index 0000000..e019e62 --- /dev/null +++ b/lib/game/player/equipment.rb @@ -0,0 +1,42 @@ +class Game + class Player + module Equipment + def self.included(base) + base.send :attr_reader + end + + def initialize(*args) + super + @space = Hash.new(2) + end + + def equip(item) + if !equiped?(item) && space_for(item) + equipment(item.on) << item + end + end + + def unequip(item) + if equiped?(item) + equipment(item.on).delete(item) + end + end + + def equiped?(item) + equipment(item.on).include?(item) + end + + def space_for(item) + items_size = equipment(item.on).sum(&:size) + (@space[item.on] - items_size) >= item.size + end + + def equipment(on=nil) + @equipment ||= {} + + return @equipment if on.nil? + @equipment[on] ||= [] + end + end + end +end \ No newline at end of file diff --git a/lib/game/player/movements.rb b/lib/game/player/movements.rb index f01d8d0..d79141a 100644 --- a/lib/game/player/movements.rb +++ b/lib/game/player/movements.rb @@ -3,6 +3,7 @@ class Player module Movements def self.included(base) base.send :attr_reader, :direction + base.send :attr_accessor, :tile end def initialize(*args) diff --git a/lib/game/tile/movement.rb b/lib/game/tile/movement.rb index 85d6b7b..d9dc16e 100644 --- a/lib/game/tile/movement.rb +++ b/lib/game/tile/movement.rb @@ -11,14 +11,61 @@ def at(direction) end def direction_to(tile) - diff_x = x - tile.x - diff_y = y - tile.y + directions_to(tile).first + end - if diff_x.abs > diff_y.abs - diff_x > 0 ? Game::Map::NORTH : Game::Map::SOUTH - else - diff_y > 0 ? Game::Map::WEST : Game::Map::EAST + def elements_within(distance) + Path.new(self).elements(distance) + end + + def directions_to(tile) + Path.new(self, tile).directions + end + + class Path + def initialize(start, goal=nil) + @goal = goal + @paths = {start => {:distance => 0, :path => []}} + end + + def directions + until found_goal do + return [] unless try_paths { |tile| tile.passible? } + end + @paths[@goal][:path] end + + def elements(distance) + (1..distance).each do + break unless try_paths { |tile| tile.activatable? } + end + @paths.keys + end + + def try_paths(&blk) + @paths.to_a.map do |location, details| + Game::Map::DIRECTIONS.map do |direction| + location.try_path(@paths, direction, details, &blk) + end.any? + end.any? + end + + private + def found_goal + !!@paths[@goal] + end + end + + def try_path(paths, direction, details) + tile = at(direction) + return false unless yield(tile) + !!tile.try_tile(paths, direction, details) + end + + def try_tile(paths, direction, details) + distance = (details[:distance] + 1) + return false if paths[self] && paths[self][:distance] <= distance + paths[self] = {:distance => distance, :path => (details[:path].dup + [direction])} end def in_range?(tile, max=10) diff --git a/lib/game/tile/passible.rb b/lib/game/tile/passible.rb index 135f3b9..d855301 100644 --- a/lib/game/tile/passible.rb +++ b/lib/game/tile/passible.rb @@ -4,6 +4,10 @@ module Passible def passible?(player_objects=[]) objects.all?{|obj| !obj.respond_to?(:passible?) || obj.passible? } end + + def activatable? + true + end end end end \ No newline at end of file diff --git a/lib/game/tile/wall.rb b/lib/game/tile/wall.rb index a7c7ba1..a85d534 100644 --- a/lib/game/tile/wall.rb +++ b/lib/game/tile/wall.rb @@ -5,7 +5,7 @@ class Wall include Game::Tile::Base include Game::Tile::Movement - def passible?(player_objects) + def passible?(player_objects=[]) return false unless has_object?(Game::Object::Passage) passage = get_object(Game::Object::Passage) return true if passage.passible? @@ -13,6 +13,8 @@ def passible?(player_objects) key = player_objects.detect{|obj| obj.id == passage.id} passage.open if key.try(:use) end + + alias :activatable? :passible? end end end \ No newline at end of file diff --git a/lib/render/console/draw_map.rb b/lib/render/console/draw_map.rb index 71e9b84..650a06f 100644 --- a/lib/render/console/draw_map.rb +++ b/lib/render/console/draw_map.rb @@ -92,7 +92,7 @@ def draw_enemies(map) if active.size > 0 append "Active Enemies: ", active.size active.each do |enemy| - append space(classname_for(enemy)), enemy.hp + draw_inventry_object(Game::Player.instance, enemy) end end end diff --git a/lib/render/console/draw_tile.rb b/lib/render/console/draw_tile.rb index d36cb0e..aab484c 100644 --- a/lib/render/console/draw_tile.rb +++ b/lib/render/console/draw_tile.rb @@ -56,7 +56,7 @@ def draw_empty def draw_wall if tile.has_object?(Game::Object::Passage) - return 'D D' if tile.passible?([]) + return 'D D' if tile.passible? return 'DDD' end diff --git a/lib/render/console/player_inventry.rb b/lib/render/console/player_inventry.rb index 26eaf29..a4594e9 100644 --- a/lib/render/console/player_inventry.rb +++ b/lib/render/console/player_inventry.rb @@ -10,20 +10,21 @@ def draw_player_inventry(player) append "(EMPTY)" else player.objects.each do |object| - draw_inventry_object(object) + @selected ||= object + draw_inventry_object(player, object) end end append end - def draw_inventry_object(obj) - @selected ||= obj + def draw_inventry_object(player, obj) options = @selected == obj ? {:color => :invert} : [] strings = [space(classname_for(obj))] details = [] details << "Att: #{obj.attack}" if obj.respond_to?(:attack) details << "HP: #{obj.hp}" if obj.respond_to?(:hp) - strings << '(' << details << ')' unless details.empty? + details << "Equiped" if obj.respond_to?(:on) && player.equiped?(obj) + strings << '(' << details.join(', ') << ')' unless details.empty? append *strings, options end diff --git a/maps/level9 b/maps/level9 new file mode 100644 index 0000000..7f192bb --- /dev/null +++ b/maps/level9 @@ -0,0 +1,111 @@ +{ + "name": "2: Follow the path", + "goal": "Navigate to the Exit", + "height": 5, + "width": 5, + "data": [ + [0, 0, 0, 0, 0], + [0, 2, 3, 3, 3], + [0, 0, 0, 0, 0], + [0, 0, 0, 0, 0], + [0, 0, 0, 0, 0] + ], + "objects": [ + { + "name": "LevelExit", + "x": 4, + "y": 4, + "details": { + "modules": ["Exit"] + } + }, + { + "name": "BasicEnemy", + "x": 3, + "y": 0, + "details": { + "modules": ["Enemy"], + "attack": 5, + "health": 100 + } + }, + { + "name": "BasicEnemy", + "x": 3, + "y": 0, + "details": { + "modules": ["Enemy"], + "attack": 5, + "health": 100 + } + }, + { + "name": "BasicEnemy", + "x": 3, + "y": 0, + "details": { + "modules": ["Enemy"], + "attack": 5, + "health": 100 + } + }, + { + "name": "BasicEnemy", + "x": 3, + "y": 0, + "details": { + "modules": ["Enemy"], + "attack": 5, + "health": 100 + } + }, + { + "name": "BasicEnemy", + "x": 3, + "y": 3, + "details": { + "modules": ["Enemy"], + "attack": 10, + "health": 100 + } + }, + { + "name": "BasicEnemy", + "x": 3, + "y": 4, + "details": { + "modules": ["Enemy"], + "attack": 15, + "health": 100 + } + }, + { + "name": "BasicEnemy", + "x": 4, + "y": 3, + "details": { + "modules": ["Enemy"], + "attack": 15, + "health": 100 + } + }, + { + "name": "Sword", + "x": 0, + "y": 2, + "details": { + "modules": ["InventryItem", "Weapon"], + "attack": 20 + } + }, + { + "name": "Health", + "x": 0, + "y": 4, + "details": { + "modules": ["Healer"], + "health": 200 + } + } + ] +} \ No newline at end of file diff --git a/spec/game/player/equipment_spec.rb b/spec/game/player/equipment_spec.rb new file mode 100644 index 0000000..6c1bd9a --- /dev/null +++ b/spec/game/player/equipment_spec.rb @@ -0,0 +1,62 @@ +require 'spec_helper' + +describe Game::Player::Equipment do + subject { Game::Player.new } + let(:weapon) { Game::Object.instance('EquipWeaponTest', 'modules' => ['Weapon'], 'on' => 'hand', 'size' => 1) } + + describe '#equip' do + it 'add the item to the players list of equipment' do + subject.equip(weapon) + subject.equipment('hand').should == [weapon] + end + + it 'can equip two weapons' do + knife = Game::Object.instance('EquipWeaponTest', 'modules' => ['Weapon'], 'on' => 'hand', 'size' => 1) + subject.equip(weapon) + subject.equip(knife) + subject.equipment('hand').should == [weapon, knife] + end + + it 'can not equip the same weapon twice' do + subject.equip(weapon) + subject.equip(weapon) + subject.equipment('hand').should == [weapon] + end + + it 'does not allow equipment of an item over size limit for item posiiton' do + sword = Game::Object.instance('EquipWeaponTest', 'modules' => ['Weapon'], 'on' => 'hand', 'size' => 10) + subject.equip(weapon) + subject.equip(sword).should be_false + subject.equipment('hand').should == [weapon] + end + end + + describe '#unequip' do + it 'removes the item from the user equipment' do + subject.equip(weapon) + subject.unequip(weapon) + subject.equipment('hand').should == [] + end + + it 'returns fals if item is not equiped' do + subject.unequip(weapon).should be_false + end + end + + describe '#equipment' do + it 'return sthe hash of equipment if no vatiable passed in' do + subject.equip(weapon) + subject.equipment.should == {'hand' => [weapon]} + end + + it 'returns the items at the location if location passed in' do + subject.equip(weapon) + subject.equipment('hand').should == [weapon] + end + + it 'does not share the array across locations' do + subject.equip(weapon) + subject.equipment('foot').should == [] + end + end +end \ No newline at end of file diff --git a/spec/game/player/life_force_spec.rb b/spec/game/player/life_force_spec.rb new file mode 100644 index 0000000..c70b29d --- /dev/null +++ b/spec/game/player/life_force_spec.rb @@ -0,0 +1,47 @@ +require 'spec_helper' + +describe Game::Player::LifeForce do + subject { Game::Player.new } + let(:map) { mock(:map, :start_tile => tile) } + let(:tile) { mock(:tile, :add => true, :remove => true) } + + it 'sets an initial health' do + subject.hp.should == 100 + end + + describe '#damage' do + it 'reduces the players health' do + subject.damage(25) + subject.hp.should == 75 + end + + it 'has a minimum health of 0' do + subject.damage(101) + subject.hp.should == 0 + end + + it 'ends the game if the player hitpoints goes below 0' do + Game::Engine.new + subject.damage(101) + Game::Engine.instance.should be_ended + end + end + + describe '#heal' do + it 'increases the players health' do + subject.damage(50) + subject.heal(25) + subject.hp.should == 75 + end + + it 'has a maximum health of 100' do + subject.heal(101) + subject.hp.should == 100 + end + + it 'returns the value of any used healing energy' do + subject.damage(50) + subject.heal(101).should == 50 + end + end +end \ No newline at end of file diff --git a/spec/game/player/movements_spec.rb b/spec/game/player/movements_spec.rb index 616b6bf..6e45f88 100644 --- a/spec/game/player/movements_spec.rb +++ b/spec/game/player/movements_spec.rb @@ -4,6 +4,7 @@ subject { Game::Player.new } let(:map) { mock(:map, :start_tile => tile) } let(:tile) { mock(:tile, :add => true, :remove => true) } + before { Game::Tile.clear } describe '#load_map' do it 'sets the player start tile' do diff --git a/spec/game/player_spec.rb b/spec/game/player_spec.rb index 31650d2..6eea72a 100644 --- a/spec/game/player_spec.rb +++ b/spec/game/player_spec.rb @@ -1,46 +1,5 @@ require 'spec_helper' describe Game::Player do - let(:map) { mock(:map, :start_tile => tile) } - let(:tile) { mock(:tile, :add => true, :remove => true) } - it 'sets an initial health' do - subject.hp.should == 100 - end - - describe '#damage' do - it 'reduces the players health' do - subject.damage(25) - subject.hp.should == 75 - end - - it 'has a minimum health of 0' do - subject.damage(101) - subject.hp.should == 0 - end - - it 'ends the game if the player hitpoints goes below 0' do - Game::Engine.new - subject.damage(101) - Game::Engine.instance.should be_ended - end - end - - describe '#heal' do - it 'increases the players health' do - subject.damage(50) - subject.heal(25) - subject.hp.should == 75 - end - - it 'has a maximum health of 100' do - subject.heal(101) - subject.hp.should == 100 - end - - it 'returns the value of any used healing energy' do - subject.damage(50) - subject.heal(101).should == 50 - end - end end \ No newline at end of file diff --git a/spec/game/tile/movement_spec.rb b/spec/game/tile/movement_spec.rb index 3a62079..9cd698b 100644 --- a/spec/game/tile/movement_spec.rb +++ b/spec/game/tile/movement_spec.rb @@ -1,7 +1,20 @@ require 'spec_helper' describe Game::Tile::Movement do + def build_map(map_array) + x = -1 + map_array.map do |row| + y = -1 + x += 1 + row.map do |cell| + y += 1 + Game::Tile.build(cell, x, y) + end + end + end + subject { Game::Tile.build(0, 1, 1) } + before { Game::Tile.clear } describe '#at' do context 'retrieves the tile via the class at method' do @@ -50,12 +63,12 @@ end describe '#direction_to' do - let(:origin) { Game::Tile.build(0, 2, 2) } + let(:origin) { Game::Tile.build(0, 1, 1) } - let(:north) { Game::Tile.build(0, 0, 2) } - let(:east) { Game::Tile.build(0, 2, 4) } - let(:south) { Game::Tile.build(0, 4, 2) } - let(:west) { Game::Tile.build(0, 2, 0) } + let(:north) { Game::Tile.build(0, 0, 1) } + let(:east) { Game::Tile.build(0, 1, 0) } + let(:south) { Game::Tile.build(0, 2, 1) } + let(:west) { Game::Tile.build(0, 1, 2) } it 'when in an north direction' do origin.direction_to(north).should == Game::Map::NORTH @@ -74,6 +87,116 @@ end end + describe '#direction_to (more complex stuff)' do + let(:wall) { Game::Tile::WALL_0 } + + it 'straight line' do + tile_00 = Game::Tile.build(0, 0, 0) + tile_10 = Game::Tile.build(0, 1, 0) + tile_00.direction_to(tile_10).should == Game::Map::SOUTH + end + + it 'around a corner' do + tiles = build_map([ + [0, 0], + [0, wall] + ]) + + tiles[1][0].direction_to(tiles[0][1]).should == Game::Map::NORTH + end + + it 'a complex setup' do + tiles = build_map([ + [0, 0, 0], + [0, wall, wall], + [0, 0, 0] + ]) + + tiles[2][1].direction_to(tiles[0][2]).should == Game::Map::EAST + end + + context 'determines the best path' do + let(:tiles) { build_map([ + [0, 0, 0, 0, 0], + [0, wall, wall, wall, 0], + [0, 0, wall, 0, 0], + [0, wall, wall, 0, 0], + [0, 0, 0, 0, 0] + ]) } + + it 'finds the way out of a blind alley' do + tiles[2][1].direction_to(tiles[2][3]).should == Game::Map::EAST + end + + it 'takes the shortest path' do + tiles[2][0].direction_to(tiles[2][3]).should == Game::Map::SOUTH + end + + it 'choose a path if multiple possible' do + tiles[1][0].direction_to(tiles[2][3]).should == Game::Map::NORTH + end + + it 'will change its mind when the other path become shorter' do + tiles[0][0].direction_to(tiles[2][3]).should == Game::Map::WEST + end + end + + context 'naviagtion with moveable non-passible objects' do + let(:tiles) { build_map([ + [0, 0, 0, 0, 0], + [0, 0, 0, 0, 0] + ]) } + let(:enemy) { Game::Object.instance('PathEnemy', 'modules' => ['Enemy']) } + let(:engine) { mock(:engine, :map => map) } + let(:map) { mock(:map, :name => 'map_name') } + before do + Game::Engine.stub(:instance => engine) + end + + it 'avoids non-passible objects' do + tiles[1][1].add(enemy) + tiles[1][0].direction_to(tiles[1][4]).should == Game::Map::NORTH + end + + it 'does not crash when impassible objects' do + tiles[1][1].add(enemy) + tiles[0][1].add(enemy) + tiles[1][0].direction_to(tiles[1][4]).should be_nil + end + end + + end + + describe '#elements_with' do + let(:tiles) { build_map([ + [0, 0, 0, 0, 0], + [0, 0, 0, 0, 0] + ]) } + let(:enemy) { Game::Object.instance('PathEnemy', 'modules' => ['Enemy']) } + let(:engine) { mock(:engine, :map => map) } + let(:map) { mock(:map, :name => 'map_name') } + + it 'finds all elements within one movement' do + tiles[0][0].elements_within(1).should == [ + tiles[0][0], tiles[1][0], tiles[0][1] + ] + end + + it 'finds all elements within two movement' do + tiles[0][0].elements_within(2).should == [ + tiles[0][0], tiles[1][0], tiles[0][1], tiles[1][1], tiles[0][2] + ] + end + + it 'includes elements that contain enemy' do + Game::Engine.stub(:instance => engine) + tiles[0][1].add(enemy) + tiles[0][0].elements_within(1).should == [ + tiles[0][0], tiles[1][0], tiles[0][1] + ] + end + end + describe '#in_range?' do let(:origin) { Game::Tile.build(0, 0, 0) } diff --git a/spec/render/console/draw_map_spec.rb b/spec/render/console/draw_map_spec.rb index 8aca824..f47a56c 100644 --- a/spec/render/console/draw_map_spec.rb +++ b/spec/render/console/draw_map_spec.rb @@ -161,6 +161,10 @@ def output(element=nil) end context 'display item names when they exist' do + before do + player.stub(:equiped? => false) + end + it 'with highlight when selected item' do player.stub(:objects => [mock(:object, :name => 'Blue Key')]) output.should include "\e[7m Blue Key \e[m" @@ -171,6 +175,14 @@ def output(element=nil) mock(:object, :name => 'Green Key')]) output.should include " Green Key " end + + context 'when object is equiped' do + it 'is marked as equiped' do + player.stub(:objects => [mock(:object, :name => 'Blue Key', :on => true)], + :equiped? => true) + output.should include "\e[7m Blue Key (Equiped)\e[m" + end + end end end end