Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Luke's RPS Challenge #2116

Open
wants to merge 18 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 2 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ source 'https://rubygems.org'
ruby '3.0.2'

gem 'sinatra'
gem 'rerun'
gem 'puma'

group :test do
gem 'capybara'
Expand Down
14 changes: 14 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -15,25 +15,37 @@ GEM
xpath (~> 3.2)
diff-lcs (1.4.4)
docile (1.4.0)
ffi (1.15.5)
listen (3.7.1)
rb-fsevent (~> 0.10, >= 0.10.3)
rb-inotify (~> 0.9, >= 0.9.10)
mini_mime (1.1.1)
mini_portile2 (2.6.1)
mustermann (1.1.1)
ruby2_keywords (~> 0.0.1)
nio4r (2.5.8)
nokogiri (1.12.3)
mini_portile2 (~> 2.6.1)
racc (~> 1.4)
parallel (1.20.1)
parser (3.0.2.0)
ast (~> 2.4.1)
public_suffix (4.0.6)
puma (5.6.4)
nio4r (~> 2.0)
racc (1.5.2)
rack (2.2.3)
rack-protection (2.1.0)
rack
rack-test (1.1.0)
rack (>= 1.0, < 3)
rainbow (3.0.0)
rb-fsevent (0.11.1)
rb-inotify (0.10.1)
ffi (~> 1.0)
regexp_parser (2.1.1)
rerun (0.13.1)
listen (~> 3.0)
rexml (3.2.5)
rspec (3.10.0)
rspec-core (~> 3.10.0)
Expand Down Expand Up @@ -88,6 +100,8 @@ PLATFORMS

DEPENDENCIES
capybara
puma
rerun
rspec
rubocop (= 1.20)
simplecov
Expand Down
94 changes: 25 additions & 69 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,21 +1,31 @@
# RPS Challenge

Instructions
-------
This website allows one or two users to play a game of rock paper scissors. When one user plays alone they play against a computer that randomly picks a throw. When both players have throw their actions, the winner is decided, displayed on screena and has their score increased.

* Feel free to use google, your notes, books, etc. but work on your own
* If you refer to the solution of another coach or student, please put a link to that in your README
* If you have a partial solution, **still check in a partial solution**
* You must submit a pull request to this repo with your code by 9am Monday morning
## Installation

Task
----
```
$ git clone https://github.com/lukestorey95/rps-challenge.git
$ cd rps-challenge
$ bundle
```

## Usage

```
$ ruby app.rb
```

## How to play
Vist https://luke-rps.herokuapp.com/

Knowing how to build web applications is getting us almost there as web developers!
1. Enter player names, leave player 2 blank for single player vs computer
2. Click on rock, paper or scissors
3. Gloat over your success, or cry in your bitter defeat

The Makers Academy Marketing Array ( **MAMA** ) have asked us to provide a game for them. Their daily grind is pretty tough and they need time to steam a little.
![Design](/assets/RPS%20challenge.png)

Your task is to provide a _Rock, Paper, Scissors_ game for them so they can play on the web with the following user stories:
## User Stories

```
As a marketeer
Expand All @@ -25,62 +35,8 @@ I would like to register my name before playing an online game
As a marketeer
So that I can enjoy myself away from the daily grind
I would like to be able to play rock/paper/scissors
```

Hints on functionality

- the marketeer should be able to enter their name before the game
- the marketeer will be presented the choices (rock, paper and scissors)
- the marketeer can choose one option
- the game will choose a random option
- a winner will be declared


As usual please start by

* Forking this repo
* TEST driving development of your app

[You may find this guide to setting up a new Ruby web project helpful.](https://github.com/makersacademy/course/blob/main/pills/ruby_web_project_setup_list.md)

## Bonus level 1: Multiplayer

Change the game so that two marketeers can play against each other ( _yes there are two of them_ ).

## Bonus level 2: Rock, Paper, Scissors, Spock, Lizard

Use the _special_ rules ( _you can find them here http://en.wikipedia.org/wiki/Rock-paper-scissors-lizard-Spock_ )

## Basic Rules

- Rock beats Scissors
- Scissors beats Paper
- Paper beats Rock

In code review we'll be hoping to see:

* All tests passing
* High [Test coverage](https://github.com/makersacademy/course/blob/main/pills/test_coverage.md) (>95% is good)
* The code is elegant: every class has a clear responsibility, methods are short etc.

Reviewers will potentially be using this [code review rubric](docs/review.md). Referring to this rubric in advance may make the challenge somewhat easier. You should be the judge of how much challenge you want this at this moment.

Notes on test coverage
----------------------

Please ensure you have the following **AT THE TOP** of your spec_helper.rb in order to have test coverage stats generated
on your pull request:

```ruby
require 'simplecov'
require 'simplecov-console'

SimpleCov.formatter = SimpleCov::Formatter::MultiFormatter.new([
SimpleCov::Formatter::Console,
# Want a nice code coverage website? Uncomment this next line!
# SimpleCov::Formatter::HTMLFormatter
])
SimpleCov.start
```

You can see your test coverage when you run your tests. If you want this in a graphical form, uncomment the `HTMLFormatter` line and see what happens!
As a marketer
When I don't have any friends
I would like to be able to play against the computer
```
48 changes: 48 additions & 0 deletions app.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
require "sinatra"
require "./lib/player"
require "./lib/game"
require "./lib/round"

class RPS < Sinatra::Base
get '/' do
erb :index
end

post "/names" do
player1 = Player.new(params[:player_1_name])
if params[:player_2_name].empty?
player2 = Computer.new
else
player2 = Player.new(params[:player_2_name])
end

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You mentioned finding it difficult to draw this logic into a Model - firstly, well done on achieving the functionality of the user stories!

One thing that may make this refactor simpler is thinking about what parts of your program actually need to know about the Player. It's the Game Model that does - in order to know Player 1's move, the Computer's move, or Player 2's move.

If your Game class was responsible for taking player name params as arguments when initialised, you could use a default argument for player 2 - defaulting to nil if no player 2 name is provided.

Additionally in your Game logic, the code that handles comparing the two player moves to calculate the winner, could then determine if it's going to be using player2's move or generating a computer move if player2 is nil.

What do you think?

$game = Game.new(player1, player2)
redirect '/play'
end

get '/play' do
@game = $game

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm sure you had your reasons, but it looks to me like you could have avoided a global variable by using a session?

erb :play
end

post '/rps' do
@game = $game

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you have to assign this twice? Already assigned on line 23.

@game.new_round
@game.turn.throw(params[:throw].to_sym)
@game.switch_turn
@game.act_for_computer
@game.calculate_outcome

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a lot of method calls to keep in the route, when perhaps you could push the responsibility of calling each of these methods into the @GAMe instance, so you only need to call something like @game.generate_winner.


if @game.round.outcome.nil?
redirect '/play'
else
redirect '/result'
end
end

get '/result' do
@game = $game
erb :result
end

run! if app_file == $0
end
Binary file added assets/RPS challenge.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 2 additions & 0 deletions config.ru
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
require './app.rb'
run RPS
76 changes: 76 additions & 0 deletions lib/game.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
class Game
attr_reader :players, :turn, :round

def initialize(player_1, player_2, round = Round.new)
@players = [player_1, player_2]

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it have been cleaner to have two instance variables rather than an array?

@turn = players[0]
@round = round
end

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

are player_1 and player_2 methods necessary? It feels like these can just be instance variables of just stay as items in the players array.

def player_1
players.first
end

def player_2
players.last
end

def new_round
if round.outcome_decided?
@round = Round.new
players.each { |player| player.reset_action }
end
end

def switch_turn
if turn == player_1
@turn = players[1]
elsif turn == player_2
@turn = players[0]
end
end

def act_for_computer
if turn.computer?
turn.random_throw
switch_turn
end
end

def calculate_outcome
if player_1.thrown_action? && player_2.thrown_action?
rps_logic
end
end

private

def rps_logic
win_condition = { scissors: :paper, paper: :rock, rock: :scissors }

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Concise handling of game winner logic.


if player_1.action == player_2.action
round.set_outcome("draws with")
elsif win_condition[player_1.action] == player_2.action
round.set_winner(player_1)
round.set_looser(player_2)
round.winner.increase_score
calculate_outcome_message
else
round.set_winner(player_2)
round.set_looser(player_1)
round.winner.increase_score
calculate_outcome_message
end
end

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DRY


def calculate_outcome_message
case round.winner.action
when :rock
round.set_outcome('smashes')
when :paper
round.set_outcome('wraps')
when :scissors
round.set_outcome('cuts')
end
end
end

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like these messages

39 changes: 39 additions & 0 deletions lib/player.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
class Player
attr_reader :name, :score, :action

def initialize(name)
@name = name
@score = 0
end

def throw(throw)
@action = throw
end

def thrown_action?
!!action
end

def reset_action
@action = nil
end

def increase_score
@score += 1
end

def computer?
self.class == Computer
end
end

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is fancy! So you can create an instance of computer and copy in all the methods and values from Player? Love it

class Computer < Player
def initialize(name = "Computer")
@name = name
@score = 0
end

def random_throw
@action = [:rock, :paper, :scissors].sample
end
end
19 changes: 19 additions & 0 deletions lib/round.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
class Round
attr_reader :winner, :looser, :outcome

def outcome_decided?
!!outcome
end

def set_winner(winning_player)
@winner = winning_player
end

def set_looser(loosing_player)
@looser = loosing_player
end

def set_outcome(outcome_description)
@outcome = outcome_description
end
end
Binary file added public/images/paper.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/images/rock.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/images/scissors.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
27 changes: 27 additions & 0 deletions spec/features/rps_input_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
feature 'users can input their choice of action' do
before(:each) { players_sign_in("Luke", "Kirsty")}

scenario 'user clicks on rock, oppenent selects scissors' do
click_on 'rock'
click_on 'scissors'
expect(page).to have_content "Luke's rock smashes Kirsty's scissors"
end

scenario 'user clicks on paper, oppenent selects rock' do
click_on 'paper'
click_on 'rock'
expect(page).to have_content "Luke's paper wraps Kirsty's rock"
end

scenario 'user click on scissors, oppenent selects paper' do
click_on 'scissors'
click_on 'paper'
expect(page).to have_content "Luke's scissors cuts Kirsty's paper"
end

scenario 'user clicks on paper, opponent selects paper' do
click_on 'paper'
click_on 'paper'
expect(page).to have_content "Luke's paper draws with Kirsty's paper"
end
end
7 changes: 7 additions & 0 deletions spec/features/score_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
feature 'display scores' do
scenario 'new game displays player1 and player 2 scores as 0' do
players_sign_in("Luke", "Kirsty")
expect(page).to have_content "Luke: 0"
expect(page).to have_content "Kirsty: 0"
end
end