3
\$\begingroup\$

I create a game in the terminal the premise(for now) is walking around a field collecting points marked as $, and avoiding mines marked as !, the player is marked as @ and normal terrain marked as #.
the playing field consists of a bunch of boards, all the boards are the same size and the field is those boards connected in rectangular shape.
the game shows only the boards the player visited, so if you walk in an L shape across the board you will see only the left column and the bottom row.
when a player collects a $ it will reset the visible boards so the player wont know where the next $ spawned.
when the player steps on a mine he takes damage and when it reaches 0, the game end showing the amount of points he collected, for now I made the player to lose from 1 mine because I haven't implemented mine removal without which it will cause the player take damage as long a he stands on the mine.
here's the code:
Point class
serves storing location of different entities.
stores a board number to know which, it is needed because every board uses coordinates of its own for example a 2*2 board will have (0,0),(1,0),(0,1),(1,1) and the next board will have those same coordinates.
header

#pragma once

class Point
{
private:
    int boardNum;
    int x;
    int y;

public:
    Point();
    Point(int boardNum, int x, int y);
    ~Point();
    
    bool operator == (const Point& point);

    int GetBoardNum();
    int GetX();
    int GetY();

    void SetBoardNum(int _boardNum);
    void SetX(int _x);
    void SetY(int _y);
};

cpp

#include "Point.h"


Point::Point()
{
    boardNum = 0;
    x = 0;
    y = 0;
}
Point::Point(int boardNum, int x, int y) : boardNum(boardNum), x(x), y(y)
{

}
Point::~Point()
{

}

bool Point::operator == (const Point& point)
{
    return boardNum == point.boardNum && x == point.x && y == point.y;
}

int Point::GetBoardNum()
{
    return boardNum;
}
int Point::GetX()
{
    return x;
}
int Point::GetY()
{
    return y;
}

void Point::SetBoardNum(int _boardNum)
{
    boardNum = _boardNum;
}
void Point::SetX(int _x)
{
    x = _x;
}
void Point::SetY(int _y)
{
    y = _y;
}

Player

the player class stores a location as a Point, health which is currently not really in use and the Move method which changes the location based on the direction.
header

#pragma once
#include "Includes.h"
#include "Point.h"

class Player
{
private:
    Point currentLocation;
    int health;

public:
    Player();
    Player(int health);
    ~Player();

    int GetX();
    int GetY();
    void SetX(int x);
    void SetY(int y);
    int GetCurrentBoard();
    int GetHealth();
    Point GetCurrentLocation();

    void TakeDamage(int damageAmount);
    void SetCurrentBoard(int boardNum);

    void Move(int direction);

    void ResetPlayer(int _health);

};

cpp

#include "Player.h"

Player::Player() : currentLocation(0, 1, 1)
{
    health = 3;
}
Player::Player(int health) : health(health), currentLocation(0, 1, 1)
{

}
Player::~Player()
{
     
}

int Player::GetX() 
{
    return currentLocation.GetX();
}
int Player::GetY() 
{
    return currentLocation.GetY();
}
void Player::SetX(int x) 
{
    currentLocation.SetX(x);
}
void Player::SetY(int y) 
{
    currentLocation.SetY(y);
}
int Player::GetCurrentBoard() 
{
    return currentLocation.GetBoardNum();
}
Point Player::GetCurrentLocation()
{
    return currentLocation;
}

void Player::SetCurrentBoard(int field)
{
    currentLocation.SetBoardNum(field);
}
int Player::GetHealth()
{
    return health;
}

void Player::TakeDamage(int damageAmount)
{
    health -= damageAmount;
}

void Player::Move(int direction) //changes the location based on the direction
{
    switch (direction)
    {
    case KEY_UP:
        currentLocation.SetY(currentLocation.GetY() - 1);
        break;
    case KEY_DOWN:
        currentLocation.SetY(currentLocation.GetY() + 1);
        break;
    case KEY_RIGHT:
        currentLocation.SetX(currentLocation.GetX() + 1);
        break;
    case KEY_LEFT:
        currentLocation.SetX(currentLocation.GetX() - 1);
        break;
    default:
        break;
    }
}

void Player::ResetPlayer(int _health)
{
    currentLocation.SetBoardNum(0);
    currentLocation.SetX(1);
    currentLocation.SetY(1);
    health = _health;
}

GameField
the GameField class represents the boards and is manipulating the board for example prints only the boards that the player visited, generating new mines and $ and a bit more.
I believe the comments in my code explain each method pretty good so I wont be doing it here but if you have any questions I will answer. I also added a random number generator outside of the class.
header

#pragma once
#include "Player.h"
#include <vector>
#include <set>
#include <algorithm>
#include <iostream>
#include <random>
#include <tuple>

class GameField
{
private:
    int boardLength; // board and field dimansions
    int boardWidth;
    int fieldLength;
    int fieldWidth;

    std::vector<std::vector<char>> board; // a single board
    std::vector<std::vector<std::vector<char>>> boardList; // list 
    std::vector<std::vector<std::vector<char>>> initialBoardList; // a representaion of the field but without the player @ absrating the point he stands on

    Point collectibleLocation; // the board and coordinate for a $ to collect
    std::vector<Point> mines; // the board and coordinate for a mine to appear

    std::set<int> visibleBoards; // board numbers that are visible to the player
    Player& player;

public:
    GameField(int boardWidth, int boardLength, int fieldLength, int fieldWidth, Player& player);
    ~GameField();

    void UpdateBoard(); //updates the board based on changes made

    void PrintBoard();

    std::vector<std::vector<char>> GetBoard();
    std::vector<std::vector<std::vector<char>>> GetBoardList();
    std::vector<std::vector<std::vector<char>>> GetInitialBoardList();
    void SetPreviousBoard(int boardNum)
    {
        boardList.at(boardNum) = initialBoardList.at(boardNum);
    }
    // getters
    int GetBoardLength();
    int GetBoardWidth();
    int GetFieldLength();
    int GetFieldWidth();
    Player GetPlayer();
    Point GetCollectiobleLocation();
    std::vector<Point> GetMines();  

    void AddVisibleBoard(int board); // adds a new visible board the player just visited
    void GenNewPoint(); // generates a new mine and a new $
    void ResetVisibleBoards();

};


int GenRandomNumber(int start, int end);

cpp

#include "GameField.h"
int GenRandomNumber(int start, int end)
{
    std::random_device rd; // obtain a random number from hardware
    std::mt19937 gen(rd()); // seed the generator
    std::uniform_int_distribution<> distr(start, end); // define the range
    return distr(gen);
}
GameField::GameField(int boardWidth, int boardLength, int fieldLength, int fieldWidth, Player& player) : 
    player(player), boardLength(boardLength), boardWidth(boardWidth), fieldLength(fieldLength), fieldWidth(fieldWidth)
{
    for (int i = 0; i < boardLength; i++)
    {
        std::vector<char> line;

        for (int j = 0; j < boardWidth; j++)
        {
            line.push_back('#');
        }

        board.push_back(line);
    }

    for (int i = 0; i < fieldLength; i++)
    {
        for (int j = 0; j < fieldWidth; j++)
        {           
            boardList.push_back(board);
        }
    }

    initialBoardList = boardList;
    GenNewPoint();
    boardList.at(collectibleLocation.GetBoardNum()).at(collectibleLocation.GetX()).at(collectibleLocation.GetY()) = '$'; // puts the $ on the random locationg provided bu GenNewPoint()
    visibleBoards.insert({0}); // the first visible board is 0 thats where the player starts
    boardList.at(0).at(player.GetY()).at(player.GetX()) = '@'; // places an indicator for the player
}

GameField::~GameField()
{
}

void GameField::GenNewPoint() // generates a new $ point and a mine at a random location on the board
{
    initialBoardList.at(collectibleLocation.GetBoardNum()).at(collectibleLocation.GetX()).at(collectibleLocation.GetY()) = '#'; // erases the previous $ point that the player collected
    collectibleLocation.SetBoardNum(GenRandomNumber(0, fieldLength * fieldWidth - 1)); // generating random board number to win
    collectibleLocation.SetX(GenRandomNumber(0, boardLength - 1)); // generating x and y for the wining position
    collectibleLocation.SetY(GenRandomNumber(0, boardWidth - 1));
    initialBoardList.at(collectibleLocation.GetBoardNum()).at(collectibleLocation.GetX()).at(collectibleLocation.GetY()) = '$'; //places the $ on the field

    Point* mineToAdd;
    do
        mineToAdd = new Point(GenRandomNumber(0, fieldLength * fieldWidth - 1), GenRandomNumber(0, boardLength - 1), GenRandomNumber(0, boardWidth - 1));
    while (*mineToAdd == collectibleLocation || std::count(mines.begin(), mines.end(), *mineToAdd) || player.GetCurrentLocation() == *mineToAdd); // generates a mine until it wont collide with $, another mine or player position

    mines.push_back(*mineToAdd); // adds the mine to the vector so with every point collected more and more mines are generated
    initialBoardList.at(mineToAdd->GetBoardNum()).at(mineToAdd->GetY()).at(mineToAdd->GetX()) = '!'; // adds the indicator for the mine
}

void GameField::ResetVisibleBoards()
{
    visibleBoards.clear(); //resets visible board memory
    visibleBoards.insert(player.GetCurrentBoard()); // adds the current player board back to memory
}

void GameField::AddVisibleBoard(int board)
{
    visibleBoards.insert({ board }); // adds a new visible board a player can see happens when a player visits a new board
}

void GameField::UpdateBoard()
{
    int currentBoard = player.GetCurrentBoard();
    for (int i = 0; i < boardList.at(currentBoard).size(); i++)
    {
        for (int j = 0; j < boardList.at(currentBoard).at(i).size(); j++)
        {
            boardList.at(currentBoard).at(i).at(j) = initialBoardList.at(currentBoard).at(i).at(j); // reset the current field to original without the player
        }
    }
    boardList.at(currentBoard).at(player.GetY()).at(player.GetX()) = '@'; // place the player again at the new position
}

int GameField::GetBoardLength()
{
    return boardLength;
}
int GameField::GetBoardWidth()
{
    return boardWidth;
}

void GameField::PrintBoard()
{
    UpdateBoard(); // updates the board if any changes were made
    for (int i = 0; i < boardList.size(); i += fieldWidth) // runs on the field and prints the ones that the player visited and space on the one that the player did not
    {
        for (int j = 0; j < boardList.at(0).size(); j++)
        {
            for (int k = i; k < i + fieldWidth; k++)
            {
                for (int l = 0; l < boardList.at(l).at(j).size(); l++)
                {
                    if (!std::binary_search(visibleBoards.begin(), visibleBoards.end(), k))
                        std::cout << ' ';
                    else
                        std::cout << boardList.at(k).at(j).at(l);
                }
            }
            std::cout << '\n';
        }

    }

}
//Getters
int GameField::GetFieldLength()
{
    return fieldLength;
}
int GameField::GetFieldWidth()
{
    return fieldWidth;
}
std::vector<Point> GameField::GetMines()
{
    return mines;
}
Point GameField::GetCollectiobleLocation()
{
    return collectibleLocation;
}

std::vector<std::vector<char>> GameField::GetBoard()
{
    return board;
}
std::vector<std::vector<std::vector<char>>> GameField::GetBoardList()
{
    return boardList;
}
std::vector<std::vector<std::vector<char>>> GameField::GetInitialBoardList()
{
    return initialBoardList;
}
Player GameField::GetPlayer()
{
    return player;
}

Rogue
that's the name of the game (I stole it from that one old game that runs in the terminal).
here is the main function, and two functions one for clearing the screen as System("cls") was too slow and the terminal flickered although it still is but less, and the second one to remove the mouse writing flickering thingy, I have no idea how either of those two work I just copied them from the internet.
I also added 2 functions to check if player moved from one board to another and check if the move the player is legal basically checking the boundaries of the field.
here I create an instance of a player, field and points the player collected.
an endless while loop (or until the player steps on a mine).
first I check if the player is on a mine, the if the player lost, then if a player picked a $ if he did the I generate new mine and $ and reset all the visible boards to the player, and at last I get the key the player pressed, I check if the move is legal, inside I check if the player did not switch boards if he didn't he moves in the direction, if he did the MoveBoard function moves him to a new board.
MoveBoard functiong takes in fieldm player and the direction, checks the width and length of a board, remembers the previous board number (it is current but it will become previous if the player moves boards) and a boolean value that checks if the player did moved boards, based on the direction it checks if the player at the edge of that board in that direction if so it moves the player boards, in the end it returns if the player did moved boards.
CheckIfMovementLegal if the player trying to move outside the game field, the hard part for me here was knowing what edge the player is because im storing the boards in a single dimention vector.
cpp

#include <stdlib.h>
#include <iostream>
#include <conio.h>
#include<windows.h>
#include "GameField.h"
#include "Includes.h"

void cls();
bool CheckIfMovementLegal(GameField& field, Player& player, int direction);
bool MoveBoard(GameField& field, Player& player, int direction);
void ShowConsoleCursor(bool showFlag);

int main()
{
    ShowConsoleCursor(false);
    
    Player player;
    int playerHealth = player.GetHealth();
    GameField field(5, 5, 3, 3, player);

    field.PrintBoard();
    int points = 0;
    while (true)
    {
        std::vector<Point> mines = field.GetMines();
        if (std::count(mines.begin(), mines.end(), player.GetCurrentLocation())) // checks if a player stepped on a mine
            player.TakeDamage(3);
          
        if (player.GetHealth() <= 0)
        {
            cls();
            std::cout << "You finished with " << points << " points. GG!" << '\n';
            std::cout << "type 'r' to restart or 'q' to quite" << '\n';
            std::string userInput;
            std::cin >> userInput;
            
            if (userInput == "r")
            {
                cls();
                player.ResetPlayer(playerHealth);
                field.GenNewPoint();
                field.ResetVisibleBoards();
                field.PrintBoard();
                continue;
            }
            else
                break;
            
        }
        if (field.GetCollectiobleLocation() == player.GetCurrentLocation()) // check if a player picked a $
        {
            field.GenNewPoint(); // generate new $ and mine
            field.ResetVisibleBoards(); // remove memory of visible boards
            cls();
            field.PrintBoard();
            points++;
        }
        int direction = _getch();
        if (CheckIfMovementLegal(field, player, direction)) //checks if movement is legal then if need to be board switch and then moves player in direction
        {
            if (!MoveBoard(field, player, direction))
                player.Move(direction);
            cls();
            field.PrintBoard();
        }

    }
    
    return 0;
}

bool MoveBoard(GameField& field, Player& player, int direction)
{
    int width = field.GetBoardList().at(player.GetCurrentBoard()).size(); 
    int length = field.GetBoardList().at(player.GetCurrentBoard()).at(0).size();
    int previousBoardNum = player.GetCurrentBoard();
    bool isMovedToNewBoard = false;
    switch (direction) // checks if player on the edge of the current boards if so then move to the next board in that direction
    {
    case KEY_UP:
        if (player.GetY() == 0)
        {
            isMovedToNewBoard = true;
            field.SetPreviousBoard(previousBoardNum);
            int newBoardNum = player.GetCurrentBoard() - field.GetFieldLength();
            field.AddVisibleBoard(newBoardNum);
            player.SetCurrentBoard(newBoardNum);
            player.SetY(width - 1);
        }
        break;

    case KEY_DOWN:
        if (player.GetY() == width - 1)
        {
            isMovedToNewBoard = true;
            field.SetPreviousBoard(previousBoardNum);
            int newBoardNum = player.GetCurrentBoard() + field.GetFieldLength();
            field.AddVisibleBoard(newBoardNum);
            player.SetCurrentBoard(newBoardNum);
            player.SetY(0);
        }
        break;

    case KEY_RIGHT:
        if (player.GetX() == length - 1)
        {
            isMovedToNewBoard = true;
            field.SetPreviousBoard(previousBoardNum);
            int newBoardNum = player.GetCurrentBoard() + 1;
            field.AddVisibleBoard(newBoardNum);
            player.SetCurrentBoard(newBoardNum);
            player.SetX(0);
        }
        break;

    case KEY_LEFT:
        if (player.GetX() == 0)
        {
            isMovedToNewBoard = true;
            field.SetPreviousBoard(previousBoardNum);
            int newBoardNum = player.GetCurrentBoard() - 1;
            field.AddVisibleBoard(newBoardNum);
            player.SetCurrentBoard(newBoardNum);
            player.SetX(length - 1);
        }
        break;
    default:
        break;
    }

    return isMovedToNewBoard;
}

bool CheckIfMovementLegal(GameField& field, Player& player, int direction)
{
    int fieldLength = field.GetFieldLength();
    int fieldWidth = field.GetFieldWidth();
    int currentBoard = player.GetCurrentBoard();
    int boardWidth = field.GetBoard().size();
    int boardLength = field.GetBoard().at(0).size();
    bool flag = true;
    switch (direction) // checks if player on the edge of the field if so check if player on the edge of current board (boards are set in a single linear vector)
    {
    case KEY_UP:
        for (int i = 0; i < fieldLength; i++)
        {
            if (i == currentBoard)
            {
                flag = false;
                break;
            }
        }

        if (flag)
            return true;
        if (player.GetY() > 0)
            return true;

        break;

    case KEY_DOWN:
        for (int i = (fieldLength * fieldWidth) - 1; i > (fieldLength * fieldWidth) - fieldLength - 1; i--)
        {
            if (i == currentBoard)
            {
                flag = false;
                break;
            }
        }
        if (flag)
            return true;
        if (player.GetY() < boardWidth - 1)
            return true;
      
        break;
    case KEY_LEFT:
        for (int i = 0; i < fieldLength * fieldWidth; i += fieldLength)
        {
            if (i == currentBoard)
            {
                flag = false;
                break;
            }
        }
        if (flag)
            return true;
        if (player.GetX() > 0)
            return true;
        break;
    case KEY_RIGHT:
        for (int i = fieldLength - 1; i < fieldLength * fieldWidth; i += fieldLength)
        {
            if (i == currentBoard)
            {
                flag = false;
                break;
            }
        }
        if (flag)
            return true;
        if (player.GetX() < boardLength - 1)
            return true;
        break;
    default:
        break;
    }

    return false;
}

void ShowConsoleCursor(bool showFlag)
{
    HANDLE out = GetStdHandle(STD_OUTPUT_HANDLE);

    CONSOLE_CURSOR_INFO cursorInfo;

    GetConsoleCursorInfo(out, &cursorInfo);
    cursorInfo.bVisible = showFlag; // set the cursor visibility
    SetConsoleCursorInfo(out, &cursorInfo);
}

void cls()
{
    // Get the Win32 handle representing standard output.
    // This generally only has to be done once, so we make it static.
    static const HANDLE hOut = GetStdHandle(STD_OUTPUT_HANDLE);

    CONSOLE_SCREEN_BUFFER_INFO csbi;
    COORD topLeft = { 0, 0 };

    // std::cout uses a buffer to batch writes to the underlying console.
    // We need to flush that to the console because we're circumventing
    // std::cout entirely; after we clear the console, we don't want
    // stale buffered text to randomly be written out.
    std::cout.flush();

    // Figure out the current width and height of the console window
    if (!GetConsoleScreenBufferInfo(hOut, &csbi)) {
        // TODO: Handle failure!
        abort();
    }
    DWORD length = csbi.dwSize.X * csbi.dwSize.Y;

    DWORD written;

    // Flood-fill the console with spaces to clear it
    FillConsoleOutputCharacter(hOut, TEXT(' '), length, topLeft, &written);

    // Reset the attributes of every character to the default.
    // This clears all background colour formatting, if any.
    FillConsoleOutputAttribute(hOut, csbi.wAttributes, length, topLeft, &written);

    // Move the cursor back to the top left for the next sequence of writes
    SetConsoleCursorPosition(hOut, topLeft);
}

oh and the includes.h is basically the directions values I only use arrow keys for the game and I need those numbers inside the Player class and the Rogue.cpp.
Includes
header

#pragma once

#define KEY_UP 72
#define KEY_DOWN 80
#define KEY_LEFT 75
#define KEY_RIGHT 77

hope to see where can I improve the readability, and the structure itself I removed a bunch of useless code before posting but maybe there is still more, also Ill be happy to here ideas that I can add to the game itself, Thanks.

\$\endgroup\$

1 Answer 1

2
\$\begingroup\$

Point class:

  • Consider using an existing math library (e.g. glm). It can be more complicated to set up initially, but provides things like simple math operators (e.g. p1 += p2) out of the box.

  • Don't write setters and getters for all the member variables. Just make the variables public.

  • Using aggregate initialization makes it unnecessary to define a constructor taking each member:

    Point p{ 1, 2, 3 };

  • Use = default for the equality operator (C++ 20 onwards).

These changes mean we need much less code:

struct Point
{
    int x, y, z;

    friend bool operator==(Point const& a, Point const& b) = default;
};

Player class:

  • Again, don't hide member variables behind getters and setters unless there's a reason to do so. Making location public saves us a huge amount of code.

  • Since we're not imposing any extra conditions on health (such as making sure it's positive, or performing any other action here when the player takes damage), the health member can be public too.

  • The int direction argument sent to move appears to be an enum. We should use an enum class for it so that we gain type-safety, and not pass it around as an integer.

So the Player class could perhaps be:

enum class Direction { UP, DOWN, LEFT, RIGHT };

struct Player
{
    Point location;
    int health;

    void Move(Direction dir);
};

Since location is now public, and all Move does is increment or decrement the relevant coordinate, we could simplify this further by removing the dependency of Player on Direction. i.e. we could remove the Move function as well, and do that logic elsewhere (since the directions are listed as KEY_UP, KEY_DOWN etc., somewhere closer to fetching the key input might be appropriate).


GameField class:

  • Consider creating a Board class (or at least a typedef), as it will make the code easier to read (std::vector<Board> vs std::vector<std::vector<std::vector<char>>>

  • It's common practice to store grids as a flat vector, rather than as a 2 dimensional vector. So instead of std::vector<std::vector<char>>, we could represent the grid as std::vector<char> with size width * height. When we need to access a value at coordinates (x, y), we calculate the corresponding index into this vector as board[y * width + x].

    This way, we only have to do a single memory allocation, and have much better cache locality. We can hide all of this (along with the width and height) inside the Board class suggested above.

    Note that if we made this class slightly more generic (say, a template<class T> class Grid { ... };) we could use a Grid<char> for our boards, and a Grid<Board> for our board list!

  • GetBoard(), GetBoardList() and GetInitialBoardList() should return by const&, rather than by value. This can save us from copying a lot of extra data (e.g. in MoveBoard in Rogue.cpp, where you only need the size of the board list).

  • Don't forget to mark member functions that don't change any member state (variables) as const. e.g. int GetBoardLength() const.

  • Note that GetPlayer() would copy the player, even though GameField stores it by reference. This might lead to confusion. Since the function isn't used, it could just be deleted.

  • GenRandomNumber seems to be reseeding the random number generator with every call. This is unnecessarily expensive. It would be better to store the std::mt19937 variable (the random number generator state) somewhere and reuse it.

  • It doesn't look like the Gamefield header file needs all those includes. Most of them should be in the cpp file.

  • GenNewPoint leaks memory. It keeps allocating new Points as mineToAdd, and never deletes them. There's no need to use heap allocation here. You should add an assignment operator to your Point class Point& operator=(Point const& other) = default. Or the compiler will generate one for you if you simplify the Point class to remove the unnecessary constructors, as suggested above.


Rogue.cpp:

  • std::vector<Point> mines = field.GetMines(); Do we really want to copy the vector here? or should mines be a std::vector<Point> const&.

  • Rather than providing Reset functions, and reusing existing classes, it's probably simpler and safer to reassign the actual variables:

    player = Player(3);
    gameField = GameField(5, 5, 3, 3, player);
    gameField.PrintBoard();
    

    This way there's no risk of forgetting to add a member variable to the Reset function if you change something inside one of the classes.

    It would also be a good idea to make those "magic numbers" named constants, e.g. const int PlayerStartHealth = 3;

  • The movement / bounds checking seems... overcomplicated. (see below).


Movement / Coordinates:

I think you might be better off storing the player (and mine / collectible) coordinates in a "global" 2d space - just an x and y representing the position on the field.

It's simple to calculate which board the player is on: board = playerPos / boardSize. Similarly, we can find the coordinates inside this local "board-space" with: boardPos = playerPos % boardSize (as long as the player position is positive).

This should make bounds checking and movement easier to handle.


Graphics:

It would also be simpler to store a single 2d Grid<char> as the graphical representation of the global field, instead of storing one separately for each board.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.