Snake: Minijuego con tiles, teclado y sprites (C y ASM con SDCC)

Click here to see in English

En este tutorial vamos a ver como hacer una variante del famoso juego de la serpiente, que en este caso tiene que evitar chocar con las paredes y otros obstáculos, cada poco tiempo la serpiente crece y la velocidad aumenta... Para desarrollarlo vamos a introducir nuevos conceptos, como son la lectura del teclado para poder controlar a la serpiente, así como una introducción a los 'tiles', utilizados en multitud de juegos.

Para el teclado vamos a usar el Firmware, ya que en este caso no necesitamos varias pulsaciones simultaneas y es lo más sencillo por ahora. Vamos a usar el comando KM READ CHAR (BB09), a través de la siguiente función:

////////////////////////////////////////////////////////////////////////
//GetChar()
////////////////////////////////////////////////////////////////////////
char nGetChar;
char GetChar()
{
  __asm
    LD HL, #_nGetChar
    LD (HL), #0
    CALL #0xBB09 ;KM READ CHAR
    JP NC, _end_getchar
    LD (HL), A
    _end_getchar:
  __endasm;
  
  return nGetChar;
}
////////////////////////////////////////////////////////////////////////

Para el juego vamos a usar las letras 'opqa' y las flechas. Para ver que valor numerico tienen, hacemos un sencillo programa que nos imprima tanto el caracter como el codigo cada vez que se pulse una tecla:

////////////////////////////////////////////////////////////////////////
// snake01.c
// Mochilote - www.cpcmania.com
////////////////////////////////////////////////////////////////////////
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

////////////////////////////////////////////////////////////////////////
//GetChar()
////////////////////////////////////////////////////////////////////////
char nGetChar;
char GetChar()
{
  __asm
    LD HL, #_nGetChar
    LD (HL), #0
    CALL #0xBB09 ;KM READ CHAR
    JP NC, _end_getchar
    LD (HL), A
    _end_getchar:
  __endasm;
  
  return nGetChar;
}
////////////////////////////////////////////////////////////////////////

void main()
{
  while(1)
  {
    char nChar = GetChar();

    if(nChar != 0)
      printf("%d = %c\n\r", nChar, nChar);
  }
}
////////////////////////////////////////////////////////////////////////

Si lo cargamos en un emulador y pulsamos varias teclas obtendremos algo similar a esto:

SNAKE

 

Vamos a ver ahora como vamos a organizar el juego, para ello vamos a utilizar 'tiles'. ¿Que es un 'tile'? Un 'tile' es cada una de las celdas o subdivisiones de la pantalla que vamos a manejar en el juego. En nuestro caso concreto, cada tile podrá ser una parte de la serpiente, un borde, una piedra o bien parte del fondo. Cuando la serpiente se mueva, lo hará de 'tile' en 'tile' y cuando haya que pintar se pintará tambien 'tile' por 'tile' cuando sea necesario. Para este minijuego vamos a dividir la pantalla (modo 0 160x200) en tiles de 8x10 pixels, con lo que nos sale que tenemos 20x20 tiles.

Para manejar todo esto fácilmente desde nuestro código fuente, vamos a usar defines, enums y arrays de C, de la siguiente manera:

#define TILE_HEIGHT 10  //pixel
#define TILE_WIDTH 8  //pixel (4 bytes) 
#define MODE0_HEIGHT 200
#define MODE0_WIDTH 160
#define NUM_TILES_WIDTH MODE0_WIDTH / TILE_WIDTH
#define NUM_TILES_HEIGHT MODE0_HEIGHT / TILE_HEIGHT

enum _eTileType
{
  TileType_None,
  TileType_Border,
  TileType_Rock,
  TileType_Snake
}_eTileType;

enum _eTileType aBackgroundTiles[NUM_TILES_WIDTH][NUM_TILES_HEIGHT]; //20x20

Con lo que accediendo al array aBackgroundTiles[x][y] obtenemos o modificamos fácilmente el tipo de cualquier tile. Para manejar la serpiente vamos usar otro array con cada una de las partes de la serpiente y que ademas irá creciendo así como un enum para su movimiento:

typedef struct _tSnakePiece
{
  unsigned char nX;
  unsigned char nY;
}_tSnakePiece;

#define MAX_SNAKE_PIECES 50

_tSnakePiece aSnake[MAX_SNAKE_PIECES];
unsigned int nSnakePieces = 0;

enum _eDirection
{
  Direction_Up,
  Direction_Down,
  Direction_Left,
  Direction_Right
}_eDirection;

enum _eDirection eDirection = Direction_Right;

Vamos a ver una primera versión del juego, que en vez de pintar sprites, rellena cada tipo de tile con un color. El codigo es completamente funcional, a falta de meter los graficos. Tenemos entre otras las siguientes funciones DrawTile, KeyboardProcess y MoveSnake que son el grueso del juego y tambien tenemos las funciones SetColor, SetMode, SetCursor, InitGame, Game y ShowMenu, muy cortas y sencillas. El código fuente es el siguiente:

////////////////////////////////////////////////////////////////////////
// snake02.c
// Mochilote - www.cpcmania.com
////////////////////////////////////////////////////////////////////////
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define TILE_HEIGHT 10  //pixel
#define TILE_WIDTH 8  //pixel (4 bytes) 
#define MODE0_HEIGHT 200
#define MODE0_WIDTH 160
#define NUM_TILES_WIDTH MODE0_WIDTH / TILE_WIDTH
#define NUM_TILES_HEIGHT MODE0_HEIGHT / TILE_HEIGHT

enum _eTileType
{
  TileType_None,
  TileType_Border,
  TileType_Rock,
  TileType_Snake
}_eTileType;

enum _eTileType aBackgroundTiles[NUM_TILES_WIDTH][NUM_TILES_HEIGHT]; //20x20

#define NUM_COLORS 4
const unsigned char Palette[NUM_COLORS] = {0, 26, 12, 6};

typedef struct _tSnakePiece
{
  unsigned char nX;
  unsigned char nY;
}_tSnakePiece;

#define MAX_SNAKE_PIECES 50

_tSnakePiece aSnake[MAX_SNAKE_PIECES];
unsigned int nSnakePieces = 0;

enum _eDirection
{
  Direction_Up,
  Direction_Down,
  Direction_Left,
  Direction_Right
}_eDirection;

enum _eDirection eDirection = Direction_Right;

////////////////////////////////////////////////////////////////////////
//GetTime()
////////////////////////////////////////////////////////////////////////
unsigned char char3,char4;

unsigned int GetTime()
{
  unsigned int nTime = 0;

  __asm
    CALL #0xBD0D ;KL TIME PLEASE
    PUSH HL
    POP DE
    LD HL, #_char3
    LD (HL), D
    LD HL, #_char4
    LD (HL), E
  __endasm;

  nTime = (char3 << 8) + char4;

  return nTime;
}
////////////////////////////////////////////////////////////////////////


////////////////////////////////////////////////////////////////////////
//GetChar()
////////////////////////////////////////////////////////////////////////
char nGetChar;
char GetChar()
{
  __asm
    LD HL, #_nGetChar
    LD (HL), #0
    CALL #0xBB09 ;KM READ CHAR
    JP NC, _end_getchar
    LD (HL), A
    _end_getchar:
  __endasm;
  
  return nGetChar;
}
////////////////////////////////////////////////////////////////////////


////////////////////////////////////////////////////////////////////////
//DrawTile
////////////////////////////////////////////////////////////////////////
void DrawTile(unsigned char nTileX, unsigned char nTileY)
{
  enum _eTileType eTileType = aBackgroundTiles[nTileX][nTileY];
  unsigned int nRow = 0;

  for(nRow = 0; nRow < TILE_HEIGHT; nRow++)
  {
    unsigned int nY = nTileY * TILE_HEIGHT + nRow;
    unsigned int nX = nTileX * TILE_WIDTH;
    unsigned char *pScreen = (unsigned char *)0xC000 + ((nY / 8) * 80) + ((nY % 8) * 2048) + (nX / 2);
    unsigned char nColor = 0;
    
    switch(eTileType)
    {
      case TileType_None: nColor = 0; break;
      case TileType_Border: nColor = 192; break; //11000000
      case TileType_Rock: nColor = 12; break; //00001100
      case TileType_Snake: nColor = 204; break; //11001100
    }

    memset(pScreen, nColor, TILE_WIDTH / 2);
  }
}
////////////////////////////////////////////////////////////////////////


////////////////////////////////////////////////////////////////////////
//SetColor
////////////////////////////////////////////////////////////////////////
void SetColor(unsigned char nColorIndex, unsigned char nPaletteIndex)
{
  __asm
    ld a, 4 (ix)
    ld b, 5 (ix)
    ld c, b
    call #0xBC32 ;SCR SET INK
  __endasm;
}
////////////////////////////////////////////////////////////////////////


////////////////////////////////////////////////////////////////////////
//SetMode
////////////////////////////////////////////////////////////////////////
void SetMode(unsigned char nMode)
{
  __asm
    ld a, 4 (ix)
    call #0xBC0E ;SCR_SET_MODE
  __endasm;
}
////////////////////////////////////////////////////////////////////////


////////////////////////////////////////////////////////////////////////
//SetCursor
////////////////////////////////////////////////////////////////////////
void SetCursor(unsigned char nColum, unsigned char nLine)
{
  __asm
    ld h, 4 (ix)
    ld l, 5 (ix)
    call #0xBB75 ;TXT SET CURSOR
  __endasm;
}
////////////////////////////////////////////////////////////////////////


////////////////////////////////////////////////////////////////////////
//InitMode0
////////////////////////////////////////////////////////////////////////
void InitMode0()
{
  SetMode(0);

  //SCR SET BORDER 0
  __asm
    ld b, #0 ;black
    ld c, b
    call #0xBC38
  __endasm;
}
////////////////////////////////////////////////////////////////////////


////////////////////////////////////////////////////////////////////////
//InitGame
////////////////////////////////////////////////////////////////////////
void InitGame()
{
  unsigned char nColor = 0;
  unsigned int nX = 0;
  unsigned int nY = 0;

  for(nColor = 0; nColor < NUM_COLORS; nColor++)
    SetColor(nColor, Palette[nColor]);

  for(nX = 0; nX < NUM_TILES_WIDTH; nX++)
    for(nY = 0; nY < NUM_TILES_HEIGHT; nY++)
      aBackgroundTiles[nX][nY] = TileType_None;

  for(nX = 0; nX < NUM_TILES_WIDTH; nX++)
  {
    aBackgroundTiles[nX][0] = TileType_Border;
    aBackgroundTiles[nX][NUM_TILES_WIDTH - 1] = TileType_Border;
  }

  for(nY = 0; nY < NUM_TILES_HEIGHT; nY++)
  {
    aBackgroundTiles[0][nY] = TileType_Border;
    aBackgroundTiles[NUM_TILES_HEIGHT - 1][nY] = TileType_Border;
  }

  aBackgroundTiles[4][4] = TileType_Rock;
  aBackgroundTiles[15][4] = TileType_Rock;
  aBackgroundTiles[4][15] = TileType_Rock;
  aBackgroundTiles[15][15] = TileType_Rock;
  aBackgroundTiles[9][9] = TileType_Rock;
  aBackgroundTiles[9][10] = TileType_Rock;
  aBackgroundTiles[10][9] = TileType_Rock;
  aBackgroundTiles[10][10] = TileType_Rock;
  
  nSnakePieces = 0;
  eDirection = Direction_Right;

  for(nX = 0; nX < MAX_SNAKE_PIECES; nX++)
  {
    aSnake[nX].nX = 0;
    aSnake[nX].nY = 0;
  }

  for(nX = 0; nX < 5; nX++)
  {
    aSnake[nX].nX = nX + 5;
    aSnake[nX].nY = 12;

    aBackgroundTiles[aSnake[nX].nX][aSnake[nX].nY] = TileType_Snake;
    nSnakePieces++;
  }

  for(nX = 0; nX < NUM_TILES_WIDTH; nX++)
    for(nY = 0; nY < NUM_TILES_HEIGHT; nY++)
      if(aBackgroundTiles[nX][nY] != TileType_None)
        DrawTile(nX, nY);
}
////////////////////////////////////////////////////////////////////////


////////////////////////////////////////////////////////////////////////
//KeyboardProcess
////////////////////////////////////////////////////////////////////////
void KeyboardProcess()
{
  char nChar = GetChar();

  if(nChar == 'Q' || nChar == 'q' || nChar == -16) //Up
  {
    if(eDirection == Direction_Left || eDirection == Direction_Right)
      eDirection = Direction_Up;
  }
  else if(nChar == 'A' || nChar == 'a' || nChar == -15) //Down
  {
    if(eDirection == Direction_Left || eDirection == Direction_Right)
      eDirection = Direction_Down;
  }
  else if(nChar == 'O' || nChar == 'o' || nChar == -14) //Left
  {
    if(eDirection == Direction_Up || eDirection == Direction_Down)
      eDirection = Direction_Left;
  }
  else if(nChar == 'P' || nChar == 'p' || nChar == -13) //Right
  {
    if(eDirection == Direction_Up || eDirection == Direction_Down)
      eDirection = Direction_Right;
  }
}
////////////////////////////////////////////////////////////////////////


////////////////////////////////////////////////////////////////////////
//MoveSnake
////////////////////////////////////////////////////////////////////////
unsigned char MoveSnake(unsigned char bGrow)
{
  
  _tSnakePiece *pSnakePieceHead = &aSnake[nSnakePieces - 1];

  //new piece
  switch(eDirection)
  {
    case Direction_Right:
      aSnake[nSnakePieces].nX = pSnakePieceHead->nX + 1;
      aSnake[nSnakePieces].nY = pSnakePieceHead->nY;
      break;
    case Direction_Left:
      aSnake[nSnakePieces].nX = pSnakePieceHead->nX - 1;
      aSnake[nSnakePieces].nY = pSnakePieceHead->nY;
      break;
    case Direction_Up:
      aSnake[nSnakePieces].nX = pSnakePieceHead->nX;
      aSnake[nSnakePieces].nY = pSnakePieceHead->nY - 1;
      break;
    case Direction_Down:
      aSnake[nSnakePieces].nX = pSnakePieceHead->nX;
      aSnake[nSnakePieces].nY = pSnakePieceHead->nY + 1;
      break;
  }

  //has crashed
  if(aBackgroundTiles[aSnake[nSnakePieces].nX][aSnake[nSnakePieces].nY] != TileType_None)
    return 0;

  aBackgroundTiles[aSnake[nSnakePieces].nX][aSnake[nSnakePieces].nY] = TileType_Snake;
  DrawTile(aSnake[nSnakePieces].nX, aSnake[nSnakePieces].nY);

  nSnakePieces++;

  if(bGrow && nSnakePieces < MAX_SNAKE_PIECES)
    return 1;

  //delete tail of the snake
  aBackgroundTiles[aSnake[0].nX][aSnake[0].nY] = TileType_None;
  DrawTile(aSnake[0].nX, aSnake[0].nY);

  nSnakePieces--;
  memcpy(aSnake, &aSnake[1], sizeof(_tSnakePiece) * nSnakePieces);

  return 1;
}
////////////////////////////////////////////////////////////////////////


////////////////////////////////////////////////////////////////////////
//Game
////////////////////////////////////////////////////////////////////////
void Game()
{
  unsigned int nLastMoveTime = GetTime();
  unsigned int nGameMovements = 0;
  unsigned int nGameSpeed = 50;
  unsigned char bGrowSnake = 0;

  InitMode0();
  InitGame();

  while(1)
  {
    KeyboardProcess();

    if(GetTime() - nLastMoveTime < nGameSpeed)
      continue;

    nLastMoveTime = GetTime();
    nGameMovements++;

    if(nGameMovements % 20 == 0)
    {
      if(nGameSpeed > 15)
        nGameSpeed -= 2;

      bGrowSnake = 1;
    }

    if(!MoveSnake(bGrowSnake))
      break;

    bGrowSnake = 0;
  }

  SetMode(1);

  SetCursor(6, 7);
  printf("You have reached %d Movements", nGameMovements);

  SetCursor(8, 14);
  printf("Press Enter to play again");

  while(GetChar() != 13) {}
}
////////////////////////////////////////////////////////////////////////


////////////////////////////////////////////////////////////////////////
//ShowMenu
////////////////////////////////////////////////////////////////////////
void ShowMenu()
{
  SetMode(1);

  SetCursor(17, 5);
  printf("SNAKE");

  SetCursor(1, 8);
  printf("Use cursors or 'opqa' to move the snake");

  SetCursor(8, 16);
  printf("Press Enter to start game");

  SetCursor(3, 24);
  printf("Mochilote - www.cpcmania.com - 2012");

  while(GetChar() != 13) {}
}
////////////////////////////////////////////////////////////////////////


////////////////////////////////////////////////////////////////////////
//main
////////////////////////////////////////////////////////////////////////
void main()
{
  ShowMenu();

  while(1)
  {
    Game();
  }
}
////////////////////////////////////////////////////////////////////////

Como podemos ver en la función Game, que es el bucle principal del juego, lo que se hace es procesar el teclado y cuando toca, mover la serpiente, cada 20 movimientos de la serpiente se aumenta la velocidad y crece la serpiente. En la función MoveSnake vemos como avanza la cabeza de la serpiente, fuerza el pintado del nuevo tile y después quita la cola de la serpiente (a no ser que toque crecer) y repinta el tile para que salga el fondo. Si compilamos y ejecutamos obtendremos los siguiente:

 

 

Para que el juego sea más chulo, vamos a meter sprites, en este caso los sprites deben ser del tamaño del tile así que he dibujado los siguientes sprites para usar en el juego:

     

Una vez convertidos y adaptados (como ya hemos visto en tutoriales anteriores) tenemos lo siguiente:

////////////////////////////////////////////////////////////////////////
// sprites.h
// Mochilote - www.cpcmania.com
////////////////////////////////////////////////////////////////////////

const char SpriteBrick[] = 
{
               0x03, 0x02, 0x03, 0x02, 0x03, 0x02, 0x03, 0x02
        ,      0x03, 0x02, 0x03, 0x02, 0x03, 0x02, 0x03, 0x02
        ,      0x00, 0x00, 0x00, 0x00, 0x02, 0x03, 0x02, 0x03
        ,      0x02, 0x03, 0x02, 0x03, 0x02, 0x03, 0x02, 0x03
        ,      0x02, 0x03, 0x02, 0x03, 0x00, 0x00, 0x00, 0x00 
};


const char SpriteRock[] = 
{
               0x0C, 0x03, 0x03, 0x0C, 0x09, 0x30, 0x30, 0x06
        ,      0x12, 0x30, 0x30, 0x21, 0x12, 0x21, 0x12, 0x21
        ,      0x12, 0x21, 0x12, 0x21, 0x12, 0x21, 0x12, 0x21
        ,      0x12, 0x21, 0x12, 0x21, 0x12, 0x30, 0x30, 0x21
        ,      0x09, 0x30, 0x30, 0x06, 0x0C, 0x03, 0x03, 0x0C
};


const char SpriteSand[] = 
{
               0x0C, 0x0C, 0x0C, 0x0C, 0x18, 0x18, 0x18, 0x18
        ,      0x0C, 0x0C, 0x0C, 0x0C, 0x18, 0x18, 0x18, 0x18
        ,      0x0C, 0x0C, 0x0C, 0x0C, 0x18, 0x18, 0x18, 0x18
        ,      0x0C, 0x0C, 0x0C, 0x0C, 0x18, 0x18, 0x18, 0x18
        ,      0x0C, 0x0C, 0x0C, 0x0C, 0x18, 0x18, 0x18, 0x18
};


const char SpriteSnakeBodyHorz[] = 
{
               0x0C, 0xF0, 0xF0, 0x0C, 0x58, 0xCC, 0x44, 0xA4
        ,      0xA0, 0xCC, 0x88, 0xD8, 0xE4, 0x44, 0xCC, 0x50
        ,      0xE4, 0x88, 0xCC, 0xD8, 0xE4, 0xCC, 0x44, 0xD8
        ,      0xA0, 0xCC, 0x88, 0xD8, 0xE4, 0x44, 0xCC, 0x50
        ,      0x58, 0x88, 0xCC, 0xA4, 0x0C, 0xF0, 0xF0, 0x0C
};


const char SpriteSnakeBodyVert[] = 
{
               0x0C, 0xF0, 0xF0, 0x0C, 0x58, 0x88, 0xCC, 0xA4
        ,      0xE4, 0x44, 0xCC, 0x50, 0xA0, 0xCC, 0x88, 0xD8
        ,      0xE4, 0xCC, 0x44, 0xD8, 0xE4, 0x88, 0xCC, 0xD8
        ,      0xE4, 0x44, 0xCC, 0x50, 0xA0, 0xCC, 0x88, 0xD8
        ,      0x58, 0xCC, 0x44, 0xA4, 0x0C, 0xF0, 0xF0, 0x0C
};


const char SpriteSnakeHeadDown[] = 
{
               0x0C, 0xF0, 0xF0, 0x0C, 0x58, 0xCC, 0xCC, 0xA4
        ,      0xE4, 0xCC, 0xCC, 0xD8, 0xE4, 0xFC, 0xFC, 0xD8
        ,      0xE4, 0x7C, 0xBC, 0xD8, 0xE4, 0xCC, 0xCC, 0xD8
        ,      0xE4, 0xCC, 0xCC, 0xD8, 0x58, 0x9C, 0x6C, 0xA4
        ,      0x0C, 0xB4, 0x78, 0x0C, 0x0C, 0x1C, 0x2C, 0x0C
};


const char SpriteSnakeHeadLeft[] = 
{
               0x0C, 0x0C, 0xF0, 0xA4, 0x0C, 0x58, 0xCC, 0xD8
        ,      0x0C, 0xE4, 0xCC, 0xD8, 0x58, 0xCC, 0x6C, 0xD8
        ,      0x3C, 0x6C, 0xEC, 0xD8, 0x3C, 0x6C, 0xEC, 0xD8
        ,      0x58, 0xCC, 0x6C, 0xD8, 0x0C, 0xE4, 0xCC, 0xD8
        ,      0x0C, 0x58, 0xCC, 0xD8, 0x0C, 0x0C, 0xF0, 0xA4
};


const char SpriteSnakeHeadRight[] = 
{
               0x58, 0xF0, 0x0C, 0x0C, 0xE4, 0xCC, 0xA4, 0x0C
        ,      0xE4, 0xCC, 0xD8, 0x0C, 0xE4, 0x9C, 0xCC, 0xA4
        ,      0xE4, 0xDC, 0x9C, 0x3C, 0xE4, 0xDC, 0x9C, 0x3C
        ,      0xE4, 0x9C, 0xCC, 0xA4, 0xE4, 0xCC, 0xD8, 0x0C
        ,      0xE4, 0xCC, 0xA4, 0x0C, 0x58, 0xF0, 0x0C, 0x0C
};


const char SpriteSnakeHeadUp[] = 
{
               0x0C, 0x1C, 0x2C, 0x0C, 0x0C, 0xB4, 0x78, 0x0C
        ,      0x58, 0x9C, 0x6C, 0xA4, 0xE4, 0xCC, 0xCC, 0xD8
        ,      0xE4, 0xCC, 0xCC, 0xD8, 0xE4, 0x7C, 0xBC, 0xD8
        ,      0xE4, 0xFC, 0xFC, 0xD8, 0xE4, 0xCC, 0xCC, 0xD8
        ,      0x58, 0xCC, 0xCC, 0xA4, 0x0C, 0xF0, 0xF0, 0x0C
};

////////////////////////////////////////////////////////////////////////

Modificamos un poco el programa principal y finalmente este es el código fuente final del juego:

////////////////////////////////////////////////////////////////////////
// snake03.c
// Mochilote - www.cpcmania.com
////////////////////////////////////////////////////////////////////////
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "sprites.h"

#define TILE_HEIGHT 10  //pixel
#define TILE_WIDTH 8  //pixel (4 bytes) 
#define MODE0_HEIGHT 200
#define MODE0_WIDTH 160
#define NUM_TILES_WIDTH MODE0_WIDTH / TILE_WIDTH
#define NUM_TILES_HEIGHT MODE0_HEIGHT / TILE_HEIGHT

enum _eTileType
{
  TileType_None,
  TileType_Border,
  TileType_Rock,
  TileType_SnakeHeadUp,
  TileType_SnakeHeadDown,
  TileType_SnakeHeadLeft,
  TileType_SnakeHeadRight,
  TileType_SnakeBodyHorz,
  TileType_SnakeBodyVert
}_eTileType;

enum _eTileType aBackgroundTiles[NUM_TILES_WIDTH][NUM_TILES_HEIGHT]; //20x20

#define NUM_COLORS 9
const unsigned char Palette[NUM_COLORS] = {0, 26, 25, 18, 15, 9, 6, 22, 3};

typedef struct _tSnakePiece
{
  unsigned char nX;
  unsigned char nY;
}_tSnakePiece;

#define MAX_SNAKE_PIECES 50

_tSnakePiece aSnake[MAX_SNAKE_PIECES];
unsigned int nSnakePieces = 0;

enum _eDirection
{
  Direction_Up,
  Direction_Down,
  Direction_Left,
  Direction_Right
}_eDirection;

enum _eDirection eDirection = Direction_Right;

////////////////////////////////////////////////////////////////////////
//GetTime()
////////////////////////////////////////////////////////////////////////
unsigned char char3,char4;

unsigned int GetTime()
{
  unsigned int nTime = 0;

  __asm
    CALL #0xBD0D ;KL TIME PLEASE
    PUSH HL
    POP DE
    LD HL, #_char3
    LD (HL), D
    LD HL, #_char4
    LD (HL), E
  __endasm;

  nTime = (char3 << 8) + char4;

  return nTime;
}
////////////////////////////////////////////////////////////////////////


////////////////////////////////////////////////////////////////////////
//GetChar()
////////////////////////////////////////////////////////////////////////
char nGetChar;
char GetChar()
{
  __asm
    LD HL, #_nGetChar
    LD (HL), #0
    CALL #0xBB09 ;KM READ CHAR
    JP NC, _end_getchar
    LD (HL), A
    _end_getchar:
  __endasm;
  
  return nGetChar;
}
////////////////////////////////////////////////////////////////////////


////////////////////////////////////////////////////////////////////////
//PutSpriteMode0()
////////////////////////////////////////////////////////////////////////
void PutSpriteMode0(unsigned char *pAddress, unsigned char nWidth, unsigned char nHeight, unsigned char *pSprite)
{
  __asm
    LD L, 4(IX) 
    LD H, 5(IX) 
    LD C, 6(IX) 
    LD B, 7(IX)            
    LD E, 8(IX) 
    LD D, 9(IX) 

    _loop_alto:
      PUSH BC
      LD B,C
      PUSH HL
    _loop_ancho:
      LD A,(DE)
      LD (HL),A
      INC DE
      INC HL
      DJNZ _loop_ancho
      POP HL
      LD A,H
      ADD #0x08
      LD H,A
      SUB #0xC0
      JP NC, _sig_linea
      LD BC, #0xC050
      ADD HL,BC
    _sig_linea:
      POP BC
      DJNZ _loop_alto
  __endasm;
}
////////////////////////////////////////////////////////////////////////


////////////////////////////////////////////////////////////////////////
//DrawTile
////////////////////////////////////////////////////////////////////////
void DrawTile(unsigned char nTileX, unsigned char nTileY)
{
  enum _eTileType eTileType = aBackgroundTiles[nTileX][nTileY];

  unsigned int nY = nTileY * TILE_HEIGHT;
  unsigned int nX = nTileX * TILE_WIDTH;
  unsigned char *pScreen = (unsigned char *)0xC000 + ((nY / 8) * 80) + ((nY % 8) * 2048) + (nX / 2);
  unsigned char *pSprite = (unsigned char *)SpriteSand;

  switch(eTileType)
  {
    case TileType_Border:
      pSprite = (unsigned char *)SpriteBrick;
      break;
    case TileType_Rock:
      pSprite = (unsigned char *)SpriteRock;
      break;
    case TileType_SnakeBodyVert:
      pSprite = (unsigned char *)SpriteSnakeBodyVert;
      break;
    case TileType_SnakeBodyHorz:
      pSprite = (unsigned char *)SpriteSnakeBodyHorz;
      break;
    case TileType_SnakeHeadDown:
      pSprite = (unsigned char *)SpriteSnakeHeadDown;
      break;
    case TileType_SnakeHeadUp:
      pSprite = (unsigned char *)SpriteSnakeHeadUp;
      break;
    case TileType_SnakeHeadLeft:
      pSprite = (unsigned char *)SpriteSnakeHeadLeft;
      break;
    case TileType_SnakeHeadRight:
      pSprite = (unsigned char *)SpriteSnakeHeadRight;
      break;
  }
  
  PutSpriteMode0(pScreen, TILE_WIDTH / 2, TILE_HEIGHT, pSprite);
}
////////////////////////////////////////////////////////////////////////


////////////////////////////////////////////////////////////////////////
//SetColor
////////////////////////////////////////////////////////////////////////
void SetColor(unsigned char nColorIndex, unsigned char nPaletteIndex)
{
  __asm
    ld a, 4 (ix)
    ld b, 5 (ix)
    ld c, b
    call #0xBC32 ;SCR SET INK
  __endasm;
}
////////////////////////////////////////////////////////////////////////


////////////////////////////////////////////////////////////////////////
//SetMode
////////////////////////////////////////////////////////////////////////
void SetMode(unsigned char nMode)
{
  __asm
    ld a, 4 (ix)
    call #0xBC0E ;SCR_SET_MODE
  __endasm;
}
////////////////////////////////////////////////////////////////////////


////////////////////////////////////////////////////////////////////////
//SetCursor
////////////////////////////////////////////////////////////////////////
void SetCursor(unsigned char nColum, unsigned char nLine)
{
  __asm
    ld h, 4 (ix)
    ld l, 5 (ix)
    call #0xBB75 ;TXT SET CURSOR
  __endasm;
}
////////////////////////////////////////////////////////////////////////


////////////////////////////////////////////////////////////////////////
//InitMode0
////////////////////////////////////////////////////////////////////////
void InitMode0()
{
  SetMode(0);

  //SCR SET BORDER 0
  __asm
    ld b, #0 ;black
    ld c, b
    call #0xBC38
  __endasm;
}
////////////////////////////////////////////////////////////////////////


////////////////////////////////////////////////////////////////////////
//InitGame
////////////////////////////////////////////////////////////////////////
void InitGame()
{
  unsigned char nColor = 0;
  unsigned int nX = 0;
  unsigned int nY = 0;

  for(nColor = 0; nColor < NUM_COLORS; nColor++)
    SetColor(nColor, Palette[nColor]);

  for(nX = 0; nX < NUM_TILES_WIDTH; nX++)
    for(nY = 0; nY < NUM_TILES_HEIGHT; nY++)
      aBackgroundTiles[nX][nY] = TileType_None;

  for(nX = 0; nX < NUM_TILES_WIDTH; nX++)
  {
    aBackgroundTiles[nX][0] = TileType_Border;
    aBackgroundTiles[nX][NUM_TILES_WIDTH - 1] = TileType_Border;
  }

  for(nY = 0; nY < NUM_TILES_HEIGHT; nY++)
  {
    aBackgroundTiles[0][nY] = TileType_Border;
    aBackgroundTiles[NUM_TILES_HEIGHT - 1][nY] = TileType_Border;
  }

  aBackgroundTiles[4][4] = TileType_Rock;
  aBackgroundTiles[15][4] = TileType_Rock;
  aBackgroundTiles[4][15] = TileType_Rock;
  aBackgroundTiles[15][15] = TileType_Rock;
  aBackgroundTiles[9][9] = TileType_Rock;
  aBackgroundTiles[9][10] = TileType_Rock;
  aBackgroundTiles[10][9] = TileType_Rock;
  aBackgroundTiles[10][10] = TileType_Rock;
  
  nSnakePieces = 0;
  eDirection = Direction_Right;

  for(nX = 0; nX < MAX_SNAKE_PIECES; nX++)
  {
    aSnake[nX].nX = 0;
    aSnake[nX].nY = 0;
  }

  for(nX = 0; nX < 5; nX++)
  {
    aSnake[nX].nX = nX + 5;
    aSnake[nX].nY = 12;

    aBackgroundTiles[aSnake[nX].nX][aSnake[nX].nY] = nX < 4 ? TileType_SnakeBodyHorz : TileType_SnakeHeadRight;
    nSnakePieces++;
  }

  for(nX = 0; nX < NUM_TILES_WIDTH; nX++)
    for(nY = 0; nY < NUM_TILES_HEIGHT; nY++)
      DrawTile(nX, nY);
}
////////////////////////////////////////////////////////////////////////


////////////////////////////////////////////////////////////////////////
//KeyboardProcess
////////////////////////////////////////////////////////////////////////
void KeyboardProcess()
{
  char nChar = GetChar();

  if(nChar == 'Q' || nChar == 'q' || nChar == -16) //Up
  {
    if(eDirection == Direction_Left || eDirection == Direction_Right)
      eDirection = Direction_Up;
  }
  else if(nChar == 'A' || nChar == 'a' || nChar == -15) //Down
  {
    if(eDirection == Direction_Left || eDirection == Direction_Right)
      eDirection = Direction_Down;
  }
  else if(nChar == 'O' || nChar == 'o' || nChar == -14) //Left
  {
    if(eDirection == Direction_Up || eDirection == Direction_Down)
      eDirection = Direction_Left;
  }
  else if(nChar == 'P' || nChar == 'p' || nChar == -13) //Right
  {
    if(eDirection == Direction_Up || eDirection == Direction_Down)
      eDirection = Direction_Right;
  }
}
////////////////////////////////////////////////////////////////////////


////////////////////////////////////////////////////////////////////////
//MoveSnake
////////////////////////////////////////////////////////////////////////
unsigned char MoveSnake(unsigned char bGrow)
{
  
  _tSnakePiece *pSnakePieceHead = &aSnake[nSnakePieces - 1];
  enum _eTileType eTileType = TileType_None;
  //new piece
  switch(eDirection)
  {
    case Direction_Right:
      aSnake[nSnakePieces].nX = pSnakePieceHead->nX + 1;
      aSnake[nSnakePieces].nY = pSnakePieceHead->nY;
      eTileType = TileType_SnakeHeadRight;
      break;
    case Direction_Left:
      aSnake[nSnakePieces].nX = pSnakePieceHead->nX - 1;
      aSnake[nSnakePieces].nY = pSnakePieceHead->nY;
      eTileType = TileType_SnakeHeadLeft;
      break;
    case Direction_Up:
      aSnake[nSnakePieces].nX = pSnakePieceHead->nX;
      aSnake[nSnakePieces].nY = pSnakePieceHead->nY - 1;
      eTileType = TileType_SnakeHeadUp;
      break;
    case Direction_Down:
      aSnake[nSnakePieces].nX = pSnakePieceHead->nX;
      aSnake[nSnakePieces].nY = pSnakePieceHead->nY + 1;
      eTileType = TileType_SnakeHeadDown;
      break;
  }

  //has crashed
  if(aBackgroundTiles[aSnake[nSnakePieces].nX][aSnake[nSnakePieces].nY] != TileType_None)
    return 0;

  aBackgroundTiles[aSnake[nSnakePieces].nX][aSnake[nSnakePieces].nY] = eTileType;
  DrawTile(aSnake[nSnakePieces].nX, aSnake[nSnakePieces].nY);

  switch(aBackgroundTiles[pSnakePieceHead->nX][pSnakePieceHead->nY])
  {
    case TileType_SnakeHeadDown:
    case TileType_SnakeHeadUp:
      eTileType = TileType_SnakeBodyVert;
      break;
    case TileType_SnakeHeadLeft:
    case TileType_SnakeHeadRight:
      eTileType = TileType_SnakeBodyHorz;
      break;
  }

  aBackgroundTiles[pSnakePieceHead->nX][pSnakePieceHead->nY] = eTileType;
  DrawTile(pSnakePieceHead->nX, pSnakePieceHead->nY);

  nSnakePieces++;

  if(bGrow && nSnakePieces < MAX_SNAKE_PIECES)
    return 1;

  //delete tail of the snake
  aBackgroundTiles[aSnake[0].nX][aSnake[0].nY] = TileType_None;
  DrawTile(aSnake[0].nX, aSnake[0].nY);

  nSnakePieces--;
  memcpy(aSnake, &aSnake[1], sizeof(_tSnakePiece) * nSnakePieces);

  return 1;
}
////////////////////////////////////////////////////////////////////////


////////////////////////////////////////////////////////////////////////
//Game
////////////////////////////////////////////////////////////////////////
void Game()
{
  unsigned int nLastMoveTime = GetTime();
  unsigned int nGameMovements = 0;
  unsigned int nGameSpeed = 50;
  unsigned char bGrowSnake = 0;

  InitMode0();
  InitGame();

  while(1)
  {
    KeyboardProcess();

    if(GetTime() - nLastMoveTime < nGameSpeed)
      continue;

    nLastMoveTime = GetTime();
    nGameMovements++;

    if(nGameMovements % (nGameSpeed > 25 ? 20 : 40) == 0)
    {
      if(nGameSpeed > 15)
        nGameSpeed -= 2;

      bGrowSnake = 1;
    }

    if(!MoveSnake(bGrowSnake))
      break;

    bGrowSnake = 0;
  }

  SetMode(1);

  SetCursor(6, 7);
  printf("You have reached %d Movements", nGameMovements);

  SetCursor(8, 14);
  printf("Press Enter to play again");

  while(GetChar() != 13) {}
}
////////////////////////////////////////////////////////////////////////


////////////////////////////////////////////////////////////////////////
//ShowMenu
////////////////////////////////////////////////////////////////////////
void ShowMenu()
{
  SetMode(1);

  SetCursor(17, 5);
  printf("SNAKE");

  SetCursor(1, 8);
  printf("Use cursors or 'opqa' to move the snake");

  SetCursor(8, 16);
  printf("Press Enter to start game");

  SetCursor(3, 24);
  printf("Mochilote - www.cpcmania.com - 2012");

  while(GetChar() != 13) {}
}
////////////////////////////////////////////////////////////////////////


////////////////////////////////////////////////////////////////////////
//main
////////////////////////////////////////////////////////////////////////
void main()
{
  ShowMenu();

  while(1)
  {
    Game();
  }
}
////////////////////////////////////////////////////////////////////////

Si compilamos y ejecutamos obtenemos lo siguiente:

 

 

Pues con apenas 500 lineas de código (incluyendo comentarios y lineas en blanco) ha quedado chulo y sobretodo didáctico :-) Con poco esfuerzo podríamos modificarlo para incluir por ejemplo piedras que se mueven, ratones para comer, etc. Si alguien se anima, que me lo haga saber.

Podéis bajar un zip con todos ficheros (código fuente, bat's para compilar, binarios y dsk's) aquí: Snake.zip

 

www.CPCMania.com 2012