I am creating an Elevator object with an instance that can only go between floors one and twelve. The code works for the up and down instance methods, but I cannot get the elevator to not go above floor 12 or below floor 1. I tried using unless #floor >= 12, but there was a syntax error. I'm sure it is simple, but I am new to Ruby.
Here is the code that works:
class Elevator
##count = #floor
#The Constructor Method
def initialize(floor) #floor is an instance variable of the Elevator object.
#floor = floor
cheery_greeting
end
def cheery_greeting
puts "Hello my friend! would you like to go up or down?"
end
def self.notify()
"You are now on floor #{##count}"
end
#accessor methods
def go_up
#floor += 1
notify
end
def go_down
#floor -= 1
notify
end
I want to add a break so that it stops iterating when we reach floor twelve, so I wrote this, but it wouldn't even puts.
def floor_limit
if ##count == 12
puts "You're Way Too High!"
end
I also tried:
def go_up
unless #floor >= 12
#floor += 1
notify
end
You're mixing class instance variables with instance variables here and that's going to lead to trouble and confusion. If you're new to Ruby I strongly advise you to avoid using class instance variables, they just lead to a lot of mess. Instead focus on making each instance as self-contained as possible.
To make this more Ruby you can do a few things:
class Elevator
# Define a valid range for this elevator
FLOOR_RANGE_DEFAULT = (1..12)
# Expose the current floor and range as properties
attr_reader :floor
attr_reader :range
def initialize(floor, range = nil)
#floor = floor.to_i
#range = (range || FLOOR_RANGE_DEFAULT).to_a
end
def inspect
#range.map do |floor|
if (#floor == floor)
'[%d]' % floor
else
' %d ' % floor
end
end.join(' ')
end
end
Then your up and down code can check limits and reject if that's an invalid operation. First separate the moving code from the code that interprets up or down:
def up
move(#floor + 1)
end
def down
move(#floor - 1)
end
def move(target)
if (#range.include?(target))
#floor = target
end
#floor
end
Now you have a framework that you can build on. By using simple things like Ruby's range feature you can make a very adaptable class that can handle situations like elevators that have other limits:
e = Elevator.new(1, (1..20))
Or that stop only on odd floors:
e = Elevator.new(1, (1..20).select(&:odd?))
Or skip the 4th, 13th and 14th:
e = Elevator.new(1, (1..20).to_a - [ 4, 13, 14 ])
It doesn't take more code, it just takes the right code.
The following should do the trick.
def go_up
#floor += 1 unless #floor >= 12
notify
end
Or like this:
def go_up
#floor += 1 if #floor < 12
notify
end
They are both pretty intuitive so it's up to you.
This is a pretty detailed explanation on when to use unless.
And here is a standard if/else/unless tutorial.
Related
I am trying to make a battleship game in Ruby, but when I try to create an instance of the game board, I get "wrong number of arguments, 0 for 1". I dont see where I am going wrong as the initialize definition clearly accepts arguments.
class Board
attr_reader :grid, :default_grid
def intitalize(grid = self.class.default_grid, random = false)
#grid = grid
make_random_board if random
end
def self.default_grid
grid = Array.new(10){Array.new(10)}
end
def count
grid.flatten.count{|x| x == :s}
end
def self.random
self.new(self.default_grid, true)
end
def empty?(position = nil)
return true if position.nil?
else
false
end
def full?
grid.flatten.none?(&:nil?)
end
def place_random_ship
if full?
raise "error, board is full"
end
space = [rand(grid.length),rand(grid.length)]
until empty?(space)
space = [rand(grid.length),rand(grid.length)]
end
self[space] = :s
end
def make_random_board(count = 10)
count.times do
place_random_ship
end
end
end
emptygrid = Array.new(2){Array.new(2)}
myGame = Board.new(emptygrid)
You have a typo in your code. You should be using initialize instead of intitalize
And i believe the error you might have been getting would be ArgumentError: wrong number of arguments (1 for 0)
Which is because of your typo, the default class initialize method was used, which doesn't take in any arguments.
And something unrelated that i noticed in your code. You have defined a method named count and use variables named count. This is a code smell and i would suggest naming the method differently, because down the line, this might cause some bugs, that you might find hard to debug.
I've been trying to test a program that simulates an elevator for two days now with little success. Here's my elevator class, the program is still a work in progress and I've also commented out some methods that might not be essential to the test I'm having trouble with. I'll gladly show more code if you think it's needed
class Elevator
attr_accessor :current_floor
GROUND = 0
TOP = 15
def initialize
#floors = [] # list of floors to travel to
#pending = [] # store floors not in direction of travel
#current_floor = GROUND
#going_up = true # cannot travel downward from ground floor
#going_down = false
end
def get_input
gets.chomp
end
def run
enter_floors
sort_floors
move_to_floor
end
def enter_floors
# prompts the user for input and calls check_floor_numbers
end
def check_floor_numbers floors
# calls validate_floors to ensure user entered '2' instead of 'two'
# if proper floor numbers have been entered this method adds the number
# to #floors array other wise it calls floor_error_message
end
def floor_error_message
puts "Please enter numbers only."
enter_floors
end
def sort_floors
# if we are on the ground floor this method sorts #floors in ascending order
# if we are on the top floor it sorts #floors in descending order
# else it calls check_direction_of_travel
end
def move_to_floor
floor = #floors[0]
if #current_floor == floor
puts "You are already on floor #{floor}"
else
print_direction
(#current_floor..floor).each { |floor| puts "...#{floor}" }
#current_floor = floor # update current_floor
#floors.delete_at(0) # remove floor from list
end
check_for_more_passengers
end
def check_for_more_passengers
puts "Are there any more passengers? (Y/N)"
answer = (get_input).upcase
answer == 'Y' ? run : check_next_move
end
def check_next_move
if #floors.empty? && #pending.empty?
end_ride
else
move_to_floor
end
end
def check_direction_of_travel
# not implemented - add floor to appropriate array depending on dir
# of travel
end
def end_ride
puts "\n\nEND."
end
def print_direction
msg = " "
#going_up ? msg = "Going Up!" : msg = "Going Down!"
puts msg
end
end
I'm trying to test that the elevator can move to a specific floor. At first I was having trouble testing input from the console without running the program itself. I asked a question about this and was referred to this answer in a different question. The answer in question extract gets.chomp to a separate method then overrides the method in the tests. I ended up with something like this:
describe "it can move to a floor" do
before do
##moves = ["2", "N"]
def get_input; ##moves.next end
end
it "should move to floor 2" do
e = Elevator.new
e.run
assert_equal(e.current_floor, 2)
end
end
Problem: get_input was not properly overidden and running the test suit prompted the user for input so it was suggested that I open the Elevator class in the test itself to ensure that the method was properly overridden. Attempting to do so eventually led to a test like this:
describe "it can move to a floor" do
before do
class Elevator
attr_accessor :current_floor
##moves = ["2", "N"]
def get_input; ##moves.next end
def run; end
end
end
it "should move to floor 2" do
e = Elevator.new
e.run
assert_equal(e.current_floor, 2)
end
end
I had to override run and add an attr_accessor for current_floor because I was getting method missing errors.
Problem: This test gives the following error:
1) Failure: it can move to a floor#test_0001_should move to floor 2
[elevator_test.rb:24]: Expected: nil Actual: 2
I've tried to tidy up the Elevator class as much as possible and keep the methods as simple as I could given the parameters of the program.
Can anyone point me in the right direction towards getting this solved, with maybe pseudocode examples (if possible) to demonstrate how I should approach this problem if the answer is to refactor.
Please bear in mind that I'd also like to implement other tests like checking that the elevator class can maintain a list of floors, or that it can change direction, in the future when you answer.
Your test class ElevatorTest is redefining the Elevator to override method get_input, but it is not opening the class Elevator defined in elevator.rb, but instead it is sort of creating a new class Elevator which happens to be defined inside the class ElevatorTest. Remember every class is also a module, so now you have a new class ElevatorTest::Elevator.
To fix this issue, I have made some changes to elevator_test.rb which is shown below.
gem 'minitest', '>= 5.0.0'
require 'minitest/spec'
require 'minitest/autorun'
require_relative 'elevator'
class Elevator
##moves = ["2", "N"].each
def get_input; ##moves.next end
end
class ElevatorTest < MiniTest::Test
def test_working
assert_equal(1, 1)
end
describe "it can move to a floor" do
before do
end
it "should move to floor 2" do
e = Elevator.new
e.run
assert_equal(e.current_floor, 2)
end
end
end
Also, please remember to use .each while defining ##moves - it returns an enumerator. We can call .next only on an enumerator
Ok so I just started learning ruby and I'm making a Yhatzee game, now this is where I'm currently at:
class Yhatzee
def dices
#dices.to_a= [
dice1=rand(1..6),
dice2=rand(1..6),
dice3=rand(1..6),
dice4=rand(1..6),
dice5=rand(1..6)
]
end
def roll_dice
#dices.to_a.each do |dice|
puts dice
end
end
end
x = Yhatzee.new
puts x.roll_dice
Now the reason i typed .to_a after the array is i kept getting a "uninitialized variable #dices" error, and that seemed to fix it, i have no idea why.
anyways on to my question, i currently don't get any errors but my program still won't print anything to the screen. I expected it to print out the value of each dice in the array... any idea what I'm doing wrong? It seems to work when i do it in a procedural style without using classes or methods so i assumed it might work if i made the 'dices' method public. But no luck.
There are a few issues here. Firstly #dices is nil because it is not set anywhere. Thus when you call #dices.to_a you will get []. Also the dices method will not work either because nil does not have a to_a= method and the local variables you are assigning in the array will be ignored.
It seems a little reading is in order but I would do something like the following: (Not the whole game just refactor of your code)
class Yhatzee
def dice
#dice = Array.new(5){rand(1..6)}
end
def roll_dice
puts dice
end
end
x = Yhatzee.new
puts x.roll_dice
There are alot of additional considerations that need to be made here but this should at least get you started. Small Example of how I would recommend expanding your logic: (I did not handle many scenarios here so don't copy paste. Just wanted to give you a more in depth look)
require 'forwardable'
module Yahtzee
module Display
def show_with_index(arr)
print arr.each_index.to_a
print "\n"
print arr
end
end
class Roll
include Display
extend Forwardable
def_delegator :#dice, :values_at
attr_reader :dice
def initialize(dice=5)
#dice = Array.new(dice){rand(1..6)}
end
def show
show_with_index(#dice)
end
end
class Turn
class << self
def start
t = Turn.new
t.show
t
end
end
attr_reader :rolls
include Display
def initialize
#roll = Roll.new
#rolls = 1
#kept = []
end
def show
#roll.show
end
def roll_again
if available_rolls_and_dice
#rolls += 1
#roll = Roll.new(5-#kept.count)
puts "Hand => #{#kept.inspect}"
show
else
puts "No Rolls left" if #rolls == 3
puts "Remove a Die to keep rolling" if #kept.count == 5
show_hand
end
end
def keep(*indices)
#kept += #roll.values_at(*indices)
end
def show_hand
show_with_index(#kept)
end
def remove(*indices)
indices.each do |idx|
#kept.delete_at(idx)
end
show_hand
end
private
def available_rolls_and_dice
#rolls < 3 && #kept.count < 5
end
end
end
The main problem with this code is that you are trying to use the #dices instance variable inside of the roll_dice method, however you are not defining the instance variable anywhere (anywhere that is being used). You have created the dices method but you are not actually instantiating it anywhere. I have outlined a fix below:
class Yhatzee
def initialize
create_dices
end
def roll_dice
#dices.each do |dice|
puts dice
end
end
private
def create_dices
#dices = Array.new(5){rand(1..6)}
end
end
x = Yhatzee.new
x.roll_dice
I have done some simple refactoring:
Created an initialize method, which creates the #dice instance variable on the class initialization.
Made the 'dices' method more descriptive and changed the method visibility to private so only the class itself is able to create the #dice.
Cleaned up the creation of the dices inside of the #dice instance variable
I have omitted the .to_a from the roll_dice method, now that we create the variable from within the class and we know that it is an array and it will be unless we explicitly redefine it.
UPDATE
Although I cleaned up the implementation of the class, it was kindly pointed out by #engineersmnky that I oversaw that the roll would return the same results each time I called the roll_dice function, I have therefore written two functions which will achieve this, one that defines an instance variable for later use and one that literally just returns the results.
class Yhatzee
def roll_dice
#dice = Array.new(5){rand(1..6)} # You will have access to this in other methods defined on the class
#dice.each {|dice| puts dice }
end
def roll_dice_two
Array.new(5){rand(1..6)}.each {|dice| puts dice } # This will return the results but will not be stored for later use
end
end
x = Yhatzee.new
x.roll_dice
x.roll_dice # Will now return a new result
class Account
attr_accessor :balance
def initialize (amount=0)
self.balance = amount
end
def -(x)
self.balance -= x
end
def +(x)
self.balance += x
end
def to_s
puts "Balance is #{self.balance} !!!!!!!!!"
end
end
acc = Account.new(20)
acc -=5
Can someone explain why it works to write: puts acc (render 15 as a result) but this line:
puts acc.balance doesn't work (it says undefined method balance, however it's not a method, it should be seen as a property/attribute)
Thanks in advance
acc -= 5 is equivalent to acc = acc - 5.
acc - 5 is 15 (by Account#- method); acc = acc - 5 is like acc = 15.
Now acc is Fixnum object.
How about implement +, - as follow?
def -(x)
Account.new(self.balance - x)
end
def +(x)
Account.new(self.balance + x)
end
The source of your error is line
acc -= 5
it assings result (returned value) of substraction method to acc variable, and this result is Fixnum instance, which doesn't have balance method defined.
If you want your + and - methods to return Account instance, you need to specify this:
def +(x)
self.balance += x
self
end
etc.
BTW, accessors in Ruby ARE methods.
You want to edit your two methods to make += and -= work, so that they return the object in place:
def -(x)
self.balance -= x
self
end
def +(x)
self.balance += x
self
end
That way, the object it reassigned to itself.
Oops, there is a number of things that have been wrong with this one. I cannot explain all the problems, but let's see how far I can get:
First of all you have redefined the #+ and #- methods to give them semantics you would expect from #+= and #-=. Given this: if you use acc-5 in your example (instead of acc-=5) the behavior is close to what you expect, irb will display 15 as this is the result of your #- method (which has the side effect of subtracting 5 of balance as well).
However, as you call #-= in your example the ruby interpreter ends up assigning the methods result Fixnum 15 to your acc-variable. This will give you the error message NoMethodError: undefined method 'balance' for 15:Fixnum when you try to access #balance which happens to be defined on your Account class.
Your #- method should return an Account-object, not change the existing state of the object and return the result as a Fixnum. See http://ruby.about.com/od/oo/ss/Overloading-Operators.htm for examples on how to redefine operators. I understand that this somehow defeats your purpose as you would have liked to use #-= to modify the object in place, but it only works by creating another Account-instance in Ruby as you are not able to redefine #-= directly (see the link above).
Of course you are free to define a #withdraw-method to get the semantics you desire.
Was completing the pine exercise for Def Grandma and thought would try and take it a little further and abstract it to remove as much duplication as possible.
Hope my logic with this isn't too bizzare just tried to separate things into functions. But now if I type
Bye
The program exits immediately without proceeding to the exitPlan function.
Any advice welcome.
puts 'Say something nice to Grandma.'
puts 'You may need to shout > '
speak = gets.strip
counter = 0
speaks(speak)
def speaks(speak)
if speak != 'Bye'
talk()
else
exitPlan()
end
end
def talk()
if speak == speak.downcase
puts 'Huh Speak up Sonny'
counter -= 1
else
year = rand(1930..1951)
puts 'No not Since ' + year.to_s
counter -= 1
end
if counter < 0
counter = 0 # don't want counter going below zero.
end
puts 'Say something nice to Grandma'
speaks()
end
def exitPlan()
counter += 1
unless counter == 3
puts 'Say something nice to Grandma'
speaks()
else
puts 'good night Sonny'
end
end
Error
renshaw#renshaw-TravelMate-5740G:~/Ruby$ ruby -W dGrand.rb
Say something nice to Grandma.
You may need to shout >
Bye
dGrand.rb:6:in `<main>': undefined method `speaks' for main:Object (NoMethodError)
renshaw#renshaw-TravelMate-5740G:~/Ruby$
You need to move the lines
puts 'Say something nice to Grandma.'
puts 'You may need to shout > '
speak = gets.strip
counter = 0
speaks(speak)
to after your method definition so that the speaks method has been defined when you reach the speaks(speak) line.
Then next problem you will probably encounter is
in `exitPlan': undefined method `+' for nil:NilClass
This is because counter is a local variable so is not shared between your toplevel code and the different methods. You will need to either use a global variable i.e. $counter for this or, better, put the various methods inside a class and then use an instance variable.
I suspect there are still some other issues in your code such as you only seem to be calling gets.strip to get the input once. However, in terms of wrapping the code inside a class it's not the counter that you want to wrap as you would still need to pass that around between the various methods. It's the whole speak/talk interaction so something along the lines of
class Grandma
def initialize
#counter = 0
end
def speaks(speak)
..
end
def talk()
..
end
def exitPlan()
..
end
end
grandma = Grandma.new
grandma.speaks(speak)
and substitute the places where you're currently using the local counter variable with references to #counter
You are calling method speaks before it's defined. See Ruby: How to call function before it is defined?
Look at the other posts explaining the use of methods and globals. Here you have a gist that works with Ruby 1.9.3 because of how you used rand