El código que sigue implanta un jugador de tres-en-raya.
[~/sinatra/sinatra-tictactoe/sinatra-tictactoe-ajax(master)]$ tree
.
|--- Gemfile
|--- Gemfile.lock
|--- Procfile
|--- Rakefile
|--- Readme.md
|--- app.rb
|--- public
|   |--- css
|   |   |--- app.css
|   |   `--- style.css
|   |--- images
|   |   |--- blackboard.jpg
|   |   |--- circle.gif
|   |   `--- cross.gif
|   `--- js
|       `--- app.js
`--- views
    |--- final.erb
    |--- final.haml
    |--- game.erb
    |--- game.haml
    |--- layout.erb
    |--- layout.haml
    `--- styles.scss
5 directories, 19 files
[~/sinatra/sinatra-tictactoe/sinatra-tictactoe-ajax(master)]$ cat Rakefile desc "run server" task :default do sh "bundle exec ruby app.rb" end desc "install dependencies" task :install do sh "bundle install" end ### desc 'build css' task :css do sh "sass views/styles.scss public/css/style.css" end
[~/sinatra/sinatra-tictactoe/sinatra-tictactoe-ajax(master)]$  cat views/game.haml 
.screen
  .gameboard
    - HORIZONTALS.each do |row| 
      .gamerow
        - row.each do |p|
          %a(href=p)
            %div{:id => "#{p}", :class => "cell #{b[p]}"}
    .message
      %h1= m
[~/sinatra/sinatra-tictactoe/sinatra-tictactoe-ajax(master)]$ cat views/layout.haml 
!!!
%html
  %head
    %title tic tac toe
    -#%link{:rel=>"stylesheet", :href=>"/css/app.css", :type=>"text/css"}
    -# dynamically accessed
    -#%link{:rel=>"stylesheet", :href=>"/styles.css", :type=>"text/css"}    
    -# statically compiled
    %link{:rel=>"stylesheet", :href=>"css/style.css", :type=>"text/css"} 
    %script{:type=>"text/javascript", :src=>"http://ajax.googleapis.com/ajax/libs/jquery/1.6.4/jquery.min.js"}
    %script{:type=>"text/javascript", :src=>"/js/app.js"}
  %body
    = yield
styles.scss puede compilarse dinámicamente. Véase el fragmento de código 
que empieza por
get '/styles.css' do
en app.rb
<!DOCTYPE html>
<html>
  <head>
    <title>tic tac toe</title>
    <link href='css/style.css' rel='stylesheet' type='text/css'>
    <script src='http://ajax.googleapis.com/ajax/libs/jquery/1.6.4/jquery.min.js' type='text/javascript'></script>
    <script src='/js/app.js' type='text/javascript'></script>
  </head>
  <body>
    <div class='screen'>
      <div class='gameboard'>
        <div class='gamerow'>
          <a href='a1'>
            <div class='cell ' id='a1'></div>
          </a>
          <a href='a2'>
            <div class='cell ' id='a2'></div>
          </a>
          <a href='a3'>
            <div class='cell ' id='a3'></div>
          </a>
        </div>
        <div class='gamerow'>
          <a href='b1'>
            <div class='cell ' id='b1'></div>
          </a>
          <a href='b2'>
            <div class='cell circle' id='b2'></div>
          </a>
          <a href='b3'>
            <div class='cell ' id='b3'></div>
          </a>
        </div>
        <div class='gamerow'>
          <a href='c1'>
            <div class='cell ' id='c1'></div>
          </a>
          <a href='c2'>
            <div class='cell ' id='c2'></div>
          </a>
          <a href='c3'>
            <div class='cell cross' id='c3'></div>
          </a>
        </div>
        <div class='message'>
          <h1></h1>
        </div>
      </div>
    </div>
  </body>
</html>
![[*]](crossref.png) 
~/sinatra/sinatra-tictactoe/sinatra-tictactoe-ajax(master)]$ cat views/styles.scss 
$red:   #903;
$black: #444;
$white: #fff;
$ull:   #9900FF;
$pink:  #F9A7B0;
$main-font: Helvetica, Arial, sans-serif;
$message-font: 22px/1;
$board-left: 300px;
$board-margin: 0 auto;
$board-size: 500px;
$opacity:  0.8;
$cell-width:    $board-size/8.5;
$cell-height:   $board-size/8.5;
$cell-margin:  $cell-width/12;
$cell-padding:  $cell-width/1.3;
$background: "/images/blackboard.jpg";
$cross:      "/images/cross.gif";
$circle:     "/images/circle.gif";
body       { 
             // background-color: lightgrey; 
             font-family: $main-font;
             background: url($background) repeat; background-size: cover; 
           }
.gameboard { //margin-left: $board-left; 
             width: $board-size;
             margin: $board-margin;
             text-align:center;
           }
.gamerow   { clear: both; }
.cell      { color: blue; 
             background-color: white; 
             opacity: $opacity;
             width: $cell-width; 
             height: $cell-height; 
             margin: $cell-margin; 
             padding: $cell-padding; 
             &:hover {
               color: black ;
               background-color: $ull;
             }
             float: left; 
           }
@mixin game-piece($image) {
  background: url($image)  no-repeat; background-size: cover; 
}
.cross     { @include game-piece($cross); }
.circle    { @include game-piece($circle); }
.base-font { color: $pink; font: $message-font $main-font; }
.message   { 
             @extend .base-font;
             display: inline;
             background-color:transparent;
           }
In order to declare the processes that make our app, and scale them individually, we need to be able to tell Heroku what these processes are.
The Procfile is a simple YAML file which sits in the root of your application code and is pushed to your application when you deploy. This file contains a definition of every process you require in your application, and how that process should be started.
[~/sinatra/sinatra-tictactoe/sinatra-tictactoe-ajax(master)]$ cat Procfile #web: bundle exec unicorn -p $PORT -E $RACK_ENV #web: bundle exec ruby app.rb -p $PORT web: bundle exec ruby app.rb #web: bundle exec thin startVéase The Procfile is your friend
[~/sinatra/sinatra-tictactoe/sinatra-tictactoe-ajax(master)]$ cat Gemfile source "https://rubygems.org" gem "sinatra" gem 'haml' gem "sass", :require => 'sass' gem 'thin'
[~/sinatra/sinatra-tictactoe/sinatra-tictactoe-ajax(master)]$  cat app.rb 
require 'sinatra'
require 'sass'
require 'pp'
settings.port = ENV['PORT'] || 4567
enable :sessions
#use Rack::Session::Pool, :expire_after => 2592000
#set :session_secret, 'super secret'
#configure :development, :test do
#  set :sessions, :domain => 'example.com'
#end
#configure :production do
#  set :sessions, :domain => 'herokuapp.com'
#end
module TicTacToe
  HUMAN = CIRCLE = "circle" # human
  COMPUTER = CROSS  = "cross"  # computer
  BLANK  = ""
  HORIZONTALS = [ %w{a1 a2 a3},  %w{b1 b2 b3}, %w{c1 c2 c3} ]
  COLUMNS     = [ %w{a1 b1 c1},  %w{a2 b2 c2}, %w{a3 b3 c3} ]
  DIAGONALS   = [ %w{a1 b2 c3},  %w{a3 b2 c1} ]
  ROWS = HORIZONTALS + COLUMNS + DIAGONALS
  MOVES       = %w{a1    a2   a3   b1   b2   b3   c1   c2   c3}
  def number_of(symbol, row)
    row.find_all{ |s| session["bs"][s] == symbol }.size 
  end
  def inicializa
    @board = {}
    MOVES.each do |k|
      @board[k] = BLANK
    end
    @board
  end
  def board
    session["bs"]
  end
  def [] key
    board[key]
  end
  def []= key, value
    board[key] = value
  end
  def each 
    MOVES.each do |move|
      yield move
    end
  end
  def legal_moves
    m = []
    MOVES.each do |key|
      m << key if board[key] == BLANK
    end
    puts "legal_moves: Tablero:  #{board.inspect}"
    puts "legal_moves: m:  #{m}"
    m # returns the set of feasible moves [ "b3", "c2", ... ]
  end
  def winner
    ROWS.each do |row|
      circles = number_of(CIRCLE, row)  
      puts "winner: circles=#{circles}"
      return CIRCLE if circles == 3  # "circle" wins
      crosses = number_of(CROSS, row)   
      puts "winner: crosses=#{crosses}"
      return CROSS  if crosses == 3
    end
    false
  end
  def smart_move
    moves = legal_moves
    ROWS.each do |row|
      if (number_of(BLANK, row) == 1) then
        if (number_of(CROSS, row) == 2) then # If I have a win, take it.  
          row.each do |e|
            return e if board[e] == BLANK
          end
        end
      end
    end
    ROWS.each do |row|
      if (number_of(BLANK, row) == 1) then
        if (number_of(CIRCLE,row) == 2) then # If he is threatening to win, stop it.
          row.each do |e|
            return e if board[e] == BLANK
          end
        end
      end
    end
    # Take the center if open.
    return "b2" if moves.include? "b2"
    # Defend opposite corners.
    if    self["a1"] != COMPUTER and self["a1"] != BLANK and self["c3"] == BLANK
      return "c3"
    elsif self["c3"] != COMPUTER and self["c3"] != BLANK and self["a1"] == BLANK
      return "a1"
    elsif self["a3"] != COMPUTER and self["a3"] != BLANK and self["c1"] == BLANK
      return "c1"
    elsif self["c1"] != COMPUTER and self["c3"] != BLANK and self["a3"] == BLANK
      return "a3"
    end
    
    # Or make a random move.
    moves[rand(moves.size)]
  end
  def human_wins?
    winner == HUMAN
  end
  def computer_wins?
    winner == COMPUTER
  end
end
helpers TicTacToe
get %r{^/([abc][123])?$} do |human|
  if human then
    puts "You played: #{human}!"
    puts "session: "
    pp session
    if legal_moves.include? human
      board[human] = TicTacToe::CIRCLE
      # computer = board.legal_moves.sample
      computer = smart_move
      redirect to ('/humanwins') if human_wins?
      redirect to('/') unless computer
      board[computer] = TicTacToe::CROSS
      puts "I played: #{computer}!"
      puts "Tablero:  #{board.inspect}"
      redirect to ('/computerwins') if computer_wins?
    end
  else
    session["bs"] = inicializa()
    puts "session = "
    pp session
  end
  haml :game, :locals => { :b => board, :m => ''  }
end
get '/humanwins' do
  puts "/humanwins session="
  pp session
  begin
    m = if human_wins? then
          'Human wins'
        else 
          redirect '/'
        end
    haml :final, :locals => { :b => board, :m => m }
  rescue
    redirect '/'
  end
end
get '/computerwins' do
  puts "/computerwins"
  pp session
  begin
    m = if computer_wins? then
          'Computer wins'
        else 
          redirect '/'
        end
    haml :final, :locals => { :b => board, :m => m }
  rescue
    redirect '/'
  end
end
not_found do
  puts "not found!!!!!!!!!!!"
  session["bs"] = inicializa()
  haml :game, :locals => { :b => board, :m => 'Let us start a new game'  }
end
get '/styles.css' do
  scss :styles
end
Casiano Rodríguez León