Archive for the ‘Ruby’ Category

¿Cómo diferenciar un buen desarrollador de software en Ruby del resto?

abril 11, 2017

Rails != Ruby

La primer diferencia entre un buen desarrollador de software en Ruby de uno malo no tan bueno es que el primero sabe que una cosa es Ruby y otra cosa es Rails. Ruby es un lenguaje de programación y Rails es sólo un framework para desarrollo web con Ruby.

Si alguien usa indistintamente (como si fueran sinónimos) Ruby y Rails (o RoR) ya sabemos que no es un buen desarrollador de software en Ruby y desafortunadamente hay muchos. Si tú quieres ser un buen desarrollador de software en Ruby lo primero que debes tener claro es que Ruby existe sin Rails pero Rails no existe sin Ruby, es decir, puedes desarrollar software usando Ruby sin usar Rails (incluso puedes desarrollar una aplicación web usando Ruby sin usar Rails) pero no puedes usar Rails con un lenguaje de programación que no sea Ruby.

Sólo usa Rails para desarrollo de aplicaciones web si te ves obligado

Rails es sin duda el framework más famoso para desarrollo de aplicaciones web con Ruby pero eso no significa que sea el mejor, de hecho es un framework que definitivamente yo no recomiendo si se trata de hacer una aplicación web profesional y robusta

Si alguien quiere aprender Ruby, lo peor que puede hacer es empezar con Rails. ¿Por qué opino que Rails es tan malo?; voy a exponer sólo algunos puntos y se que a quienes son fans de Rails estos puntos (ni cualquier otro argumento) los va a convencer, pero este post no está dirigido a ellos, como dice el dicho “No hay peor ciego que el que no quiere ver”.

Muchos de los que usan Rails creen saber Ruby y en la mayoría de los casos no es así, ejemplo: en muchas aplicaciones desarrolladas con Rails encontrarán en el código el uso de blank?. Abran una sesión de Ruby y tecleen algo como “prueba”.blank? y verán algo como NoMethodError: undefined method `blank’ for “prueba”:String. Eso es porque blank? no existe en Ruby, es una extensión de Rails. Pero muchos de los que usan Rails no saben eso, porque creen que blank? es una instrucción válida en Ruby.

Otra de las cosas por las que Rails no es un framework recomendable es porque está muy ligado a un ORM llamado Active Record y es uno de los peores ORM que existen para Ruby, el mejor ORM para Ruby que yo he usado es Sequel, seguido de DataMapper.

Active Record no soporta llaves primarias compuestas, el default es que la llave primaria de las tablas en la base de datos este formada por un solo campo llamado id, de tipo entero y además autoincrementable. Si en la Universidad tuviste un profesor(a) de Bases de Datos que más o menos supiera de lo que estaba hablando sabrás que una base de datos en la cual la llave primaria de todas sus tablas esté formada por un solo campo que además se llame igual en todas y tenga que ser de tipo entero es una base de datos que no te encontrarás en producción en el mundo real; a menos que esa base de datos la haya creado alguien que no tiene idea de lo que es el diseño de base de datos, o que sea una base de datos que usa una aplicación hecha en Rails.

Supongamos que tienes que desarrollar una aplicación web para el área de control escolar de una institución educativa. Tienes una tabla de asignaturas en donde cada asignatura tiene una clave que es un campo alfanumérico, tienes otra tabla de estudiantes en donde cada estudiante tiene una matrícula que también es un campo alfanumérico. Lo lógico es que no existan dos asignaturas con la misma clave y tampoco existan dos estudiantes con la misma matrícula. ¿No sería razonable que la llave primaria en la tabla de asignaturas fuera la clave de asignatura y en la tabla estudiantes la llave primaria fuera la matrícula?. Aquí hay un post que ejemplifica lo que hay que hacer en Rails para usar una llave primaria que no sea de tipo entero Non-Integer primary keys in Rails

Y dado que la relación entre estudiantes y asignaturas es del tipo muchos a muchos, tendremos una tabla para saber qué estudiantes están inscritos en una asignatura o visto de otra forma, todas las asignaturas en las que está inscrito un estudiante; ¿no sería lo mejor que la llave primaria de esta tabla estuviera formada por los campos clave de asignatura y matrícula? y además esos campos serían también llaves foráneas ya que son heredados de las tablas de asignaturas y estudiantes respectivamente. Para cualquiera que sepa lo básico de bases de datos relacionales esto es obvio.

Aquí están los diagramas Entidad-Relación (obvio las tablas tendrían más campos además de los que se muestran en el ejemplo y la base de datos contendría otras tablas además de estas)

Base de datos bien diseñada

Base de datos estilo Rails

Hay una gema llamada composite_primary_keys cuyo propósito es solucionar esta situación, pero no funciona 100% como debería.

El argumento de los fans de Rails es que no “luches” en contra de las “convenciones” de Rails, el problema aquí es que esas “convenciones” están mal diseñadas, si para desarrollar una aplicación web usando rails tengo que seguir sus “convenciones”, lo que implica que haga un mal diseño de base de datos (entre otras malas prácticas) o si intento hacer un diseño de base de datos decente tengo que estarme peleando con la herramienta y “parches” para hacer que las cosas funcionen como deben entonces lo mejor es no usar Rails.

Existen otros frameworks para desarrollo web en Ruby, Sinatra por ejemplo es un framework que a diferencia de Rails, no se interpone en tu camino. Eso si, Rails puede hacer algunas cosas automáticamente por ti y en Sinatra tu tienes que programar todo, así que podemos elegir entre usar un framework con el que desarrollemos una aplicación en la que escribamos menos código pero muy probablemente esté mal hecha (si seguimos sus “convenciones”) o usar un framework en el que todo el código lo hayamos tecleado nosotros pero las cosas pueden estar bien hechas (si nosotros las hacemos bien).

Si eres flojo, no te gusta mucho programar, o si no te importa que la aplicación que vas a desarrollar utilice una base de datos mal diseñada por ejemplo, puedes usar Rails, o si en tu trabajo te obligan a usarlo porque son de los que se van con lo que la mayoría usa sin conocer realmente las implicaciones de esa decisión.

Otra muestra de porque en mi opinión Rails no es un buen framework: Estos son los archivos y directorios que tendrías si usas Rails para desarrollar una aplicación web que lo único que haga sea mostrar la hora actual

Para desarrollar la misma aplicación web usando Sinatra sólo necesitamos un archivo con el siguiente código

require 'sinatra'

get '/' do
“La hora acutal es: #{Time.now}”
end

Si usas Sinatra es más fácil que sepas exactamente qué código es Ruby y qué codigo pertenece a instrucciones del ORM (sin importar si el ORM que usas es Active Record, DataMapper, Sequel, etc.), un buen desarrollador de software en Ruby jamás intentará usar blank? si no está desarrollando en Rails; pero ningún buen desarrollador de software en Ruby usaría Rails, a menos que en su trabajo se vea obligado desde luego.

Si tú eres un buen desarrollador de software en Ruby pero te obligan a usar Rails en tu trabajo te aconsejo que busques otro trabajo o que trates de convencer a tus jefes de que usen otro framework, aunque eso muchas veces es más difícil.

Si tú eres un buen desarrollador de software en Ruby y por lo tanto no usas Rails, te felicito, no conozco a muchos

Si programas en Ruby y hasta ahora has estado usando Rails cuando necesitas desarrollar una aplicación web, espero que esta información te ayude a empezar a usar otro framework, yo uso Sinatra pero existen otros como Cuba, Padrino, etc. Te invito a que investigues y encontrarás muchas más buenas razones para no seguir usando Rails

Para algunos existen buenas razones para usar Rails, aunque esas razones para mi no son suficientes, algunas de estas razones son que hay mucha más documentación para Rails que para otros frameworks, la mayoría de las ofertas de trabajo para desarrolladores de software en Ruby son usando Rails, etc. Estos argumentos son ciertos, y el último de ellos de hecho es lamentable, pero si tu objetivo es aprender y ser un buen desarrollador de software en Ruby y no tanto usar x framework porque es el que la mayoría usa o porque con eso te será más fácil encontrar un trabajo, entónces probablemte tampoco para ti esos argumentos sean suficientes.

Anuncios

Procesamiento de archivos con Ruby

febrero 1, 2017

En este post voy a mostrar un ejemplo de procesamiento de archivos con Ruby haciendo referencia a un caso real que se me presentó.

Se tiene un archivo csv llamado datos_clientes.csv y un archivo txt llamado solo_correos.txt; el archivo csv contiene una lista de clientes con datos como nombre, correo electrónico, teléfono, etc. y el archivo txt sólo contiene direcciones de correo electrónico.

El contenido del archivo datos_clientes.csv puede ser algo como esto:

NAME,E-MAIL
SALOMON RINCON TORRES,rtmex@yahoo.com
TOP SYSTEMS S.A DE C.V.,ventas@topsystems.com.mx
SALOMON RINCON TORRES,salomonrincon@softwarelibrepuebla.org
JOHN DOE,ejemplo@gmail.com

y el contenido del archivo solo_correos.txt podría parecerse a esto:

ventas@topsystems.com.mx
ejemplo@gmail.com
rtmex@yahoo.com

Imaginemos que ambos registros tienen cientos o miles de líneas

El objetivo es leer cada uno de los correos del archivo solo_correos.txt y buscarlo en el archivo datos_clientes.csv, si se encuentra, guardar el correo y el nombre del cliente al que pertenece dicho correo en un nuevo archivo csv (en este caso el archivo se llamará resultado.csv).

Aquí el código

require 'csv'

# Declara el encabezado para el archivo que se creará
encabezado = ['correo', 'nombre']
correo = nil
nombre = nil
num_linea = 0

CSV.open('resultado.csv', 'w', write_headers: true, headers: encabezado) do |archivo_nuevo|
    # Abre el archivo que contiene cada una de las líneas que se van a buscar en otro archivo
    File.open("solo_correos.txt") do |archivo|
         archivo.each do |linea|
             num_linea += 1
             puts "Linea #{num_linea} del archivo solo_correoa.txt = "+linea.strip
             # Abre un archivo delimitado por comas
             # En la segunda columna de este archivo, se busca la línea que se leyó del primer archivo
             CSV.foreach('datos_clientes.csv', headers: true) do |linea2|
                 if linea2[1].strip == linea.strip
                    puts "El correo "+linea.strip+" pertenece a "+linea2[0].strip
                    correo = linea.strip
                    nombre = linea2[0].strip
                    archivo_nuevo << [correo, nombre]
                    break
                 end
             end
         end
    end
end

El programa va mostrando el número de línea y su contenido conforme va procesando el archivo solo_correos.txt. Cada vez que en el archivo datos_clientes.csv encuentra el correo que leyó del archivo solo_correos.txt, muestra a que cliente pertenece dicho correo.

Lo que iría mostrando el programa mientras va procesando el archivo solo_correos.txt sería algo como esto:

Linea 3 del archivo solo_correos.txt = rtmex@yahoo.com
El correo rtmex@yahoo.com pertenece a SALOMON RINCON TORRES

Cuando el programa termina, tenemos un archivo csv llamado resultado.csv con 2 columnas, la primera contiene el correo electrónico y la segunda el nombre del cliente al que pertenece dicho correo.

El juego de gato en Ruby ahora para Web

marzo 20, 2012

En un post anterior (https://salomonrt.wordpress.com/2010/04/03/juego-de-gato-%C2%BFinvensible-en-ruby/) puse el código fuente del juego de gato en Ruby, dicho programa hay que ejecutarlo desde la terminal. Decidí hacer una versión modificada de ese código para que funcionara desde un navegador web (para lo cual utilicé Sinatra).

Aquí les dejo el código.

Programa principal

Archivo gatoweb.rb

#       Copyright 2012 SALOMON RINCON TORRES <rtmex@yahoo.com>
#
#       This program is free software; you can redistribute it and/or modify
#       it under the terms of the GNU General Public License as published by
#       the Free Software Foundation; either version 2 of the License, or
#       (at your option) any later version.
#       
#       This program is distributed in the hope that it will be useful,
#       but WITHOUT ANY WARRANTY; without even the implied warranty of
#       MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#       GNU General Public License for more details.
#       
#       You should have received a copy of the GNU General Public License
#       along with this program; if not, write to the Free Software
#       Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
#       MA 02110-1301, USA.

#encoding: utf-8
require 'sinatra'

opcion_programa = 0

# Inicializa las variablee globales
$mensaje = nil
$encabezado = "Juego de gato hecho en Ruby con Sinatra"
$tablero = Array.new(3){Array.new(3)}
$casillas_disponibles = (1..9).to_a
$gana_programa = false
$caracter_usuario = nil
$caracter_prog = nil

def inicializa
    $gana_programa = false
    # Inicializa el tablero
    for i in 0..2 do
        for j in 0..2 do
            $tablero[j][i]=nil
        end
    end
    $casillas_disponibles.clear
    for i in 1..9 do
        $casillas_disponibles.push(i)
    end
end

# Tira al azar en una esquina disponible
def tira_esquina
    opcion = 0
    esquina_seleccionada = 0
    # Tira al azar en una esquina, se basa en cuatro opciones:
    # opcion 1 será la esquina superior izquierda
    # opcion 2 será la esquina superior derecha
    # opcion 3 será la esquina inferior izquierda
    # opcion 4 será la esquina inferior derecha
    while not $casillas_disponibles.include?(esquina_seleccionada)
          opcion = rand(4)+1
          case opcion
               when 1 then esquina_seleccionada = 1
               when 2 then esquina_seleccionada = 3
               when 3 then esquina_seleccionada = 7
               when 4 then esquina_seleccionada = 9
          end
    end

    return esquina_seleccionada
end

# Verifica en que casilla debe tirar quién este jugando con el caracter que recibe como parámetro
# para ganar horizontalmente
def gana_horizontal(caracter)
    tiros_hechos = 0
    contador = 0
    columna = -1
    fila = 0
    ganador_en = 0

    # El siguiente ciclo termina cuando se hayan recorrido todos los renglónes o se haya encontrado
    # un renglón en donde se han hecho dos tiros
    while contador <= 2 and tiros_hechos < 2
        for j in 0..2 do
            if $tablero[j][contador]==caracter then
               tiros_hechos = tiros_hechos+1
            elsif $tablero[j][contador]==nil
                  columna = j
            end
        end
        # Si en el renglón actual había dos tiros y una casilla vacía, ahi se debe tirar para ganar
        if tiros_hechos == 2 and columna >= 0 then
           fila = contador
        else
            tiros_hechos = 0
            columna = -1
            contador = contador + 1
        end
    end

    if tiros_hechos == 2 then
       case fila
            when 0 then
                   # Significa que se encontró un tiro ganador en el primer renglón
                   ganador_en = columna + 1
            when 1 then
                   # Significa que se encontró un tiro ganador en el segundo renglón
                   ganador_en = columna + 4
            when 2 then
                   # Significa que se encontró un tiro ganador en el tercer renglón
                   ganador_en = columna + 7
       end
    end

    return ganador_en
end

# Verifica en que casilla debe tirar quién este jugando con el caracter que recibe como parámetro
# para ganar verticalmente
def gana_vertical(caracter)
    tiros_hechos = 0
    contador = 0
    columna = 0
    fila = -1
    ganador_en = 0

    # El siguiente ciclo termina cuando se hayan recorrido todos los renglónes o se haya encontrado
    # un renglón en donde se han hecho dos tiros
    while contador <= 2 and tiros_hechos < 2
        for j in 0..2 do
            if $tablero[contador][j]==caracter then
               tiros_hechos = tiros_hechos+1
            elsif $tablero[contador][j]==nil
                  fila = j
            end
        end
        # Si en la columna actual había dos tiros y una casilla vacía, ahi se debe tirar para ganar
        if tiros_hechos == 2 and fila >= 0 then
           columna = contador
        else
            tiros_hechos = 0
            fila = -1
            contador = contador + 1
        end
    end

    if tiros_hechos == 2 then
       case fila
            when 0 then
                   # Significa que se encontró un tiro ganador en el primer renglón
                   ganador_en = columna + 1
            when 1 then
                   # Significa que se encontró un tiro ganador en el segundo renglón
                   ganador_en = columna + 4
            when 2 then
                   # Significa que se encontró un tiro ganador en el tercer renglón
                   ganador_en = columna + 7
       end
    end

    return ganador_en
end

# Verifica en que casilla debe tirar quién este jugando con el caracter que recibe como parámetro
# para ganar diagonalmente
def gana_diagonal(caracter)
    tirosd1 = 0
    tirosd2 = 0
    contador = 0
    fila1 = -1
    fila2 = -1
    ganador_en = 0
    diagonal_1 = [0,1,2]
    diagonal_2 = [2,1,0]

    # El siguiente ciclo termina cuando se hayan recorrido todos los renglónes o se haya encontrado
    # que se han hecho dos tiros en una de las 2 diagonales diagonal
    while contador <= 2
          # Checa si hay un tiro a la casilla correspondiente en la diagonal 1
          if $tablero[diagonal_1.at(contador)][contador]==caracter then
             tirosd1 = tirosd1+1
          elsif $tablero[diagonal_1.at(contador)][contador]==nil
                fila1 = contador
          end

          # Checa si hay un tiro a la casilla correspondiente en la diagonal 2
          if $tablero[diagonal_2.at(contador)][contador]==caracter then
             tirosd2 = tirosd2+1
          elsif $tablero[diagonal_2.at(contador)][contador]==nil
                fila2 = contador
          end

          contador = contador+1
    end

    if tirosd1 == 2 and fila1>=0 then
       # Significa que se encontró el tiro ganador en la diagonal 1
       case fila1
            when 0 then
                   # Significa que se encontró el tiro ganador en el primer renglón
                   ganador_en = 1
            when 1 then
                   # Significa que se encontró el tiro ganador en el segundo renglón
                   ganador_en = 5
            when 2 then
                   # Significa que se encontró el tiro ganador en el tercer renglón
                   ganador_en = 9
       end
    elsif tirosd2 == 2 and fila2>=0 then
          # Significa que se encontró el tiro ganador en la diagonal 2
          case fila2
               when 0 then
                    # Significa que se encontró el tiro ganador en el primer renglón
                    ganador_en = 3
               when 1 then
                    # Significa que se encontró el tiro ganador en el segundo renglón
                    ganador_en = 5
               when 2 then
                    # Significa que se encontró el tiro ganador en el tercer renglón
                    ganador_en = 7
          end
    end

    return ganador_en
end

def renglon_tirou
    conta = 0
    renglones = Array.new

    for i in 0..2 do
        conta = 0
        for j in 0..2 do
            if $tablero[j][i]==$caracter_usuario then
               conta = conta+1
            end
        end
        if conta > 0 then
           renglones << i
        end
    end

    return renglones
end

# Ejecuta el tiro del programa
def tiro_programa
    veces_tiradas = 0
    espacios = 0
    renglon_seleccionado = -1
    columna = -1
    tiro = 0
    tiros_en_renglon = 0
    tiros_en_columna = 0
    tiros_usario = 0
    esquina_opuesta_ocupada = 1
    # Crea un array con las 4 esquinas en que puede intentar tirar
    esquinas_intentadas = [1,3,7,9]
    # Elimina del array las esquinas que no están disponibles
    esquinas_intentadas.delete_if {|elemento| not $casillas_disponibles.include?(elemento)}

    # Verifica cuántas veces ha tirado el programa
    for i in 0..2 do
        for j in 0..2 do
            if $tablero[j][i]==$caracter_prog then
               veces_tiradas = veces_tiradas+1
            end
        end
    end

    if veces_tiradas == 0 then
       # Primero intenta tirar al centro
       if $casillas_disponibles.include?(5) then
          tiro = 5
       else
           # Tira al azar en una esquina
           tiro = tira_esquina
       end
    else
        # Verifica si hay una casilla en la cual el programa gana horizontalmente y si es asi tira en ella
        tiro = gana_horizontal($caracter_prog)
        if tiro == 0 then
           # Significa que el programa no gana horizontalmente tirando en alguna casilla
           # Verifica si hay una casilla en la cual el programa gana verticalmente y si es asi tira en ella
           tiro = gana_vertical($caracter_prog)
           if tiro == 0 then
              # Significa que el programa no gana verticalmente tirando en alguna casilla
              # Verifica si hay una casilla en la cual el programa gana diagonalmente y si es asi tira en ella
              tiro = gana_diagonal($caracter_prog)
              if tiro == 0 then
                 # Significa que el programa no gana diagonalmente tirando en alguna casilla
                 # Entónces checa si el usuario gana diagonalmente tirando en alguna casilla y si es asi lo bloquea
                 tiro = gana_diagonal($caracter_usuario)
                 if tiro == 0
                    # Significa que el usuario no gana diagonalmente tirando en alguna casilla
                    # Entónces checa si el usuario gana horizontalmente tirando en alguna casilla y si es asi lo bloquea
                    tiro = gana_horizontal($caracter_usuario)
                    if tiro == 0 then
                       # Significa que el usuario no gana horizontalmente tirando en alguna casilla
                       # Entónces checa si el usuario gana verticalmente tirando en alguna casilla y si es asi lo bloquea
                       tiro = gana_vertical($caracter_usuario)
                       if tiro == 0 then
                          # Significa que el usuario no gana verticalmente tirando en alguna casilla
                          # Si todas las esquinas están disponibles, tira en alguna de ellas
                          if esquinas_intentadas.length==4 then
                             tiro = tira_esquina
                          end
                          # Si hay 3 esquinas disponibles,
                          # busca tirar en una esquina correspondiente a un renglón y columna
                          # en donde el usuario ya había tirado y que la esquina opuesta esté vacía
                          if esquinas_intentadas.length==3 and $casillas_disponibles.count<7 then
                             begin
                                  tiro = tira_esquina
                                  case tiro
                                       when 1,3 then
                                            if renglon_tirou.include?(0) then
                                               if tiro == 1 then
                                                  if $casillas_disponibles.include?(9) and $tablero[0].index($caracter_usuario) then
                                                     renglon_seleccionado=0
                                                  end
                                               else
                                                   if $casillas_disponibles.include?(7) and $tablero[2].index($caracter_usuario) then
                                                      renglon_seleccionado=0
                                                   end
                                               end
                                            end
                                       when 7,9 then
                                            if renglon_tirou.include?(2) then
                                               if tiro == 7 then
                                                  if $casillas_disponibles.include?(3) and $tablero[0].index($caracter_usuario) then
                                                     renglon_seleccionado=2
                                                  end
                                               else
                                                   if $casillas_disponibles.include?(1) and $tablero[2].index($caracter_usuario) then
                                                      renglon_seleccionado=2
                                                   end
                                               end
                                            end
                                  end
                             end until renglon_seleccionado>=0 
                          elsif tiro==0 then
                                # Entónces busca tirar en el renglón en donde el programa haya tirado el mayor número de veces
                                # o el renglón con el mayor número de casillas disponibles
                                casillas_disponibles = []
                                tiros_programa = []
                                max_disponibles = nil
                                max_tiros_programa = nil
                                 
                                # Guarda en el arreglo casillas_disponibles, el número de casillas disponibles
                                # para cada renglón, primero el número de casillas disponibles para el renglón 0
                                # después el número de casillas disponibles para el renglón 1 y al final
                                # el número de casillas disponibles para el renglón 2
                                # Guarda en el arreglo tiros_programa, el número de veces que el programa ha tirado
                                # en cada renglón, primero el número de veces en el renglón 0
                                # después el número de veces en el renglón 1 y al final
                                # el número de veces en el renglón 2
                                for i in 0..2 do
                                    espacios = 0
                                    veces_tiradas = 0
                                    for j in 0..2 do
                                        case $tablero[j][i]
                                             when nil then espacios = espacios+1
                                             when $caracter_prog then veces_tiradas = veces_tiradas+1
                                        end
                                    end
                                    casillas_disponibles.push(espacios)
                                    tiros_programa.push(veces_tiradas)
                                end
                                
                                # Guarda en la variable nmax_casillas_disp el número máximo de casillas disponibles
                                nmax_casillas_disp = casillas_disponibles.max
                                # Guarda en la variable max_disponibles el renglón en donde hay más casillas disponibles
                                max_disponibles = casillas_disponibles.index(casillas_disponibles.max)
                                # Busca si hay otro renglón que tenga el mismo número de casillas disponibles
                                # y que además el programa ya haya tirado en ese renglón previamente
                                max_disponibles_preferido = nil
                                for i in 0..2 do
                                    if casillas_disponibles[i] == nmax_casillas_disp then
                                       for j in 0..2 do
                                           if $tablero[j][i] == $caracter_prog then
                                              max_disponibles_preferido = i
                                              break
                                           end
                                       end
                                    end
                                end
                                
                                # Guarda en la variable max_tiros_programa el renglón en donde el programa ha tirado más veces
                                max_tiros_programa = tiros_programa.index(tiros_programa.max)
                                
                                if max_disponibles_preferido then
                                   renglon_seleccionado = max_disponibles_preferido
                                   for i in 0..2 do
                                       if $tablero[i][renglon_seleccionado] == nil then
                                          columna = i
                                          break
                                       end
                                   end
                                else
                                    # Entónces busca tirar en la columna en donde el programa haya tirado el mayor número de veces
                                    # o la columna con el mayor número de casillas disponibles
                                    casillas_disponibles = []
                                    tiros_programa = []
                                    max_disponibles = nil
                                    max_tiros_programa = nil
                                    
                                    # Guarda en el arreglo casillas_disponibles, el número de casillas disponibles
                                    # para cada columna, primero el número de casillas disponibles para la columna 0
                                    # después el número de casillas disponibles para la columna 1 y al final
                                    # el número de casillas disponibles para la columna 2
                                    for i in 0..2 do
                                        casillas_disponibles.push($tablero[i].count(nil))
                                    end
                                    # Guarda en la variable max_disponibles la columna en donde hay más casillas disponibles
                                    max_disponibles = casillas_disponibles.index(casillas_disponibles.max)
                                    
                                    # Guarda en el arreglo tiros_programa, el número de veces que el programa ha tirado
                                    # en cada columna, primero el número de veces en la columna 0
                                    # después el número de veces en la columna 1 y al final
                                    # el número de veces en la columna 2
                                    for i in 0..2 do
                                        tiros_programa.push($tablero[i].count($caracter_prog))
                                    end
                                    # Guarda en la variable max_tiros_programa la columna en donde el programa ha tirado más veces
                                    max_tiros_programa = tiros_programa.index(tiros_programa.max)
                                    
                                    if $tablero[max_tiros_programa].index(nil) and tiros_programa.uniq.count>1 then
                                       # Selecciona la columna en donde el programa ha tirado más veces
                                       columna = max_tiros_programa
                                    else
                                        # Como en todas las columnas el programa ha tirado igual número de veces
                                        # selecciona la columna con mas casillas disponibles
                                        columna = max_disponibles
                                    end                                 
                                    
                                    renglon_seleccionado = $tablero[columna].index(nil)
                                end
                                
                                case renglon_seleccionado
                                     when 0 then tiro = columna+1
                                     when 1 then tiro = columna+4
                                     when 2 then tiro = columna+7
                                end
                          end
                       end
                    end
                 end
              else
                  $gana_programa = true
              end
           else
               $gana_programa = true
           end
        else
            $gana_programa = true
        end
    end
    return tiro
end

# Procesa el tiro del programa
def procesa_tiro
    opcion_programa = tiro_programa
    case opcion_programa
         when 1 then $tablero[0][0] = $caracter_prog
         when 2 then $tablero[1][0] = $caracter_prog
         when 3 then $tablero[2][0] = $caracter_prog
         when 4 then $tablero[0][1] = $caracter_prog
         when 5 then $tablero[1][1] = $caracter_prog
         when 6 then $tablero[2][1] = $caracter_prog
         when 7 then $tablero[0][2] = $caracter_prog
         when 8 then $tablero[1][2] = $caracter_prog
         when 9 then $tablero[2][2] = $caracter_prog
    end
    # Elimina del array casillas_disponibles la posición en donde tiró el programa
    $casillas_disponibles.delete(opcion_programa)
    redirect '/imprime_tablero'
end

# menú principal
get '/' do
  inicializa
  erb :index
end

# permite al usuario seleccionar el caracter con el que va a tirar
# e indicar quién tira primero
post '/configura' do
  case params[:CaracterUsuario]
       when "X"
            $caracter_usuario = "X"
            $caracter_prog = "O"
       when "O"
            $caracter_usuario = "O"
            $caracter_prog = "X"
  end
  if params[:IniciaUsuario]
     redirect '/imprime_tablero'
  else
      if not $casillas_disponibles.empty? then
         procesa_tiro
      end
  end
end

# muestra el tablero
get '/imprime_tablero' do
  erb :tablero
end

# procesa el tiro del usuario
get '/casilla/:numero' do
  case params[:numero].to_i
       when 1 then $tablero[0][0] = $caracter_usuario
       when 2 then $tablero[1][0] = $caracter_usuario
       when 3 then $tablero[2][0] = $caracter_usuario
       when 4 then $tablero[0][1] = $caracter_usuario
       when 5 then $tablero[1][1] = $caracter_usuario
       when 6 then $tablero[2][1] = $caracter_usuario
       when 7 then $tablero[0][2] = $caracter_usuario
       when 8 then $tablero[1][2] = $caracter_usuario
       when 9 then $tablero[2][2] = $caracter_usuario
  end
  # Elimina del array casillas_disponibles la posición en donde tiró el usuario
  $casillas_disponibles.delete(params[:numero].to_i)
  if $casillas_disponibles.empty?
     redirect '/imprime_tablero'
  else
      procesa_tiro
  end
end

Hoja de estilos

Archivo public/stylesheets/style.css


html
{
background: #fff;
font: normal 18px Arial, Tahoma, Helvetica, sans-serif;
margin: 0;
padding: 0;
height: 100%;
}

body
{
margin: 0;
min-height: 100%;
position: relative;
}

#principal
{
width: 800px;
margin-left: auto;
margin-right: auto;
margin-top: 0px;
margin-bottom: 0px;
padding: 0px;
padding-bottom: 90px;
}

#encabezado
{
width: 100%;
padding: 15px 0 0 45px;
height: 80px;
}

#contenido
{
width: 900px;
background-color: #FFFFFF;
margin-left: auto;
margin-right: auto;
margin-top: 25px;
margin-bottom: 0px;
}

#contenido table
{
font: normal 80px Arial, Tahoma, Helvetica, sans-serif;
margin: auto;
}

#contenido a
{
text-decoration:  none;
}

td.colcentro
{
border-left: 3px solid black;
border-right: 3px solid black;
}

td.rowcentro
{
border-top: 3px solid black;
border-bottom: 3px solid black;
}

.numero
{
text-align: center;
color: white;
}

h1, h2, h3, p
{
padding: 0;
margin: 0;
}

.division
{
margin: 25px 0;
height: 1px;
width: 900px;
border: 0;
background: black;
background: -webkit-gradient(linear, 0 0, 100% 0, from(white), to(white), color-stop(50%, black));
text-align: left;
}

Vistas

Archivo views/layout.erb


<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>Juego de gato</title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="description" content="Juego de gato" />
<meta name="keywords" content="Gato, Sinatra, ruby, tic tac toe" />
<link href="/stylesheets/style.css" rel="stylesheet" type="text/css" />

</head>
<body>
<%= yield %>
</body>
</html>

Archivo views/index.erb

<div id="principal">
	<div id="encabezado"><h1><%= $encabezado %></h1></div>
		<p><h2>Inicio</h2>
	    <hr class="division" />
	    <div id="contenido">
	    <form action="/configura" method="post" accept-charset="utf-8">
		    <lable>Caracter con el que tira el usuario</label>
		    <input type="radio" name="CaracterUsuario" value="X" checked>X
		    <input type="radio" name="CaracterUsuario" value="O">O
		    <br>
		    <label>Tira primero el usuario</label><input type="checkbox" name="IniciaUsuario" id="IniciaUsuario" value="true"/>
			<br><br>
			<p>
			   <input type="submit" value="Aceptar"/>
			</p>  
		</form>
		</div>
</div>

Archivo views/tablero.erb

<div id="principal">
	<div id="encabezado"><h1><%= $encabezado %></h1></div>
	    <p><h2>Tablero</h2>
	    <hr class="division" />
	    <div id="contenido">
		    <table class="tablero">
                  <% num = 1
                     for i in 0..2 do %>
                         <tr>
                            <% for j in 0..2 do %>
                                <% case num 
                                        when 2, 8 %>
                                             <td class="colcentro">
                                     <% when 4, 6 %> 
                                             <td class="rowcentro">
                                     <% when 5 %>
                                             <td class="colcentro rowcentro">
                                     <% else %>
                                             <td>
                                <% end 
                                   if $tablero[j][i]==nil then
                                      if !$gana_programa then %>
                                         <a href="casilla/<%= num %>" class="numero"><%= num %></a>
                                   <% else %>
                                       <p class="numero"><%= num %></p>
                                   <% end
                                   else %>
                                       <%= $tablero[j][i] %>
                                <% end
                                   num = num+1 %>
                                   </td>
                            <% end %>
                         </tr>
                  <% end %>
		    </table>
		    <br>
		    <a href="/">Inicio</a>
		 <% if $casillas_disponibles.empty? or $gana_programa then
                       if $gana_programa then %>
                          <p>Gana el programa!</p>
                    <% else %>
                          <p>No hay ganador!</p>
                    <% end
               end %>
	</div>
</div>

Instalar Sinatra + PostgreSQL + Datamapper en Debian Squeeze

septiembre 28, 2011

Sinatra es un DSL para desarrollo web utilizando el lenguaje de programación Ruby. A diferencia de Rails, Sinatra NO obliga a seguir el patrón Modelo Vista Controlador (aunque es posible desarrollar aplicaciones web de ese tipo con Sinatra). He notado que la mayoría de las personas que desarrollan aplicaciones web con acceso a base de datos, utilizan MySQL o incluso SQLite, pero yo prefiero PostgreSQL y no hay razón por la que no se pueda utilizar con Sinatra, así que en este post voy a utilizar ambos junto con un ORM llamado Datamapper para crear los modelos y erb para crear las vistas.

Instalar Ruby

Debian Squeeze trae la versión 1.8.7 de Ruby y la versión 1.9.2, vamos a instalar la versión más reciente (note que el paquete debian se llama ruby1.9.1 pero trae la versión 1.9.2 de Ruby)

sudo apt-get install ruby1.9.1

Una vez instalado, creamos un enlace simbólico al que llamaremos simplemente ruby

cd /usr/bin
sudo ln -s ruby1.9.1 ruby

Instalar RubyGems

De la página http://rubygems.org/pages/download descargamos el archivo .zip o .tgz y lo descomprimimos. Si se descarga el archivo rubygems-1.8.10.tgz, al descomprimirlo se crea un directorio rubygems-1.8.10, hay que cambiarse a ese directorio y ejecutar

sudo ruby setup.rb

Ahora creamos un enlace simbólico al que llamaremos gem

cd /usr/bin
sudo ln -s gem1.9.1 gem

Instalar Sinatra

sudo gem install sinatra

Ya tenemos instalado Sinatra, para probarlo, haremos un programa en Ruby con el clásico hola mundo :). El archivo lo guardamos con el nombre holamundo.rb


# holamundo.rb
require 'sinatra'

get '/' do
'Hola Mundo!'
end

Ahora desde la terminal tecleamos

ruby holamundo.rb

Y aparece lo siguiente:

== Sinatra/1.2.6 has taken the stage on 4567 for development with backup from WEBrick
[2011-09-26 13:39:22] INFO  WEBrick 1.3.1
[2011-09-26 13:39:22] INFO  ruby 1.9.2 (2010-08-18) [i486-linux]
[2011-09-26 13:39:22] INFO  WEBrick::HTTPServer#start: pid=3076 port=4567

Abrimos un navegador web y nos vamos a

http://localhost:4567

y veremos el texto Hola Mundo!

Para parar la ejecución, vamos a la terminal y tecleamos Ctrl-C y aparece

== Sinatra has ended his set (crowd applauds)
[2011-09-26 13:40:50] INFO  going to shutdown …
[2011-09-26 13:40:50] INFO  WEBrick::HTTPServer#start done.

Si en lugar del servidor web WEBrick queremos utilizar Thin, debemos instalar los paquetes ruby1.9.1-dev y build-essential

sudo apt-get install ruby1.9.1-dev build-essential

y ahora si instalamos Thin

sudo gem install thin

Instalar PostgreSQL

sudo apt-get install postgresql postgresql-contrib libpq-dev

Debemos poner un password al usuario postgres

sudo passwd postgres

Nos cambiamos al usuario postgres y asignamos un password al usuario postgres dentro de PostgreSQL

su postgres
psql -c “ALTER USER postgres WITH PASSWORD ‘password_aqui'” -d template1

Aparece ALTER ROLE y nos muestra nuevamente el prompt

Todavia como el usuario postgres crear un nuevo usuario en PostgreSQL. Para crear el usuario en PostgreSQL vamos a indicar que sea superusuario (-s), que PostgreSQL nos pida indicar el password para dicho usuario (-P) y nos muestre en pantalla los mensajes que esto genere (-e de –echo)

En este caso el usuario que voy a crear se llamará sysdba y le pondré como password la palabra s3cret

createuser -P -s -e sysdba

Aparece lo siguiente:

Ingrese la contraseña para el nuevo rol:
Ingrésela nuevamente:
CREATE ROLE sysdba PASSWORD ‘md51767410185836193fcba975fe021e85b’ SUPERUSER CREATEDB CREATEROLE INHERIT LOGIN;

Salimos del usuario postgres con exit

Editamos el archivo /etc/postgresql/8.4/main/pg_hba.conf para indicar que el usuario que acabamos de crear puede accesar a la linea de comandos de PostgreSQL (psql)

sudo nano /etc/postgresql/8.4/main/pg_hba.conf

Agregamos la linea
local   all         sysdba                           md5

en la sección # Database administrative login by UNIX sockets

y queda algo parecido a esto

# Database administrative login by UNIX sockets
local   all         postgres                          ident
local   all         sysdba                            md5

Reiniciamos el servidor PostgreSQL

sudo /etc/init.d/postgresql restart

Aparece

Restarting PostgreSQL 8.4 database server: main.

Nos conectamos a la línea de comandos de PostgreSQL con el usuario que creamos (en este caso sysdba) y creamos una base de datos para pruebas

psql -U sysdba template1

Aparece:

psql (8.4.8)
Digite «help» para obtener ayuda.

template1=#

Tecleamos \l para ver la lista de las bases de datos que existen actualmente

template1=# \l
Listado de base de datos
Nombre   |  Dueño   | Codificación | Collation  |   Ctype    |      Privilegios
———–+———-+————–+————+————+———————–
postgres  | postgres | UTF8         | es_MX.utf8 | es_MX.utf8 |
template0 | postgres | UTF8         | es_MX.utf8 | es_MX.utf8 | =c/postgres
: postgres=CTc/postgres
template1 | postgres | UTF8         | es_MX.utf8 | es_MX.utf8 | =c/postgres
: postgres=CTc/postgres
(3 filas)

template1=#

Crearemos una nueva base de datos (en este caso se llamará development)

template1=# CREATE DATABASE development;
CREATE DATABASE
template1=#

Verificamos que se haya creado la base de datos

template1=# \l
Listado de base de datos
Nombre    |  Dueño   | Codificación | Collation  |   Ctype    |      Privilegios
————-+———-+————–+————+————+———————–
development | sysdba  | UTF8         | es_MX.utf8 | es_MX.utf8 |
postgres    | postgres | UTF8         | es_MX.utf8 | es_MX.utf8 |
template0   | postgres | UTF8         | es_MX.utf8 | es_MX.utf8 | =c/postgres
: postgres=CTc/postgres
template1   | postgres | UTF8         | es_MX.utf8 | es_MX.utf8 | =c/postgres
: postgres=CTc/postgres
(4 filas)

template1=#

Salimos de la terminal de PostgreSQL

template1=# \q

Instalar el gem para conectarse desde Ruby a PostgreSQL

sudo gem install pg

Para checar que está funcionando, entramos a la linea de comandos de ruby (desde la terminal tecleamos irb1.9.1) y tecleamos

require ‘pg’

Nos debe regresar true. Salimos con quit

$ irb1.9.1
irb(main):001:0> require ‘pg’
=> true
irb(main):002:0> quit

Instalar Datamapper

sudo gem install data_mapper

Instalar el adaptador que permite a Datamapper comunicarse con la base de datos (en este caso PostgreSQL)

sudo gem install dm-postgres-adapter

Ya tenemos instalado todo lo necesario para desarrollar aplicaciones web con acceso a base de datos utilizando Sinatra!. Para probarlo pongo a continuación un pequeño programa con las vistas y un archivo css.

NOTA:


Si están instalando una versión más nueva de PostgreSQL (como la 9.4) y/o una versión más nueva de las gemas de ruby (como rubygems-2.6.7), es posible que al instalar la gema dm-postgres-adapter les marque un error algo asi como “Failed to build gem native extension.” y más abajo indica que no encontró el archivo postgres.h. La solución es instalar el paquete postgresql-server-dev-9.4

sudo apt-get install postgresql-server-dev-9.4

Después de esto, intentar nuevamente la instalación de la gema dm-postgres-adapter y ya debe instalarse sin problemas


Programa principal

El programa contiene un modelo en Datamapper llamado Linea, este modelo crea la tabla lineas en la base de datos development que creamos después de que instalamos PostgreSQL La tabla contiene sólo 2 campos; el campo cve_linea que es la llave primaria y el campo linea.

El programa hace las funciones de creación, lectura, actualización, borrado y listado de registros en la tabla

Archivo sinatra_test.rb


require 'sinatra'
require 'data_mapper'

configure do
# Se conecta a la base de datos
DataMapper.setup(:default, 'postgres://localhost/development')
DataMapper.setup(:default, {
  :adapter  => 'postgres',
  :host     => 'localhost',
  :username => 'sysdba' ,
  :password => 's3cret',
  :database => 'development'})
end

# Modelos
class Linea
  include DataMapper::Resource
  property :cve_linea, String, :length => 4, :required => true, :key => true
  property :linea, String, :length => 30, :required => true
end

DataMapper.auto_upgrade!

def campo_vacio(campo)
    campo.empty?
end

# menú principal
get '/' do
  erb :index
end

# muestra error debido a que un campo requerido se sejó vacío
get '/campo_vacio' do
  erb :error_campovacio
end

# *******************************
# LINEA
# *******************************

# nueva linea
get '/linea' do
  erb :reg_linea
end

# muestra el listado de lineas
get '/listalineas' do
  @lineas = Linea.all :order => :cve_linea
  erb :lista_lineas
end

# guarda la información de la linea
post '/linea' do
  @linea = Linea.new(:cve_linea => params[:cve_linea].upcase, :linea => params[:linea].upcase)
  if !campo_vacio(params[:cve_linea]) and !campo_vacio(params[:linea])
     if @linea.save
        redirect "/linea/#{@linea.cve_linea}"
     else
         redirect '/linea'
     end
  else
      redirect '/campo_vacio'
  end
end

# muestra los detalles de la linea
get '/linea/:cve_linea' do
  @linea = Linea.get(params[:cve_linea])
  if @linea
     erb :cons_linea
  else
      redirect '/linea'
  end
end

# muestra la forma para editar la linea
get '/editlinea/:cve_linea' do
  @linea = Linea.get(params[:cve_linea])
  if @linea
     erb :edit_linea
  else
      redirect '/listalineas'
  end
end

# muestra la forma para borrar la linea
get '/borralinea/:cve_linea' do
  @linea = Linea.get(params[:cve_linea])
  if @linea
     erb :borra_linea
  else
      redirect '/listalineas'
  end
end

# edita la linea
put '/editlinea/:cve_linea' do
  @linea = Linea.get(params[:cve_linea])
  if @linea
     @linea.linea = params[:linea].upcase
     @linea.save
     redirect '/listalineas'
  end
end

# borra la linea
delete '/borralinea/:cve_linea' do
  @linea = Linea.get(params[:cve_linea])
  if @linea
     @linea.destroy
     redirect '/listalineas'
  end
end

En el directorio en donde guardamos el programa principal (sinatra_test.rb) creamos dos directorios, uno llamado public y otro llamado views.
Dentro del directorio public creamos otro directorio que llamaremos stylesheets. Dentro de este directorio stylesheets vamos a poner nuestro archivo css (que llamaremos style.css). Sólo contendrá el código que nos permita “mostrar” el texto en mayúsculas automáticamente en los campos de captura. el programa sinatra_test.rb es el que convierte a mayúsculas el texto antes de guardarlo en la tabla.

Desde luego, ustedes pueden agregar el código css necesario para embellecer a su gusto la presentación de la aplicación.

Archivo public/stylesheets/style.css

.campo_entrada
{
 text-transform: uppercase;
}

Ahora en el directorio views vamos a crear las vistas

Archivo views/layout.erb

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>Ejemplo Sinatra</title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="description" content="Acceso a base de datos en PostgreSQL" />
<meta name="keywords" content="Sinatra, Ruby, Datamapper" />
<link href="/stylesheets/style.css" rel="stylesheet" type="text/css" />
</head>
<body>
<%= yield %>
</body>
</html>

Archivo views/error_campovacio.erb


<div>


La información no se guardó debido a que dejó un campo vacío, por favor verifique!


<a href="/">Menú Principal</a>

</div>

Archivo views/index.erb


<h1>Menú Principal</h1>




<a href="/linea">Registro de líneas</a>



<a href="/listalineas">Listado de líneas</a>

Archivo views/reg_linea.erb


<div>

<form action="/linea" method="post" accept-charset="utf-8">
      <label>Clave de línea </label><input type="text" name="cve_linea" id="cve_linea" class="campo_entrada" maxlength=4>
      
      <label>Línea </label><input type="text" name="linea" id="linea" class="campo_entrada" maxlength=30>
      
      <input type="submit" value="Guardar"/>
    </form>



<a href="/">Menú Principal</a>

</div>

Archivo views/cons_linea.erb


<div>
    <label>Clave de línea </label><%= @linea.cve_linea %>
    
    <label>Línea </label><%= @linea.linea %>


<a href="/linea">Registro de líneas</a>



<a href="/">Menú Principal</a>

</div>

Archivo views/edit_linea.erb


<div>

<form action="/editlinea/<%= @linea.cve_linea %>" method="post" accept-charset="utf-8">
       <input type="hidden" name="_method" value="put">
       <label>Clave de línea </label><%= @linea.cve_linea %>
       
       <label>Línea </label><input type="text" name="linea" id="linea" class="campo_entrada" maxlength=30 value="<%= @linea.linea %>">
       
       <input type="submit" value="Guardar"/>
    </form>



<a href="/listalineas">Listado de líneas</a>



<a href="/">Menú Principal</a>

</div>

Archivo views/borra_linea.erb


<div>


Se borrará la siguiente línea: 


<form action="/borralinea/<%= @linea.cve_linea %>" method="post" accept-charset="utf-8">
       <input type="hidden" name="_method" value="delete">
       <label>Clave de línea </label><%= @linea.cve_linea %>
       
       <label>Línea </label><%= @linea.linea %>
       
       <input type="submit" value="Borrar"/>
    </form>



<a href="/listalineas">Listado de líneas</a>



<a href="/">Menú Principal</a>

</div>

Archivo views/lista_lineas.erb



Líneas registradas: <%= @lineas.length %>


<table>

<tr>

<th>Clave de línea</th>


<th>Línea</th>


<th></th>


<th></th>

</tr>

<% @lineas.each do |linea| %>

<tr>

<td><%= linea.cve_linea %></td>


<td><%= linea.linea %></td>


<td><a href="/editlinea/<%= linea.cve_linea %>">[editar]</a></td>


<td><a href="/borralinea/<%= linea.cve_linea %>">[borrar]</a></td>

   </tr>

<% end %>
</table>



<a href="/">Menú Principal</a>

Juego de gato ¿invensible? en Ruby

abril 3, 2010

Recientemente me he interesado en Ruby, así que para ir practicando decidí programar el juego de gato (tres en raya, tic-tac-toe, etc.) utilizando Ruby 1.9.

Debo aclarar que es lo primero que desarrollo en Ruby y seguramente el código se puede optimizar mucho, ya que no dudo que haya cosas que se puedan hacer de una mejor forma, así que desde ahora ofrezco una disculpa a los expertos en Ruby que pudieran leer este post, espero ir mejorando en Ruby conforme vaya adquiriendo más experiencia con este lenguaje de programación.

Aquí el código

#       gato.rb
#       
#       Copyright 2010 SALOMON RINCON TORRES <rtmex@yahoo.com>
#       
#       This program is free software; you can redistribute it and/or modify
#       it under the terms of the GNU General Public License as published by
#       the Free Software Foundation; either version 2 of the License, or
#       (at your option) any later version.
#       
#       This program is distributed in the hope that it will be useful,
#       but WITHOUT ANY WARRANTY; without even the implied warranty of
#       MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#       GNU General Public License for more details.
#       
#       You should have received a copy of the GNU General Public License
#       along with this program; if not, write to the Free Software
#       Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
#       MA 02110-1301, USA.


#!/usr/bin/ruby1.9

tablero = Array.new(3){Array.new(3)}
opcion = "-1"
caracter_usuario = nil
caracter_prog = nil
tira_usuario = 1
casillas_disponibles = (1..9).to_a
gana_programa = 0
opcion_programa = 0

def imprime_tablero(t)
    num = 1
    for i in 0..2 do
        print "#{num}|#{num+1}|#{num+2}   "
        for j in 0..2 do
            if j < 2 then
               if t[j][i]==nil then
                  print " "+"|"
               else
                   print t[j][i].to_s+"|"
               end
            else 
                print t[j][i]         
            end
        end
        puts
        num = num+3
        if i < 2 then
           5.times { |linea| print "-" }
           3.times { |linea| print " " }
           5.times { |linea| print "-" }
        end
        puts
    end
    
    return 
end

# Tira al azar en una esquina disponible
def tira_esquina(disp)
    opcion = 0
    esquina_seleccionada = 0
    # Tira al azar en una esquina, se basa en cuatro opciones:
    # opcion 1 será la esquina superior izquierda
    # opcion 2 será la esquina superior derecha
    # opcion 3 será la esquina inferior izquierda
    # opcion 4 será la esquina inferior derecha
    while not disp.include?(esquina_seleccionada)
          opcion = rand(4)+1
          case opcion
               when 1 then esquina_seleccionada = 1
               when 2 then esquina_seleccionada = 3
               when 3 then esquina_seleccionada = 7
               when 4 then esquina_seleccionada = 9
          end          
    end    
    
    return esquina_seleccionada
end

# Verifica en que casilla debe tirar quién este jugando con el caracter que recibe como parámetro
# para ganar horizontalmente
def gana_horizontal(caracter, t)
    tiros_hechos = 0
    contador = 0
    columna = -1
    fila = 0
    ganador_en = 0
        
    # El siguiente ciclo termina cuando se hayan recorrido todos los renglónes o se haya encontrado
    # un renglón en donde se han hecho dos tiros
    while contador <= 2 and tiros_hechos < 2
        for j in 0..2 do            
            if t[j][contador]==caracter then 
               tiros_hechos = tiros_hechos+1            
            elsif t[j][contador]==nil
                  columna = j
            end
        end
        # Si en el renglón actual había dos tiros y una casilla vacía, ahi se debe tirar para ganar
        if tiros_hechos == 2 and columna >= 0 then
           fila = contador           
        else
            tiros_hechos = 0
            columna = -1
            contador = contador + 1
        end
    end
        
    if tiros_hechos == 2 then
       case fila
            when 0 then
                   # Significa que se encontró un tiro ganador en el primer renglón
                   ganador_en = columna + 1                
            when 1 then
                   # Significa que se encontró un tiro ganador en el segundo renglón
                   ganador_en = columna + 4
            when 2 then
                   # Significa que se encontró un tiro ganador en el tercer renglón
                   ganador_en = columna + 7
       end       
    end
        
    return ganador_en
end

# Verifica en que casilla debe tirar quién este jugando con el caracter que recibe como parámetro
# para ganar verticalmente
def gana_vertical(caracter, t)
    tiros_hechos = 0
    contador = 0
    columna = 0
    fila = -1
    ganador_en = 0
        
    # El siguiente ciclo termina cuando se hayan recorrido todos los renglónes o se haya encontrado
    # un renglón en donde se han hecho dos tiros
    while contador <= 2 and tiros_hechos < 2
        for j in 0..2 do            
            if t[contador][j]==caracter then 
               tiros_hechos = tiros_hechos+1            
            elsif t[contador][j]==nil
                  fila = j
            end
        end
        # Si en la columna actual había dos tiros y una casilla vacía, ahi se debe tirar para ganar
        if tiros_hechos == 2 and fila >= 0 then
           columna = contador
        else
            tiros_hechos = 0
            fila = -1
            contador = contador + 1
        end
    end
        
    if tiros_hechos == 2 then
       case fila
            when 0 then
                   # Significa que se encontró un tiro ganador en el primer renglón
                   ganador_en = columna + 1                
            when 1 then
                   # Significa que se encontró un tiro ganador en el segundo renglón
                   ganador_en = columna + 4
            when 2 then
                   # Significa que se encontró un tiro ganador en el tercer renglón
                   ganador_en = columna + 7
       end
    end
        
    return ganador_en
end

# Verifica en que casilla debe tirar quién este jugando con el caracter que recibe como parámetro
# para ganar diagonalmente
def gana_diagonal(caracter, t)
    tirosd1 = 0
    tirosd2 = 0
    contador = 0    
    fila1 = -1
    fila2 = -1
    ganador_en = 0
    diagonal_1 = [0,1,2]
    diagonal_2 = [2,1,0]
        
    # El siguiente ciclo termina cuando se hayan recorrido todos los renglónes o se haya encontrado
    # que se han hecho dos tiros en una de las 2 diagonales diagonal
    while contador <= 2
          # Checa si hay un tiro a la casilla correspondiente en la diagonal 1
          if t[diagonal_1.at(contador)][contador]==caracter then 
             tirosd1 = tirosd1+1            
          elsif t[diagonal_1.at(contador)][contador]==nil
                fila1 = contador
          end
          
          # Checa si hay un tiro a la casilla correspondiente en la diagonal 2
          if t[diagonal_2.at(contador)][contador]==caracter then 
             tirosd2 = tirosd2+1            
          elsif t[diagonal_2.at(contador)][contador]==nil
                fila2 = contador
          end
          
          contador = contador+1
    end
        
    if tirosd1 == 2 and fila1>=0 then
       # Significa que se encontró el tiro ganador en la diagonal 1       
       case fila1
            when 0 then
                   # Significa que se encontró el tiro ganador en el primer renglón
                   ganador_en = 1                
            when 1 then
                   # Significa que se encontró el tiro ganador en el segundo renglón
                   ganador_en = 5
            when 2 then
                   # Significa que se encontró el tiro ganador en el tercer renglón
                   ganador_en = 9
       end
    elsif tirosd2 == 2 and fila2>=0 then
          # Significa que se encontró el tiro ganador en la diagonal 2          
          case fila2
               when 0 then
                    # Significa que se encontró el tiro ganador en el primer renglón
                    ganador_en = 3                
               when 1 then
                    # Significa que se encontró el tiro ganador en el segundo renglón
                    ganador_en = 5
               when 2 then
                    # Significa que se encontró el tiro ganador en el tercer renglón
                    ganador_en = 7
          end          
    end
    
    return ganador_en
end

def renglon_tirou(t, c_usuario)
    conta = 0
    renglones = Array.new
    
    for i in 0..2 do
        conta = 0
        for j in 0..2 do
            if t[j][i]==c_usuario then 
               conta = conta+1
            end            
        end
        if conta > 0 then
           renglones << i
        end
    end
    
    return renglones
end

# Ejecuta el tiro del programa
def tiro_programa(disp, c_programa, t, c_usuario)
    veces_tiradas = 0
    espacios = 0
    renglon_seleccionado = -1
    columna = -1
    tiro = 0
    gana = 0
    tiros_en_renglon = 0
    tiros_en_columna = 0
    tiros_usario = 0
    esquina_opuesta_ocupada = 1    
    # Crea un array con las 4 esquinas en que puede intentar tirar
    esquinas_intentadas = [1,3,7,9]
    # Elimina del array las esquinas que no están disponibles
    esquinas_intentadas.delete_if {|elemento| not disp.include?(elemento)}    
        
    # Verifica cuántas veces ha tirado el programa
    for i in 0..2 do        
        for j in 0..2 do
            if t[j][i]==c_programa then 
               veces_tiradas = veces_tiradas+1
            end            
        end
    end
        
    if veces_tiradas == 0 then
       # Primero intenta tirar al centro
       if disp.include?(5) then
          tiro = 5
       else
           # Tira al azar en una esquina         
           tiro = tira_esquina(disp)
       end
    else
        # Verifica si hay una casilla en la cual el programa gana horizontalmente y si es asi tira en ella
        tiro = gana_horizontal(c_programa, t)
        if tiro == 0 then
           # Significa que el programa no gana horizontalmente tirando en alguna casilla           
           # Verifica si hay una casilla en la cual el programa gana verticalmente y si es asi tira en ella
           tiro = gana_vertical(c_programa, t)
           if tiro == 0 then
              # Significa que el programa no gana verticalmente tirando en alguna casilla
              # Verifica si hay una casilla en la cual el programa gana diagonalmente y si es asi tira en ella
              tiro = gana_diagonal(c_programa, t)
              if tiro == 0 then
                 # Significa que el programa no gana diagonalmente tirando en alguna casilla
                 # Entónces checa si el usuario gana diagonalmente tirando en alguna casilla y si es asi lo bloquea
                 tiro = gana_diagonal(c_usuario, t)
                 if tiro == 0
                    # Significa que el usuario no gana diagonalmente tirando en alguna casilla
                    # Entónces checa si el usuario gana horizontalmente tirando en alguna casilla y si es asi lo bloquea
                    tiro = gana_horizontal(c_usuario, t)
                    if tiro == 0 then
                       # Significa que el usuario no gana horizontalmente tirando en alguna casilla
                       # Entónces checa si el usuario gana verticalmente tirando en alguna casilla y si es asi lo bloquea
                       tiro = gana_vertical(c_usuario, t)
                       if tiro == 0 then
                          # Significa que el usuario no gana verticalmente tirando en alguna casilla
                          # Si todas las esquinas están disponibles, tira en alguna de ellas
                          if esquinas_intentadas.length==4 then
                             tiro = tira_esquina(disp)
                          end
                          # Si hay 3 esquinas disponibles,
                          # busca tirar en una esquina correspondiente a un renglón y columna
                          # en donde el usuario ya había tirado y que la esquina opuesta esté vacía
                          if esquinas_intentadas.length==3 and disp.count<7  then
                             begin
                                 tiro = tira_esquina(disp)
                                 case tiro
                                      when 1,3 then
                                           if renglon_tirou(t, c_usuario).include?(0) then
                                              if tiro == 1 then
                                                 if disp.include?(9) and t[0].index(c_usuario) then
                                                    renglon_seleccionado=0
                                                 end
                                              else
                                                  if disp.include?(7) and t[2].index(c_usuario) then
                                                     renglon_seleccionado=0
                                                  end
                                              end
                                           end
                                      when 7,9 then
                                           if renglon_tirou(t, c_usuario).include?(2) then
                                              if tiro == 7 then
                                                 if disp.include?(3) and t[0].index(c_usuario) then
                                                    renglon_seleccionado=2
                                                 end
                                              else
                                                  if disp.include?(1) and t[2].index(c_usuario) then
                                                     renglon_seleccionado=2
                                                  end
                                              end
                                           end
                                 end
                             end until renglon_seleccionado>=0  
                          elsif tiro==0 then
                                # Entónces busca tirar en el renglón en donde el programa haya tirado el mayor número de veces
                                # o el renglón con el mayor número de casillas disponibles
                                casillas_disponibles = []
                                tiros_programa = []
                                max_disponibles = nil
                                max_tiros_programa = nil
                                 
                                # Guarda en el arreglo casillas_disponibles, el número de casillas disponibles
                                # para cada renglón, primero el número de casillas disponibles para el renglón 0
                                # después el número de casillas disponibles para el renglón 1 y al final
                                # el número de casillas disponibles para el renglón 2
                                # Guarda en el arreglo tiros_programa, el número de veces que el programa ha tirado
                                # en cada renglón, primero el número de veces en el renglón 0
                                # después el número de veces en el renglón 1 y al final
                                # el número de veces en el renglón 2
                                for i in 0..2 do
                                    espacios = 0
                                    veces_tiradas = 0
                                    for j in 0..2 do
                                        case t[j][i]
                                             when nil then espacios = espacios+1
                                             when c_programa then veces_tiradas = veces_tiradas+1
                                        end
                                    end
                                    casillas_disponibles.push(espacios)
                                    tiros_programa.push(veces_tiradas)
                                end
                                
                                # Guarda en la variable nmax_casillas_disp el número máximo de casillas disponibles
                                nmax_casillas_disp = casillas_disponibles.max
                                # Guarda en la variable max_disponibles el renglón en donde hay más casillas disponibles
                                max_disponibles = casillas_disponibles.index(casillas_disponibles.max)
                                # Busca si hay otro renglón que tenga el mismo número de casillas disponibles
                                # y que además el programa ya haya tirado en ese renglón previamente
                                max_disponibles_preferido = nil
                                for i in 0..2 do
                                    if casillas_disponibles[i] == nmax_casillas_disp then
                                       for j in 0..2 do
                                           if t[j][i] == c_programa then
                                              max_disponibles_preferido = i
                                              break
                                           end
                                       end
                                    end
                                end
                                
                                # Guarda en la variable max_tiros_programa el renglón en donde el programa ha tirado más veces
                                max_tiros_programa = tiros_programa.index(tiros_programa.max)
                                
                                if max_disponibles_preferido then
                                   renglon_seleccionado = max_disponibles_preferido
                                   for i in 0..2 do
                                       if t[i][renglon_seleccionado] == nil then
                                          columna = i
                                          break
                                       end
                                   end
                                else
                                    # Entónces busca tirar en la columna en donde el programa haya tirado el mayor número de veces
                                    # o la columna con el mayor número de casillas disponibles
                                    casillas_disponibles = []
                                    tiros_programa = []
                                    max_disponibles = nil
                                    max_tiros_programa = nil
                                    
                                    # Guarda en el arreglo casillas_disponibles, el número de casillas disponibles
                                    # para cada columna, primero el número de casillas disponibles para la columna 0
                                    # después el número de casillas disponibles para la columna 1 y al final
                                    # el número de casillas disponibles para la columna 2
                                    for i in 0..2 do
                                        casillas_disponibles.push(t[i].count(nil))
                                    end
                                    # Guarda en la variable max_disponibles la columna en donde hay más casillas disponibles
                                    max_disponibles = casillas_disponibles.index(casillas_disponibles.max)
                                    
                                    # Guarda en el arreglo tiros_programa, el número de veces que el programa ha tirado
                                    # en cada columna, primero el número de veces en la columna 0
                                    # después el número de veces en la columna 1 y al final
                                    # el número de veces en la columna 2
                                    for i in 0..2 do
                                        tiros_programa.push(t[i].count(c_programa))
                                    end
                                    # Guarda en la variable max_tiros_programa la columna en donde el programa ha tirado más veces
                                    max_tiros_programa = tiros_programa.index(tiros_programa.max)
                                      
                                    if t[max_tiros_programa].index(nil) and tiros_programa.uniq.count>1 then
                                       # Selecciona la columna en donde el programa ha tirado más veces
                                       columna = max_tiros_programa
                                    else
                                        # Como en todas las columnas el programa ha tirado igual número de veces
                                        # selecciona la columna con mas casillas disponibles
                                        columna = max_disponibles
                                    end                                 
                                    
                                    renglon_seleccionado = t[columna].index(nil)
                                end
                                
                                case renglon_seleccionado
                                     when 0 then tiro = columna+1
                                     when 1 then tiro = columna+4
                                     when 2 then tiro = columna+7
                                end
                          end
                       end
                    end
                 end
              else
                  gana = 1
              end
           else
               gana = 1
           end
        else
            gana = 1
        end
    end
    return tiro, gana
end

# Mientras que la opción seleccionada por el usuario no esté en el rango de 0 a 2,
# el programa seguirá pidiendo que seleccione un aopción
while not (0..2).include?(opcion.to_i)
   # Pregunta al usuario que caracter desea usar (X ó O)
   puts "Seleccione el caracter que desea usar para jugar o pulse [0] para salir."
   puts "[1] = X, [2] = O"
   
   opcion = gets.chomp    
   if opcion == "1" then
      caracter_usuario = "X"
      caracter_prog = "O"
   elsif opcion == "2"
       caracter_usuario = "O"
       caracter_prog = "X"
   end
end

if opcion != "0" then
   puts
   opcion = "-1"
   # Pregunta al usuario si quiere tirar primero
   while not (0..2).include?(opcion.to_i)
         puts "Indique quien debe tirar primero o pulse [0] para salir."
         puts "[1] = Tira primero Ud., [2] = Tira primero el programa"
   
         opcion = gets.chomp    
         tira_usuario = opcion == "1" ? 1: 0
   end
   
   while opcion != "0"
         if tira_usuario == 0 and not casillas_disponibles.empty? then
            opcion_programa, gana_programa = tiro_programa(casillas_disponibles, caracter_prog, tablero, caracter_usuario)
            case opcion_programa
                 when 1 then tablero[0][0] = caracter_prog
                 when 2 then tablero[1][0] = caracter_prog
                 when 3 then tablero[2][0] = caracter_prog
                 when 4 then tablero[0][1] = caracter_prog
                 when 5 then tablero[1][1] = caracter_prog
                 when 6 then tablero[2][1] = caracter_prog
                 when 7 then tablero[0][2] = caracter_prog
                 when 8 then tablero[1][2] = caracter_prog
                 when 9 then tablero[2][2] = caracter_prog
            end 
            # Elimina del array casillas_disponibles la posición en donde tiró el programa
            casillas_disponibles.delete(opcion_programa)            
            # Indica que le toca tirar al usuario
            tira_usuario = 1
         end
         puts imprime_tablero(tablero)
         
         if casillas_disponibles.empty? or gana_programa == 1 then
            # Como ya no hay casillas disponibles para tirar o ya ganó el progama, termina el programa
            if gana_programa == 1 then
               print "Gana el programa!"
            else
                print "No hay ganador!"
            end
            puts
            opcion = "0"
         else
             print "Pulse el numero en donde desea tirar o [0] para salir"
             puts
             # Espera a que el usuario seleccione en donde desea tirar         
             opcion = gets.chomp
             if opcion != "0" then            
                # Verifica que el usuaruo haya indicado una casilla vacia
                if casillas_disponibles.include?(opcion.to_i) then
                   case opcion
                        when "1" then tablero[0][0] = caracter_usuario
                        when "2" then tablero[1][0] = caracter_usuario
                        when "3" then tablero[2][0] = caracter_usuario
                        when "4" then tablero[0][1] = caracter_usuario
                        when "5" then tablero[1][1] = caracter_usuario
                        when "6" then tablero[2][1] = caracter_usuario
                        when "7" then tablero[0][2] = caracter_usuario
                        when "8" then tablero[1][2] = caracter_usuario
                        when "9" then tablero[2][2] = caracter_usuario
                   end
                   # Elimina del array casillas_disponibles la posición en donde tiró el usuario
                   casillas_disponibles.delete(opcion.to_i)
                   # Indica que le toca tirar al programa
                   tira_usuario = 0
                end
             end
         end
   end
end