banan-os/userspace/programs/snake/main.cpp

294 lines
5.6 KiB
C++

#include <BAN/Vector.h>
#include <fcntl.h>
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <termios.h>
#include <time.h>
enum Direction
{
None,
Unknown,
Left,
Right,
Up,
Down,
};
struct Point
{
int x, y;
bool operator==(const Point& other) const { return x == other.x && y == other.y; }
};
bool g_running = true;
Point g_grid_size = { 21, 21 };
Direction g_direction = Direction::Up;
Point g_head = { g_grid_size.x / 2, g_grid_size.y / 2 };
size_t g_tail_target = 3;
int g_score = 0;
BAN::Vector<Point> g_tail;
Point g_apple;
Direction query_input()
{
char c;
if (read(STDIN_FILENO, &c, 1) != 1)
return Direction::None;
switch (c)
{
case 'w': case 'W':
return Direction::Up;
case 'a': case 'A':
return Direction::Left;
case 's': case 'S':
return Direction::Down;
case 'd': case 'D':
return Direction::Right;
default:
return Direction::Unknown;
}
}
const char* get_tail_char(Direction old_dir, Direction new_dir)
{
const size_t old_idx = static_cast<size_t>(old_dir) - 2;
const size_t new_idx = static_cast<size_t>(new_dir) - 2;
// left, right, up, down
constexpr const char* tail_char_map[4][4] {
{ "", "", "", "" },
{ "", "", "", "" },
{ "", "", "", "" },
{ "", "", "", "" },
};
return tail_char_map[old_idx][new_idx];
}
void set_grid_tile(Point point, const char* str, int off_x = 0)
{
printf("\e[%d;%dH%s", (point.y + 1) + 1, (point.x + 1) * 2 + 1 + off_x, str);
}
__attribute__((format(printf, 1, 2)))
void print_score_line(const char* format, ...)
{
printf("\e[%dH\e[m", g_grid_size.y + 3);
va_list args;
va_start(args, format);
vprintf(format, args);
va_end(args);
}
void update_apple()
{
BAN::Vector<Point> free_tiles;
for (int y = 0; y < g_grid_size.y; y++)
for (int x = 0; x < g_grid_size.x; x++)
if (const Point point { x, y }; g_head != point && !g_tail.contains(point))
MUST(free_tiles.push_back(point));
if (free_tiles.empty())
{
print_score_line("You won!\n");
exit(0);
}
g_apple = free_tiles[rand() % free_tiles.size()];
set_grid_tile(g_apple, "\e[31mO");
}
void setup_grid()
{
// Move cursor to beginning and clear screen
printf("\e[H\e[2J");
// Render top line
printf("╔═");
for (int x = 0; x < g_grid_size.x; x++)
printf("══");
printf("\n");
// Render side lines
for (int y = 0; y < g_grid_size.y; y++)
printf("\e[%dC║\n", g_grid_size.x * 2 + 1);
// Render Bottom line
printf("╚═");
for (int x = 0; x < g_grid_size.x; x++)
printf("══");
printf("");
// Render snake head
printf("\e[32m");
set_grid_tile(g_head, "O");
// Generate and render apple
srand(time(0));
update_apple();
// Render score
print_score_line("Score: %d", g_score);
fflush(stdout);
}
void update()
{
auto input = Direction::None;
auto new_direction = Direction::None;
while ((input = query_input()) != Direction::None)
{
switch (input)
{
case Direction::Up:
if (g_direction != Direction::Down)
new_direction = Direction::Up;
break;
case Direction::Down:
if (g_direction != Direction::Up)
new_direction = Direction::Down;
break;
case Direction::Left:
if (g_direction != Direction::Right)
new_direction = Direction::Left;
break;
case Direction::Right:
if (g_direction != Direction::Left)
new_direction = Direction::Right;
break;
default:
break;
}
}
const auto old_direction = g_direction;
if (new_direction != g_direction && new_direction != Direction::None)
g_direction = new_direction;
auto old_head = g_head;
switch (g_direction)
{
case Direction::Up:
g_head.y--;
break;
case Direction::Down:
g_head.y++;
break;
case Direction::Left:
g_head.x--;
break;
case Direction::Right:
g_head.x++;
break;
default:
ASSERT_NOT_REACHED();
}
if (g_head.x < 0 || g_head.y < 0 || g_head.x >= g_grid_size.x || g_head.y >= g_grid_size.y)
{
g_running = false;
return;
}
for (auto point : g_tail)
{
if (point == g_head)
{
g_running = false;
return;
}
}
MUST(g_tail.insert(0, old_head));
if (g_tail.size() > g_tail_target)
{
const auto comp = g_tail.size() >= 2 ? g_tail[g_tail.size() - 2] : g_head;
const auto back = g_tail.back();
if (comp.y == back.y)
{
if (comp.x == back.x + 1)
set_grid_tile(back, " ", +1);
if (comp.x == back.x - 1)
set_grid_tile(back, " ", -1);
}
set_grid_tile(back, " ");
g_tail.pop_back();
}
if (g_head == g_apple)
{
g_tail_target++;
g_score++;
update_apple();
print_score_line("Score: %d", g_score);
}
printf("\e[32m");
if (g_direction == Direction::Left)
set_grid_tile(g_head, "", +1);
if (g_direction == Direction::Right)
set_grid_tile(g_head, "", -1);
set_grid_tile(old_head, get_tail_char(old_direction, g_direction));
set_grid_tile(g_head, "O");
fflush(stdout);
}
int main()
{
// Make stdin non blocking
if (fcntl(STDIN_FILENO, F_SETFL, fcntl(STDIN_FILENO, F_GETFL) | O_NONBLOCK))
{
perror("fcntl");
return 1;
}
// Set stdin mode to non-canonical
termios tcold, tcnew;
if (tcgetattr(STDIN_FILENO, &tcold) == -1)
{
perror("tcgetattr");
return 1;
}
tcnew = tcold;
tcnew.c_lflag &= ~(ECHO | ICANON);
if (tcsetattr(STDIN_FILENO, TCSANOW, &tcnew))
{
perror("tcsetattr");
return 1;
}
printf("\e[?25l");
setup_grid();
timespec delay;
delay.tv_sec = 0;
delay.tv_nsec = 100'000'000;
while (g_running)
{
nanosleep(&delay, nullptr);
update();
}
// Restore stdin mode
if (tcsetattr(STDIN_FILENO, TCSANOW, &tcold))
{
perror("tcsetattr");
return 1;
}
// Reset ansi state
printf("\e[m\e[?25h\e[%dH", g_grid_size.y + 4);
return 0;
}