TEMA 2.2
NÚMEROS ALEATORIOS Y CONTROL DE TIEMPO





2.2.1 - MÚMEROS ALEATORIOS. FUNCIÓN RND

Cuando diseñamos un programa informático lo que intentamos es resolver un problema obteniendo un resultado lo más exacto y fiable posible, usando la gran precisión de los cálculos que hacen los ordenadores. De hecho, una de las normas básicas para asegurar que un algoritmo funciona correctamente es poder asegurar y demostrar que para unos mismos datos de entrada los resultados o datos de salida van a ser siempre los mismos.

En algunos casos como son los videojuegos nos puede interesar que suceda justamente lo contrario de lo que se ha dicho en el párrafo anterior, es decir, que cada vez que ejecutemos el programa sucedan cosas distintas y aparentemente impredecibles. Otras aplicaciones donde tendría que suceder esto serían programas simuladores de cálculos y fenómenos científicos y algunas aplicaciones matemáticas o estadísticas especiales.

Para poder llegar a estas situaciones nos encontramos con el terrible problema de conseguir que el ordenador (tan perfecto, exacto, preciso, etc...) sea capaz de generar números de cualquier manera sin responder a ningún orden aparente. Esto técnicamente es imposible y por lo tanto en vez de hablar de números aleatorios sería más correcto llamarlos números pseudo-aleatorios porque no son completamente impredecibles, pero lo son casi, lo suficiente como para resolver nuestras necesidades.

Para generar estos números (pseudo-) aleatorios el ordenador puede usar mecanismos tales como medir el tiempo entre dos pulsaciones de teclas, o el necesario para enviar o recibir un dato a través de una red. Como esto puede ser difícil de conseguir en algunos casos, es mucho más cómodo usar lo que se conoce como "generador de números aleatorios" que es una formula matemática que al aplicarla repetidas veces pasándole los valores anteriores nos va devolviendo una serie de números sin orden aparente. Una de estas fórmulas se llama "Generador de Marsaglia" y viene a ser algo así como:

zN = (2111111111 * zN-4 + 1492 * zn-3 + 1776 * zn-2 + 5115 * zn-1 + C) MOD 232

C = FLOOR((2111111111 * zN-4 + 1492 * zn-3 + 1776 * zn-2 + 5115 * zn-1 + C) / 232)

Esta fórmula nos devolvería una serie de números que no empezaría a repetirse hasta que lo hayamos calculado 3 * 10 elevado a 47 veces, un 3 con 47 ceros detrás, más que suficiente. Pero que nadie se asuste, de hecho no se ni si están bien copiados los números. En QBasic (y en otros lenguajes de programación) no nos hace falta programar tantos relíos, basta con usar el valor que nos devuelva la función RND cada vez que queramos obtener un número aleatorio. Vamos con un ejemplo de los más sencillo del curso de programación:

CLS
PRINT RND

Al ejecutarlo se escribiría en pantalla el número aleatorio que devuelva la función RND, un valor decimal (casi) impredecible, por ejemplo:

.7055475

Ya lo tenemos ahí, un número generado por el ordenador sin responder a ningún criterio aparente (Lleva un cero delante, pero QBasic se lo quita al escribirlo en la pantalla y solo se ve el punto decimal), pero todavía hay un problema, ejecuta el programa varias veces y mira el resultado.
Puedes comprobar que siempre sale el mismo número, ya no es tan impredecible. Esto se debe a que no hemos inicializado el generador de números aleatorios de QBasic.

Si tratas de descifrar algo de la fórmula de Marsaglia que hay más arriba observarás que para calcular un número pseudo-aleatorio se necesitan algunos de los calculados anteriormente. Esto va muy bien si ya los hemos calculado, pero si es la primera vez no los tenemos. En este caso tendremos que usar lo que se conoce como "Semilla", que es un número arbitrario usado para que empiece a funcionar el generador. Si este número es el mismo siempre los números aleatorios devueltos serán siempre una misma serie, por lo tanto hay que usar como semilla un número lo más "aleatorio" posible como por ejemplo el número de segundos que han pasado a partir de las doce de la noche, cosa que nos devuelve la función TIMER. Para inicializar el generador de números aleatorios usaremos la instrucción RANDOMIZE seguida de la semilla, normalmente TIMER. Vamos con el ejemplo completo:

RANDOMIZE TIMER
CLS
PRINT RND

Ahora sí que cada vez que ejecutemos el programa tendremos un número distinto. El valor solo se repetiría si ejecutamos el programa otro día a la misma hora, mismo minuto, mismo segundo. Algo difícil de conseguir y un riesgo aceptable en programas que no necesiten elevadas medidas de seguridad como los nuestros, pero no en otras aplicaciones como pudieran ser máquinas recreativas. De hecho, en sistemas más serios como Lunux se puede observar como al apagar el ordenador se guarda en algún sitio la semilla aleatoria para poderla seguir usando la próxima vez.

La instrucción RANDOMIZE TIMER es algo así como remover las bolas en un sorteo de lotería, debemos usarla una vez al principio de los programas que usen la función RND, pero no es necesario usarla más, de hecho repetirla en sitios como dentro de un bucle podría resultar contraproducente.

El valor devuelto por RND va a ser un número decimal de precisión sencilla mayor o igual que cero y menor que uno, es decir, alguna vez podría salir el cero, pero nunca saldrá el uno. Este valor lo podemos multiplicar o redondear para adaptarlo a nuestras necesidades.
Vamos con un programa que contiene dos ejemplos:

CLS
RANDOMIZE TIMER
valorDado = INT(RND*6)+1
PRINT "Hemos lanzado un dado y ha salido"; valorDado
IF RND > 0.5 THEN
	PRINT "Hemos lanzado una moneda y ha salido 'cara'"
ELSE
	PRINT "Hemos lanzado una moneda y ha salido 'cruz'"
END IF

Que daría este resultado (Con estos u otros valores):

Hemos lanzado un dado y ha salido 4
Hemos lanzado una moneda y ha salido 'cruz'

En el primer caso multiplicamos el valor de RND por seis, ya tenemos un número decimal que puede ir desde 0 a 5.999 (Nunca 6). A continuación lo redondeamos hacia abajo usando la función INT, ya tenemos un entero entre 0 y 5. Como queremos un valor posible entre 1 y 6, que son las caras que tienen los dados, no hay más que sumarle 1.
En el segundo caso lo que hacemos es comprobar el valor de RND para hacer una cosa u otra. Como en este caso queremos que las dos partes del IF tengan las mismas posibilidades ponemos el punto de comprobación en la mitad, en 0.5. Si quisiéramos variar las posibilidades no tendríamos más que cambiar ese número. Si queremos más de dos posibilidades podemos usar un SELECT CASE con varios intervalos.

Por último vamos a ver como conseguir que RND nos repita el último número que generó sin tener que grabarlo en ninguna variable. Para conseguirlo bastaría con escribir RND(0) en vez de RND. Vamos con un ejemplo:

CLS
RANDOMIZE TIMER
PRINT "Un número aleatorio.............:"; RND
PRINT "Repitamos el mismo número.......:"; RND(0)
PRINT "Otra vez más....................:"; RND(0)
PRINT "Ahora un número aleatorio nuevo.:"; RND
PRINT "Vamos a sacarlo otra vez........:"; RND(0)

El resultado podría ser (con otros números):

Un número aleatorio.............: .2051972
Repitamos el mismo número.......: .2051972
Otra vez más....................: .2051972
Ahora un número aleatorio nuevo.: .1969156
Vamos a sacarlo otra vez........: .1969156

Como se puede ver, si llamamos a RND sin argumentos, como hemos hecho hasta ahora, nos da un número aleatorio, pero si lo hacemos pasándole como argumento el cero entre paréntesis (Tiene que ser obligatoriamente el cero) lo que haces es repetir el mismo número en vez de calcular uno nuevo, que sería el siguiente de la serie.

Si usamos RND(0) por primera vez en el programa, sin haber usado antes RND, no pasa nada, tendremos un número aleatorio.
Esto no se utiliza demasiado, pero en algún caso podemos ahorrarnos una variable y una instrucción de asignación si queremos usar el mismo número en varios sitios.





2.2.2 - CONTROL DE TIEMPO

El ordenador lleva instalado un reloj digital que usa para muchas cosas, como por ejemplo para almacenar en los directorios la fecha y hora en que se guardó cada archivo. Por supuesto nosotros también podemos utilizar este valor en nuestros programas.

La forma más sencilla de obtener la hora ya la hemos visto en el apartado anterior. Es usar el valor devuelto por la función TIMER, un entero largo con los segundos que han pasado desde las doce de la noche. Con esta función ya podemos hacer un programa que nos salude de distinta forma dependiendo del momento del día que sea. Allá vamos...

CLS
SELECT CASE TIMER
	CASE IS < 28800: PRINT "Buenas madrugadas"
	CASE 28801 TO 43200: PRINT "Buenos días"
	CASE 43201 TO 72000: PRINT "Buenas Tardes"
	CASE ELSE: PRINT "Buenas noches"
END SELECT

El resultado, si ejecutamos el programa a las diez de la mañana sería:

Buenos días

Es una forma de que nuestro programa sea algo "inteligente", aunque todavía se podría mejorar (mucho). Lo que hemos hecho es ver cuantos segundos han pasado a las ocho de la mañana, a mediodía y a las ocho de la tarde para definir los intervalos, nada más.

También podemos usar la función TIMER para cronometrar el tiempo que tarda en pasar algo. Bastaría con almacenar la hora en una variable antes de empezar, hacer lo que sea, y al terminar almacenar la hora en otra variable y restarlas. Obtendríamos el número de segundos. Vamos a cronometrar lo que tarda el ordenador en escribir en pantalla los números del 1 al 10.000. Allá vamos...

CLS
inicio=TIMER
FOR n = 1 TO 10000
	PRINT n
NEXT
final = TIMER
tiempo =  final - inicio
PRINT "Ha tardado"; tiempo; "segundos"

Este programa, al final de la tira de 10.000 números nos daría un mensaje con el número de segundos que ha tardado.

Todo funcionaría bien excepto si durante la ejecución del programa han dado las doce de la noche, obteniéndose en este caso un número negativo. Para solucionar el problema habría que colocar la siguiente línea justo antes de sacar el mensaje. Lo que hacemos es sumarle todos los segundos de un día si ha dado negativo.

IF tiempo < 0 THEN tiempo = tiempo + 86400

Ahora vamos a recordar dos funciones que nos devuelven la fecha y la hora en modo texto, son DATE$ y TIME$.

CLS
PRINT "Hoy es "; DATE$
PRINT "Son las "; TIME$

Esta daría como resultado algo como:

Hoy es 01-20-
Son las 10:42:35

Con la hora no hay nada que aclarar, pero la fecha está en formato americano, primero el mes, después el día y al final el año. El mes y el día siempre con dos cifras y el año con cuatro. Estas funciones pueden ser más intuitivas pero para manejar las cifras las tenemos que separar usando la función MID$. Vamos con el ejemplo:

CLS
PRINT "El día es "; MID$(DATE$, 4, 2)
PRINT "El mes es "; MID$(DATE$, 1, 2)
PRINT "El año es "; MID$(DATE$, 7, 4)
PRINT
PRINT "La hora es "; MID$(TIME$, 1, 2)
PRINT "Los minutos son "; MID$(TIME$, 4, 2)
PRINT "Los minutos son "; MID$(TIME$, 7, 2)

Y el resultado podría ser:

El día es 20
El mes es 01
El año es 

La hora es 10
Los minutos son 42
Los minutos son 35

Muy sencillo. Lo único que hemos hecho es "recortar" con la función MID$ el trozo de cadena donde está cada cosa, que siempre va a estar en el mismo sitio, y separarlo para hacer con él lo que queramos. Y si nos hiciera falta sumarlos o compararlos podríamos convertirlos a enteros usando la función VAL.

Aplicando esto mismo, vamos con un ejemplo de una función a la que vamos a llamar FECHA$ y nos va a devolver la fecha actual como la usamos normalmente en castellano: Día, mes y año separados por barras.

FUNCTION FECHA$
	aux$ = MID$(DATE$, 4, 2)
	aux$ = aux$ + "/" + MID$(DATE$, 1, 2)
	aux$ = aux$ + "/" +MID$(DATE$, 7, 4)
	FECHA$ = aux$
END FUNCTION

Al llamarla desde cualquier parte de nuestro programa nos devolvería, en formato cadena algo como 20/01/, que queda más bonito que 01-20-.

Para terminar vamos a ver como cambiar la fecha y hora del sistema desde nuestro programa. Algo que no es habitual ni recomendable, pero que en algún caso nos puede hacer falta.
Sería usando las instrucciones, no funciones sino instrucciones, DATE$ y TIME$ y asignándoles cadenas que contengan la fecha o la hora en el formato adecuado. Vamos con unos ejemplos:

DATE$ = "04-20-"
DATE$ = "04-20-92"
DATE$ = "04-06-1994"

TIME$ = "10:24:35"
TIME$ = "10:24"
TIME$ = "10"

La fecha podemos establecerla poniendo siempre el mes primero y después el día y el año, este último dato puede tener dos o cuatro cifras. En el último ejemplo podríamos dudar entre si la fecha es el 4 de junio o el 6 de abril, se almacenaría este último, 6 de abril. Cuidado con esto, siempre mes, día, año.

En el caso de la hora si no ponemos los minutos o los segundos se entenderán que son 00.

Si intentamos asignar una fecha o una hora no válida se producirá un error de tiempo de ejecución. Normalmente no es recomendable modificar estas cosas desde los programas, si lo hacemos mal y no nos damos cuenta estaremos un tiempo usando el ordenador con el reloj mal, provocando problemas en los directorios, los antivirus, el planificador de tareas programadas, etc.













CuRSo De iNTRoDuCCióN a La PRoGRaMaCióN CoN QBaSiC
© 2004 Juan M. González