Last Updated:

How to Write a snake Game in C++

 

Once upon a time, when monitors were green and 64KB of RAM on board was considered the norm, there was a toy called Snake. It was also known as SnakeBoaPython and even Worm. Over time, many clones of this game have appeared for various platforms: from Flash to mobile phones and smartphones. But that text-mode implementation apparently died along with the computers for which it was written.

And so, for a couple of free evenings, another clone of the legendary Snake was written, which I present to your attention: Oldschool Snake.

snake Game
  • How to play
  • License
  • Implementation Notes
  • Source code

How to play

Snake control by cursor keys. Esc - End the game. To exit the game, press Esc or the N key to the question "Once more?". The snake should not bump into the walls and its own tail. It's death. The snake does not know how to crawl with its tail forward. To try to force her to do so is certain death. It is necessary, of course, to feed the snake with dollars. When the snake eats, it grows.

Top 10 is determined by rating. The overall rating consists of the sum of the rating points received for each food eaten. Rating points are directly proportional to the length of the snake and inversely proportional to the time spent to achieve the next portion of food.

End of game
snake Game 2

License

GNU GPL. That is, you can freely distribute, study the source code, make changes to the source code, use in your non-commercial projects.

Implementation Notes

The toy is very simple. The basis of the toy was written in a couple of evenings. True, then, probably, a week was spent on debugging and, mainly, on testing and additional sawing. It is possible that there are unfinished bugs lurking somewhere. Shooting is permitted.

This version of the program already includes some opportunities to improve the game. But at the same time, authenticity is lost.

The program is written for Windows 2000 Professional (and above). To migrate to other OSes, you must rewrite the class implementation and have a port of the .CScreenconio.h

Compiled TDM-GCC 4.8.1 64-bit. I did not check with other compilers.

Questions, remarks, suggestions, errors - please in the comments. But, I will say right away, do not judge strictly for the style - it was written quickly and, as they say, "for yourself".

 

main.cpp

/*
 * (c) Productive Code. 2013
* GNU GPL * * Game "Oldschool Snake * */
#include <iostream> #include <conio.h> #include "CScreen.h" #include "CGame.h" using namespace std; int main() { setlocale(LC_ALL, "English"); try { CScreen screen; screen.cursor_show(false); screen.text_attr((WORD)0x0a); screen.cls(); CGame game(screen, 80, 24, 120); game.logo(); game.read_top10(); game.top10(false); game.pak(18); do { game.game_loop(); game.top10(true); } while (game.once_more()); game.goodbye(); } catch(CSScreenException& ex) { cerr << "*** " << ex.what() << endl; } return 0; }

CScreen.h

/*
* (c) Productive Code. 2013
* GNU GPL
 *
 */

#ifndef __CSCREEN_H__
#define __CSCREEN_H__

#include <windows.h>

/*
Exception class for CScreen class
*/

class CSScreenException {
public:
    CSScreenException(int _err) : err(_err) {}
    const char *what(); // returns a string describing the error
    int err; // error code
};


/*
The CScreen class contains system dependent calls for output to the console.

This implementation is intended for OS MS Windows 2000 Professional
and later.

Coordinate system:
    (0, 0) - upper left corner of the screen
    X-axis - horizontal to the right
    Y-axis - vertical down (positive direction)
*/

class CScreen {
public:
    CScreen();
    ~CScreen();
    void cursor_show(bool visible); // show/hide cursor
    void text_attr(WORD attr); // set text/background color
    void pos(int x, int y, char ch = 0); // cursor positioning and
                                                    // character output if ch != 0
    void pos_str(int x, int y, const char *str); // cursor position
                                                    // and string output
    void cls(); // clear screen

private:
    HANDLE hConsoleOutput;
    CONSOLE_CURSOR_INFO oldCursorInfo, curCursorInfo;
    WORD oldTextAttr;
};


#endif // __CSCREEN_H__

CScreen.cpp

/*   
* (c) Productive Code. 2013
* GNU GPL * */
#include "CScreen.h" #include <conio.h> const char *msgs[] = { "", "Failed GetStdHandle(): INVALID_HANDLE_VALUE", "Failed GetConsoleCursorInfo()", "Failed SetConsoleCursorInfo()", "Failed SetConsoleCursorPosition()" }; const char *CSScreenException::what() { return msgs[err]; } CScreen::CScreen() { hConsoleOutput = GetStdHandle(STD_OUTPUT_HANDLE); if (hConsoleOutput == INVALID_HANDLE_VALUE) throw CSScreenException(1); // "INVALID_HANDLE_VALUE" if (!GetConsoleCursorInfo(hConsoleOutput, &oldCursorInfo)) throw CSScreenException(2); curCursorInfo.dwSize = oldCursorInfo.dwSize; curCursorInfo.bVisible = oldCursorInfo.bVisible; CONSOLE_SCREEN_BUFFER_INFO csbi; GetConsoleScreenBufferInfo(hConsoleOutput, &csbi); oldTextAttr = csbi.wAttributes; } CScreen::~CScreen() { SetConsoleCursorInfo(hConsoleOutput, &oldCursorInfo); SetConsoleTextAttribute(hConsoleOutput, oldTextAttr); } void CScreen::cursor_show(bool visible) { curCursorInfo.bVisible = visible; if (!SetConsoleCursorInfo(hConsoleOutput, &curCursorInfo)) throw CSScreenException(3); } void CScreen::text_attr(WORD attr) { SetConsoleTextAttribute(hConsoleOutput, attr); } void CScreen::pos(int x, int y, char ch) { COORD point; point.X = static_cast<SHORT>(x); point.Y = static_cast<SHORT>(y); if (!SetConsoleCursorPosition(hConsoleOutput, point)) throw CSScreenException(4); if (ch > 0) _putch(ch); } void CScreen::pos_str(int x, int y, const char *str) { pos(x, y); _cprintf("%s", str); } void CScreen::cls() { system("cls"); }

SCoord.h

/*
* (c) Productive Code. 2013
  *GNU GPL
  *
  */

#ifndef __SCOORD_H__
#define __SCOORD_H__

struct SCoord {
     int x, y;
     SCoord() : x(0), y(0) {}
     SCoord(int _x, int _y) : x(_x), y(_y) {}
     SCoord& operator +=(const SCoord& op);
};

SCoord operator +(const SCoord& op1, const SCoord& op2);
bool operator ==(const SCoord& op1, const SCoord& op2);

#endif // __SCOORD_H__

SCoord.cpp

/*  
* (c) Productive Code. 2013
* GNU GPL * */
#include "SCoord.h" SCoord& SCoord::operator +=(const SCoord& op) { x += op.x; y += op.y; return *this; } SCoord operator +(const SCoord& op1, const SCoord& op2) { return SCoord(op1.x + op2.x, op1.y + op2.y); } bool operator ==(const SCoord& op1, const SCoord& op2) { return op1.x == op2.x && op1.y == op2.y; }

CSnake.h

/*
* (c) Productive Code. 2013
 *GNU GPL
 *
 */

#ifndef __CSNAKE_H__
#define __CSNAKE_H__

#include <vector>

#include "SCoord.h"
#include "cscreen.h"

using namespace std;

typedef vector<SCoord> CoordVector;

class CSnake {
public:
    csnake();
    void reset(SCoord start_pos); // "reset" the snake
    void draw(CScreen& scr); // initial drawing of the snake on the screen
    void move(const SCoord& delta, CScreen& scr); // movement of the snake by increment of coordinates
    void grow(const SCoord& pos, int growbits); // increasing the length of the snake
    bool into(const SCoord& pos); // check if the coordinates hit the body of the snake
    SCord head(); // method returns snake head coordinates
    intsize(); // method returns the length of the snake

private:
    CoordVector worm; // coordinate vector of snake body segments
    char head_mark; // character used to draw the snake's head
    unsigned int drawn; // length of the rendered body of the snake
};

#endif // __CSNAKE_H__

CSnake.cpp

/*
* (c) Productive Code. 2013
 *GNU GPL
 *
 */

#include "CSnake.h"

const char SNAKE_TAIL = '@'; // symbol for drawing segments of the snake's body, except for the head


CSnake::CSnake() {
    head_mark = '<';
}

void CSnake::reset(SCoord start_pos) {
    worm.clear();
    worm.reserve(1000); // reserve memory
    worm.push_back(start_pos); // add head coordinates
    worm.push_back(start_pos); // add tail coordinates
    worm[0].x++; // x coordinate of the tail - 1 to the right
}

void CSnake::draw(CScreen& scr) {
    unsigned int wsize = worm.size() - 1;
    for (unsigned int i = 0; i < wsize; i++)
        scr.pos(worm[i].x, worm[i].y, SNAKE_TAIL);
    scr.pos(worm[wsize].x, worm[wsize].y, head_mark);
    drawn = worm.size();
}

void CSnake::move(const SCoord& delta, CScreen& scr) {
    // When moving the snake, only the position of the head (and the first segment) is redrawn
    // and tail position. The remaining segments of the snake are not redrawn.

    // Redraw the tail.
    // The length of the snake increases as the snake grows (grow() method),
    // but the snake on the screen does not change. Therefore, if the rendered length of the snake
    // coincides with the real length, then the last segment of the snake (tail) is overwritten on the screen.
    // Otherwise, the tail stays in place, the head moves by one,
    // and the rendered length is increased.
    if (drawn == worm.size())
        scr.pos(worm[0].x, worm[0].y, ' ');
    else
        drawn++;

    // shift coordinates in the vector without drawing
    for (unsigned int i = 1; i < worm.size(); i++)
        worm[i - 1] = worm[i];

    worm[worm.size()-1] += delta; // head coordinate is incremented

    // select a symbol for drawing the head depending on the direction of movement
    if (delta.x < 0)
        head_mark = '<';
    else if (delta.x > 0)
        head_mark = '>';
    else if (delta.y < 0)
        head_mark = 'A';
    else // (delta.y > 0)
        head_mark = 'V';

    // Redrawing the head and first segment of the snake.
    scr.pos(worm[worm.size() - 1].x, worm[worm.size() - 1].y, head_mark);
    scr.pos(worm[worm.size() - 2].x, worm[worm.size() - 2].y, SNAKE_TAIL);
}

void CSnake::grow(const SCoord& pos, int growbits) {
    for (int i = 0; i < growbits; ++i)
        worm.insert(worm.begin(), pos);
}

bool CSnake::into(const SCoord& pos) {
    for (unsigned int i = 0; i < worm.size(); i++)
        if (worm[i].x == pos.x && worm[i].y == pos.y)
            return true;
    return false;
}

SCoord CSnake::head() {
    return worm[worm.size() - 1];
}

int CSnake::size() {
    return static_cast<int>(worm.size());
}

CGame.h

/*
* (c) Productive Code. 2013
 *GNU GPL
 *
 */

#ifndef __CGAME_H__
#define __CGAME_H__

#include <time.h>
#include <fstream>
#include <utility>

#include "cscreen.h"
#include "CSnake.h"
#include "SCoord.h"

using namespace std;

const int NAMELENGTH = 16; // buffer size for player name

// Structure for storing the result of the game

struct SRecord {
    charname[NAMELENGTH]; // Player name
    double rating; // rating
    int length; // snake length
    double game_time; // game time
    time_t date; // date and time the game ends

    SRecord();
    void as_string(char *buffer); // formatted result string
};


class CGame {
public:
    CGame(CScreen& _scr, int _width = 80, int _height = 24, int _latency = 100);
    voidgame_loop(); // main game loop
    void top10(bool after_game); // work with the top 10 table
    bool once_more(); // issue a request and receive a response from the player
    void pak(int y); // "Press any key for continue..."
    void read_top10(); // read top 10 results from table file
    void write_top10(); // write the top 10 results to the table file
    void logo(); // output game splash screen
    void goodbye(); // output copyright at the end of the game

private:
    enum Command { CMD_NOCOMMAND = 0, CMD_EXIT, CMD_LEFT, CMD_RIGHT, CMD_UP, CMD_DOWN };
    enum State { STATE_OK, STATE_EXIT, STATE_DIED };

    typedef pair<int, Command> CmdPair;

    int width, height; // width and height of the playing field
    int latency; // delay between position changes in milliseconds
    CScreen scr; // visualization subsystem
    Csnake snake; // snake
    double duration_game; // game duration
    double rating, rating_i; // final and partial rating

    SRecord ttop10[10]; // top 10 table

    CmdPair cmd_table[5]; // game control command table

    void draw_field(); // drawing the playing field
    Scoord make_food(); // calculate position for food
    void print_stat(); // output current statistics below the playing field
    command get_command(); // receive a command from the keyboard
    void top10_table(); // output a table of the top 10 results
};



#endif // __CGAME_H__

CGame.cpp

 /*
* (c) Productive Code. 2013
 *GNU GPL
 *
 */

#include "CGame.h"

#include <iostream>
#include <cstring>
#include <conio.h>

// format string for formatting the result of the game
const char *recordFormatStr = "%-15s %9.4f %4u %7.2f %s";

SRecord::SRecord() {
    name[0] = '\0';
    rating = 0.0;
    length = 0;
    game_time=0;
    date = static_cast<time_t>(0);
}

void SRecord::as_string(char *buffer) {
    sprintf(buffer, recordFormatStr, name, rating, length, game_time, ctime(&date));
}

ostream& operator << (ostream& os, const SRecord& rec) {
    os
        << rec.rating << ' '
        << rec.length << ' '
        << rec.game_time << ' '
        << rec.date << ' '
        << rec.name << endl;
    return os;
}

istream& operator >> (istream& is, SRecord& rec) {
    is
        >> rec.rating
        >> rec.length
        >> rec.game_time
        >> rec.date;
    is.ignore(1);
    is.getline(&rec.name[0], 16);
    return is;
}

// Function for comparing results by rating.
// Needed by qsort() to sort in descending order.
int rec_compare(const void *_op1, const void *_op2) {
    const SRecord *op1 = reinterpret_cast<const SRecord *>(_op1);
    const SRecord *op2 = reinterpret_cast<const SRecord *>(_op2);
    return static_cast<int>(op2->rating - op1->rating);
}

/*----- end of struct SRecord --------------------------------*/


// clear keyboard buffer
void clearkeys() {
    while (_kbhit())
        _getch();
}

// Constructor
// _scr - an object that outputs to the console
// _width - playing field width (x)
// _height - height of the playing field (y)
// _latency - delay between position changes in milliseconds

CGame::CGame(CScreen& _scr, int _width, int _height, int _latency) :
    width(_width), height(_height), latency(_latency), scr(_scr) {

    srand(static_cast<unsigned int>(time(NULL)));

    duration_game = 0;
    rating = 0.0;

    // initialization of the game control command table
    cmd_table[0] = CmdPair(27, CMD_EXIT); // escape key
    cmd_table[1] = CmdPair('K', CMD_LEFT); // left arrow
    cmd_table[2] = CmdPair('M', CMD_RIGHT); // right arrow
    cmd_table[3] = CmdPair('H', CMD_UP); // up arrow
    cmd_table[4] = CmdPair('P', CMD_DOWN); // arrow to down
}

CGame::Command CGame::get_command() {
    int ch;

    ch = _getch();
    if (ch == 0 || ch == 0xe0) {
        ch = _getch();
    }

    for (int i = 0; i < 5; i++) {
        if (cmd_table[i].first == ch) {
            return cmd_table[i].second;
        }
    }
    return CMD_NOCOMMAND;
}

// The food coordinate is calculated randomly.
// Restriction: the coordinate must not fall into the body of the snake.
SCoord CGame::make_food() {
    Scoord food;
    do {
        food.x = rand() % (width - 2) + 1;
        food.y = rand() % (height - 2) + 1;
    } while (snake.into(food));

    return food;
}


const char BORDER = '#'; // symbol for drawing the frame of the playing field


void CGame::draw_field() {

    scr.cls();

    for (int y = 0; y < height; y++) {
        if (y == 0 || y == height - 1) {
            for (int x = 0; x < width; x++)
                scr.pos(x, y, BORDER);
        }
        else {
            scr.pos(0, y, BORDER);
            scr.pos(width - 1, y, BORDER);
        }
    }
    scr.pos(0, height);
    _cprintf("Length: **** Rating: ****.**** (****.****) Time: ****.**");
}


void CGame::print_stat() {
    scr.pos(8, height);
    _cprintf("%04u", snake.size());
    scr.pos(22, height);
    _cprintf("%09.4f", rating);
    scr.pos(33, height);
    _cprintf("%09.4f", rating_i);
    scr.pos(51, height);
    _cprintf("%07.2f", duration_game);
}

void CGame::top10_table() {
    scr.cls();
    charbuf[80];

    scr.pos_str(width / 2 - 12, 2, "***** T O P 1 0 *****");
    scr.pos_str(5, 4, "Name Rating Length Time Date");


    for (int i = 0; i < 10; i++) {
        ttop10[i].as_string(buf);
        scr.pos_str(5, 5 + i, buf);
    }
}

void CGame::top10(bool after_game) {

    charbuf[80];
    char buf_encoded[NAMELENGTH];

    top10_table(); // show table of top 10 results

    time_tdate = time(NULL);
    if (after_game) {
        // if the game has been played, then show the current result
        scr.pos(5, 16);
        _cprintf(recordFormatStr, "Your result", rating, snake.size(), duration_game, ctime(&date));
    }

    if (rating > ttop10[9].rating) { // if the game's rating is greater than the lowest of the top 10...
        // request player name
        scr.pos_str(5, 20, "Your name: _");
        scr.pos(16, 20);
        cin.getline(&buf[0], NAMELENGTH);
        clearkeys();
        OemToCharBuff(buf, buf_encoded, static_cast<DWORD>(NAMELENGTH));
        // replace the last entry in the top 10 table
        strcpy(ttop10[9].name, buf_encoded);
        ttop10[9].date = date;
        ttop10[9].game_time = duration_game;
        ttop10[9].length = snake.size();
        ttop10[9].rating = rating;