Sinatra Blackjack
Step up and place your bets! I’ve created a simple Blackjack game here: https://salty-crag-8411.herokuapp.com/. I hope you’ll give it a try! I built it with Sinatra and deployed it to Heroku.
Like Rails, Sinatra is a Domain-Specific Language, or DSL, based on Ruby. Sinatra uses a simple syntax for specifying routes with Ruby blocks, which are outlined on Sinatra’s Getting Started page.
In my Sinatra Blackjack game, these routes help control the flow of the Blackjack game, as the player competes with the dealer for the highest Blackjack score (21). Just like Rails, it uses the MVC pattern to render or redirect the appropriate page. Sinatra is a pure joy to work with; simple but powerful! Here’s how I used it to build a simple Blackjack game.
Game Start
Sinatra serves the ‘/’ (root) route, and evaluates the conditional in the 'do'
block:
get '/' do
if session[:player_name]
redirect '/game'
else
redirect '/new_player'
end
end
If the session[:player_name]
contains a value (because the user has entered a name into the input form), the game is redirected to the /game
view. If it’s empty, the user must enter a name, so it redirects to the /new_player
route.
get '/new_player' do
erb :new_player
end
post '/new_player' do
if params[:player_name].empty? || params[:player_name].to_i != 0
@error = "Please enter your name."
halt erb(:new_player)
else
session[:player_name] = params[:player_name]
session[:player_bankroll] = INITIAL_PLAYER_BANLROLL
redirect '/bet'
end
end
The app does not use a database. Instead, the following types of session[:id]
(cookies) are used to store persistent data:
[:player_bankroll]
: The player’s total amount of money available to bet[:player_name]
: The player’s name[:bet_amount]
: The value of the player’s bet[:dealer_cards]
: The dealer’s hand[:player_cards]
: The player’s hand[:deck]
: The deck[:turn]
: Whose turn it is to hit/stand
When a user vists https://salty-crag-8411.herokuapp.com/, he or she is prompted for a name. This name is stored in session[:player_name]
.
Next, the player is prompted for a bet.
The player is taken to “/bet” and the ‘bet.html.erb’ view is rendered:
get '/bet' do
erb :bet
end
post '/bet' do
if session[:player_bankroll].to_i == 0
redirect '/no_money'
elsif params[:bet_amount].empty? || params[:bet_amount].to_i == 0
@error = "You must place a bet."
halt erb(:bet)
elsif (params[:bet_amount].to_i < 5) || (params[:bet_amount].to_i > session[:player_bankroll].to_i)
@error = "Your bet must be between $5 and $#{session[:player_bankroll]}."
halt erb(:bet)
else
session[:bet_amount] = params[:bet_amount]
redirect '/game'
end
After the player enters a bet amount, a series of checks are performed:
- If the player has no money, the game is redirected to ‘/no_money’ and the game is over.
- If the player tries to enter anything other than a number, the dealer asks for the player to try again.
- If the bet is less than the minimum ($5), or more than the player’s total bankroll, the player is asked to bet again.
- If the bet is valid, the bet amount is saved as
session[:bet_amount]
, and the player is directed to the ‘/game’ route.
In the '/game'
route, the cards are shuffled and dealt.
- If the player has 21, it’s a Blackjack.
- If the player’s cards total more than 21, it’s a bust.
- Oherwise the player is offered the chance to hit or stand.
get '/game' do
session[:turn] = session[:player_name]
# create a deck and put it in session
suits = ["H", "D", "C", "S"]
values = ["2", "3", "4", "5", "6", "7", "8", "9", "10", "J", "Q", "K", "A"]
session[:deck] = suits.product(values).shuffle!
# deal cards
session[:dealer_cards] = []
session[:player_cards] = []
session[:dealer_cards] << session[:deck].pop
session[:player_cards] << session[:deck].pop
session[:dealer_cards] << session[:deck].pop
session[:player_cards] << session[:deck].pop
player_total = calculate_total(session[:player_cards])
if player_total == BLACKJACK_AMOUNT
winner!("#{session[:player_name]} hit Blackjack!")
elsif player_total > BLACKJACK_AMOUNT
loser!("#{session[:player_name]} busted at #{player_total}.")
end
erb :game
end
The Player’s Turn
If the player chooses to hit, he/she is redirected to /game/player/hit
.
post '/game/player/hit' do
session[:player_cards] << session[:deck].pop
player_total = calculate_total(session[:player_cards])
if player_total == BLACKJACK_AMOUNT
winner!("#{session[:player_name]} hit Blackjack!")
elsif player_total > BLACKJACK_AMOUNT
loser!("#{session[:player_name]} busted at #{player_total}.")
end
erb :game, layout: false
end
If the player chooses to stand, he/she is redirected to /game/player/stand
.
post '/game/player/stand' do
@success = "#{session[:player_name]} stands."
@show_hit_or_stand_buttons = false
redirect '/game/dealer'
end
The Dealer’s Turn
Now, it’s the dealer’s turn. The dealer must hit until he/she has at least 17. We can use a constant, DEALER_MINIMUM_HIT_AMOUNT
, to represent this requirement.
get '/game/dealer' do
session[:turn] = "dealer"
@show_hit_or_stand_buttons = false
dealer_total = calculate_total(session[:dealer_cards])
if dealer_total == BLACKJACK_AMOUNT
loser!("Dealer hit Blackjack.")
elsif dealer_total > BLACKJACK_AMOUNT
winner!("Dealer busted at #{dealer_total} .")
elsif dealer_total >= DEALER_MINIMUM_HIT_AMOUNT
redirect '/game/compare'
else
@show_dealer_hit_button = true
end
erb :game, layout: false
end
Until the dealer reaches at least 17, he/she must hit:
post '/game/dealer/hit' do
session[:dealer_cards] << session[:deck].pop
redirect '/game/dealer'
end
Choosing a Winner
Finally, if neither the player nor the dealer have hit Blackjack or busted, we need a way to compare the values of their hands, and declare a winner:
get '/game/compare' do
@show_hit_or_stand_buttons = false
player_total = calculate_total(session[:player_cards])
dealer_total = calculate_total(session[:dealer_cards])
if player_total < dealer_total
loser!("#{session[:player_name]} stands at #{player_total}, and the dealer stands at #{dealer_total}.")
elsif player_total > dealer_total
winner!("#{session[:player_name]} stands at #{player_total}, and the dealer stands at #{dealer_total}.")
else
tie!("Both #{session[:player_name]} and the dealer stand at #{player_total}.")
end
erb :game, layout: false
end
Ending the Game
At the end of each match, the player is routed back to the beginning, where he/she is free to place a new bet or quit. There are two scenarios where he game must end:
- The player has money, but leaves the table
- The player is out of money
get '/no_money' do
erb :no_money
end
get '/game_over' do
erb :game_over
end
Both of these scenarios can be handled very easily with the appropriate views:
<h3>Thanks for playing!</h3>
<p>Sorry, <%= session[:player_name] %>. You have <span class="game_over_bankroll_amount">$<%= session[:player_bankroll] %></span>, and the minimum bet is $5...If you want to try your luck again, please see the Cashier for more chips first.</p>
<br />
<a class="btn btn-info btn-large" href="/new_player"><i class="icon-user"></i> Start Over</a>
<h3>Thanks for playing!</h3>
<h4>You're walking away with <span class="game_over_bankroll_amount"> $<%= session[:player_bankroll] %></span>. Looks like you're ready to hit Vegas!</h4>
<p>(Unless you've changed your mind, in which case you might want to keep playing)...?</p>
<br />
<a class="btn btn-info btn-large" href="/bet"><i class="icon-user"></i> Keep Playing</a>
Ajax!
One cool bonus, which you should notice as you play the game, is that the entire page does not reload with each action. The application includes jQuery code to prevent all but essential reloads for the following game actions:
- when the player hits
- when the player stands
- when the dealer hits
$(document).ready(function() {
player_hits();
player_stands();
dealer_hits();
});
function player_hits() {
$(document).on("click", "form#hit_form input", function(){
$.ajax({
type: "POST",
url: "/game/player/hit"
}).done(function(msg) {
$('#game').replaceWith(msg);
});
return false;
});
};
function player_stands() {
$(document).on("click", "form#stand_form input", function(){
$.ajax({
type: "POST",
url: "/game/player/stand"
}).done(function(msg) {
$('#game').replaceWith(msg);
});
return false;
});
};
function dealer_hits() {
$(document).on("click", "form#dealer_hit input", function(){
$.ajax({
type: "POST",
url: "/game/dealer/hit"
}).done(function(msg) {
$('#game').replaceWith(msg);
});
return false;
});
};
This provides for a smoother user experience, and it saves some bandwidth. I hope you’ve enjoyed this tour of my first Sinatra web app. I had a lot fun building it.
If you enjoyed this post, you might want to subscribe, so you don't miss the next one!