Snake: Minigame with tile's, keyboard and sprites (C & ASM with SDCC)

Pincha aquí para verlo en español

In this tutorial we will see how to make a variant of the famous snake game, which in this case has to avoid colliding with the walls and other obstacles, every little time the snake grows and the speed increases... To develop it we will introduce new concepts, such as reading the keyboard to control the snake, and an introduction to the 'tiles', used in many games.

For the keyboard we will use the firmware, since in this case we do not need multiple simultaneous keypresses and is the simplest for now. Let's use the command KM READ CHAR (BB09), through the following function:

////////////////////////////////////////////////////////////////////////
//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;
}
////////////////////////////////////////////////////////////////////////

For the game we will use the letters 'opqa' and arrows. To find its numerical value, we make a simple program that will print both the character and the code every time you press a key:

////////////////////////////////////////////////////////////////////////
// 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);
  }
}
////////////////////////////////////////////////////////////////////////

If we run it on an emulator and press several keys get something like this:

SNAKE

 

Let's see now how we will organize the game, for this we will use 'tiles'. What is a 'tile'? A 'tile' is each one of the cells or subdivisions of the screen that we manage in the game. In our case, each tile can be a part of the snake, a border, a stone or part of the background. When the snake moves, will move one by one and also be painted 'tile' for 'tile', when necessary. To this minigame will divide the screen (160x200 mode 0), in tiles of 8x10 pixels, which leaves us we 20x20 tiles..

To handle all this easily from our source code, we will use defines, enums and arrays of C, as follows:

#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

By using aBackgroundTiles array we easily obtain or modify the type of a tile. To handle the snake we use another array with each of the parts of the snake (that will grow) and also have an enum for movement:

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;

Let's see an early version of the game, instead of painting sprites, each type of tile filled with a color. The code is fully functional, with the absence to put the graphics. We include the following functions DrawTile, KeyboardProcess and MoveSnake that are the bulk of the game and also have the functions SetColor, SetMode, SetCursor, InitGame, Game and ShowMenu, very short and simple. The source code is as follows:

////////////////////////////////////////////////////////////////////////
// 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();
  }
}
////////////////////////////////////////////////////////////////////////

As we see in Game function, which is the main loop of the game, what we do is to process the keyboard, and when appropriate the snake moves, every 20 movements of the snake, it increases the speed and the snake grows. In function MoveSnake we see how moves the head of the serpent, the new tile is painted and then remove the tail of the snake (unless the snake grow) and repaints to show the tile background. If you compile and run we get the following:

 

 

To make the game more cool, let's get sprites, the sprites in this case should be of the size of the tile so I have drawn these sprites to use in the game:

     

Once converted and adapted (as we have seen in previous tutorials) we have the following:

////////////////////////////////////////////////////////////////////////
// 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
};

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

We modified the program a bit and finally this is the game's source code:

////////////////////////////////////////////////////////////////////////
// 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();
  }
}
////////////////////////////////////////////////////////////////////////

If you compile and run we get the following:

 

 

With just 500 lines of code (including comments and blank lines) has been a cool game and especially educational :-) With little effort we could modify it to include for example stones that move, mouse to eat etc.. If anyone is encouraged, please let me know.

You could download a zip with all files (source code, bat to compile, binary and dsk's) here: Snake.zip

 

www.CPCMania.com 2012