4
\$\begingroup\$

Previous question:

Text-based Snake game on Window

Summary of improvements:

  • Removed unnecessary functions such as clearScreen()
  • Added new Matrix and range helper classes
  • Implemented double-buffer by using WriteConsoleOutputA
  • Added the color
  • Split into separate files

How can I improve this game?

World.h

#pragma once
#include <Windows.h>
#include <vector>
#include <deque>
#include "Matrix.h"
#include "Utility.h"
#include "Points.h"
#include "Random.h"
#include "Enum.h"

namespace Tile
{
    enum
    {
        Empty,
        Wall,
        Food,
        Snake
    };
}

using Vector = std::vector<std::vector<int>>;

class World : public NonCopyable
{
public:
    World();

    void performAI();
    bool moveSnake();
    std::size_t numberEaten() const;
    void draw() const;

    template<typename T>
    void loadFromMemory(T begin, T end);

private:
    static Matrix<WorldSize::RowNum, WorldSize::ColNum, CHAR_INFO> mWorld;

    bool randomChance(double time)
    {
        static Random<double> random{ 0.0, 1.0 };
        return random.get() < time;
    }

    void placeFood();

    bool worldBoundry(const Point2D& pt) const;

    bool isCollide(const Point2D& head) const;

    Point2D getNextPosition(int dx, int dy);

    const std::size_t numRows = 17;
    const std::size_t numCols = 15;
    Vector mField;

    std::deque<Point2D> snake;
    std::minstd_rand rndEngine;
    int dx = 1, dy = 0;
    std::size_t numEaten = 0;
    const double mTurnRate = 0.2;
};

template<typename T>
void World::loadFromMemory(T begin, T end)
{
    std::vector<unsigned> words;
    words.reserve(std::distance(begin, end));

    for (auto i = begin; i != end; ++i)
    {
        words.push_back(*i);
    }

    for (const auto& i : range(0, mField.size()))
    {
        for (const auto& j : range(0, mField[0].size()))
        {
            mField[i][j] = words[j + numRows * i];
        }
    }
}

World.cpp

#include "World.h"
#include "Win32.h"

Matrix<WorldSize::RowNum, WorldSize::ColNum, CHAR_INFO> World::mWorld;

World::World()
    :rndEngine()
{
    mField.resize(numCols, std::vector<int>(numRows));
    snake.emplace_back(Point2D(7, 7));
}

std::size_t World::numberEaten() const
{
    return numEaten;
}

void World::placeFood()
{
    std::size_t row = rndEngine() % numRows;
    std::size_t col = rndEngine() % numCols;

    if (mField[row][col] == Tile::Empty)
    {
        mField[row][col] = Tile::Food;
    }
}

void World::draw() const
{
    int rowOffist = 5;
    int colOffist = 30;

    for (const auto& i : range(0, mField.size()))
    {
        for (const auto& j : range(0, mField[0].size()))
        {
            switch (mField[i][j])
            {
            case Tile::Wall:
                mWorld[rowOffist + i][colOffist + j].Char.AsciiChar = ' ';
                mWorld[rowOffist + i][colOffist + j].Attributes = 117;
                break;
            case Tile::Food:
                mWorld[rowOffist + i][colOffist + j].Char.AsciiChar = ' ';
                mWorld[rowOffist + i][colOffist + j].Attributes = 165;
                break;
            case Tile::Snake:
                mWorld[rowOffist + i][colOffist + j].Char.AsciiChar = ' ';
                mWorld[rowOffist + i][colOffist + j].Attributes = 180;
                break;
            default:
                mWorld[rowOffist + i][colOffist + j].Char.AsciiChar = ' ';
                mWorld[rowOffist + i][colOffist + j].Attributes = 0;
                break;
            }
        }
    }

    renderScreen(mWorld);
}

bool World::worldBoundry(const Point2D& point) const
{
    return point.col < numCols - 1 && point.row < numRows - 1;
}

bool World::isCollide(const Point2D& head) const
{
    return !worldBoundry(std::move(head)) ||
        mField[head.row][head.col] == Tile::Snake ||
        mField[head.row][head.col] == Tile::Wall;
}

Point2D World::getNextPosition(int dx, int dy)
{
    Point2D nextSpot = snake.front();

    nextSpot.col += dx;
    nextSpot.row += dy;

    return nextSpot;
}

void World::performAI()
{
    Point2D nextSpot = getNextPosition(dx, dy);

    if (isCollide(nextSpot) || randomChance(mTurnRate))
    {
        int leftDx = -dy;
        int leftDy = dx;

        int rightDx = dy;
        int rightDy = -dx;

        bool canMoveLeft = !isCollide(getNextPosition(leftDx, leftDy));
        bool canMoveRight = !isCollide(getNextPosition(rightDx, rightDy));

        bool turnLeft = false;
        double timeToTurn = 0.5;

        if (!canMoveLeft && !canMoveRight)
        {
            return;
        }
        else if (canMoveLeft && !canMoveRight)
        {
            turnLeft = true;
        }
        else if (!canMoveLeft && canMoveRight)
        {
            turnLeft = false;
        }
        else
        {
            turnLeft = randomChance(timeToTurn);
        }

        dx = turnLeft ? leftDx : rightDx;
        dy = turnLeft ? leftDy : rightDy;
    }
}

bool World::moveSnake()
{
    Point2D nextHead = getNextPosition(dx, dy);

    if (isCollide(nextHead))
    {
        return false;
    }

    bool isFood = (mField[nextHead.row][nextHead.col] == Tile::Food);

    mField[nextHead.row][nextHead.col] = Tile::Snake;

    snake.push_front(nextHead);

    if (isFood)
    {
        placeFood();
        ++numEaten;
    }
    else
    {
        mField[snake.back().row][snake.back().col] = Tile::Empty;
        snake.pop_back();
    }

    return true;
}

Game.h

#pragma once
#include <string>
#include "World.h"

class Game : public NonCopyable
{
public:
    void run();
    void loadReource();

private:
    static std::vector<unsigned> loadFromFile(const std::string& fileName);
    void displayResult();
    bool update();
    void draw();
    World mWorld;
    const double mWaitTime = 0.1;
    const size_t mMaxFood = 7;
};

Game.cpp

#include "Game.h"
#include <future>
#include <iostream>
#include <fstream>
#include <chrono>


void Game::displayResult()
{
    if (mWorld.numberEaten() == mMaxFood)
    {
        std::cout << "Yay!  The snake won!\n\n Score: " << mWorld.numberEaten() << '\n';
    }
    else
    {
        std::cout << "Oh no!  The snake crashed!\n\n Score: " << mWorld.numberEaten() << '\n';
    }
}

void Game::run()
{
    auto start = std::chrono::high_resolution_clock::now();

    while (mWorld.numberEaten() < mMaxFood)
    {
        auto end = std::chrono::high_resolution_clock::now();

        double timeTakenInSeconds = (end - start).count()
            * (static_cast<double>(std::chrono::high_resolution_clock::period::num)
            / std::chrono::high_resolution_clock::period::den);

        if (timeTakenInSeconds > mWaitTime)
        {
            if (!update())
            {
                break;
            }
            start = std::chrono::high_resolution_clock::now();
        }

        draw();
    }

    displayResult();
}

bool Game::update()
{
    mWorld.performAI();
    if (!mWorld.moveSnake())
    {
        return false;
    }
    return true;
}

void Game::draw()
{
    mWorld.draw();
}

void Game::loadReource()
{
    std::future<std::vector<unsigned>> resources = std::async(std::launch::async, loadFromFile, "test.txt");

    std::vector<unsigned> words = resources.get();

    //mWorld.loadFromMemory<decltype(words.begin())>(words.begin(), words.end());
    mWorld.loadFromMemory(words.begin(), words.end());

}

std::vector<unsigned> Game::loadFromFile(const std::string& fileName)
{
    std::ifstream file(fileName);

    if (!file)
    {
        throw "Can't open the file " + fileName;
    }

    std::vector<unsigned> words;

    for (unsigned word; file >> word;)
    {
        words.push_back(word);
    }

    return words;
}

Matrix.h

#pragma once

#include <cstddef>
#include "NonCopyable.h"

// helper class to be able using [row][col] in 1 dimentional array
template <std::size_t Rows, std::size_t Cols, typename T = double>
class Matrix : public NonCopyable
{
public:
    using class_type = Matrix <Rows, Cols, T>;

    T& at(std::size_t row, std::size_t col)
    {
        // This uses the const_cast/static_cast trick to implement the non-const at
        // in terms of the const at.  For more information on this trick, see Scott
        // Meyers' "Effective C++."
        return const_cast<T&>(static_cast<const class_type*>(this)->at(row, col));
    }

    const T& at(std::size_t row, std::size_t col) const
    {
        return *(begin() + row * numCols() + col);
    }

    std::size_t numRows() const
    {
        return Rows;
    }

    std::size_t numCols() const
    {
        return Cols;
    }

    std::size_t size() const
    {
        return numRows() * numCols();
    }

    // proxy approach to be able using [row][col] with Matrix object
    class MutableReference
    {
    public:
        T& operator[](std::size_t col)
        {
            return parent->at(row, col);
        }

    private:
        // Private constructor is the only way to make an instance of this type.
        // The Matrix is allowed to access it.
        MutableReference(Matrix* owner, std::size_t row)
            : parent(owner)
            , row(row)
        {}

        friend class Matrix;

        Matrix* const parent;
        const std::size_t row;
    };

    class ImmutableReference
    {
    public:
        const T& operator[](std::size_t col) const
        {
            return parent->at(row, col);
        }

    private:
        // Private constructor is the only way to make an instance of this type.
        // The Matrix is allowed to access it.
        ImmutableReference(const Matrix* owner, std::size_t row)
            : parent(owner)
            , row(row)
        {}

        friend class Matrix;

        const Matrix* const parent;
        const std::size_t row;
    };

    MutableReference operator[](std::size_t row)
    {
        return MutableReference(this, row);
    }

    ImmutableReference operator[](std::size_t row) const
    {
        return ImmutableReference(this, row);
    }

    using iterator = T*;
    using const_iterator = const T*;

    iterator data()
    {
        return elems;
    }

    const_iterator data() const
    {
        return elems;
    }

    iterator begin()
    {
        return elems;
    }

    const_iterator begin() const
    {
        return elems;
    }

    iterator end()
    {
        return begin() + size();
    }

    const_iterator end() const
    {
        return begin() + size();
    }

private:
    T elems[Rows * Cols];
};

Utility.h

#pragma once

#include <type_traits>

// helper class for for-range loop 
template<typename T, bool enable = std::is_integral<T>::value || std::is_enum<T>::value>
struct range_impl
{
    struct iterator
    {
        const T operator * () const noexcept
        {
            return value;
        }

            iterator& operator ++() noexcept
        {
            ++value;
            return *this;
        }

            friend  const bool operator != (const iterator& lhs, const iterator& rhs) noexcept
        {
            return lhs.value != rhs.value;
        }

        T value;
    };

    std::size_t size() const
    {
        return last - first;
    }

    const iterator begin() const noexcept
    {
        return{ first };
    }

        const iterator end() const noexcept
    {
        return{ last };
    }

    T first;
    T last;
};

template<typename T>
struct range_impl<T, false>
{
    range_impl(T first, T last)
        : first(first)
        , last(last)
    {}

    std::size_t size() const
    {
        return std::distance(first, last);
    }

    const T begin() const noexcept
    {
        return{ first };
    }

        const T end() const noexcept
    {
        return{ last };
    }

    T first;
    T last;
};

template<typename T1, typename T2>
range_impl<typename std::common_type<T1, T2>::type> range(T1 first, T2 last) noexcept
{
    return{ first, last };
}

NonCopyable.h

#pragma once

struct NonCopyable
{
    NonCopyable() = default;
    virtual ~NonCopyable() = default;

    NonCopyable(const NonCopyable &) = delete;
    NonCopyable(const NonCopyable &&) = delete;
    NonCopyable& operator = (const NonCopyable&) = delete;
};

Random.h

#include <random>
#include "Utility.h"

template<typename T>
struct Random : public NonCopyable
{
    Random(float min, float max)
        : mUniformDistribution(min, max)
    {}

    T get()
    {
        return mUniformDistribution(mEngine);
    }

    std::default_random_engine mEngine{ std::random_device()() };

    template <typename U>
    static auto dist() -> typename std::enable_if<std::is_integral<U>::value, std::uniform_int_distribution<U>>::type;

    template <typename U>
    static auto dist() -> typename std::enable_if<std::is_floating_point<U>::value, std::uniform_real_distribution<U>>::type;

    decltype(dist<T>()) mUniformDistribution;
};

Points.h

#pragma once

#include <cstddef>

struct Point2D
{
    Point2D()
        : row(0)
        , col(0)
    {}

    Point2D(std::size_t r, std::size_t c)
        : row(r)
        , col(c)
    {}
    virtual ~Point2D() = default;

    std::size_t row, col;
};

Win32.h

#pragma once

#include <Windows.h>
#include "Matrix.h"
#include "Enum.h"

void renderScreen(const Matrix<WorldSize::RowNum, WorldSize::ColNum, CHAR_INFO>& buffer, int width = 80, int height = 25)
{
    COORD charBufferSize{ width, height };
    COORD characterPosition{ 0, 0 };
    SMALL_RECT writeArea{ 0, 0, width - 1, height - 1 };

    WriteConsoleOutputA(GetStdHandle(STD_OUTPUT_HANDLE), buffer.data(), charBufferSize, characterPosition, &writeArea);
}

Enum.h

#pragma once

namespace WorldSize
{
    enum
    {
        ColNum = 80,
        RowNum = 25
    };
}

main.cpp

#include "Game.h"

int main()
{
    Game game;
    game.loadReource();
    game.run();
}

test.txt

1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 
1 2 0 0 0 0 0 0 0 0 0 0 0 0 0 2 1 
1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 
1 0 0 0 1 0 0 0 0 0 0 0 1 0 0 0 1 
1 0 0 1 1 1 0 0 2 0 0 1 1 1 0 0 1 
1 0 0 0 1 0 0 0 0 0 0 0 1 0 0 0 1 
1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 
1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 
1 0 0 0 1 0 0 0 0 0 0 0 1 0 0 0 1 
1 0 0 0 1 0 0 0 0 0 0 0 1 0 0 0 1 
1 0 0 0 1 0 0 0 2 0 0 0 1 0 0 0 1 
1 0 0 0 1 0 0 0 0 0 0 0 1 0 0 0 1 
1 0 0 0 1 0 0 0 0 0 0 0 1 0 0 0 1 
1 2 0 0 0 0 0 0 0 0 0 0 0 0 0 2 1 
1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.