Generador de islas aleatorias
Tras hacer el generador de ciudades me entró el gusanillo y he pensado otros talleres de programación para niños, se trata de desarrollar clases cortas como introducción a la programación de algoritmos y su aplicación directa en R.
Hablando con mi hijo mayor, pensamos que podía ser interesante crear un modelo para crear mapas de islas del tesoro… Como a mi me gustan mucho los mapas la idea me pareció genial y nos hemos puesto manos a la obra.
Muchos de los juegos de actualidad como el minecraft usan algoritmos de generación aleatoria de mapas para crear sus mundos de juego, así que es un punto que conecta muy bien con los peques actuales.
Método muy simple
Pensando cómo generar islas, creo que se nos ha ocurrido un método muy simple y en las pruebas vi que funcionaba bastante bien, así que nos lo quedamos.
Se trata de generar un polígono de n vértices a partir de un circulo simple. Lo que hacemos es añadir a la formula del círculo una irregularidad aleatoria en que se suma al radio en cada iteracción de cálculo.
Probemos, dividimos la circunferencia entera (\(2 \pi\)) en N partes, y calculamos las coordenadas de ese punto sobre la circunferencia añadiendo al radio un número aleatorio:
R=3000 # m de dario
I= 1000 # esto marca el factor de irregularidad de la cirunferecia
N=16 # el numero de puntos en que dividimos la circunferencia para representar
paso<-2*pi/N # angulo en rad de cada paso de calculo
# inicializamos una tabla de datos
coord<-data.frame(x=NA,y=NA)
# bucle de cálculo que recorre la circunferencia
for(i in 0:N-1){
x<-(R+rnorm(1,I,I))*cos(paso*i)
y<-(R+rnorm(1,I,I))*sin(paso*i)
coord<-rbind(coord,c(x,y))
}
coord<-na.omit(coord) # quitamos NA
# Añadimos el punto inicial para cerrar el poligono
coord<-rbind(coord,c(coord[1,1],coord[1,2]))
# pintamos los puntos
plot(coord,col="darkslategray",lwd=3, main="Isla aleatoria")
#pintamos el poligono
polygon(coord[,1],coord[,2],border = "burlywood4",col="bisque4")
Este algoritmo genera polígonos raros, que pueden parecer islas si usamos un valor de N
bajo (<20), pues si es alto las formas ya son menos realistas.
Para mejorar la apariencia, y hacer los lados más fractales, vamos a añadir un procedimiento recursivo también muy simple que aporte algo de caos sobre el contorno.
Por cierto, podemos ver una lista con los colores definidos en R aquí
Generador de caos
Sobre los polígonos generados en el punto anterior vamos a aplicar un procedimiento que nos aumente la irregularidad costera para un mayor realismo. Las costas suelen comportarse como líneas fractales, como expuso en su libro La geometría fractal de la naturaleza el matemático Mandelbrot.
Los procesos recursivos suelen dar buenos resultados así que vamos a probar una función que calcule el punto medio de cada lado del polígono isla.
Este punto medio lo desplazamos una distancia perpendicular al lado de amplitud aleatoria y formamos un nuevo polígono que divide cada lado en dos tomando un nuevo punto medio en cada uno.
Este proceso puede ser iterativo y repetirse varias veces hasta que salga una geometría singular de costa, pero eso lo haremos con otra función más adelante.
# CReamos la función punto medio
puntomedio<-function(x1,y1,x2,y2){
# calcula el punto medio del lado y lo mueve
# un porcentaje aleatorio sobre al perpendicular del lado
xmed<-(x1+x2)/2
ymed<-(y1+y2)/2
# calculamos la tangente para sacar la perpendicular
vx<- -(y2-y1) # por anlgulos es el eje opuesto
vy<-(x2-x1)
# Este parametro d es importante y marca la desviación
# del nuevo punto respecto al lado
d<-0.2*runif(1,-1,1)
# coord del nuevo punto medio final
x0<-xmed+d*vx
y0<-ymed+d*vy
return(c(x0,y0))
}
# creamos un poligono nuevo
n_pol<-data.frame(x=NA,y=NA)
# aplicamos la funión de punto medio
for (i in 1:nrow(coord)-1){
n_pol<-rbind(n_pol,c(coord[i,1],coord[i,2]))
n_pol<-rbind(n_pol,puntomedio(coord[i,1],coord[i,2],coord[i+1,1],coord[i+1,2]))
n_pol<-rbind(n_pol,c(coord[i+1,1],coord[i+1,2]))
}
n_pol<-na.omit(n_pol)
plot(n_pol,col="darkslategray",lwd=3, main="Isla aleatoria")
polygon(n_pol[,1],n_pol[,2],border = "burlywood4",col="bisque4")
Organizar y programar el algoritmo
Hemos visto que este sistema funciona, por lo que vamos a organizar el proceso y programar una función que englobe todos los pasos y nos genere en cada llamada una nueva isla.
Crear funciones
Lo primero es definir la función que genera el polígono aleatorio inicial de isla a partir del círculo. Esta función tiene de argumentos el radio medio de la isla (del circulo creador) y el número de vértices del polígono que se genera. Llamaremos a esa función pol_cero()
. Hemos hecho algunos cambios en el proceso de generación que nos han dado mejor apariencia.
Después hay que hacer una función que subdivida recursivamente cada lado del polígono en dos usando la función puntomedio()
que hicimos en el paso anterior. Llamaremos a esta función div_pol()
porque divide los lados y nos debe dar un nuevo polígono con más lados.
Luego necesitamos otra función que ejecute la función anterior de división del polígono de manera recursiva un número de veces N. Esta función nos dará el grado de caos de la costa y la llamaremos div_pol_n()
.
Finalmente para crear una isla juntaremos estas tres funciones en otra que nos simplifique la llamada final. A esta ultima función la llamaremos crea_isla()
.
Vamoossss a la faena:
# 1. unción que genera un primer poligono aleatorio de isla
pol_cero<-function(R=3000,nvert=5){
# R= diametro medio de la isla en m
I<- R/2 # Amplitud de desviación media de irregularidades
#N<-N # número de puntos base del boceto siempre <20
paso<-2*pi/nvert
# creamos poligono inicial como data.frame
pol_coord<-data.frame(x=NA,y=NA)
a<-runif(1,0.5,10) # añadimos una funcion seno a la amplitud
b<-runif(1,0.5,10) # añadimos otra funcion seno a la amplitud
for(i in 1:nvert-1){
#x<-(R+rnorm(1,I,I/3))*cos(paso*i)
#y<-(R+rnorm(1,I,I/3))*sin(paso*i)
x<-abs((R+I*sin(paso*i*a)+I*sin(paso*i*b)))*cos(paso*i)
y<-abs((R+I*sin(paso*i*a)+I*sin(paso*i*b)))*sin(paso*i)
pol_coord<-rbind(pol_coord,c(x,y))
}
pol_coord<-na.omit(pol_coord)
# Añadimos al final el punto origen para cerrar el poligono
pol_coord<-rbind(pol_coord,c(pol_coord[1,1],pol_coord[1,2]))
return(pol_coord)
}
#plot(pol_cero(,7),type="l")
# función que divide en 2 cada lado del poligono
# los datos de entrada deben ser un data.frame
div_pol<-function(poligon){
n_pol<-data.frame(x=NA,y=NA)
# aplicamos la funión de punto medio
for (i in 1:nrow(poligon)-1){
n_pol<-rbind(n_pol,c(poligon[i,1],poligon[i,2]))
n_pol<-rbind(n_pol,puntomedio(poligon[i,1],poligon[i,2],
poligon[i+1,1],poligon[i+1,2]))
n_pol<-rbind(n_pol,c(poligon[i+1,1],poligon[i+1,2]))
}
n_pol<-na.omit(n_pol)
return(n_pol)
}
#2. funcion recursiva
div_pol_n<-function(poligon, N){
z<-poligon
for(i in 1:N){
z<- div_pol(z)
}
return(z)
}
#plot(div_pol_n(pol_cero(,3),5),type="l")
#3. funcion final que crea y pinta una isla
crea_isla<-function(R=3000,nver=6,N=5){
#N=4
z<-pol_cero(R,nver)
z<-div_pol_n(z,N)
plot(z,col="darkslategray", cex=0.2,main=paste("Isla aleatoria: R=",R," nver=",nver," N=",N ))
# pinta fondo
polygon(c(-min(z[,1])^2,-min(z[,1])^2,max(z[,1])^2,max(z[,1])^2),c(-min(z[,2])^2,max(z[,2])^2,max(z[,2])^2,-min(z[,2])^2), col="cornflowerblue")
#Pinta la isla
polygon(z[,1],z[,2],border = "black",col="bisque4", lwd = 3)
}
# Por ultimo vemo un ejemplo
crea_isla()
Ejemplos
Hecho el trabajo duro, vamos a probar la función y ver una muestra de resultados:
# ajusta la grafica para 6 dibujos
par(mfrow=c(3,2))
par(mar=c(0,0,0,01)+.8)
# genera 6 radios aleatorios
radio<-5000 #as.integer(rnorm(6,8000,3000))
n_pun<-as.integer(runif(6,3,15))
caos=as.integer(runif(6,3,6))
# llama a la funcion 6 veces
#sapply(radio,crea_isla,N=14)
mapply(crea_isla,R=radio,nver=n_pun,N=caos)
## [[1]]
## NULL
##
## [[2]]
## NULL
##
## [[3]]
## NULL
##
## [[4]]
## NULL
##
## [[5]]
## NULL
##
## [[6]]
## NULL
# vuelve al modo una
par(mfrow=c(1,1))
Incluso podemos añadirle muchas cosas más al mapa, y hacer de verdad un mapa del tesoro, pero eso lo dejamos para otro día:
library(prettymapr)
crea_isla()
addscalebar()
addnortharrow(pos = "topright")