The problem

From the Practice section of the web-apps README:

Cards. Make a page at /cards that deals a five-card hand from a shuffled deck, a new hand every time the page loads. Build the deck from these two arrays:

ranks = [2, 3, 4, 5, 6, 7, 8, 9, 10, "Jack", "Queen", "King", "Ace"]
suits = ["Clubs", "Diamonds", "Hearts", "Spades"]

Show each card's name ("Queen of Hearts"). Then show its picture: public/images/cards/ has one per card, named like queen_of_hearts.svg, and the page can show it with <img src="/images/cards/queen_of_hearts.svg">. Add a link that deals again.

You've dealt this hand before, in lab 5 of ruby-intro. That time it went to the terminal with puts; this time it goes on a page.

1. The route and the controller

get("/cards", { :controller => "cards", :action => "index" })
bin/rails generate controller cards
class CardsController < ApplicationController
  def index
    render({ :template => "cards/index" })
  end
end

2. The deck

A loop inside a loop: for every rank, every suit. That's 13 times 4, 52 cards. Start with an empty array and push each card onto it:

<% ranks = [2, 3, 4, 5, 6, 7, 8, 9, 10, "Jack", "Queen", "King", "Ace"] %>
<% suits = ["Clubs", "Diamonds", "Hearts", "Spades"] %>
<% deck = [] %>
<% for rank in ranks %>
  <% for suit in suits %>
    <% card = { "name" => "#{rank} of #{suit}", "image" => "#{rank}_of_#{suit}".downcase } %>
    <% deck.push(card) %>
  <% end %>
<% end %>

Each card needs two things: a name to show ("Queen of Hearts") and the name of its picture (queen_of_hearts). They differ in two ways, the spaces and the capital letters, so a hash holds both, the way a row from the database holds its columns. card["name"] and card["image"] read them the way company["name"] did.

"#{rank}_of_#{suit}" is "Queen_of_Hearts"; .downcase makes it "queen_of_hearts", the file's name. A number works too: "#{10}_of_Clubs".downcase is "10_of_clubs".

3. Shuffle, then deal five

<% hand = deck.shuffle[0, 5] %>

shuffle gives back the deck in a random order. [0, 5] takes five cards, starting from the first. Every refresh is a new request, so the view runs again and the deck is shuffled again.

This is why the hand comes from a shuffled deck, instead of picking five cards with rand the way the dice did: a deck has one Queen of Hearts. Five rand picks could pick the same card twice; five cards off the top of a shuffled deck can't. Two dice can match. Two cards in one hand can't.

4. The hand on the page

<h1>Cards</h1>
<p>
  <% for card in hand %>
    <img src="/images/cards/<%= card["image"] %>.svg" width="100" alt="<%= card["name"] %>">
  <% end %>
</p>
<ul>
  <% for card in hand %>
    <li><%= card["name"] %></li>
  <% end %>
</ul>
<a href="/cards">Deal again</a>

The alt text is what a screen reader says for the picture, and what the browser shows if the picture can't load. The card's name is exactly right for it.

The whole view

<% ranks = [2, 3, 4, 5, 6, 7, 8, 9, 10, "Jack", "Queen", "King", "Ace"] %>
<% suits = ["Clubs", "Diamonds", "Hearts", "Spades"] %>
<% deck = [] %>
<% for rank in ranks %>
  <% for suit in suits %>
    <% card = { "name" => "#{rank} of #{suit}", "image" => "#{rank}_of_#{suit}".downcase } %>
    <% deck.push(card) %>
  <% end %>
<% end %>
<% hand = deck.shuffle[0, 5] %>
<h1>Cards</h1>
<p>
  <% for card in hand %>
    <img src="/images/cards/<%= card["image"] %>.svg" width="100" alt="<%= card["name"] %>">
  <% end %>
</p>
<ul>
  <% for card in hand %>
    <li><%= card["name"] %></li>
  <% end %>
</ul>
<a href="/cards">Deal again</a>

That's a lot of Ruby for a view. It works, and for one page it's fine. Next week, Ruby that works something out moves into the controller, and the view goes back to mostly showing it.

When it went wrong

  • Broken images. The file name: lowercase, underscores, .svg on the end. View the page source and compare one src with a file name in public/images/cards/.
  • The same card twice in a hand. Five rand picks instead of one shuffled deck.
  • 52 cards on the page. The loop over deck instead of hand.
  • "Queen of Hearts" in the image name. The name used where the image belongs: spaces and capitals, so no file matches.