Mecánica del juego
Tenemos una cuadrícula de 30x30 posiciones por la que va deambulando una serpiente que tenemos que dirigir en pos de su comida (la comida va apareciendo aleatoriamente por el tablero). Cada vez que la serpiente come una pieza crece una unidad en tamaño y el jugador debe evitar chocarse tanto contra los bordes del tablero como contra él mismo (a medida que la serpiente crece se hace más complicado evitar chocarnos contra nosotros mismos).
Es un juego muy conocido por antiguos propietarios de móviles de la marca Nokia, ya que esteos terminales los solían traer de serie, además, el juego no tiene fin en el sentido de que nunca "se gana", simplemente hay que tratar de sobrevivir lo máximo posible sin chocarnos contra los bordes o contra nosotros mismos a medida que comemos y crecemos en longitud.
Diseño de la pantalla
La única pantalla que tiene el juego está gestionada por la clase "SnakeMainScreen" (en la carpeta "games/snake"). Consiste en un tablero central, que alberga 30 x 30 huecos de 2 x 2 pixels cada uno. Con estas dimensiones tenemos un tablero que ocupa 60 x 60 pixels y que se coloca en el centro de la pantalla. A este tablero se le añade un borde de 2 pixels de anchura en todo su perímetro, por lo que al final tenemos que el tablero ocupa un total de 64x64 pixels. Los huecos de los lados son de 32 pixels a izquierda (donde se coloca un dibujo estático) y de 32 pixels a la derecha (donde va el contador de frutas comidas "fru").
Desarrollo
Cada posición del tablero de 30x30 es un byte que podrá tener uno de los siguientes valores:
- 0: para indicar que esta posición está vacía.
- SnakeDirection::TO_UP: para indicar que hay serpiente en esta posición y que el siguiente punto de la serpiente (en dirección a su cabeza) está en la posición de arriba.
- SnakeDirection::TO_DOWN: igual pero indicando que el siguiente punto de la serpiente (en dirección a su cabeza) está en la posición de abajo.
- SnakeDirection::TO_LEFT: ídem hacia la izquierda.
- SnakeDirection::TO_RIGHT: ídem hacia la derecha.
- FOOD: para indicar que en esa posición hay una pieza de comida.
Aparte de estos datos en el tablero, se mantienen las coordenadas de la cabeza y de la cola de la serpiente y no es necesario almacenar su longitud. Esta forma de modelar la serpiente en el tablero nos permite simplificar tanto el movimiento como el crecimiento de la misma:
- Para movernos basta con hacer avanzar la cabeza en la dirección actual o la indicada por la última pulsación de los botones de dirección, y la cola en la dirección que indique la propia celda del tablero (recordemos que los valores SnakeDirection::TO_XXXX indican el siguiente elemento de la serpiente en dirección a su cabeza).
- Cada vez que la serpiente come (la cabeza se encuentra con comida) lo único diferente que se hace es que la cola NO avance, por lo que, de forma efectiva, estamos haciendo crecer la serpiente.
Como se puede ver, no es necesario guardar ni controlar el tamaño de la serpiente puesto que la cola siempre es capaz de "encontrar su camino" (aunque se realicen zigzags en bloque y en celdas adyacentes del tablero).
A continuación el código de la función miembro "SnakeMainScreen::advance", que es la encargada de gestionar el avance de la serpiente. Como se puede ver en el código en caso de que la cabeza de la serpiente se encuentre con comida la única diferencia es que la cola no avanza.
void SnakeMainScreen::advance(Collide &c) { c = Collide::NO; // check direction int16_t newHeadX = this->headX + this->dir->x; int16_t newHeadY = this->headY + this->dir->y; if ((newHeadX < 0) || (newHeadX >= BOARD_WIDTH) || (newHeadY < 0) || (newHeadX >= BOARD_HEIGHT)) c = Collide::YES; else { // no collision to borders uint16_t oldHeadOffset = (this->headY * BOARD_WIDTH) + this->headX; uint8_t oldHeadValue = this->board[oldHeadOffset]; uint16_t newHeadOffset = (newHeadY * BOARD_WIDTH) + newHeadX; uint8_t newHeadValue = this->board[newHeadOffset]; if ((newHeadValue == SnakeDirection::TO_UP) || (newHeadValue == SnakeDirection::TO_DOWN) || (newHeadValue == SnakeDirection::TO_LEFT) || (newHeadValue == SnakeDirection::TO_RIGHT)) c = Collide::YES; else if (newHeadValue == 0) { uint16_t tailOffset = (this->tailY * BOARD_WIDTH) + this->tailX; uint8_t tailValue = this->board[tailOffset]; int16_t newTailX, newTailY; this->calculateNewTail(tailValue, newTailX, newTailY); this->board[tailOffset] = 0; this->drawBoardPosition(this->tailX, this->tailY, 0); this->tailX = newTailX; this->tailY = newTailY; } else if (newHeadValue == FOOD) { int16_t foodX, foodY; this->allocateNewFood(foodX, foodY); this->drawBoardPosition(foodX, foodY, 1); this->fruitCounter++; this->display.drawNumber(100, 32, this->fruitCounter, 3, Display::ShowZeros::YES); } this->board[oldHeadOffset] = this->dir->to; this->board[newHeadOffset] = this->dir->to; this->drawBoardPosition(newHeadX, newHeadY, 1); this->display.notifyFrameBufferChanged(); this->headX = newHeadX; this->headY = newHeadY; } }
Conclusiones y cierre de la serie
La consola GabrielBoy ha sido una primera aproximación al problema de implementar una consola portátil desde cero. La parte software es la parte que menos me ha costado ya que es un mundo al que estoy muy acostumbrado, mientras que la parte más compleja para mi ha sido planificar el espacio, hacer prototipos, las soldaduras, los diseño y la impresión 3D de la caja, etc.
Quiero dar las gracias a Aristóbulo, por su paciencia a la hora de enseñarme a usar el FreeCAD y por ayudarme en el diseño y la impresión de la caja de la consola.
Todo el código fuente y los diseños están disponibles en la sección soft.
Lo sentimos. No se permiten nuevos comentarios después de 90 días.