
Para uno de los capítulos de Programando Otra Historia estuve haciendo un desarrollo con Gambas3 y como hace tiempo que no programo en BASIC y quería hacerlo rápido dije: "Vamos a usar Copilot, ¿qué puede salir mal?"
El código está abierto, por si quieres presenciar el desastre: Puzzle8.
En principio instalé Debian 12 (Bookworm) como máquina virtual: git
y gambas3
. Todo en un entorno GNOME. Al principio Gambas no quería iniciarse y parece que faltaba especificar el uso de GTK3, puedes ver todo lo necesario para el proyecto en su README.md
.
Una vez dibujado el primer formulario comencé la dinámica de forma inocente y simple con una pregunta:
estoy usando Gambas y quiero generar un tablero de puzzle 15 barajado, ¿cómo puedo hacer ese código?
La respuesta muy entusiasta, y propone el primer código:
¡Genial! Crear un tablero de puzzle 15 barajado en Gambas puede ser una actividad divertida e interesante. A continuación, te muestro un ejemplo de cómo puedes hacerlo en Gambas:
PUBLIC SUB Main()
DIM board AS Integer[] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]
DIM i AS Integer
DIM temp AS Integer
DIM index AS Integer
' Barajar el tablero usando el algoritmo de Fisher-Yates
FOR i = 15 TO 1 STEP -1
index = INT(Rnd(0, 1) * (i + 1))
temp = board[i]
board[i] = board[index]
board[index] = temp
NEXT
' Mostrar el tablero barajado
FOR i = 0 TO 15
PRINT board[i];
IF ((i + 1) MOD 4) = 0 THEN PRINT
NEXT
END
En principio, puedes ver que toma muchas asunciones:
- Crea un tablero dentro de un vector mono-dimensional.
- Utiliza un algoritmo de Fisher-Yates para barajar las piezas.
- Muestra el resultado por consola.
En principio, creé la interfaz con un conjunto de botones llamados Cell1
, Cell2
, Cell3
, Cell4
, ..., Cell15
. Así que le indiqué su existencia para ver los cambios en la presentación:
' Posicionar los botones según el tablero barajado
FOR i = 1 TO 15
btn = Me["Cell" & i]
x = (board[i - 1] MOD 4) * 50 ' Asumiendo que cada celda es de 50x50 píxeles
y = (board[i - 1] \ 4) * 50
btn.X = x
btn.Y = y
NEXT
Localiza cada pieza y la posiciona según su número y el contenido del tablero. Aquí se sucede un cambio sutil:
- Asume el tamaño de los botones 50x50 píxeles.
-
Cambia el significado de
board
. El índicei
es el número impreso en el botón y se emplea como posición para calcularx
ey
.
El primer caso podría haberse solucionado tomando btn.Width
y btn.Height
de una de las celdas. El hecho de agregar el número directamente sin siquiera sugerir una constante o variable queda mal.
El segundo es un fallo grave. El código correcto hubiese sido:
Dim num As Integer
For i = 1 To 15
num = board[i - 1]
If num > 0 Then
btn = Me["Cell" & num]
btn.X = ((i - 1) MOD 4) * btn.Width
btn.Y = ((i - 1) \ 4) * btn.Height
EndIf
Next
Porque se supone que el índice i
recorre el tablero y por lo tanto el punto x
e y
parte del cálculo bidimensional de ese valor. Por otro lado, el contenido de board
es el número que hay que localizar dado el nombre de la celda (CellN
).
Esto no lo tuve en cuenta, como dije, quise acabar rápido y por tanto, es una deuda técnica el cambio de concepto de almacenar el tablero board
a ahora tener el contenido de las cells
.
Como me centré en la visualización gráfica y que no me estropeara mucho lo que se veía le di información sobre la vista más precisa:
la celda es de 56x48 y la primera celda comienza en (8, 16) teniendo una distancia de 8 entre cada botón
Dándome un código ajustado, pero como dije, ya cambiado el concepto de qué es lo que realmente contiene board
:
FOR i = 1 TO 15
btn = Me["Cell" & i]
x = 8 + (board[i - 1] MOD 4) * (56 + 8) ' 56 es el ancho de la celda, 8 es la distancia entre celdas
y = 16 + (board[i - 1] \ 4) * (48 + 8) ' 48 es la altura de la celda, 8 es la distancia entre celdas
btn.X = x
btn.Y = y
NEXT
Curiosamente, creé todos los botones bajo un mismo grupo y eso permitió nombrar el procedimiento a cargo de la gestión del evento de presionado como Cells_MouseDown
, solo quedaba la función, siguiente entrada:
vale, he creado la sub Cells_MouseDown() que se ejecuta cada vez que una pieza es presionada, solo deben desplazarse las piezas que tengan el hueco cerca y hacia el hueco modificando su posición en el tablero
PRIVATE EMPTY_CELL AS Integer[] = [3, 3] ' Posición inicial del hueco (x, y)
PUBLIC SUB Cells_MouseDown()
DIM btn AS Button
DIM x AS Integer
DIM y AS Integer
DIM btnIndex AS Integer
DIM cellIndex AS Integer
' Identificar el botón presionado
btn = LAST.Control
btnIndex = VAL(Right$(btn.Name, 2)) - 1
' Obtener la posición del botón presionado
x = (btn.X - 8) / (56 + 8)
y = (btn.Y - 16) / (48 + 8)
' Verificar si el hueco está cerca del botón presionado
IF (ABS(EMPTY_CELL[0] - x) = 1 AND EMPTY_CELL[1] = y) OR _
(ABS(EMPTY_CELL[1] - y) = 1 AND EMPTY_CELL[0] = x) THEN
' Mover el botón presionado al hueco
btn.X = 8 + EMPTY_CELL[0] * (56 + 8)
btn.Y = 16 + EMPTY_CELL[1] * (48 + 8)
' Actualizar la posición del hueco
EMPTY_CELL[0] = x
EMPTY_CELL[1] = y
' Actualizar el tablero
cellIndex = EMPTY_CELL[1] * 4 + EMPTY_CELL[0]
board[cellIndex] = btnIndex
ENDIF
END
El código se ve bastante bien e incluso me gustó la parte de comprobar con valores absolutos si el movimiento es posible. No obstante, a los errores:
-
LAST.Control
no existe. En verdad es soloLast
. Le pregunté sobre el error y no solo no supo de qué se trataba sino que dio otra solución creando un gestor de eventos que no es posible hacer en Gambas (al menos tal y como lo sugería), ¿alucinación o confusión con otro BASIC? -
Agrega un nuevo concepto
EMPTY_CELL
que en principio indica que será para puntosx
ey
, después veremos que no. Además, no aporta la parte del código donde debe inicializarse por primera vez. -
Por último, este código
VAL(Right$(btn.Name, 2))
. Si tomamos que las celdas deCell1
aCell9
solo tienen un número, obteniendo el texto desde la derecha con 2 posiciones obviamente darál1
,l2
, ...,l9
. Al intentar convertir conVAL(...)
no nos origina un error pero retorna un valor nulo y por tanto falla el código.
Obviamente, no funcionó y tuve que hacer cambios para adecuar el código. Además, en este punto me di cuenta de que mejor que botones, se quedarían etiquetas de texto planas. Así que cambié, le hice saber y modificó parte del código para tomar el control.
Voy a saltarme la parte en la que le solicité un modal, porque aunque dio información correcta fue imprecisa y me dejó con muchas dudas que después no supo resolver.
El puzzle no tiene solución
Uno de los problemas de puzzle-8, puzzle-15 y toda esta gama de puzzles es que si utilizas un algoritmo aleatorio para ir introduciendo las piezas, esto es equivalente a extraer la piezas físicas del puzzle y volver a introducirlas desordenadas.
El problema es que estos puzzles no tienen soluciones a menos que partan de una solución o su composición cumpla con una ecuación concreta. A saber:
Si el lado (L) del puzzle, ya sea alto o ancho porque suelen ser cuadrados, es par o impar difiere la forma de comprobar la solución:
- Si L es impar (puzzle-8 que es un 9x9), entonces las inversiones de sus números deben ser pares. A continuación veremos qué son las inversiones.
-
Si L es par, entonces depende de en qué fila esté el hueco (FH):
- Si la FH es impar entonces las inversiones de sus números deben ser impares.
- Si la FH es par entonces las inversiones de sus números deben ser pares.
¿Qué son las inversiones? Te preguntarás ahora, no te preocupes, yo también me lo pregunté. Cuando escribimos todos los números en orden en una sola línea (mono-dimensional) comenzamos a apuntar para el primero cuántos números le siguen que sean mayores.
La suma de todos estos números mayores son las inversiones.
Una vez tengamos las inversiones para todos los números, sumamos y debe salir un número par o impar.
Pidamos barajar de forma válida
cómo puedo hacer un código de barajado que sea seguro que generará un tablero resoluble sin que se extienda mucho el código?
Entonces, tenemos el código:
PUBLIC SUB Main()
' Inicializar el tablero resuelto
board = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 0]
' Realizar 100 movimientos aleatorios para barajar el tablero
DO FOR i = 1 TO 100
MakeRandomMove()
LOOP
' Posicionar las etiquetas en el formulario según el tablero barajado
PositionLabels()
END
En principio, el código no es válido porque los datos iniciales para el tablero no son válidos. Esto lo reescribí como:
board = New Integer[Max_pieces + 1]
For i = 0 To Max_pieces
board[i] = i
Next
Huelga decir que yo sí cree muchas variables al inicio del código par intentar usar cuantos menos números, mejor.
En el procedimiento PositionLabels()
no hice caso, es buena idea crear funciones y procedimientos, sobre todo cuando reutilizamos o queremos dejar el código más comprensible pero en este caso no lo vi necesario.
El bucle tal y como lo presenta tampoco me pareció correcto y simplemente dejé:
For i = 1 To 100
make_random_move
Next
Ahora, viene el plato fuerte. Esta función es un despropósito:
' Función para realizar un movimiento aleatorio
PRIVATE SUB MakeRandomMove()
DIM moves AS Integer[] = [-1, 1, -4, 4] ' Movimientos posibles: izquierda, derecha, arriba, abajo
DIM move AS Integer
DIM newPos AS Integer
DO
move = moves[INT(Rnd(0, 1) * moves.Max)]
newPos = (EMPTY_CELL[1] * 4 + EMPTY_CELL[0]) + move
LOOP UNTIL newPos >= 0 AND newPos < 16 AND _
((move = -1 AND EMPTY_CELL[0] > 0) OR _
(move = 1 AND EMPTY_CELL[0] < 3) OR _
(move = -4 AND EMPTY_CELL[1] > 0) OR _
(move = 4 AND EMPTY_CELL[1] < 3))
' Intercambiar la celda vacía con la nueva posición
board[EMPTY_CELL[1] * 4 + EMPTY_CELL[0]] = board[newPos]
board[newPos] = 0
EMPTY_CELL[0] = newPos MOD 4
EMPTY_CELL[1] = newPos \ 4
END
La idea de poner los posibles movimientos en un vector me gustó. No obstante y porque cambié de Puzzle-15 a Puzzle-8 preferí usar variables en lugar de esos -4, 4
.
Después, volvemos al problema de que board
no es el tablero per se y por tanto el intercambio y la búsqueda de posición no son correctas.
En este punto mezcla lo que contiene EMPTY_CELL
que recordemos inicialmente proponía los puntos x
e y
pero en píxeles. También me di cuenta de un pequeño detalle. Como board
no es el tablero sino que son las celdas numeradas, la celda con el número 15 es la correspondiente al hueco. Así que se lo hice saber:
he decidido almacenar en empty_cell los valores reales de X e Y porque en board[15] siempre se almacena el hueco
En principio lo ve perfecto, pero sigue usando el mismo código. Después de intentar aclararlo un par de veces le planté mi código y le pedí que implementase la función desde cero:
PRIVATE SUB make_random_move()
DIM moves AS Integer[] = [-1, 1, -4, 4] ' Movimientos posibles: izquierda, derecha, arriba, abajo
DIM move AS Integer
DIM newPos AS Integer
DO
move = moves[INT(Rnd(0, 1) * moves.Max)]
newPos = (empty_cell[1] * 4 + empty_cell[0]) + move
LOOP UNTIL newPos >= 0 AND newPos < 16 AND _
((move = -1 AND empty_cell[0] > 0) OR _
(move = 1 AND empty_cell[0] < 3) OR _
(move = -4 AND empty_cell[1] > 0) OR _
(move = 4 AND empty_cell[1] < 3))
' Intercambiar la celda vacía con la nueva posición
board[(empty_cell[1] * 4 + empty_cell[0])] = board[newPos]
board[newPos] = 0
empty_cell[0] = newPos MOD 4
empty_cell[1] = newPos \ 4
END
Sigue mal. En principio, como no usa LET
y move
es una palabra reservada hay conflicto. Después el cambio entre PascalCase, camelCase y snake_case me mareaba. Eso intenté homogeneizarlo en el código final.
Pero el código sigue mal. Le digo:
recuerda que empty_cell no contiene la posición de fila, columna, sino la posición real de X e Y
Ni caso. Intento otro enfoque:
dime una cosa, este código:
emptyIndex = (empty_cell[1] - 16) / (48 + 8) * 4 + (empty_cell[0] - 8) / (56 + 8)
¿no sería igual a este otro?
emptyIndex = board[15]
Me responde que no exactamente porque una calcula la posición de la celda vacía para obtener el índice del hueco y la otra es el índice del hueco :-|
Le vuelvo a decir:
creo que te estás equivocando porque emptyIndex es el índice dentro de board para obtener el hueco y eso se hace con board[15]
Y ahora ya sí, ve la luz: Tienes razón.
Pero el código sigue sin funcionar. Le comento:
creo que has liado lo que son los índices con lo que son las posiciones, en el tablero, los índices hacen mención al número que representan y por eso board[15] es siempre el hueco, pero board[newPos] no es válido porque los índices de board no son posiciones
Finalmente, me puse a pensar en qué había hecho mal Copilot y lo solucioné yo mismo.
Conclusiones
Esta conversación podría ser la que se obtiene con un programador junior, no digo que no, hay gente para todo y estoy seguro de que si te toca una persona arrogante que no quiere escuchar o que la lía, te pasará esto.
El problema con Copilot es que es un chat. No podemos hacer revisiones en partes del código específico e iterar sobre una base de código. El código es tratado y reescrito desde cero cada vez y eso es muy confuso y difícil de llevar. Sobre todo cuando la base de código crece.
Copilot es una gran herramienta y hace fácil algunas tareas, pero como se ve, depender en exceso de ella puede llevar a deuda técnica en un estadio muy temprano del desarrollo. Ya ni decir de situaciones donde haya mayor stress y presión por obtener algo para producción. Esto puede ser un desastre.
En mi opinión, toma estas herramientas con cautela, pero tómalas.