Snake w C++ dla konsoli Windows - prośba o konstruktywną krytykę.

0

Węża miałem w osobnych plikach, ale tu wklejam całość.


#include <iostream>
#include <conio.h>
#include <time.h>
#include <Windows.h>
#include <vector>


void gotoxy(int x, int y)
{
    HANDLE hCon;
    COORD dwPos;
    dwPos.X=x;
    dwPos.Y=y;
    hCon=GetStdHandle(STD_OUTPUT_HANDLE);
    SetConsoleCursorPosition(hCon, dwPos);
}

class Snake {
    static const int board_Width = 20;
    static const int board_Height = 40;
    struct Segment
    {
        Segment(){};
        Segment(int x, int y) : X(x), Y(y){};
        int X;
        int Y;
    };
    char Board[board_Width][board_Height];
    std::vector<Segment> body;
    Segment Food;

public:
    bool collision{false};
    enum class Direction{UP, DOWN, LEFT, RIGHT};

void SetFood() {
    Food.X=2 + rand() % (board_Width - 4);
    Food.Y=2 + rand() % (board_Height - 4);
}

void WriteToBoard() {
    for (auto x : body) Board[x.X][x.Y]=char(219);
    Board[Food.X][Food.Y]=char(176);
}   

void EnlargeBody(int x, int y){
    body.push_back(Segment(x,y));
}

void WipeBoard() {
    for (int a = 0; a < board_Width; a++)
        for (int b = 0; b < board_Height; b++) Board[a][b] = char(178);
}

void ShowBoard() {
    gotoxy(0, 0);
    for (int a = 0; a < board_Width; a++) {
        for (int b = 0; b < board_Height; b++) std::cout << Board[a][b];
        std::cout << "\n";
    }
}

bool CheckCollision() {
    for (auto x =body.rbegin(); x<body.rend() - 1; x++)
        if ((body[0].X==x->X) && (body[0].Y == x->Y)) {
            return true;
        }
    if ((body[0].X==Food.X) && (body[0].Y==Food.Y))
    {
        EnlargeBody(0,0);
        SetFood();
    }
    return false;
}

void MoveSnake(Direction direction) {
    int deltaV{0};
    int deltaH{0};
    switch (direction) {
        case Direction::UP:
            deltaV=1; break;
        case Direction::DOWN:
            deltaV=-1; break;
        case Direction::LEFT:
            deltaH=-1; break;
        case Direction::RIGHT:
            deltaH=1; break;
    }
        for (auto x =body.rbegin(); x<body.rend() - 1; x++) *x = *(x+1);
        body[0].X += deltaH;
        body[0].Y += deltaV;
        if (body[0].X < 1) body[0].X=board_Width - 2;
        if (body[0].X > board_Width - 2) body[0].X=1;
        if (body[0].Y < 1) body[0].Y=board_Height - 2;
        if (body[0].Y > board_Height - 2) body[0].Y=1;
    if (CheckCollision()) collision=true;
}

};


int main() {
    srand(time(NULL));
    Snake snake;
    snake.SetFood();
    for (int i=22; i<27; i++) snake.EnlargeBody(11,i);
    int key{0};
    Snake::Direction direction = Snake::Direction::DOWN;
    do{
        if (_kbhit())
        {
            key=getch();
            switch (key) {
                case 224: continue;
                case 77: if (direction!=Snake::Direction::DOWN)
                    direction = Snake::Direction::UP;
                break;
                case 75: if (direction!=Snake::Direction::UP)
                    direction = Snake::Direction::DOWN;
                break;
                case 72: if (direction!=Snake::Direction::RIGHT)
                    direction = Snake::Direction::LEFT;
                    break;
                case 80: if (direction!=Snake::Direction::LEFT)
                    direction = Snake::Direction::RIGHT;
                    break;
            }
        }
        snake.WipeBoard();
        snake.MoveSnake(direction);
        snake.WriteToBoard();
        snake.ShowBoard();
        Sleep(70);
    }while (!snake.collision);
    std::cout << "Koniec";
    std::cin.get();
    return 0;
}

2
  1. dlaczego wąż ma w sobie planszę? to powinien być osobny byt.
  2. dlaczego wąż ma jedzonko? to powinien byc osobny byt.
  3. 224, 77, 72, 80. magiczne liczby.
  4. wcięcia nie są konsekwentne.
  5. snake collision konczy gre co jest dosc nieintuicyjne, bo kolizja z jedzonkiem to tez kolizja.
  6. klasa wepchana na sile, bo robi wszystko, a jednoczesnie jej metody nie chca zadnych argumentow, wiec to taki wielki zbiornik na nieparametryzowane wszystko
  7. SetFood losuje pozycje jedzonka, co niezbyt zgadza sie z nazwa metody
0

No właśnie, mam niestety trudności z komunikowaniem się klas między sobą. Dlatego wszystko ładuję do jednego wora. Jak mam pozbyć się tego nawyku? Czy ktoś z forumowiczów mógłby pokazać mi jakiś przykład? Plansza to też ma być klasa?

0

Pisane na kolanie, też wymaga lepszego podziału, ale zawsze to jakiś krok do przodu.

Klasa z kierunkiem to dobry pomysł, więc tutaj zasadniczo różnicy nie ma.

enum class direction {
    up, down, left, right
};

Z racji tego, że to w końcu snake, to dorzuciłem operator porównania i przesuwanie według kierunku do koordynatu.

struct coord {
    int x, y;
    
    bool operator==(coord c) const {
        return x == c.x && y == c.y;
    }
    
    void move(direction dir, int value = 1) {
        switch(dir) {
            case direction::up: y += value; break;
            case direction::down: y -= value; break;
            case direction::right: x += value; break;
            case direction::left: x -= value; break;
        }
    }
};

Sam obiekt w grze, czy to będzie człon węża czy owocek, myślę że różnicy nie ma. Stąd nowa struktura

struct game_object {
    coord coord;
    char character;
};

Czas na węża. Teoretycznie można byłoby kolizje wydzielić do osobnego systemu, ale nie chcę specjalnie ci komplikować, dlatego wąż też może zwrócić informacje o kolizji jego głowy z jego ciałem.

struct snake {
    vector<game_object> segments;
    direction direction;
    
    snake(coord coord, char character, int segments_count, enum direction dir):
        segments(segments_count, game_object { coord, character }), direction { dir } {}
        
    game_object &head() {
        return segments.front();
    }
    
    const game_object &head() const {
        return segments.front();
    }
    
    void update() {
        for(auto i = segments.size(); i --> 1;){
            segments[i].coord = segments[i - 1].coord;
        }
        
        head().coord.move(direction);
    }
    
    bool does_head_collide_with_body() const {
        return end(segments) != find_if(
            begin(segments)+1, end(segments), [this](auto segment) {
                return segment.coord == head().coord;
            }
        );
    }
};

Chcąc przerzucić trochę logiki na strukturę gry, sama plansza niesie informacje raptem o wymiarach i o tym, czy koordynat jest w jej środku.


struct board {
    int width, height;
    
    bool is_coord_inside(coord coord) const {
        return coord.x >= 0 && coord.x < width
            && coord.y >= 0 && coord.y < height;
    }
};

Teraz struktura samej gry. Przerzuciłem na nią główną pętle gry, obsługę wejścia od gracza, rysowanie. Rysowanie można byłoby przerzucić albo na węża, albo też do osobnego systemu rysowania.

struct game {
    snake snake;
    board board;
    
    void start() {
        initialize();
        
        int sleep_in_ms = 100;
        do {
            handle_player_input();
            update();
            draw();
            this_thread::sleep_for(
                chrono::duration<int, milli> { sleep_in_ms }
            );
        } while(!fatal_collision());
        finalize();
    }
    
    void initialize() {
        initscr();
		noecho();
		keypad(stdscr, TRUE);
		cbreak();
		timeout(1);
		curs_set(FALSE);
    }
    
    bool fatal_collision() const {
        return is_snake_outside_board() || snake.does_head_collide_with_body();
    }
    
    bool is_snake_outside_board() const {
        return !board.is_coord_inside(snake.head().coord);
    }
    
    direction handle_direction_input(direction previous) {
        int ch = getch();
        switch (ch) {
            case KEY_LEFT:
              if (previous != direction::right) return direction::left;
            case KEY_RIGHT:
              if (previous != direction::left) return direction::right;
            case KEY_DOWN:
              if (previous != direction::up) return direction::up;
            case KEY_UP:
              if (previous != direction::down) return direction::down;
            default:
              return previous;
        }
    }
    
    
    void handle_player_input() {
        snake.direction = handle_direction_input(snake.direction);
    }
    
    void update() {
        snake.update();
    }
    
    void draw() const {
        erase();
        draw_snake();
        refresh();
    }
    
    void draw_snake() const {
        for(auto &object : snake.segments) {
            mvaddch(object.coord.y, object.coord.x, object.character);
        }
    }
    
    void finalize() {
        endwin();
        cout << "Zginąłeś. Sorka." << endl;
    }
};

Po wszystkim zostaje to tylko spiąć w mainie i odpalić:

int main(int argc, const char * argv[]) {
    coord starting_pos = {2, 2};
    char snake_character = '#';
    int starting_segments_count = 5;
    direction dir = direction::right;
    int width = 20;
    int height = 20;
    
    game game {
        snake { starting_pos, snake_character, starting_segments_count, dir },
        board { width, height }
    };
    
    game.start();

    return 0;
}

No ale to zawsze jakiś krok do przodu.
Jakieś 5 lat temu zrobiłem też wersję z conio.h i buforowanym terminalem, możesz do niej zajrzeć: https://github.com/spartanPAGE/spartan-snake-implementation

1 użytkowników online, w tym zalogowanych: 0, gości: 1