tsnake/tsnake.cpp

737 lines
22 KiB
C++

/* tsnake.cpp */
#include <iostream>
#include <string>
#include <deque>
#include <algorithm>
#include <stdexcept>
#include <cmath>
#include <curses.h>
#include <stdlib.h>
#include <time.h>
#include "InputParser.h"
/* defines */
#define VERSION "0.1.5"
// number of mpas
#define N_MAPS 5
// characters
#define EMPTY ' '
#define SNAKE 'o'
#define FOOD '@'
#define WALL '#'
#define WATER '^'
// directions
#define UP 0
#define DOWN 1
#define RIGHT 2
#define LEFT 3
// initial lenght
#define START_LEN 4
// alignments
#define ALIGN_RIGHT 0
#define ALIGN_LEFT 1
// return codes
#define R_QUIT 0
#define R_RESTART_NEW 1
#define R_RESTART_SAME 2
// color locations
#define C_DEFAULT 1
#define C_FOOD 2
#define C_SNAKE 3
#define C_STATUS 4
#define C_BORDER 5
#define C_GREEN 6
#define C_WALL 7
#define C_SNAKE_H 8
#define C_WATER 9
/* structs */
struct point {
int x, y;
};
struct pointf {
float x, y;
};
struct game_state {
// current position of the head of the snake
point pos;
// current direction
int dir;
// current score
int score;
// snake speed in seconds/cell
float speed;
// are we running?
bool running;
// game is paused
bool paused;
// snake queue
std::deque<point> snake;
// current food position
point food;
// current food relative position
pointf food_rel;
// flag that goes up when we eat
bool f_eat;
// timers
clock_t curr, last;
// game window
WINDOW* gamew;
// game window size
int gw_w, gw_h;
};
/* function definitions */
void update(game_state* state, int newy, int newx);
void do_chdir(game_state* state, int newy, int newx, int newdir, int opposite_dir);
int out_of_boudns(game_state* state, int y, int x);
int collision_check(game_state* state, int y, int x);
void draw_map(game_state* state, int map);
void print_bottom(char* text);
void create_food(game_state* state);
void print_status(std::string status, int align, int col);
int ask_end();
void redraw_entities(game_state* state);
void term_size_check();
bool speed_scl(game_state* state, float scale);
bool speed_add(game_state* state, float add);
bool speed_up(game_state* state);
bool speed_down(game_state* state);
int start_game(int start_length, int map);
/* global variables */
bool cheat;
template <typename T>
T clip(const T& n, const T& lower, const T& upper) {
return std::max(lower, std::min(n, upper));
}
int main(int argc, char** argv)
{
InputParser ip(argc, argv);
if(ip.exists("-h") || ip.exists("--help")){
std::cout << "tsnake version " << VERSION << std::endl;
std::cout << std::endl;
std::cout << "tsnake is a simple terminal snake game written in C++ with ncurses." << std::endl;
std::cout << "Move the snake with the arrows, [wasd] or [hjkl]." << std::endl;
std::cout << "Use [p] to pause game, [r] to restart it with a new map and" << std::endl;
std::cout << "[q] to quit." << std::endl;
std::cout << "The game starts with a speed of 2 m/s and every 20 points the speed" << std::endl;
std::cout << "is increased by one m/s until a maximum of 20 m/s." << std::endl;
std::cout << "If cheat mode is enabled (--cheat), the speed can be increased and" << std::endl;
std::cout << "decreased with [+] and [-]." << std::endl;
std::cout << "The game has a few maps which can be chosen using the -m option." << std::endl;
std::cout << "Otherwise, maps are cycled automatically when the game is restarted." << std::endl;
std::cout << std::endl;
std::cout << "Usage: tsnake [OPTION]..." << std::endl;
std::cout << std::endl;
std::cout << "Options:" << std::endl;
std::cout << " -h, --help";
std::cout << "\t\tshows usage information" << std::endl;
std::cout << " -v, --version";
std::cout << "\t\tprints version and exit" << std::endl;
std::cout << " -n [SIZE]";
std::cout << "\tset inital size of snake, which defaults to " << START_LEN << std::endl;
std::cout << " -m [MAP_NUM]";
std::cout << "\tnumber of the first map to use as an integer, which is cycled\n\t\tusing (map \% nmaps)" << std::endl;
std::cout << " -c";
std::cout << "\t\tdeactivate colors" << std::endl;
std::cout << " -x, --cheat";
std::cout << "\tactivate cheat mode where speed can be increased and\n\t\tdecreased with '+' and '-'" << std::endl;
return 0;
}
if(ip.exists("-v") || ip.exists("--version")){
std::cout << "tsnake version " << VERSION << std::endl;
return 0;
}
/* initial snake length */
int start_length = START_LEN;
if(ip.exists("-n")){
try{
start_length = ip.getInt("-n");
}catch(const std::invalid_argument& ia){
std::cout << "Bad argument: n = '" << ip.getStr("-n") << "', must be an integer" << std::endl;
return 0;
}
}
/* first map */
int map_id = 0;
if(ip.exists("-m")){
try{
map_id = ip.getInt("-m");
}catch(const std::invalid_argument& ia){
std::cout << "Bad argument: m = '" << ip.getStr("-m") << "', must be an integer" << std::endl;
return 0;
}
}
/* cheat mode */
cheat = ip.exists("--cheat") || ip.exists("-x");
/* colors */
bool colors = !ip.exists("-c");
/* init random */
srand(time(NULL));
/* initialize curses */
initscr();
if(colors){
start_color();
// use default colors to not break current color scheme
use_default_colors();
}
keypad(stdscr, TRUE);
cbreak();
noecho();
curs_set(0);
short r, g, b;
color_content(COLOR_BLACK, &r, &g, &b);
// set bright black black
init_color(8, r, g, b);
init_pair(C_STATUS, COLOR_BLACK, COLOR_CYAN);
init_pair(C_FOOD, COLOR_CYAN, COLOR_MAGENTA);
init_pair(C_SNAKE, COLOR_RED, COLOR_GREEN);
init_pair(C_SNAKE_H, COLOR_GREEN, COLOR_RED);
init_pair(C_DEFAULT, COLOR_WHITE, COLOR_BLACK);
init_pair(C_BORDER, COLOR_BLUE, COLOR_BLACK);
init_pair(C_GREEN, COLOR_GREEN, COLOR_BLACK);
init_pair(C_WALL, COLOR_BLACK, COLOR_RED);
init_pair(C_WATER, COLOR_BLACK, COLOR_BLUE);
clear();
/* start the game */
start_length = clip(start_length, 1, (COLS - 5) / 2);
int ret = R_RESTART_SAME;
while(ret != R_QUIT){
if(ret == R_RESTART_SAME)
ret = start_game(start_length, map_id);
else if(ret == R_RESTART_NEW)
ret = start_game(start_length, ++map_id);
}
/* clean up */
endwin();
exit(0);
}
void term_size_check(){
if(LINES < 15 || COLS < 60){
clear();
endwin();
std::cout << "Terminal too small! (min [60,15], current [" << COLS << "," << LINES << "]" << std::endl;
exit(0);
}
}
int start_game(int start_length, int map)
{
/* new game state */
game_state state;
/* auxiliary variables */
float secs;
clock_t start;
int ch;
term_size_check();
/* create window */
state.gw_w = COLS;
state.gw_h = LINES - 1;
state.gamew = newwin(state.gw_h, state.gw_w, 0, 0);
/* initialize the map if any */
draw_map(&state, map);
/* start player at [start_length,5] going right */
state.pos.x = start_length + 1;
state.pos.y = 2;
state.dir = RIGHT;
state.score = 0;
start = clock();
/* init snake */
wattron(state.gamew, COLOR_PAIR(C_SNAKE));
state.snake.clear();
for(int i = 0; i < start_length; i++) {
point p = {state.pos.x + 1 - start_length + i, state.pos.y};
state.snake.push_front(p);
mvwaddch(state.gamew, p.y, p.x, SNAKE);
}
wattroff(state.gamew, COLOR_PAIR(C_SNAKE));
wattron(state.gamew, COLOR_PAIR(C_SNAKE_H));
mvwaddch(state.gamew, state.snake.front().y, state.snake.front().x, SNAKE);
wattroff(state.gamew, COLOR_PAIR(C_SNAKE_H));
/* init food */
create_food(&state);
refresh();
wrefresh(state.gamew);
/* speed in m/s */
state.speed = 2.0;
/* clocks */
state.last = 0;
/* async char read */
nodelay(stdscr, TRUE);
/* run */
state.running = true;
state.paused = false;
bool resize = false;
do {
resize = state.gw_w != COLS || state.gw_h != LINES - 1;
if(resize){
term_size_check();
state.gw_w = COLS;
state.gw_h = LINES - 1;
wresize(state.gamew, state.gw_h, state.gw_w);
draw_map(&state, map);
redraw_entities(&state);
wrefresh(state.gamew);
} else if(!state.paused){
state.curr = clock();
/* get char async, see nodelay() */
ch = getch();
/* test inputted key and determine direction */
if(ch != ERR){
switch (ch) {
case KEY_UP:
case 'w':
case 'k':
if(state.dir != DOWN)
do_chdir(&state, state.pos.y - 1, state.pos.x, UP, DOWN);
break;
case KEY_DOWN:
case 's':
case 'j':
if(state.dir != UP)
do_chdir(&state, state.pos.y + 1, state.pos.x, DOWN, UP);
break;
case KEY_LEFT:
case 'a':
case 'h':
if(state.dir != RIGHT)
do_chdir(&state, state.pos.y, state.pos.x - 1, LEFT, RIGHT);
break;
case KEY_RIGHT:
case 'd':
case 'l':
if(state.dir != LEFT)
do_chdir(&state, state.pos.y, state.pos.x + 1, RIGHT, LEFT);
break;
case 'q':
// show quit action
state.running = false;
state.curr = state.last = clock();
break;
case 'p':
// pause
state.paused = true;
break;
case 'r':
// restart
return R_RESTART_NEW;
case '+':
if(cheat && speed_up(&state))
state.curr = state.last = clock();
break;
case '-':
if(cheat && speed_down(&state))
state.curr = state.last = clock();
break;
}
}
float dt = ((float)(state.curr - state.last) / CLOCKS_PER_SEC);
float secs_per_cell = 1.0F / state.speed;
if (dt > secs_per_cell) {
state.last = state.curr;
/* auto-move */
switch (state.dir) {
case UP:
state.pos.y--;
break;
case DOWN:
state.pos.y++;
break;
case RIGHT:
state.pos.x++;
break;
case LEFT:
state.pos.x--;
break;
}
if(collision_check(&state, state.pos.y, state.pos.x)) {
/* end */
state.running = false;
break;
} else {
/* update */
update(&state, state.pos.y, state.pos.x);
}
}
/* chech food */
if(state.food.x == state.pos.x && state.food.y == state.pos.y){
create_food(&state);
state.f_eat = true;
state.score++;
if (state.score % 20 == 0)
speed_up(&state);
}
}else{
// paused
ch = getch();
if(ch == 'p'){
state.paused = false;
}
}
/* status */
attron(COLOR_PAIR(C_STATUS));
mvhline(LINES - 1, 0, EMPTY, COLS);
attroff(COLOR_PAIR(C_STATUS));
secs = ((float)(state.curr - start) / CLOCKS_PER_SEC);
std::string st = "Score: " + std::to_string(state.score) + " | " + std::to_string((int) secs) + " seconds | " + std::to_string((int) state.speed) + " m/s";
print_status(" [p]Pause [r]Restart [q]Quit", ALIGN_LEFT, A_BOLD | COLOR_PAIR(C_STATUS));
print_status(st, ALIGN_RIGHT, COLOR_PAIR(C_STATUS));
/* title */
wattron(state.gamew, COLOR_PAIR(C_BORDER));
box(state.gamew, 0, 0);
if(!state.paused) {
std::string mapstr;
mapstr.append(" TSNAKE ");
mapstr.append(VERSION);
mapstr.append(" - MAP " + std::to_string(map % N_MAPS + 1) + " (" + std::to_string(COLS) + "x" + std::to_string(LINES) + ") ");
mvwaddstr(state.gamew, 0, 4, mapstr.c_str());
}else{
wattron(state.gamew, COLOR_PAIR(C_GREEN));
mvwaddstr(state.gamew, 0, 4, " GAME PAUSED - [p]Continue ");
wattroff(state.gamew, COLOR_PAIR(C_GREEN));
}
wattroff(state.gamew, COLOR_PAIR(C_BORDER));
/* refresh */
refresh();
wrefresh(state.gamew);
}
while (state.running);
/* done */
std::string msg3 = "YOUR SCORE: " + std::to_string(state.score);
std::string msg4 = "You lasted " + std::to_string((int) secs) + " seconds";
std::string msg1 = "[r] Restart (new map)";
std::string msg0 = "[s] Restart (same map)";
std::string msg2 = "[q] Quit";
int minl = msg1.size();
int ew_w = clip(COLS / 2, minl, COLS);
int ew_h = clip(LINES / 2, 4, LINES);
WINDOW* endw = newwin(ew_h, ew_w, (LINES - ew_h) / 2, (COLS - ew_w) / 2);
wbkgd(endw, COLOR_PAIR(C_STATUS));
nodelay(stdscr, FALSE);
box(endw, 0, 0);
/* title */
mvwaddstr(endw, 0, 2, " GAME FINISHED ");
/* score and seconds in green */
mvwaddstr(endw, ew_h / 2 - 3, ew_w / 2 - msg3.size() / 2, msg3.c_str());
mvwaddstr(endw, ew_h / 2 - 2, ew_w / 2 - msg4.size() / 2, msg4.c_str());
wattron(endw, A_BOLD);
mvwaddstr(endw, ew_h / 2 + 1, ew_w / 2 - minl / 2, msg1.c_str());
mvwaddstr(endw, ew_h / 2 + 2, ew_w / 2 - minl / 2, msg0.c_str());
mvwaddstr(endw, ew_h / 2 + 3, ew_w / 2 - minl / 2, msg2.c_str());
wattroff(endw, A_BOLD);
wrefresh(endw);
return ask_end();
}
int ask_end()
{
int opt = getch();
switch(opt){
case 'r':
/* new game, new map */
return R_RESTART_NEW;
case 's':
/* new game, same map */
return R_RESTART_SAME;
case 'q':
/* quit */
return R_QUIT;
default:
return ask_end();
}
}
void print_status(std::string status, int align, int col)
{
attron(col);
switch(align){
case ALIGN_LEFT:
move(LINES - 1, 0);
printw(status.c_str());
break;
case ALIGN_RIGHT:
move(LINES - 1, COLS - status.size() - 1);
printw(status.c_str());
break;
}
attroff(col);
}
void redraw_entities(game_state* state){
/* snake */
// head
wattron(state->gamew, COLOR_PAIR(C_SNAKE_H));
mvwaddch(state->gamew, state->snake[0].y, state->snake[0].x, SNAKE);
wattron(state->gamew, COLOR_PAIR(C_SNAKE_H));
// body
wattron(state->gamew, COLOR_PAIR(C_SNAKE));
for(unsigned int i = 1; i < state->snake.size(); i++){
mvwaddch(state->gamew, state->snake[i].y, state->snake[i].x, SNAKE);
}
wattron(state->gamew, COLOR_PAIR(C_SNAKE));
/* food */
// reposition
state->food.x = (int) round((float) state->gw_w * state->food_rel.x);
state->food.y = (int) round((float) state->gw_h * state->food_rel.y);
// draw
wattron(state->gamew, COLOR_PAIR(C_FOOD));
mvwaddch(state->gamew, state->food.y, state->food.x, FOOD);
wattroff(state->gamew, COLOR_PAIR(C_FOOD));
}
point rd(game_state* state)
{
point c;
while(1){
c.x = rand() % (state->gw_w - 2) + 1;
c.y = rand() % (state->gw_h - 2) + 1;
if((mvwinch(state->gamew, c.y, c.x) & A_CHARTEXT) == EMPTY)
return c;
}
}
void create_food(game_state* state)
{
point newp = rd(state);
state->food.x = newp.x;
state->food.y = newp.y;
state->food_rel.x = (float) newp.x / (float) state->gw_w;
state->food_rel.y = (float) newp.y / (float) state->gw_h;
wattron(state->gamew, COLOR_PAIR(C_FOOD));
mvwaddch(state->gamew, state->food.y, state->food.x, FOOD);
wattroff(state->gamew, COLOR_PAIR(C_FOOD));
}
void update(game_state* state, int newy, int newx)
{
point aux = state->snake.front();
wattron(state->gamew, COLOR_PAIR(C_SNAKE));
mvwaddch(state->gamew, aux.y, aux.x, SNAKE);
wattroff(state->gamew, COLOR_PAIR(C_SNAKE));
point newpoint = {newx, newy};
state->snake.push_front(newpoint);
wattron(state->gamew, COLOR_PAIR(C_SNAKE_H));
mvwaddch(state->gamew, newy, newx, SNAKE);
wattroff(state->gamew, COLOR_PAIR(C_SNAKE_H));
if(!state->f_eat){
point erase = state->snake.back();
state->snake.pop_back();
mvwaddch(state->gamew, erase.y, erase.x, EMPTY);
}else{
state->f_eat = false;
}
move(state->pos.y, state->pos.x);
}
void do_chdir(game_state* state, int newy, int newx, int newdir, int opposite_dir)
{
if (!collision_check(state, newy, newx)) {
update(state, newy, newx);
state->pos.x = newx;
state->pos.y = newy;
state->dir = newdir;
state->curr = state->last = clock();
} else if (state->dir != opposite_dir){
state->running = false;
}
}
bool speed_up(game_state* state)
{
return speed_add(state, 1.0);
}
bool speed_down(game_state* state)
{
return speed_add(state, -1.0);
}
bool speed_scl(game_state* state, float scale)
{
float cpy = state->speed;
state->speed = clip(state->speed * scale, 1.0F, 20.0F);
return cpy != state->speed;
}
bool speed_add(game_state* state, float add)
{
float cpy = state->speed;
state->speed = clip(state->speed + add, 1.0F, 20.0F);
return cpy != state->speed;
}
void print_bottom(const char* text)
{
move(LINES - 1, 0);
printw(text);
}
int out_of_bounds(game_state* state, int y, int x)
{
return y <= 0 || x <= 0 || y >= state->gw_h - 1 || x >= state->gw_w - 1;
}
int collision_check(game_state* state, int y, int x)
{
int testch = mvwinch(state->gamew, y, x) & A_CHARTEXT;
return (testch != EMPTY && testch != FOOD) || out_of_bounds(state, y, x);
}
void draw_map(game_state* state, int map)
{
map = map % N_MAPS;
int y;
for (y = 0; y < LINES; y++) {
mvwhline(state->gamew, y, 0, EMPTY, state->gw_w);
}
/* actual map */
wattron(state->gamew, COLOR_PAIR(C_WALL));
switch(map){
case 0:
{
/* small pool with a fence */
// pool
wattroff(state->gamew, COLOR_PAIR(C_WALL));
wattron(state->gamew, COLOR_PAIR(C_WATER));
for(int y = state->gw_h * 0.4; y <= state->gw_h * 0.6; y++){
mvwhline(state->gamew, y, state->gw_w / 3, WATER, state->gw_w / 3);
}
wattroff(state->gamew, COLOR_PAIR(C_WATER));
wattron(state->gamew, COLOR_PAIR(C_WALL));
// 5 fences
int tx = state->gw_w * 0.2;
int ty = state->gw_h * 0.2;
int bx = state->gw_w * 0.8;
int by = state->gw_h * 0.8;
mvwvline(state->gamew, ty, tx, WALL, by - ty);
mvwvline(state->gamew, ty, bx, WALL, by - ty);
mvwhline(state->gamew, by, tx, WALL, bx - tx + 1);
mvwhline(state->gamew, ty, tx, WALL, state->gw_w * 0.21);
mvwhline(state->gamew, ty, state->gw_w * 0.6, WALL, state->gw_w * 0.21);
break;
}
case 1:
{
/* two walls with a pool in the middle */
// pool
wattroff(state->gamew, COLOR_PAIR(C_WALL));
wattron(state->gamew, COLOR_PAIR(C_WATER));
for(int y = state->gw_h / 3; y <= state->gw_h * 2 / 3; y++){
mvwhline(state->gamew, y, state->gw_w / 3, WATER, state->gw_w / 3);
}
wattroff(state->gamew, COLOR_PAIR(C_WATER));
wattron(state->gamew, COLOR_PAIR(C_WALL));
// 2 walls
mvwhline(state->gamew, state->gw_h * 0.2, state->gw_w * 0.3, WALL, state->gw_w * 0.7);
mvwhline(state->gamew, state->gw_h * 0.8, 0, WALL, state->gw_w * 0.7);
break;
}
case 2:
{
/* just one vertical wall */
mvwvline(state->gamew, 0, state->gw_w / 2, WALL, state->gw_h * 0.7);
break;
}
case 3:
{
/* three walls */
mvwvline(state->gamew, 0, state->gw_w / 2, WALL, state->gw_h * 0.7);
mvwhline(state->gamew, state->gw_h / 2, 0, WALL, state->gw_w * 0.3333);
mvwhline(state->gamew, state->gw_h / 2, state->gw_w * 0.6666, WALL, state->gw_w * 0.3333 + 1);
break;
}
case 4:
{
/* still three walls, centered */
int hl = state->gw_w * 0.7;
int vl = state->gw_h * 0.8;
mvwvline(state->gamew, (state->gw_h - vl) / 2, state->gw_w / 2, WALL, vl);
mvwhline(state->gamew, state->gw_h * 0.3333, (state->gw_w - hl) / 2, WALL, hl);
mvwhline(state->gamew, state->gw_h * 0.6666, (state->gw_w - hl) / 2, WALL, hl);
break;
}
}
wattroff(state->gamew, COLOR_PAIR(C_WALL));
}