I'm learning Ruby and RSpec, and I've hit a snag wherein most learning materials available have become deprecated and I lack the vocabulary to sift through the wreckage.
class Session
def initialize(winning_score = 0)
#winning_score = winning_score
play
end
def play
get_players
max_score
while #game is in play
print_score
#play game
end
winner
end
def get_players
puts "\nPlayer X name:"
p1 = gets.chomp.upcase
#player1 = Player.new(p1, "X", 0)
puts "\nPlayer O name:"
p2 = gets.chomp.upcase
#player2 = Player.new(p2, "O", 0)
end
def max_score
puts "\nBest out of how many?"
max = gets.chomp
#winning_score = (max.to_f/2).ceil
end
def print_score
puts "\n#{#player1.name}: #{#player1.score} \n#{#player2.name}: #{#player2.score}"
end
def winner
if #player1.score == #winning_score
puts "\n#{#player1.name} WINS!!!"
elsif #player2.score == #winning_score
puts "\n#{#player2.name} WINS!!!"
end
end
end
class Player
attr_accessor :name, :mark, :score
def initialize(name, mark, score)
#name = name
#mark = mark
#score = score
end
end
Rpec:
describe "Play" do
before(:each) do
allow(x).to receive(:puts)
allow(x).to receive(:print)
end
let(:x) { Session.new }
it "displays game score" do
#player1 = Player.new("p1", "X", 0)
#player2 = Player.new("p2", "O", 2)
expect(x).to receive(:puts).with("\np1: 0 \np2: 2")
x.print_score
x.play
end
end
... I think that's all the applicable bits of code... The problem is that the file being tested and the RSpec file keep talking through each other, and I keep getting this sort of thing:
1) play displays game score
Failure/Error: expect(x).to receive(:puts).with("\np1: 0 \np2: 2")
#<Session:0x007fc16b9f5d38> received :puts with unexpected arguments
expected: ("\np1: 0 \np2: 2")
got: ("\n\t\t: 0 \n\tEND: 0"), ("\nPlayer X name:"), ("\nPlayer O name:"), ("\nBest out of how many?"), ("\n\tIT \"GETS AND CREATES PLAYERS\" DO WINS!!!")
# ./tictactoe_spec.rb:36:in `block (2 levels) in <top (required)>'
...where the noise is other methods gets.chomping the running RSpec code and storing it as the player names... I can't figure out how to prevent this from happening, clear/reset it, or what the correct course of action even is... Please advise.
Well, you are setting the #player1 and #player2 instance variables in the test context. While you need them inside your session object.
I think a good approach here would be to stub the get_players method, but we will have to change it a bit.
class Session
#...
def get_players
#player1 = get_player("X")
#player2 = get_player("O")
end
def get_player(mark)
puts "\nPlayer #{mark} name:"
name = gets.chomp.upcase
Player.new(name, mark, 0)
end
#...
end
Now you can stub those get_player calls
# ...
it "displays game score" do
allow(x).to recive(:get_player).with("X") { Player.new("p1", "X", 0) }
allow(x).to recive(:get_player).with("O") { Player.new("p2", "O", 2) }
expect(x).to receive(:puts).with("\np1: 0 \np2: 2")
x.print_score
x.play
end
The solution was to implement an optional output source at initialization that defaults to stdout, then create a double of that output source for the methods to puts to. After talking to a few more experienced devs, it seems to be a fairly common thing to do. Simplifying the code a bit would probably also be pretty thoroughly helpful... It's pretty bad looking back at it.
Related
Creating a rock, paper, scissors game with randomisation and a rule engine but the winner_is method seems to be returning nil. I can't really figure out what the variables within it are returning since it still prints them fine before all the if conditions.
class RPS
def initialize(guess:)
#guess = guess.capitalize
end
def rule_engine
{
'Rock': ['Scissors'],
'Paper': ['Rock'],
'Scissors': ['Paper']
}
end
def sys_guess
rand 12345
sys_guesses = %w{Rock Paper Scissors}
sys_guesses.sample
end
def winner_is
puts sys_guess
puts #guess
if rule_engine[sys_guess.to_sym].include? #guess
puts "Computer wins"
elsif rule_engine[#guess.to_sym].include? sys_guess
puts "You win!"
else
puts "Tie"
end
end
end
rps = RPS.new(guess: gets)
rps.winner_is
Here's your problem:
rps = RPS.new(guess: gets)
The string you get from gets includes a trailing newline. Naturally, this key is not found in your hash, even with .to_sym. Trim the newline with .chomp, for example.
I get the feeling that you actually wanted to use string keys. If so, this would be the syntax:
def rule_engine
{
'Rock' => ['Scissors'],
'Paper' => ['Rock'],
'Scissors' => ['Paper']
}
end
Have a problem, when run a code allways have error.
Expect: for the user add win or reduce his balance.
undefined method `balance=' for #<Dice:0x0000563d4d4dfd88 #name="foo", #balance=600, #bet=300>
Did you mean? balance
(repl):22:in `increase_decrease_cash'
(repl):62:in `<class:Game>'
(repl):29:in `<main>'
This error always comes out, retried everything I could guess, but nothing came of it and I don’t understand how it can be googled
class Dice
attr_accessor :name, :bet
attr_reader :balance
def initialize(name, balance, bet)
#name = name
#balance = balance
#bet = bet
end
def self.roll
#roll_dice = rand(1..2)
end
def self.check_bet
if #player.bet > #player.balance
puts "Enter number from 1 to #{#player.balance}"
end
end
def self.increase_decrease_cash
if #roll == #my_number
#player.balance += #player.bet
else
#player.balance -= #player.bet
end
end
end
class Game < Dice
#player = Dice.new("foo", 600, 0)
puts "Hello #{#player.name} your balance is: #{#player.balance}"
puts "Bones throwing count times"
a = 2 #gets.chomp.to_i
while a > 0 do
puts ""
puts "Enter your bet !!!"
# PLAYER BET
#player.bet = 300 #gets.chomp.to_i
check_bet
puts "Respected #{#player.name} your bet is: #{#player.bet}"
puts "Now select number 1-2"
# BONES ROLL
#my_number = roll # gets.chomp.to_i
puts "###################"
puts "Now we throw bones"
#roll = roll
puts "Nuber is #{roll}"
if #roll == #my_number
puts "Your win, you get #{#player.bet}"
else
puts "You lose #{#player.bet}"
end
p "$$$$"
p #player.balance
p "$$$$"
a -= 1
increase_decrease_cash
end
end
This error always comes out, retried everything I could guess, but nothing came of it and I don’t understand how it can be googled
attr_reader creates only the get method for balance. You need both get and set method for balance. Because you set balance in the initialize method. So, you should use attr_accessor instead of attr_reader.
attr_accessor :balance
I am building a Tic-Tac-Toe game to be played on the command line.
module TicTacToe
class Player
attr_accessor :symbol
def initialize(symbol)
#symbol = symbol
end
end
class Board
attr_reader :spaces
def initialize
#spaces = Array.new(9)
end
def to_s
output = ""
0.upto(8) do |position|
output << "#{#spaces[position] || position}"
case position % 3
when 0, 1 then output << " | "
when 2 then output << "\n-----------\n" unless position == 8
end
end
output
end
def space_available(cell, sym)
if spaces[cell].nil?
spaces[cell] = sym
else
puts "Space unavailable"
end
end
end
class Game < Board
attr_reader :player1, :player2
def initialize
play_game
end
def play_game
#player1 = Player.new("X")
#player2 = Player.new("O")
puts Board.new
#current_turn = 1
turn
end
def move(player)
while victory != true
puts "Where would you like to move?"
choice = gets.chomp.to_i
space_available(choice, player.symbol)
puts Board
#current_turn += 1
turn
end
end
def turn
#current_turn.even? ? move(#player2) : move(#player1)
end
def victory
#still working on this
end
end
end
puts TicTacToe::Game.new
The method that is to take a user's cell choice (space_available) and alter the array with their piece ('X' or 'O') is giving me an error. I can't find why my code is throwing this particular error.
The problem is that you don't call the parent constructor in your Game class, therefore #spaces is not initialized.
Your hierarchy decision is questionable, but to make it work, you can simply change the Game constructor to:
def initialize
super
play_game
end
You are calling spaces[cell]. The error is telling you that you are calling [] on nil, which means that spaces must be nil.
Perhaps you mean #spaces? Otherwise - you need to tell the program how spaces is defined and how it is initialized. A simple spaces = {} unless spaces would work
Another way of initialising your spaces variable would be to call super when you initialize the Game:
class Game < Board
attr_reader :player1, :player2
def initialize
super
play_game
end
...
Sandi Metz says in SOLID OOPS concepts from GORUCO that presence of if..else blocks in Ruby can be considered to be a deviation from Open-Close Principle. What all methods can be used to avoid not-urgent if..else conditions? I tried the following code:
class Fun
def park(s=String.new)
puts s
end
def park(i=Fixnum.new)
i=i+2
end
end
and found out that function overloading does not work in Ruby. What are other methods through which the code can be made to obey OCP?
I could have simply gone for:
class Fun
def park(i)
i=i+2 if i.class==1.class
puts i if i.class=="asd".class
end
end
but this is in violation to OCP.
With your current example, and wanting to avoid type detection, I would use Ruby's capability to re-open classes to add functionality you need to Integer and String:
class Integer
def park
puts self + 2
end
end
class String
def park
puts self
end
end
This would work more cleanly when altering your own classes. But maybe it doesn't fit your conceptual model (it depends what Fun represents, and why it can take those two different classes in a single method).
An equivalent but keeping your Fun class might be:
class Fun
def park_fixnum i
puts i + 2
end
def park_string s
puts s
end
def park param
send("park_#{param.class.to_s.downcase}", param)
end
end
As an opinion, I am not sure you will gain much writing Ruby in this way. The principles you are learning may be good ones (I don't know), but applying them forcefully "against the grain" of the language may create less readable code, regardless of whether it meets a well-intentioned design.
So what I would probably do in practice is this:
class Fun
def park param
case param
when Integer
puts param + 2
when String
puts param
end
end
end
This does not meet your principles, but is idiomatic Ruby and slightly easier to read and maintain than an if block (where the conditions could be far more complex so take longer for a human to parse).
You could just create handled classes for Fun like so
class Fun
def park(obj)
#parker ||= Object.const_get("#{obj.class}Park").new(obj)
#parker.park
rescue NameError => e
raise ArgumentError, "expected String or Fixnum but recieved #{obj.class.name}"
end
end
class Park
def initialize(p)
#park = p
end
def park
#park
end
end
class FixnumPark < Park
def park
#park += 2
end
end
class StringPark < Park
end
Then things like this will work
f = Fun.new
f.park("string")
#=> "string"
f.instance_variable_get("#parker")
#=> #<StringPark:0x1e04b48 #park="string">
f = Fun.new
f.park(2)
#=> 4
f.instance_variable_get("#parker")
#=> #<FixnumPark:0x1e04b48 #park=4>
f.park(22)
#=> 6 because the instance is already loaded and 4 + 2 = 6
Fun.new.park(12.3)
#=> ArgumentError: expected String or Fixnum but received Float
You could do something like this:
class Parent
attr_reader :s
def initialize(s='')
#s = s
end
def park
puts s
end
end
class Child1 < Parent
attr_reader :x
def initialize(s, x)
super(s)
#x = x
end
def park
puts x
end
end
class Child2 < Parent
attr_reader :y
def initialize(s, y)
super(s)
#y = y
end
def park
puts y
end
end
objects = [
Parent.new('hello'),
Child1.new('goodbye', 1),
Child2.new('adios', 2),
]
objects.each do |obj|
obj.park
end
--output:--
hello
1
2
Or, maybe I overlooked one of your twists:
class Parent
attr_reader :x
def initialize(s='')
#x = s
end
def park
puts x
end
end
class Child1 < Parent
def initialize(x)
super
end
def park
x + 2
end
end
class Child2 < Parent
def initialize(x)
super
end
def park
x * 2
end
end
objects = [
Parent.new('hello'),
Child1.new(2),
Child2.new(100),
]
results = objects.map do |obj|
obj.park
end
p results
--output:--
hello
[nil, 4, 200]
And another example using blocks, which are like anonymous functions. You can pass in the desired behavior to park() as a function:
class Function
attr_reader :block
def initialize(&park)
#block = park
end
def park
raise "Not implemented"
end
end
class StringFunction < Function
def initialize(&park)
super
end
def park
block.call
end
end
class AdditionFunction < Function
def initialize(&park)
super
end
def park
block.call 1
end
end
class DogFunction < Function
class Dog
def bark
puts 'woof, woof'
end
end
def initialize(&park)
super
end
def park
block.call Dog.new
end
end
objects = [
StringFunction.new {puts 'hello'},
AdditionFunction.new {|i| i+2},
DogFunction.new {|dog| dog.bark},
]
results = objects.map do |obj|
obj.park
end
p results
--output:--
hello
woof, woof
[nil, 3, nil]
Look at the is_a? method
def park(i)
i.is_a?(Fixnum) ? (i + 2) : i
end
But even better not to check a type, but use duck typing:
def park(i)
i.respond_to?(:+) ? (i + 2) : i
end
UPD: After reading comments. Yes, both examples above don't solve the OCP problem. That is how I would do it:
class Fun
# The method doesn't know how to pluck data. But it knows a guy
# who knows the trick
def pluck(i)
return __pluck_string__(i) if i.is_a? String
__pluck_fixnum__(i) if i.is_a? Fixnum
end
private
# Every method is responsible for plucking data in some special way
# Only one cause of possible changes for each of them
def __pluck_string__(i)
puts i
end
def __pluck_fixnum__(i)
i + 2
end
end
I understand or equal to operation in ruby but can you explain what
you have done with:
Object.const_get("#{obj.class}Park").new(obj)
In ruby, something that starts with a capital letter is a constant. Here is a simpler example of how const_get() works:
class Dog
def bark
puts 'woof'
end
end
dog_class = Object.const_get("Dog")
dog_class.new.bark
--output:--
woof
Of course, you can also pass arguments to dog_class.new:
class Dog
attr_reader :name
def initialize(name)
#name = name
end
def bark
puts "#{name} says woof!"
end
end
dog_class = Object.const_get("Dog")
dog_class.new('Ralph').bark
--output:--
Ralph says woof!
And the following line is just a variation of the above:
Object.const_get("#{obj.class}Park").new(obj)
If obj = 'hello', the first portion:
Object.const_get("#{obj.class}Park")
is equivalent to:
Object.const_get("#{String}Park")
And when the String class object is interpolated into a string, it is simply converted to the string "String", giving you:
Object.const_get("StringPark")
And that line retrieves the StringPark class, giving you:
Object.const_get("StringPark")
|
V
StringPark
Then, adding the second portion of the original line gives you:
StringPark.new(obj)
And because obj = 'hello', that is equivalent to:
StringPark.new('hello')
Capice?
I'm having some trouble understanding scope in ruby.
Here is a link to a repo if you'd like to download/run what I'm talking about to see for yourself:
https://github.com/minervadreaming/killshit
I have several .rb files present - specifically, I'm having an issue calling a class method from an instance. Seems like I'm not properly following scope, but I'm not sure how or why.
Here is a snippet from room.rb:
module KillShit
class Room
attr_reader :player, :monster, :game
def initialize(player, monster, game)
#player = player
#monster = monster
#game = game
end
def action
outline
Player.describe(player)
vs
Monster.describe(monster)
outline
#rolling a d20 to see who takes a turn
turn = rand(1..100)
if turn <= 20
monster_attack
else
puts "What would you like to do?"
puts "1. Attack!"
puts "2. Defend!"
puts "3. Run away!"
#Give the player magic if they're at least level 2
if player.maglevel >= 1
puts "4. Cast spell"
else
end
prompt; action = gets.chomp
if action == "1"
attack(player, monster)
elsif action == "2"
defend
elsif action == "3"
flee
elsif action == "4" && player.maglevel >= 1
magic
else
action
end
end
end
def magic
puts "What magic would you like to cast?"
if player.maglevel == 1
puts "1. Heal"
puts "2. Fireball"
puts "3. Tremor"
prompt; magic = gets.chomp
if magic == "1"
Spells.heal(player)
elsif magic == "2"
Spells.fireball(player, monster)
elsif magic == "3"
Spells.tremor(player, monster)
else
magic
end
elsif player.maglevel == 2
puts "1. Greater Heal"
puts "2. Firestorm"
puts "3. Earthquake"
prompt; magic = gets.chomp
if magic == "1"
Spells.greaterheal(player)
elsif magic == "2"
Spells.firestorm(player, monster)
elsif magic == "3"
Spells.earthquake(player, monster)
else
magic
end
else
end
end
end
end
As you can see, when the player chooses to cast a spell it calls out to a Spells class, which I have in spells.rb. Here is a snippet from that code:
require_relative 'room'
require_relative 'player'
require_relative 'monster'
module KillShit
class Spells
attr_accessor :player, :monster
#Checking if user has enough MP to cast spell
def self.mp_check(req, player)
req_mp = req
if player.mp < req_mp
puts "#{player.name} doesn't have enough MP!"
action
else
end
end
def self.heal(player)
req_mp = 3
Spells.mp_check(req_mp, player)
player.mp -= req_mp
amt = rand(3..10)
player.hp += amt
puts "#{player.name} has been healed by #{amt} HP!"
Room.action
end
end
end
The problem is in the "Room.action" call at the end of the self.heal method (or just "action" as was my first attempt, as can be seen in the self.mp_check method). When it is hit, the following error is received:
spells.rb:27:in `heal': undefined method `action' for KillShit::Room:Class (NoMethodError)
I thought that it might've been because I needed to define "action" as a class method with "self." but that isn't doing it. I also thought that it's because I'm trying to call "action" from the Class itself as opposed to the instance of the class with which I am working, and so I should call .action but I can't figure out how to do so.
Any ideas? What should I be reading up on to better understand the concept behind what's happening here?
Thanks!
You Should call the method Room#action like:
KillShit::Room.action
Just after a brief look - you did not defined class but instance method action.
Class methods need to by referenced with keyword self or class name directly, so you would need fix the definition this way:
module KillShit
class Room
…
def self.action
…
end
…
end
end
Updated answer:
module KillShit
class Room
def self.action
puts 'action in Room !'
end
end
class Spells
def self.heal(player)
Room.action
end
end
end
KillShit::Spells.heal('Wallace')
# action in Room !