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.