Control de display LCD con soporte para caracteres en español 
El control de displays LCD alfanuméricos desde microcontroladores es un tópico ampliamente abordado en muchas webs y tutoriales. El juego de caracteres utilizado por este tipo de displays es de tipo ASCII con algunos símbolos adicionales, sobre todo asiáticos, y se echan en falta varios de los símbolos propios del español (tildes, diéresis, etc.). A lo largo de este post se plantea un sencillo algoritmo de gestión del display LCD que permite la utilización de texto en español de forma transparente al usuario.

Características del display

Los displays LCD alfanuméricos estándar almacenan los mapas de bits (los dibujos) de su juego de caracteres en una ROM interna, con la excepción de los 8 primeros caracteres (del 0 al 7). Los mapas de bits de estos 8 primeros caracteres se almacenan en una parte de la RAM del LCD denominada CGRAM (Character Generator RAM). Es esta parte de la RAM la que hay que utilizar para representar los caracteres extendidos no ASCII del español en nuestro display.

El mapa de bits de cada carácter está formado por una matriz de 5x7 puntos que se almacena en la CGRAM como 8 bytes consecutivos: 1 byte por cada línea horizontal (de la cual sólo son significativos los 5 bits más bajos) y un 1 byte adicional para la línea de cursor que siempre se pone a 0 para displays de 5x7. Como son configurables sólo los 8 primeros caracteres del juego de caracteres, esto nos da 8 * 8 = 64 bytes para la CGRAM.
        . . . x .       0 0 0 1 0
. . x . . 0 0 1 0 0
. x x x . 0 1 1 1 0
á --> . . . . x --> 0 0 0 0 1
. x x x x 0 1 1 1 1
x . . . x 1 0 0 0 1
. x x x x 0 1 1 1 1
. . . . . 0 0 0 0 0

Gestión de los caracteres

En español tenemos 16 caracteres que se salen de la simbología ASCII:
á é í ó ú ü ñ Á É Í Ó Ú Ü Ñ ¿ ¡

Como no es posible cargar en la CGRAM los mapas de bits de estos 16 caracteres de forma simultánea, es necesario implementar algún tipo de gestión a nivel software que cargue en CGRAM sólo los caracteres que necesitamos en cada momento.

Para la gestión de los caracteres se han utilizado las siguientes estructuras de datos (en pseudocódigo):
EntradaTabla {
caracterLatin1
mapaDeBits
caracterLcd
caracterLcdDefecto
vecesUsado
marcaCarga
}

EntradaTabla tabla[16]
Cola caracteresLcdDisponibles

Cada entrada de la tabla de caracteres especiales incluye el carácter “ISO-8859-1” o “latin1” correspondiente y el mapa de bits que lo dibuja en el display. caracterLcd es el carácter del LCD (1 al 7, no vamos a usar la entrada 0 por si acaso) que está mapeando a este carácter especial. caracterLcdDefecto es el carácter de la ROM que mapeará a este carácter latin1 en caso de que no esté disponible ningún hueco en la CGRAM (por ejemplo ‘á’ tiene como carácter por defecto ‘a’).

vecesUsado indica cuántas veces está siendo usado esa entrada de la tabla de caracteres extendidos: cada vez que se utiliza un carácter especial, se incrementa este contador de la entrada correspondiente y cada vez que se deja de utilizar en alguna parte de la pantalla (se borra o se sustituye por otro), se decrementa este contador de la entrada correspondiente. Cuando un contador llega a 0, el hueco que ocupaba esa entrada en la CGRAM es marcado como vacío (metido en la cola de caracteres LCD disponibles.

marcaCarga indica cuando una entrada de la tabla debe ser cargada en la CGRAM. La carga de los bitmaps de las entradas marcadas se realiza a posteiori para no influir en la escritura de los caracteres. Primero se escribe en la DDRAM (la memoria de pantalla) y al final, si es necesario enviar bitmaps de caracteres no ASCII, se escribe en la CGRAM.
inicializar(EntradaTabla e)
e.caracterLcd = 0
e.vecesUsado = 0
e.marcaCarga = NO
fin

inicializarCola
meter(caracteresLcdDisponibles, 1)
meter(caracteresLcdDisponibles, 2)
meter(caracteresLcdDisponibles, 3)
meter(caracteresLcdDisponibles, 4)
meter(caracteresLcdDisponibles, 5)
meter(caracteresLcdDisponibles, 6)
meter(caracteresLcdDisponibles, 7)
fin

buscarEntrada(c) {
Devuelve la entrada en “tabla” que cumpla “caracterLatin1 == c”, o NULL en caso de no haber ninguna entrada.
fin

reemplazarCaracter(nuevo, viejo)
EntradaTabla e = buscarEntrada(viejo)
si (e != NULL) {
e.vecesUsado = e.vecesUsado - 1
si (e.vecesUsado == 0) {
meter(caracteresLcdDisponibles, e.caracterLcd)
e.caracterLcd = 0
fin si
fin si
e = buscarEntrada(nuevo);
si (e == NULL)
devolver nuevo
en otro caso
si (e.caracterLcd > 0)
e.vecesUsado = e.vecesUsado + 1
en otro caso
si (caracteresLcdDisponibles está vacía)
devolver e.caracterLcdDefecto
e.caracterLcd = sacar(caracteresLcdDisponibles)
e.vecesUsado = 1
e.marcaCarga = SI
fin si
devolver e.caracterLcd
fin si
fin

procesar
para todos los segmentos de texto que haya que cambiar en el display
para todos los caracteres del segmento de texto
nuevo = nuevo carácter
viejo = actual carácter
v = reemplazarCaracter(nuevo, viejo)
emitir(v)
fin para
fin para
para todas las entradas e de la tabla con marcaCarga = SI hacer
cargar e.mapaDeBits en la CGRAM correspondiente al carácter e.caracterLcd
e.marcaCarga = NO
fin para
fin

Ejemplo de traza

Imaginemos que tenemos la pantalla en blanco (recién inicializada): Todas las entradas de la tabla las tenemos inicializadas (caracterLcd=0, vecesUsado=0, marcaCarga=NO) y la cola “caracteresLcdDisponibles” inicializada con los 7 huecos.

Para escribir la palabra “Máquina”, el proceso irá llamando a “reemplazarCaracter” por cada nueva letra:
    reemplazarCaracter(‘M’, ‘’)
No hay entrada en la tabla para el carácter ‘’
No hay entrada en la tabla para el carácter ‘M’
devuelve ‘M’
reemplazarCaracter(‘á’, ‘’)
No hay entrada en la tabla para el carácter ‘’
Hay una entrada e para el carácter ‘á’
como e.caracterLcd = 0, entonces
la cola de caracteres disponibles no está vacía
e.caracterLcd = 1
e.vecesUsado = 1
e.marcarCarga = SI
devuelve e.caracterLcd (1)
reemplazarCaracter(‘q’, ‘’)
No hay entrada en la tabla para el carácter ‘’
No hay entrada en la tabla para el carácter ‘q’
devuelve ‘q’
reemplazarCaracter(‘u’, ‘’)
No hay entrada en la tabla para el carácter ‘’
No hay entrada en la tabla para el carácter ‘u’
devuelve ‘u’
...

Al final del proceso de escritura de la cadena ‘Máquina’ tenemos que la cola de caracteres disponibles está así: [2, 3, 4, 5, 6, 7] y que la entrada de la tabla de caracteres correspondiente a la letra ‘á’ tiene caracterLcd=1, vecesUsado=1, el resto de entradas siguen como al principio.



De esta forma se va alojando espacio en la CGRAM del display LCD en función de los caracteres especiales que necesitamos en todo momento. Nótese que, en caso de que ya no nos queden huecos libres (la cola de huecos esté vacía), devolvemos el carácter por defecto.

Políticas alternativas de sustitución de caracteres

Una política alternativa podría ser establecer una prioridad por cada carácter de la tabla: en caso de vaciado de la cola de huecos, se sacrifica el carácter con menor prioridad de los que estén siendo usados en ese momento. Un criterio de prioridad podría ser en función del valor de “vecesUsado”. De esta forma se sacrificarían los caracteres menos usados.

Nótese que esta política de “sacrificado” de caracteres menos prioritarios obligaría a refrescar los caracteres a sacrificar y cambiarlos por los caracteres por defecto correspondientes. En todas las posiciones donde se encuentren.

En esta implementación no se lleva a cabo ninguna política de sacrificado de caracteres. Cuando la cola de caracteres disponibles se acaba, se imprime el carácter por defecto.

Implementación

Este algoritmo de gestión de caracteres extendidos para displays LCD se ha implementado sobre un Arduino Leonardo en C++.



Se ha optado por una anchura de bus de 4 bits para minimizar el número de cables. El código fuente puede descargarse de la sección soft.

[ añadir comentario ] ( 791 visualizaciones )   |  [ 0 trackbacks ]   |  enlace permanente
  |    |    |    |   ( 3 / 897 )
Tres en raya con el Arduino utilizando el algoritmo de decisión minimax 
Ampliando un post anterior en el que desarrollé un juego de tres en raya con el Arduino, he desarrollado una implementación “inteligente” del mismo. En la anterior versión, si bien el jugador jugaba contra la máquina, las posiciones que jugaba la máquina eran totalmente aleatorias y no seguían ningún criterio. En este caso la máquina utiliza un algoritmo de decisión (el minimax) para calcular el siguiente movimiento y así intentar ganar a su oponente humano.

Antecedentes

En la implementación anterior del tres en raya teníamos una clase abstracta denominada TTTGamePlayer de la cual heredaban las clases TTTHumanGamePlayer y TTTRandomGamePlayer. TTTHumanGamePlayer además de heredar de TTTGamePlayer, también heredaba de KeyMatrixListener, de esta forma los eventos de los pulsadores se transforman en movimientos del jugador humano.

La clase TTTRandomGamePlayer era una clase que inicializaba una semilla aleatoria. Dentro de esta clase el método getSpot(), que es el método virtual puro de TTTGamePlayer que debe devolver el movimiento que desea realizar el jugador, devolvía en este caso una posición aleatoria de entre todas las posiciones libres que quedaban en el tablero.

Algoritmo Minimax

El algoritmo minimax es un algoritmo de decisión muy simple desarrollado a partir del Teorema Minimax de John von Neumann en 1926 (para juegos de suma cero e información perfecta). El algoritmo es muy sencillo:

- Cuando nos toque mover a nosotros, calculamos el árbol de decisión del juego.

- Los nodos hoja del árbol representarán todas las posibles formas de terminar el juego. Asignamos a cada uno de los nodos hoja del árbol un valor numérico directamente proporcional a nuestro beneficio (o inversamente proporcional al beneficio de nuestro oponente).

- Vamos ascendiendo en el árbol escogiendo, alternativamente, el mínimo o el máximo de los hijos (minimax) en función de si cada decisión debe ser tomada por mí o por mi oponente (yo busco maximizar mi beneficio y mi oponente busca minimizarlo o, lo que es lo mismo, maximizar el suyo propio), así hasta la raíz. De esta forma podemos elegir el movimiento que minimize nuestra pérdida esperada.

Con un ejemplo del 3 en raya se ve mejor. Imaginemos que somos X y que nos toca mover a nosotros:
o x x
x . . sig. mov. = x, beneficio = max(-1, -1, 0) = 0
o o .

o x x
x x . sig. mov. = o, beneficio = min(0, -1) = -1
o o .

o x x
x x o sig. mov. = x, beneficio = max(0) = 0
o o .

o x x
x x o tablas, beneficio = 0
o o x

o x x
x x . gana o, beneficio = -1
o o o

o x x
x . x sig. mov. = o, beneficio = min(1, -1) = -1
o o .

o x x
x o x sig. mov. = x, beneficio = max(1) = 1
o o .

o x x
x o x gana x, beneficio = 1
o o x

o x x
x . x gana o, beneficio = -1
o o o

o x x
x . . sig. mov = o, beneficio = min(1, 0) = 0
o o x

o x x
x o . sig. mov. = x, beneficio = max(1) = 1
o o x

o x x
x o x gana x, beneficio = 1
o o x

o x x
x . o sig. mov. = x, beneficio = max(0) = 0
o o x

o x x
x x o tablas, beneficio = 0
o o x

A partir del nodo raíz, el beneficio máximo se corresponde con el movimiento:
o x x        o x x
x . . ---> x . .
o o . o o x

En este caso “beneficio” se refiere a “beneficio para X”. Como se puede ver, partiendo del estado del tablero indicado por la raíz del árbol, el mejor movimiento será el de marcar la fila 3 y la columna 3.

Hardware

El montaje lo he realizado utilizando un Arduino Leonardo, conectando la matriz de pulsadores a los puertos A0, A1, A2, D11, D12 y D13

Y la matriz de leds a los puertos D0, D1, D2, D3, D4 y D5

Si vamos a realizar el montaje para otro modelo de Arduino hay que comprobar el mapeo de los puertos en el microcontrolador y modificar la implementación de las clases MyLedMatrixManager y MyKeyMatrixManager (como se trata de una implementación en C++, no tenemos a nuestra disposición la abstracción de puertos que nos proporciona el lenguaje Arduino).

Implementación

La clase TTTMinimaxGamePlayer es la que implementa el algoritmo minimax: Al igual que las clases TTTRandomGamePlayer y TTTHumanGamePlayer, hereda de TTTGamePlayer con la diferencia de que en su método getSpot() calcula el siguiente movimiento a realizar utilizando el algoritmo de decisión minimax.

uint8_t TTTMinimaxGamePlayer::getSpot() {
    uint8_t ret = 0;
    this->minimax(*this->board, true, ret);
    return ret;
}

int8_t TTTMinimaxGamePlayer::minimax(TTTGameBoard &board, bool me, uint8_t &bestMovement) {
    uint8_t winner = board.getWinner();
    if (board.isFinished() || (winner != 0)) {
        int8_t ret = 0;
        if (winner == this->number)
            ret = 1;
        else if (winner == this->opponentNumber)
            ret = -1;
        return ret;
    }
    else {
        int8_t limit = 0;
        if (me)
            limit = -10;
        else
            limit = 10;
        TTTGameBoard auxBoard;
        uint8_t numMovements = 0;
        for (uint8_t m = board.getFirstAvailableMovement(); m != 0; m = board.getNextAvailableMovement(), numMovements++) {
            auxBoard.copyFrom(board);
            if (me)
                auxBoard.set(m, this->number);
            else
                auxBoard.set(m, this->opponentNumber);
            uint8_t childBestMovement;
            int8_t v = minimax(auxBoard, !me, childBestMovement);
            if (me) {
                if (v > limit) {
                    limit = v;
                    bestMovement = m;
                }
            }
            else {
                if (v < limit) {
                    limit = v;
                    bestMovement = m;
                }
            }
        }
        return limit;
    }
}

Se trata de una implementación recursiva y el parametro “me” indica cuando se está simulando un movimiento propio (true) o un movimiento del oponente (false). A pesar de usarse una implementación recursiva, nótese que no se realizarán más de 8 llamadas recursivas: Cada nivel es un movimiento adicional y un tablero de 3 en raya sólo tiene 9 posiciones.

A modo de visión global, el diagrama de clases queda ahora así:


En la sección soft puede descargarse el código fuente del proyecto en C++. Aquí una pequeña guía sobre cómo desarrollar en C++ para el Arduino.



[ añadir comentario ] ( 1798 visualizaciones )   |  [ 0 trackbacks ]   |  enlace permanente
  |    |    |    |   ( 3 / 852 )
Luces del belén controladas por Arduino con ciclo día-noche 
El año pasado por estas mismas fechas planteé y desarrollé unas luces para el belén controladas por una placa Arduino y que generaban una cadencia de destellos en función de la luz ambiente: a menor luz ambiente, más destellos y a mayor luz ambiente, menos destellos. Este año he planteado y desarrollado una segunda revisión un poco más avanzada del concepto de luces del belén.

Aspectos funcionales

La idea es que las luces del belén varíen sus destellos, no en función de la luz ambiente, sino en función de la hora que sea en cada momento. Es preciso definir lo que se entiende por “día” y por “noche” (en intervalo horario) y se debe especificar un patrón de comportamiento de las luces para cada uno de esos intervalos horarios.

Lo que he hecho en este caso es definir, para cada tramo horario, la probabilidad de que una luz cualquiera de las 5 brille. Dicha probabilidad será de 0.1 cuando estemos en el intervalo horario que hemos definido como “día” y de 0.8 cuando estemos en el intervalo horario que hemos definido como “noche”.

Aspectos técnicos

El problema es principalmente saber cuándo estamos en el intervalo horario “día” y cuándo estamos en el intervalo horario “noche”. Para ello, he recurrido al circuito desarrollado en el anterior post, en el que conecté un chip RTC (el clásico DS1307) a la placa Arduino mediante el bus I2C.



El circuito consta de varias partes:

- Por un lado tenemos la parte del reloj RTC con los dos hilos del bus I2C interconectando el Arduino y el DS1307 (SDA y SCL). El chip RTC tiene conectada una pila para mantener la hora cuando el resto del circuito esté apagado y un cristal de cuarzo de 32768Hz necesario para que el reloj sea preciso.

- Por otro lado tenemos las luces en sí: 5 leds blancos de alta luminosidad.

- Y finalmente tenemos un pulsador y un led adicional etiquetados como “Reset RTC” y “Led indicador de reset del RTC” respectivamente. El pulsador sirve para reiniciar la hora del RTC a las 00:00 (para ponerlo en hora, vamos) y el led indica que se ha puesto en hora de forma satisfactoria. Este último led se deja en la misma placa, no forma parte de las luces parpadeantes del belén. Para evitar que pulsaciones accidentales cambien la hora del chip RTC, es necesario mantener pulsado el botón al menos 2 segundos para que tenga efecto.

Las clases I2C y RTC desarrolladas en el post anterior han tenido que ser readaptadas para hacerlas no bloqueantes (básicamente usando autómatas). Las principales clases son las siguientes:

ADCManager: Es una utility class que permite leer las entradas analógicas. Se utiliza para inicializar el generador de números pseudoaleatorios.
I2C: Otra utility class para gestionar la comunicación a través del bus I2C del microcontrolador (SDA y SCL). Incluye un autómata para controlar los estados de espera del bus.
RTC: Es otra utility class que llama internamente a los métodos de I2C utilizando comandos específicos del RTC DS1307.
RTCListener: Es una clase abstracta que debe ser implementada por aquel objeto que desee ser avisado cada vez que cambia la hora.
RTCObserver: Una utility class encargada de consultar el RTC cada 250 milisegundos y de avisar al objeto que implemente RTCListener cada vez que cambie la hora. Lo he hecho así para no “estresar” el RTC.
StarsManager: Es la clase encargada de generar los destellos. Hereda de RTCListener y, cada vez que cambia la hora, comprueba en qué intervalo se encuentra (“día” o “noche”) y modifica, si procede, la probabilidad de destello de los leds del belén.

Todo el código fuente, en C++, puede ser descargado de la sección soft. Para compilar el proyecto sólo es necesario tener instalada la toolchain de GNU para AVR, dicha toolchain se instala junto con el resto de software que viene con el Arduino. Sólo hay que revisar el fichero Makefile y cambiar las rutas para adecuarlas a la ruta de instalación que tengamos en nuestro ordenador.



[ añadir comentario ] ( 774 visualizaciones )   |  [ 0 trackbacks ]   |  enlace permanente
  |    |    |    |   ( 3 / 702 )
Conectar un reloj de tiempo real al microcontrolador AVR 
Los microcontroladores AVR poseen una interface de bus I2C que permite conectarlos a EEPROMs, RTCs, DACs y muchos otros periféricos. El bus I2C es un estándar ampliamente utilizado para la interconexión de dispositivos a bajo nivel y en este post analizaré cómo conectar un microcontrolador AVR (presentes en la familia Arduino) con un chip RTC (Real Time Clock) utilizando este bus I2C.



La inicialización del bus I2C la podemos encapsular dentro de una clase estática:

#include "I2C.h"
#include <stdint.h>
#include <avr/io.h>

using namespace avelino;
using namespace std;

void I2C::init() {
    TWSR = 0x00;
    // TWBR = 12;   // 400KHz
    TWBR = 72;    // 100KHz
    TWCR = (1 << TWEN);
}

void I2C::start() {
    TWCR = (1 << TWINT) | (1 << TWSTA) | (1 << TWEN);
    while ((TWCR & (1 << TWINT)) == 0)
        ;
}   
    
void I2C::stop() {
    TWCR = (1 << TWINT) | (1 << TWSTO) | (1 << TWEN);
}

void I2C::write(uint8_t v) {
    TWDR = v;
    TWCR = (1 << TWINT) | (1 << TWEN);
    while ((TWCR & (1<<TWINT)) == 0)
        ;
}

uint8_t I2C::readACK() {
    TWCR = (1 << TWINT) | (1 << TWEN) | (1 << TWEA);
    while ((TWCR & (1 << TWINT)) == 0)
        ;
    return TWDR;
}

uint8_t I2C::readNACK() {
    TWCR = (1 << TWINT) | (1 << TWEN);
    while ((TWCR & (1 << TWINT)) == 0)
        ;
    return TWDR;
}

uint8_t I2C::getStatus() {
    return (TWSR & 0xF8);
}

En este caso configuramos la velocidad I2C a 100KHz ya que es la velocidad a la que trabaja el chip RTC DS1307.

A continuación podemos definir otra clase estática para acceder al RTC:

#include "RTC.h"
#include "I2C.h"

using namespace avelino;
using namespace std;

void RTC::init() { 
    I2C::init();
    // read halt bit
    I2C::start();
    I2C::write(0xD0);
    I2C::write(0x00);
    I2C::start();
    I2C::write(0xD1);
    uint8_t v = I2C::readNACK();
    I2C::stop();
    if ((v & 0x80) != 0) {
        // clock is disabled, enabling
        I2C::start();
        I2C::write(0xD0);
        I2C::write(0x00);
        I2C::write(v & 0x7F);
        I2C::stop();
    }
}       

void RTC::read(uint8_t &hour, uint8_t &minute, uint8_t &second) {
    I2C::start();
    I2C::write(0xD0);
    I2C::write(0x00);
    I2C::start();
    I2C::write(0xD1);
    second = I2C::readACK();
    minute = I2C::readACK();
    hour = I2C::readNACK();
    I2C::stop();
}

El método init, tras inicializar el bus I2C, consulta la dirección de memoria 0 del RTC que, además del secundero del reloj, también almacena el halt bit (bit 7). Este bit se encuentra a 1 de fábrica y debe ser puesto a 0 para que el RTC arranque. En el if se comprueba si este bit está a 1, si es así, se pone a 0.

Por ahora no nos estamos preocupando de la hora real. Cuando el DS1307 se activa comienza a contar como si fuesen las 0:00 horas de 1 de enero de 2000.

A continuación, para ver que el RTC funciona bien, podemos hacer un sencillo programa que cambie el estado del led de la placa Arduino por cada segundo que pasa:

#include <stdint.h>
#include "Led.h"
#include "RTC.h"

using namespace avelino;
using namespace std;

uint8_t hour, minute, second, prevSecond;
    
int main() {
    RTC::init();
    Led::init();
    while (1) {
        RTC::read(hour, minute, second);
        if (second != prevSecond) {
            Led::change();
            prevSecond = second;
        }
    }   
}       

Voilà, ya tenemos nuestro microcontrolador conectado al reloj de tiempo real.



[ añadir comentario ] ( 597 visualizaciones )   |  [ 0 trackbacks ]   |  enlace permanente
  |    |    |    |   ( 2.9 / 705 )
Tres en raya con el Arduino 
Partiendo del diseño hardware de los leds y los interruptores multiplexados realizado en anteriores posts de este blog he realizado una implementación “tonta” del juego de tres en raya.

Aspectos funcionales

La idea es realizar un juego de tres en raya utilizando la matriz de 3x3 leds en combinación con la matriz de 3x3 pulsadores descritas ambas en post anteriores. Esta implementación inicial tiene las siguientes características y limitaciones:
- El microcontrolador utiliza un algoritmo tonto para realizar movimientos: elige aleatoriamente sobre qué celda jugar en cada turno.
- Siempre comienza jugando el microcontrolador.
- En lugar de círculos y aspas utilizamos la marca “luz fija” y “luz parpadeante”. El microcontrolador juega siempre con “luz parpadeante”.
- En cuanto el juego termina (ya sea porque gana el microcontrolador, porque gana el jugador o porque quedan en tablas), el juego se para.

El funcionamiento es el siguiente:
- Tras el reset, el microcontrolador mueve y marca una casilla del tablero (el microcontrolador siempre juega con la marca “luz parpadeante”).
- El microcontrolador espera a que el jugador humano mueva activando el pulsador correspondiente a la casilla que quiere marcar.
- Al activar el pulsador, se marca el led correspondiente como “luz fija” e inmediatamente después, el microcontrolador vuelve a mover.
Así sucesivamente hasta que gane uno de los dos jugadores o el juego quede en tablas. Como se puede adivinar, al ser el jugador de la máquina un jugador “tonto” que elige sus movimientos de forma aleatoria, es muy fácil ganar :-). En sucesivas versiones intentaré ir mejorando la “inteligencia” del microcontrolador.

Diagrama de clases


Las principales clases serían las siguientes:
Game: Se encarga de la mecánica abstracta de cualquier juego de dos jugadores, los turnos, quién gana, etc.
GameBoard: Tablero de cualquier juego (abstracto).
TTTGameBoard: Tablero del juego del tres en raya.
TTTLedGameBoard: Tablero del juego del tres en raya especializado en visualizar su estado en la matriz de leds.
GamePlayer: Jugador de cualquier juego (abstracto).
TTTGamePlayer: Jugador del juego del tres en raya.
TTTRandomGamePlayer: Jugador “tonto” del tres en raya. Elige los movimientos aleatoriamente.
TTTHumanGamePlayer: Jugador humano del tres en raya. Elige los movimientos leyéndolos de la matriz de pulsadores.

A continuación, se puede ver la rutina principal. Se utilizan dos estados de juego ("jugando" y "terminado"):

#include "util.H"
#include "Timer.H"
#include "MyLedMatrixManager.H"
#include "MyKeyMatrixManager.H"
#include "TTTRandomGamePlayer.H"
#include "Game.H"
#include "TTTHumanGamePlayer.H"
#include "TTTLedGameBoard.H"

using namespace std;
using namespace avelino;

#define GAME_STATUS_PLAYING  1
#define GAME_STATUS_FINISHED 2

MyLedMatrixManager ledMatrixManager;
MyKeyMatrixManager keyMatrixManager;
TTTRandomGamePlayer p1;
TTTHumanGamePlayer p2;
Game g;
TTTLedGameBoard board;
uint8_t gameStatus;

int main() {
    Timer::init();
    ledMatrixManager.init();
    board.init(ledMatrixManager);
    p1.init(1, board);
    p2.init(2, board);
    keyMatrixManager.init(p2);
    g.init(p1, p2, board);
    gameStatus = GAME_STATUS_PLAYING;
    while (true) {
        ledMatrixManager.run();
        if (gameStatus == GAME_STATUS_PLAYING) {
            if (!g.isFinished()) {
                keyMatrixManager.run();
                g.run();
                board.show();
            }
            else 
                gameStatus = GAME_STATUS_FINISHED;
        }
        else if (gameStatus == GAME_STATUS_FINISHED) {
            //
        }
    }
    return 0;
} 


Consideraciones adicionales a tener en cuenta

La generación de números aleatorios

Para la implementación de la clase TTTRandomGamePlayer (el jugador “tonto”) se necesitan las funciones “srand” y “rand” de la librería estándar de C (u otras similares). En lugar de la clásica solución
srand(time(NULL));

que no puede ser utilizada en este caso ya que el sistema carece de reloj de tiempo real, opté por utilizar como semilla aleatoria la lectura de una de las entradas analógicas del microcontrolador que se encuentra sin cablear (al aire):
ADCManager::init();
srand(ADCManager::get(0));

Siendo ADCManager una clase con dos métodos estáticos: “init” inicializa el subsistema de conversión analógico-digital del microcontrolador y “get” lee la entrada analógica que se le pasa por parámetros.

Evaluación del tablero

La evaluación del tablero del tres en raya para determinar quién ha ganado tras cada movimiento la he implementado basándome en el algoritmo descrito en el documento “A general algorithm for tic-tac-toe board evaluation” del profesor Aaron Gordon (departamento de ciencias de la computación y sistemas de información de la universidad de Fort Lewis, Estados Unidos). Dicho algoritmo organiza la cuadrícula de 3x3 en forma de cuadrado mágico: un cuadrado en el que la suma de las columnas, las filas y las diagonales siempre da el mismo resultado. El algoritmo es muy ingenioso y eficiente y para entenderlo bien vale la pena leerse el artículo (aunque está en inglés, es muy sencillo de leer y se entiende perfectamente). De todas formas, en un futuro post abordaré el estudio de este interesante algoritmo.

En la sección soft puede descargarse el código fuente del proyecto.



[ 2 comentarios ] ( 909 visualizaciones )   |  [ 0 trackbacks ]   |  enlace permanente
  |    |    |    |   ( 3 / 706 )

<< <Anterior | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | Siguiente> >>