Introduction to the Game
The Snake game, also known as the Snake and Ladder game, is a classic casual game available across multiple platforms including PC and mobile devices. It's simple yet engaging. Players control the snake's direction to eat food, causing the snake to grow longer. Originally designed as a single-player experience, the game has evolved to include various modes like team battles, bounty hunts, and challenge modes. This article will guide you through creating a basic version of the Snake game using C language.
Technical Overview
This tutorial uses C programming language to build a minimal implementation of the Snake game. The project incorporates fundamental C programming concepts and utilizes Win32 API for enhanced functionality.
Win32 API Overview
API stands for Application Programming Interface, which refers to a collection of functions provided by the Win32 operating system for developers to perform various operations on computer systems, such as creating windows, drawing graphics, and interacting with hardware devices. These functions act like building blocks that can be used to create richly featured and flexible applications. In essence, Win32 API is the programming interface for Microsoft Windows 32-bit platforms.
The following sections explain several Win32 API functions used in this project.
1. Console Commands
After launching the Snake program, a window will appear — this is the console, also referred to as cmd.
We can use console commands to set the console size and title.
mode Command
The mode command allows us to adjust the console window dimensions.
Example code:
system("mode con cols=100 lines=32");
This sets the console width to 100 and height to 32.
Important Note: The aspect ratio of the console is not 1:1, so it's crucial to understand this when designing the game layout.
Note: Using system() requires including <stdlib.h>.
title Command
The title command changes the console window's name.
Example code:
system("title Snake Game");
This updates the console window's title to "Snake Game".
2. GetStdHandle
Retrieves a handle to the specified standard device (standard input, output, or error).
Syntax:
HANDLE WINAPI GetStdHandle(
_In_ DWORD nStdHandle
);
Parameters include:
Return Value: If successful, returns a handle to the specified device, or a redirected handle set by a previous call to
SetStdHandle. Unless the application has set less privileged standard handles viaSetStdHandle, the handle hasGENERIC_READandGENERIC_WRITEaccess permissions.If the functon fails, the return value is
INVALID_HANDLE_VALUE.If the application has no associated standard handles (such as services running on an interactive desktop), and these handles have not been redirecetd, the return value is
NULL.
For more details, see the official documentation on GetStdHandle.
3. GetConsoleCursorInfo
Retrieves information about the cursor's size and visibility for the specified console screen buffer.
Syntax:
BOOL WINAPI GetConsoleCursorInfo(
_In_ HANDLE hConsoleOutput,
_Out_ PCONSOLE_CURSOR_INFO lpConsoleCursorInfo
);
Return Value:
Non-zero if successful.
Zero if failed.
4. CONSOLE_CURSOR_INFO
Contains information about the console cursor.
Syntax:
typedef struct _CONSOLE_CURSOR_INFO {
DWORD dwSize;
BOOL bVisible;
} CONSOLE_CURSOR_INFO, *PCONSOLE_CURSOR_INFO;
Members:
dwSize
Percentage of the character cell filled by the cursor. Values range from 1 to 100. The appearance varies from fully filled cells to a horizontal line at the bottom of the cell.bVisible
Visibility status of the cursor. Set to TRUE if visible.
To hide the cursor in the game, we set bVisible to false.
CursorInfo.bVisible = false;
5. SetConsoleCursorInfo
Sets the cursor size and visibility for the specified console screen buffer.
Syntax:
BOOL WINAPI SetConsoleCursorInfo(
_In_ HANDLE hConsoleOutput,
_In_ const CONSOLE_CURSOR_INFO *lpConsoleCursorInfo
);
Parameters:
hConsoleOutput [in]
Handle to the console screen buffer. Must have GENERIC_READ access permission.lpConsoleCursorInfo [in]
Pointer to aCONSOLE_CURSOR_INFOstructure containing specifications for the cursor.
Return Value:
Non-zero if successful.
Zero if failed.
To hide the cursor, we must first retrieve the current cursor info, modify it, and then apply it:
HANDLE hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
CONSOLE_CURSOR_INFO CursorInfo;
GetConsoleCursorInfo(hOutput, &CursorInfo);
CursorInfo.bVisible = false;
SetConsoleCursorInfo(hOutput, &CursorInfo);
6. COORD
Defines coordinates for character cells in the console screen buffer. The origin (0,0) is located at the top-left cell of the buffer.
Syntax:
typedef struct _COORD {
SHORT X;
SHORT Y;
} COORD, *PCOORD;
7. SetConsoleCursorPosition
Sets the cursor position in the specified console screen buffer.
Syntax:
BOOL WINAPI SetConsoleCursorPosition(
_In_ HANDLE hConsoleOutput,
_In_ COORD dwCursorPosition
);
Parameters:
hConsoleOutput [in]
Handle to the console screen buffer. Must have GENERIC_READ access permission.dwCursorPosition [in]
ACOORDstructure specifying the new cursor position in characters. Coordinates must be within the console screen buffer boundaries.
Return Value:
Non-zero if successful.
Zero if failed.
8. GetAsyncKeyState
Determines whether a key is currently pressed or released, and whether it was pressed since the last call to GetAsyncKeyState.
Syntax:
SHORT GetAsyncKeyState(
[in] int vKey
);
Parameters:
[in] vKeyType: int
Virtual key code. Refer to virtual key codes for more information.
Constants can be used to distinguish between left and right keys.
Return Value:
Type: SHORT
Non-zero if the key was pressed since the last call to
GetAsyncKeyStateand is currently down. If the most significant bit is set, the key is down. If the least significant bit is set, the key was pressed since the last call.Returns zero under the following conditions:
- Current desktop is not active
- Foreground thread belongs to another process, and desktop does not allow hooks or logging.
These APIs form the foundation for implementing the Snake game.
Complete Snake Game Code
snake.h
#pragma once
#include<stdlib.h>
#include<windows.h>
#include<stdbool.h>
#include<stdio.h>
#include<time.h>
// Initial snake position
#define POS_X 24
#define POS_Y 5
#define WALL L'□'
#define BODY L'●'
#define FOOD L'▲'
#define KEY_PRESS(VK) ( (GetAsyncKeyState(VK) & 0x1) ? 1 : 0 )
// Snake node structure
typedef struct SnakeNode
{
// Body coordinates
int x;
int y;
struct SnakeNode* next;
}SnakeNode;
// Snake state enumeration
enum SNAKE_STATUS
{
OK,
KILL_BY_WALL,
KILL_BY_SELF,
END_NORMAL
};
// Direction enumeration
enum DIRECTION
{
UP,
DOWN,
LEFT,
RIGHT
};
// Snake data structure
typedef struct Snake
{
SnakeNode* pSnake;// Head node pointer
SnakeNode* pFood;// Food node pointer
enum SNAKE_STATUS pStatus;// Game status
enum DIRECTION dir;// Movement direction
int food_weight;// Points per food item
int score;// Total score
int sleep_time;// Delay time, affects movement speed
}Snake;
// Initialize game
void GameStart(Snake* snake);
// Set cursor position
void SetPos(short x, short y);
// Display welcome screen
void WelcomeToGame();
// Create game map
void CreatMap();
// Initialize snake body
void InitSnake(Snake* snake);
// Generate food
void CreateFood(Snake* snake);
// Main game loop
void GameRun(Snake* snake);
// Display game information
void GameInfo(Snake* snake);
void GameStop();
void SnakeFast(Snake* snake);
void SnakeSlow(Snake* snake);
// Move snake
void SnakeMode(Snake* snake);
// End game
void GameEnd(Snake* snake);
snake.c
#include"snake.h"
// Set cursor position
void SetPos(short x, short y)
{
COORD pos = { x, y };
HANDLE hOutput = NULL;
hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
SetConsoleCursorPosition(hOutput, pos);
}
// Welcome screen
void WelcomeToGame()
{
SetPos(40, 16);
wprintf(L"Welcome to Snake Game");
SetPos(40, 28);
system("pause");
system("cls");
SetPos(35, 15);
wprintf(L"Use ↑↓←→ to move, F3 to speed up, F4 to slow down");
SetPos(35, 16);
wprintf(L"Speed increases score");
SetPos(40, 28);
system("pause");
system("cls");
}
// Create game border
void CreatMap()
{
// Top wall
for (int i = 0; i < 29; i++)
{
wprintf(L"%lc", WALL);
}
// Bottom wall
SetPos(0, 26);
for (int i = 0; i < 29; i++)
{
wprintf(L"%lc", WALL);
}
// Left wall
for (int i = 1; i <= 25; i++)
{
SetPos(0, i);
wprintf(L"%lc", WALL);
}
// Right wall
for (int i = 1; i <= 25; i++)
{
SetPos(56, i);
wprintf(L"%lc", WALL);
}
}
// Initialize snake
void InitSnake(Snake* snake)
{
int i = 0;
for (i = 0; i < 5; i++)// Initial length of 5 segments
{
SnakeNode* newSnakeNode = (SnakeNode*)malloc(sizeof(SnakeNode));
if (newSnakeNode == NULL)
{
perror("malloc() fail");
return;
}
// Initialize segment properties
newSnakeNode->next = NULL;
newSnakeNode->x = POS_X + i * 2;
newSnakeNode->y = POS_Y;
// Insert at head
if (snake->pSnake == NULL)
{
snake->pSnake = newSnakeNode;
}
else
{
newSnakeNode->next = snake->pSnake;
snake->pSnake = newSnakeNode;
}
}
// Render initial snake
SnakeNode* cur = snake->pSnake;
for (i = 0; i < 5; i++)
{
SetPos(cur->x, cur->y);
wprintf(L"%lc", BODY);
cur = cur->next;
}
snake->dir = RIGHT;
snake->food_weight = 10;
snake->score = 0;
snake->sleep_time = 200;
snake->pStatus = OK;
}
// Generate food
void CreateFood(Snake* snake)
{
int x = 0;
int y = 0;
// Create food node
SnakeNode* food = (SnakeNode*)malloc(sizeof(SnakeNode));
if (food == NULL)
{
perror("malloc()fail");
return;
}
again:
do
{
x = rand() % 53 + 2;
y = rand() % 25 + 1;
} while (x % 2 != 0);
SnakeNode* cur = snake->pSnake;
while (cur)
{
if (x == cur->x && y == cur->y)
{
goto again;
}
cur = cur->next;
}
food->x = x;
food->y = y;
food->next = NULL;
SetPos(x, y);
wprintf(L"%lc", FOOD);
snake->pFood = food;
}
void GameStart(Snake* snake)
{
//0. Set console size, hide cursor
system("mode con cols=100 lines=32");
system("title Snake Game");
HANDLE hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
// Hide cursor
CONSOLE_CURSOR_INFO CursorInfo;
GetConsoleCursorInfo(hOutput, &CursorInfo);
CursorInfo.bVisible = false;
SetConsoleCursorInfo(hOutput, &CursorInfo);
//1. Welcome screen
WelcomeToGame();
//2. Create map
CreatMap();
//3. Initialize snake
InitSnake(snake);
//4. Generate food
CreateFood(snake);
}
// Display game info
void GameInfo(Snake* snake)
{
SetPos(68, 17);
printf("F3 Speed Up, F4 Slow Down");
SetPos(68, 18);
printf("ESC Exit, SPACE Pause");
}
void GameStop()
{
while (1)
{
Sleep(200);
if (KEY_PRESS(VK_SPACE))
{
break;
}
}
}
void SnakeFast(Snake* snake)
{
if (snake->sleep_time > 80)
{
snake->sleep_time -= 20;
snake->food_weight += 2;
}
}
void SnakeSlow(Snake* snake)
{
if (snake->sleep_time < 280)
{
snake->sleep_time += 20;
snake->food_weight -= 2;
}
}
int NextIsFood(Snake* snake, SnakeNode* NextNode)
{
return snake->pFood->x == NextNode->x && snake->pFood->y == NextNode->y;
}
void EatFood(Snake* snake, SnakeNode* NextNode)
{
NextNode->next = snake->pSnake;
snake->pSnake = NextNode;
SnakeNode* cur = snake->pSnake;
while (cur)
{
SetPos(cur->x, cur->y);
wprintf(L"%lc", BODY);
cur = cur->next;
}
free(snake->pFood);
snake->pFood = NULL;
snake->score += snake->food_weight;
CreateFood(snake);
}
void NextNoFood(Snake* snake, SnakeNode* NextNode)
{
NextNode->next = snake->pSnake;
snake->pSnake = NextNode;
SnakeNode* cur = snake->pSnake;
while (cur->next->next)
{
cur = cur->next;
}
SetPos(cur->next->x, cur->next->y);
printf(" ");
free(cur->next);
cur->next = NULL;
cur = snake->pSnake;
while (cur)
{
SetPos(cur->x, cur->y);
wprintf(L"%lc", BODY);
cur = cur->next;
}
}
// Wall collision
void kill_by_wall(Snake* snake)
{
if (snake->pSnake->x == 0 || snake->pSnake->x == 56
|| snake->pSnake->y == 0 || snake->pSnake->y == 27)
{
snake->pStatus = KILL_BY_WALL;
}
}
// Self collision
void kill_by_self(Snake* snake)
{
SnakeNode* cur = snake->pSnake->next;
while (cur)
{
if (cur->x == snake->pSnake->x && cur->y == snake->pSnake->y)
{
snake->pStatus = KILL_BY_SELF;
break;
}
cur = cur->next;
}
}
void SnakeMode(Snake* snake)
{
SnakeNode* NextNode = (SnakeNode*)malloc(sizeof(SnakeNode));
if (NextNode == NULL)
{
perror("malloc()fail");
return;
}
switch(snake->dir)
{
case UP:
NextNode->x = snake->pSnake->x;
NextNode->y = snake->pSnake->y - 1;
break;
case DOWN:
NextNode->x = snake->pSnake->x;
NextNode->y = snake->pSnake->y + 1;
break;
case LEFT:
NextNode->x = snake->pSnake->x - 2;
NextNode->y = snake->pSnake->y;
break;
case RIGHT:
NextNode->x = snake->pSnake->x + 2;
NextNode->y = snake->pSnake->y;
break;
}
if (NextIsFood(snake, NextNode))
{
EatFood(snake, NextNode);
}
else
{
NextNoFood(snake, NextNode);
}
// Wall collision check
kill_by_wall(snake);
// Self collision check
kill_by_self(snake);
}
// Run game loop
void GameRun(Snake* snake)
{
// Show game info
GameInfo(snake);
do
{
SetPos(68, 14);
printf("Points per food: %2d", snake->food_weight);
SetPos(68, 15);
printf("Total Score: %d", snake->score);
if (KEY_PRESS(VK_UP) && snake->dir != DOWN)
{
snake->dir = UP;
}
else if (KEY_PRESS(VK_DOWN) && snake->dir != UP)
{
snake->dir = DOWN;
}
else if (KEY_PRESS(VK_LEFT) && snake->dir != RIGHT)
{
snake->dir = LEFT;
}
else if (KEY_PRESS(VK_RIGHT) && snake->dir != LEFT)
{
snake->dir = RIGHT;
}
else if (KEY_PRESS(VK_ESCAPE))
{
snake->pStatus = END_NORMAL;
}
else if (KEY_PRESS(VK_SPACE))
{
GameStop();
}
else if (KEY_PRESS(VK_F3))
{
SnakeFast(snake);
}
else if (KEY_PRESS(VK_F4))
{
SnakeSlow(snake);
}
// Move snake
SnakeMode(snake);
Sleep(snake->sleep_time);
} while (snake->pStatus == OK);
}
// End game cleanup
void GameEnd(Snake* snake)
{
SetPos(25, 14);
switch (snake->pStatus)
{
case END_NORMAL:
printf("Game exited normally");
break;
case KILL_BY_SELF:
printf("Collided with self, game over");
break;
case KILL_BY_WALL:
printf("Hit wall, game over");
break;
}
SnakeNode* cur = snake->pSnake;
while (cur)
{
SnakeNode* del = cur;
cur = cur->next;
free(del);
del = NULL;
}
}
test.c
#include <locale.h>
#include "snake.h"
test()
{
int ch = 0;
do
{
system("cls");
// Create snake instance
Snake snake = { 0 };
// Initialize game
GameStart(&snake);
// Includes:
// 1. Welcome screen
// 2. Map creation
// 3. Snake initialization
// 4. Food generation
// Run game
GameRun(&snake);
// Cleanup
GameEnd(&snake);
SetPos(25, 23);
system("pause");
SetPos(25, 24);
printf("Play Again? (Y/N):");
ch = getchar();
while(getchar()!='\n');
} while (ch == 'Y' || ch == 'y');
SetPos(0, 28);
}
int main()
{
// Localize
setlocale(LC_ALL, "");
srand((unsigned int)time(NULL));
test();
return 0;
}
Note: Localization settings with setlocale()
Usage reference: setlocale - C++ Reference (cplusplus.com)
Since wide characters are used for display, localization must be configured.