Showing posts with label laberintos. Show all posts
Showing posts with label laberintos. Show all posts

Saturday, August 02, 2014

Ya tenemos ganador del reto de la programación lúdica sobre los laberintos


Hace ya un par de semanas pusimos en el reto de la programación lúdica, la creación de un laberinto. Hubo una discreta participación, la cual quiero creer, se debió en parte a que el reto sonaba demasiado complejo y aunque no lo era tanto, requería quizás de más horas de las que muchos podrían haberse ocupado para resolverlo. Se recibieron algunas participaciones que invalidé porque parecían copia de algún programa de Internet y ante mi petición de aclarar el asunto hubo incluso silencio. Y que conste, no estoy acusando a nadie, solamente que la idea de los retos es que los que participan los resuelvan porque ése es el chiste, amén de que en eso reside la diversión. Finalmente, hubo tres finalistas. Uno usó Java (Salvador González), otro Javascript (Gabriel Martínez), con ayuda de JQuery y un tercero, escrito en Delphi. La decisión no fue fácil pero me parece que el ganador hizo la versión más manejable y visualmente más adecuada a la interfaz gráfica. El ganador es pues Guillermo Cañedo y este es su código (en Delphi):
 
unit Unit1;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, ExtCtrls, StdCtrls, Spin;

type
  TMurosSet = set of (arriba, abajo, derecha, izquierda);

  PCelda = ^TCelda;
  TCelda = record
      i, j, pos: Integer;
      visitado: Boolean;
      etiqueta: String;
      muros: TMurosSet;
      adyacentes: TList;
  end;

  PRef = ^TRef;
  TRef = record
      pos: Integer;
      muroComun: TMurosSet;
  end;

  TForm1 = class(TForm)
    Panel1: TPanel;
    Panel2: TPanel;
    Button1: TButton;
    Lienzo: TPaintBox;
    rens: TSpinEdit;
    cols: TSpinEdit;
    Label1: TLabel;
    Label2: TLabel;
    Button2: TButton;
    Label4: TLabel;
    procedure LienzoPaint(Sender: TObject);
    procedure rensChange(Sender: TObject);
    procedure FormCreate(Sender: TObject);
    procedure Button1Click(Sender: TObject);
    procedure FormDestroy(Sender: TObject);
    procedure Button2Click(Sender: TObject);
    procedure FormResize(Sender: TObject);
  private
    { Private declarations }
  public
    { Public declarations }
    dx, dy: Integer;
    Q: TList;
    solucion: TStringList;
    verSol: Boolean;
    procedure calcTamCeldas;
    function ObtieneListaDeNodos: TList;
    function obtieneNodo(i, j: Integer): PCelda;
    function obtieneNodosAdyacentes(i, j: Integer): TList;
    function refCelda(i, j: Integer): Integer;
    procedure DFS(nodo: PCelda);
    procedure DerribarMuro(A, B: PCelda);
    function obtieneNodosPorDondePuedePasar(nodo: PCelda): TStringList;
    function HayHabitacionesF: Boolean;
    function getMuroRandom(nodo: Pcelda): PCelda;
    function obtieneHabitacionF: PCelda;
    procedure etiquetar(nodo: pCelda; etq: String);
    function obtieneMuroQueIntercepta(A, B: PCelda): TMurosSet;
    function Regresa: PCelda;
  end;

var
  Form1: TForm1;

implementation

{$R *.dfm}


procedure TForm1.LienzoPaint(Sender: TObject);
var
   i, j, rx, ry: integer;
   R: TRect;
   celda: PCelda;
begin
    R.Left := 0;
    R.Top := 0;
    R.Right := Lienzo.Width;
    R.Bottom := Lienzo.Height;
    Lienzo.Canvas.Brush.Color := clWhite;
    Lienzo.Canvas.Brush.Style := bsSolid;
    Lienzo.Canvas.FillRect(R);

    Lienzo.Canvas.Pen.Color := clBlack;
    Lienzo.Canvas.Pen.Width := 1; 
    if Q <> Nil then
      begin
          for i := 0 to (Q.Count - 1) do
               begin
                   celda := Q[i];

                   rx := celda^.j*dx;
                   ry := celda^.i*dy;
                   
                   if arriba in celda^.muros then
                       begin
                           Lienzo.Canvas.MoveTo(rx, ry);
                           Lienzo.Canvas.LineTo(rx + dx, ry);
                       end;
                       
                   if abajo in celda^.muros then
                       begin
                           Lienzo.Canvas.MoveTo(rx, ry + dy);
                           Lienzo.Canvas.LineTo(rx + dx, ry + dy);
                       end;

                   if derecha in celda^.muros then
                       begin
                           Lienzo.Canvas.MoveTo(rx + dx, ry);
                           Lienzo.Canvas.LineTo(rx + dx, ry + dy);
                       end;

                   if izquierda in celda^.muros then
                       begin
                           Lienzo.Canvas.MoveTo(rx, ry);
                           Lienzo.Canvas.LineTo(rx, ry + dy);
                       end;

               end;

           if (verSol) and (solucion.Count > 0) then
               begin
                   Lienzo.Canvas.Pen.Color := clRed;
                   Lienzo.Canvas.Brush.Style := bsClear;
                   Lienzo.Canvas.Pen.Width := round(dy/3); 
                   celda := Q[StrToInt(solucion[0])];
                   rx := celda^.j*dx;
                   ry := celda^.i*dy;
                   Lienzo.Canvas.MoveTo(round(rx), round(ry + dy/2));

                   for i := 0 to (solucion.Count - 1) do
                       begin
                           celda := Q[StrToInt(solucion[i])];
                           rx := celda^.j*dx;
                           ry := celda^.i*dy;
                           Lienzo.Canvas.LineTo(round(rx + dx/2), round(ry + dy/2));
                       end;
                       
                   Lienzo.Canvas.LineTo(round(rx + dx), round(ry + dy/2));

               end;
      end
    else
      begin

           for j := 0 to cols.Value do
               begin
                   Lienzo.Canvas.MoveTo(dx*j, 0);
                   Lienzo.Canvas.LineTo(dx*j, Lienzo.Height);
               end;

           for j := 0 to rens.Value do
               begin
                   Lienzo.Canvas.MoveTo(0, dy*j);
                   Lienzo.Canvas.LineTo(Lienzo.Width, dy*j);
               end;
            
      end;
end;

function TForm1.ObtieneListaDeNodos: TList;
var
   i, j: integer;
   P: PCelda;
begin
   Result := TList.Create;
   for i := 0 to rens.Value - 1 do
       for j := 0 to cols.Value - 1 do
           begin
              New(P);
              P^.i := i;
              P^.j := j;
              P^.visitado := false;
              P^.etiqueta := 'u';
              P^.muros := [arriba, abajo, derecha, izquierda];
              P^.pos := refCelda(i, j);
              P^.adyacentes := obtieneNodosAdyacentes(i, j);
              Result.Add(P);
           end;
end;


function TForm1.obtieneNodo(i, j: Integer): PCelda;
var
   k: integer;
   P: PCelda;
begin
   Result := nil;
   for k := 0 to (Q.Count - 1) do
       begin
           P := Q.Items[k];
           if (P^.i = i) and (P^.j = j) then
               begin
                   Result := P;
                   Exit;
               end;
       end;
end;


function TForm1.refCelda(i, j: Integer): Integer;
begin
  Result := i*cols.Value + j;
end;


function TForm1.obtieneNodosAdyacentes(i, j: Integer): TList;

    procedure Add(k: Integer; muro: TMurosSet);
    var
        P: PRef;
    begin
        New(P);
        P^.pos := k;
        P^.muroComun := muro;
        Result.Add(P);
    end;

begin

    Result := TList.Create;
    if i - 1 >= 0 then Add(refCelda(i - 1, j), [arriba]);
    if i + 1 <= rens.Value - 1 then Add(refCelda(i + 1, j), [abajo]);     if j - 1 >= 0 then Add(refCelda(i, j - 1), [izquierda]);
    if j + 1 <= cols.Value - 1 then Add(refCelda(i, j + 1), [derecha]);
end;

procedure LimpiaLista2(MyList: TList);
var
   i: Integer;
   ARecord: PRef;
begin
   if MyList <> Nil then
      begin
           for i := 0 to (MyList.Count - 1) do
               begin
                   ARecord := MyList.Items[i];
                   Dispose(ARecord);
               end;
           MyList.Free;
       end;
end;


procedure LimpiaLista(MyList: TList);
var
   i: Integer;
   ARecord: PCelda;
begin
   if MyList <> Nil then
      begin
           for i := 0 to (MyList.Count - 1) do
               begin
                   ARecord := MyList.Items[i];
                   LimpiaLista2(ARecord^.adyacentes);
                   Dispose(ARecord);
               end;
           MyList.Free;
       end;
end;

procedure TForm1.rensChange(Sender: TObject);
begin
   Button2.Enabled := false;
   verSol := false;
   LimpiaLista(Q);
   Q := nil;
   solucion.Clear;
   calcTamCeldas;
   FormResize(Sender);
   Lienzo.Refresh;
end;

procedure TForm1.calcTamCeldas;
begin
   // dx := Round(Lienzo.Width/cols.Value);
   // dy := Round(Lienzo.Height/rens.Value);
   dx := 12;
   dy := 12;
   Lienzo.Width := dx*cols.Value + 1;
   Lienzo.Height := dy*rens.Value + 1;
end;


procedure TForm1.FormCreate(Sender: TObject);
begin
   solucion := TStringList.Create;
   calcTamCeldas;
end;

function TForm1.HayHabitacionesF: Boolean;
var
   i: Integer;
   P: PCelda;
begin
    Result := False;
    for i := 0 to (Q.Count - 1) do
        begin
            P := Q[i];
            if P^.etiqueta = 'F' then
                begin
                    Result := true;
                    Exit;
                end;
        end;
end;


function TForm1.obtieneHabitacionF: PCelda;
var
   i: Integer;
   P: PCelda;
   L : TStringList;
begin

   L := TStringList.Create;
   for i := 0 to (Q.Count - 1) do
       begin
           P := Q[i];
           if P^.etiqueta = 'F' then L.Add(IntToStr(i));
       end;
       
   if L.Count > 0 then
       Result := Q[StrToInt(L[Random(L.Count)])]
   else
       Result := nil;
   L.Free;
end;

function TForm1.getMuroRandom(nodo: Pcelda): PCelda;
var
   i: Integer;
   Ref: Pref;
   vecino: PCelda;
   L : TStringList;
begin
   L := TStringList.Create;
   for i := 0 to nodo^.adyacentes.Count - 1 do
       begin
           Ref := nodo^.adyacentes[i];
           vecino := Q[Ref^.pos];
           if vecino^.etiqueta = 'I' then L.Add(IntToStr(i));
       end;

   if L.Count > 0 then
       begin
           Ref := nodo^.adyacentes[StrToInt(L[Random(L.Count)])];
           Result := Q[Ref^.pos];
       end
   else
       Result := nil;
   
   L.Free;
end;

procedure TForm1.etiquetar(nodo: pCelda; etq: String);
var
   i: integer;
   Ref: PRef;
   hijo: PCelda;
begin
   for i := 0 to nodo^.adyacentes.Count - 1 do
       begin
           Ref := nodo^.adyacentes[i];
           hijo := Q[Ref^.pos];
           if hijo^.etiqueta <> 'I' then hijo^.etiqueta := etq;
       end;
end;


function TForm1.obtieneMuroQueIntercepta(A, B: PCelda): TMurosSet;
var
    i: Integer;
    Ref: PRef;
begin
    Result := [];
    for i := 0 to A^.adyacentes.Count - 1 do
        begin
            Ref := A^.adyacentes[i];
            if B^.pos = Ref^.pos then
                begin
                    Result := Ref^.muroComun;
                    Exit;
                end;
        end;
end;

procedure TForm1.DerribarMuro(A, B: PCelda);
begin
    A^.muros := A^.muros - obtieneMuroQueIntercepta(A, B);
    B^.muros := B^.muros - obtieneMuroQueIntercepta(B, A);
end;

function TForm1.obtieneNodosPorDondePuedePasar(nodo: PCelda): TStringList;
var
   i: Integer;
   Ref: PRef;
   adyacente: PCelda;
begin
   Result := TStringList.Create;
   for i := 0 to nodo^.adyacentes.Count - 1 do
       begin
           Ref := nodo^.adyacentes[i];
           adyacente := Q[Ref^.pos];
           if (not adyacente^.visitado) and (Ref^.muroComun * nodo^.muros = []) then Result.Add(IntTostr(Ref^.pos));
       end;
end;

function TForm1.Regresa: PCelda;
var
   L: TStringList;
begin
   L := TStringList.Create;
   Result := nil;
   while (solucion.Count > 0) and (L.Count = 0) do
       begin
           Result := Q[StrToInt(solucion[solucion.Count - 1])];
           L.Free;
           L := obtieneNodosPorDondePuedePasar(Result);
           solucion.Delete(solucion.Count - 1);
       end;

   L.Free;
end;


procedure TForm1.DFS(nodo: PCelda);
var
   celda: PCelda;
   Childs: TStringList;
begin
   nodo^.visitado := true;
   solucion.Add(IntToStr(nodo^.pos));
   Childs := obtieneNodosPorDondePuedePasar(nodo);
   if Childs.Count > 0 then
      begin
          celda := Q[StrToInt(Childs[Random(Childs.Count)])];
          DFS(celda);
      end;
   Childs.Free;
end;


procedure TForm1.Button1Click(Sender: TObject);
var
  vecino, I, F: PCelda;
  Ref: PRef;
  k: Integer;
  val: Boolean;
begin
    Button1.Enabled := false;
    Button2.Enabled := false;
    verSol := false;
    if verSol then Button2.Caption := 'Ocultar Solución' else Button2.Caption := 'Ver Solución';
    Randomize;
    LimpiaLista(Q);
    Q := ObtieneListaDeNodos;


    I := obtieneNodo(Random(rens.Value), Random(cols.Value));
    I^.etiqueta := 'I';
    for k := 0 to I^.adyacentes.Count - 1 do
        begin
            Ref := I^.adyacentes[k];
            vecino := Q[Ref^.pos];
            vecino^.etiqueta := 'F';
        end;

    repeat
        F := obtieneHabitacionF;
        I := getMuroRandom(F);
        DerribarMuro(I, F);
        F^.etiqueta := 'I';
        etiquetar(F, 'F');
    until not HayHabitacionesF;


    //// BUSCA LA SOLUCION ////
    solucion.Clear;
    I := obtieneNodo(Random(rens.Value), 0);
    I^.muros := I^.muros - [izquierda];

    repeat
        DFS(I);
        F := Q[StrToInt(solucion[solucion.Count - 1])];
        val := (F^.j = cols.Value - 1);
        if not val then I := Regresa;
    until val;
    
    F^.muros := F^.muros - [derecha];


    Lienzo.Refresh;
    Button1.Enabled := true;
    Button2.Enabled := solucion.Count > 0;
end;


procedure TForm1.FormDestroy(Sender: TObject);
begin
    solucion.Free;
    LimpiaLista(Q);
end;

procedure TForm1.Button2Click(Sender: TObject);
begin
    verSol := not verSol;
    Lienzo.Refresh;
    if verSol then Button2.Caption := 'Ocultar Solución' else Button2.Caption := 'Ver Solución';
end;

procedure TForm1.FormResize(Sender: TObject);
begin
    Lienzo.Left := Round((Panel2.ClientWidth - Lienzo.Width)/2);
    Lienzo.Top := Round((Panel2.ClientHeight - Lienzo.Height)/2);
end;

end.
 
Cabe indicar que Guillermo mandó versiones mejoradas de tanto en tanto, las cuales optimizaron la solución, lo cual fue lo que finalmente decidió que se le otorgara el primer lugar. Guillermo Cañedo Ramírez, 44 años, estudió Ingeniería Eléctrica y una maestría en Sistemas Eléctricos de Potencia en el Instituto Tecnológico de Morelia. He aquí la descripción de lo que hizo: "La estrategia que seguí", nos dice, "está basada en 2 algoritmos:" Dependiendo del número de renglones y columnas que se quieran del laberinto, se define una matriz de m renglones y n columnas y se almacena toda la información en una lista dinámica con TList (disponible en Delphi) de apuntadores a una estructura de datos TCelda.

TMurosSet = set of (arriba, abajo, derecha, izquierda);

  PCelda = ^TCelda;
  TCelda = record
      i, j, pos: Integer;
      visitado: Boolean;
      etiqueta: String;   
      muros: TMurosSet;
      adyacentes: TList;
  end;
 
y la lista de habitaciones adyacentes apunta a una estructura de datos del tipo TRef.

  PRef = ^TRef;
  TRef = record
      pos: Integer;
      muroComun: TMurosSet;
  end;
 
Primero se etiquetan todas las celdas o habitaciones de la matriz con 'u'

 1) El Algoritmo de Prim's para generar el laberinto, es como sigue:
  • a) Elegir una celda o habitación al azar y etiquetarla como I,
  • b) Etiquetar las habitaciones adyacentes de I con F
  • c) Obtener al azar una habitación etiquetada como F y derribar el muro que colinda con la habitación I,
  • d) Etiquetar F como I y
  • e) Etiquetar las adyacentes del nuevo I pero que no sean I como F
  • f) Repetir del c) al e) hasta que ya no haya mas habitaciones F
2) El Algoritmo de Búsqueda Primero en Profundidad con retroceso para hallar la solución:
  • a) Etiquetar todas las habitaciones del laberinto como no visitadas.
  • b) Elegir al azar una habitación de entrada de la primer columna y se establece como entrada del laberinto, eliminando su muro izquierdo
  • c) Establecer la habitación como nodo raíz y marcarla como visitada y guardar la posición del nodo en una lista que sera la solución.
  • d) Obtener las habitaciones adyacentes que no estén visitadas y que no tengan muro colindante para poder pasar.
  • e) Elegir al azar una de ellas y de manera recursiva repetir de c) a e) hasta que ya no haya habitaciones adyacentes sin visitar y sin muro o que la columna del nodo raíz sea igual al total de columnas del laberinto
  • f) si e) no se cumple entonces ir en sentido inverso con nuestra lista de la solución e ir eliminando el ultimo elemento hasta que haya un camino por donde pasar.
  • g) Al finalizar se marca el último nodo de la lista con la solución como salida del laberinto derribando el muro derecho.
Actualmente desarrolla aplicaciones educativas y juegos para tabletas y smartphones en forma independiente en Taos Games. Felicitamos a Guillermo y vienen más retos, con más premios. Hemos estado trabajando con algunas empresas para que nos den apoyo, así que ahora, a redoblar esfuerzos porque los premios empezarán a ponerse más atractivos... Poco a poco, pero verán que empezarán a incrementarse.

A quien le interese el código de Guillermo y el de los otros dos concursantes, poueden escribirme a morsa@la-morsa.com y se los mandaré por si les interesa estudiarlo.

Sunday, July 06, 2014

Programación lúdica: generación de laberintos


Hay muchas tareas en programación que las consideramos sencillas… hasta que tenemos que programarlas. Una idea que lleva tiempo en mi cabeza es la que se refiere a la creación y resolución de laberintos. ¿Qué tan difícil será crear un laberinto y más aún, hacer un programa que dé con la solución?

Esta es pues el nuevo reto de la programación lúdica: Escríbase un programa que genere un laberinto, y además, lo resuelva satisfactoriamente. Las dos partes son obligatorias.

Para esto, es interesante entender algunas ideas sobre este tema. Dice la Wikipedia: “Un laberinto es un pasatiempo gráfico consistente en trazar una línea desde un punto de origen situado en el exterior de un laberinto a uno de destino situado generalmente en el centro o bien en el lado opuesto. La dificultad consiste en encontrar un camino directo hasta el lugar deseado. El laberinto, por su propia configuración, contiene diferentes vías sin salida (de mayor o menor longitud) y sólo un recorrido correcto. Puede adoptar diferentes formas: cuadrado, ovalado, redondo, cuadrangular, etcétera”.

Crear un laberinto se puede hacer de la siguiente manera:

Considérese que el laberinto es una cuadrícula de celdas. Con esto en mente, empiece en cualquier celda (al azar incluso). Marque la celda actual como visitada y obtenga una lista de sus vecinos. Para cada vecino, puede empezar con uno de ellos al azar y: si el vecino no ha sido visitado, quite la pared entre esta celda y la del vecino, y entonces recursivamente con ese vecino como la celda actual, genérese el mismo algoritmo para los otros vecinos. Desde luego es una sugerencia. Cada quien puede usar la técnica que deseé.

Este programa, probablemente puedan encontrarlo en la red. No se trata pues de copiarlo de algún sitio. Si descubro una copia o si tengo dudas, me tomo la libertad de preguntarle al autor directamente cómo hizo, para garantizar así que la solución fue escrita y no copiada por el autor del programa. Juguemos pues limpiamente y aprendamos todos de estos retos.


¿El premio? Una taza con el logotipo de la Morsa a la mejor solución, en donde los criterios serán qué tan versátil es el programa: ¿Puedo cambiar el tamaño del laberinto creado? ¿Es muy lento, es muy rápido? ¿Qué opciones da al usuario para la creación del laberinto? Cabe decir que no ganará el primero que envíe su solución (aunque en caso de tener que desempatar, se dará prioridad a quien lo haya entregado antes)



Esto solamente aplica a los programadores que vivan en el DF (mandar a provincia o a otros países una taza es estúpidamente costoso). En caso de que los concursantes sean de otros países o de la provincia mexicana, el premio será una memoria USB de 8 GBytes al menos y se les enviará por correo certificado. Y sí, sé que no son los grandes premios pero mientras no tengamos patrocinadores, esto es lo que hay.

¡A darle entonces!