/* * Remigiusz Jan Andrzej Modrzejewski, http://lrem.net/ <lrem at go2.pl> * Distributed under the GPL, for more details see: * http://lrem.net/zracer.xhtml * * ZRacer - a simple arcade game in ncurses * * ZRacer is a racing game where 1 - 2 players race on a randomly * generated racecourse with split-screen and using the same keyboard. * * Conventions taken: * - coordinates order is (y, x) * - (0, 0) is upper left corner of everything * - as a result, finish line is at line 0 * - car doesn't take up the whole rectangle (for collision checking) * - when a car crashes, it's window is frozen and no input is taken * - the track is stored as its ascii-art representation */ #include <curses.h> #include <cstdarg> #include <cstdio> #include <string> #include <vector> #include <ctime> #include <cassert> #include <cstdlib> using namespace std; // This one defines a random function with results in range 0..1 (double). #define drand()((double)rand()/RAND_MAX) // Various constants #define MAX_CAR_SIZE 20 #define MAX_PLAYERS 2 #define INF 123456789 #define KEY_ESC 27 // Missing in ncurses... #define RESULTS_COLORS 11 #define MESSAGE_LENGTH 100 // Action values #define ACCELERATE -1 #define BRAKE 1 #define LEFT -1 #define RIGHT 1 // Make passage, but don't exceed available space. #define MINIMAL_WIDTH\ (min(settings.players*settings.car_size*2.5, (double)settings.race_width-2)) // Main menu item defines, for convenience. #define MENU_QUIT 0 #define MENU_START 1 #define MENU_OPTIONS 2 /* * In-game settings. This needn't really by a struct, but it looks more * readable to access settings by "settings.delay" than "delay". */ struct _settings { // Basic game delay, is not equal to move time, but is a factor. timespec delay; // The axis of splitscreen. bool vertical_split; // Whether both players race on the same-looking racecourse. bool similar_track; // Or maybe literally the same one? (Collisions possible) bool shared_track; // The sizes of the course, better make it bigger than the car ;) int race_length, race_width; // The minimal width of the road the players can drive. int minimal_width; // The number of participants. int players; // Character with which the cars are drawn. char character; // The size of the car. int car_size; // The distance interval at which the speed of the car changes. int speed_base; // The chance of generating a rock on a given line double rock_chance; // The chance of generating a turning on a given line double turn_chance; // Keys players use to interact with the game. int controls[MAX_PLAYERS][4]; void reset(void) { delay.tv_sec = 0; // Hundredth of a second * const. delay.tv_nsec = 1000000*25; similar_track = vertical_split = true; shared_track = true; race_length = 500; // Zero makes these 2 variables adjusted to screen size. race_width = 0; minimal_width = 0; players = 1; character = '^'; car_size = 10; speed_base = 5; rock_chance = 0.025; turn_chance = 0.125; // Arrow keys for first player controls[0][0]=KEY_UP; controls[0][1]=KEY_DOWN; controls[0][2]=KEY_LEFT; controls[0][3]=KEY_RIGHT; // WSAD for second controls[1][0]='w'; controls[1][1]='s'; controls[1][2]='a'; controls[1][3]='d'; } void editor(void); void _edit_players(void); void _edit_length(void); void _edit_width(void); void _edit_rocks(void); void _edit_turns(void); void _edit_delay(void); void _edit_sharing(void); } settings; class car_image { /* * This is where we store the image of the car. * For performance purposes, it is created only once when scaled. */ bool storage [MAX_CAR_SIZE+1][MAX_CAR_SIZE+1]; // And as list of used pixels coords. vector<pair<int, int> > dots; char character; int color, size; /* * Internal functions. First one is _clear() - it just sets all * the storage area to false. Second one is line() - takes coords * of 2 points and draws a line between them (or more literally * just marks certain values within storage as true). */ void _clear(void); void _line(int, int, int, int); public: /* * Constructor, takes input from the settings. */ car_image(void); /* * This one simply draws the car on the given window. It assumes * that this can clearly be done, so all the checks need to be * done earlier. The parameters it takes are the window and position * of upper left corner of the car. */ void display(WINDOW*, int, int); /* * Draws the explosion of the car. Parameters are windows, position * of upper left corner of the car. Position is relative to the window, * _not_ the track. */ void explode(WINDOW*, int, int); /* * Collision happens only when an obstacle is on a pace taken by the * car. It is possible to have the obstacle between the car's "ribs". * This checks whether given pace relative to *car's position* is * taken by the car. */ bool collision_check(int, int); /* * These are simple mutators. */ void set_character(char); void set_color(int); // And a simple accessor. vector<pair<int, int> > get_dots(void); }; class track { /* * These are the most important data for the game. The track should * be generated only once each game, preferably shared between players. */ vector<vector<char> > circuit; public: /* * Two constructors. One takes input from the settings, second just * copies another track. */ track(void); track(track*); /* * Takes the number of the top line to display and the window. * Window's height and width are grabbed by getmaxyx(). * There's an assertion that the window is wide enough. */ void display(WINDOW*, int); /* * Tells whether there's an obstacle at a given pace. */ bool taken(int, int); void mark(int, int, car_image*); void unmark(int, int, car_image*); }; class player_handler { WINDOW* screen; track* course; car_image* car; // last_move is the time value of the previous player's action // (y, x) is the position of the upper left corner of the car // top_line is the top displayed line of the course // commands store what player clicked int last_move, y, x, top_line, command_x, command_y, screen_height; int controls[4]; public: /* * This constructor prepares the part of the screen for the player. * It decides which part of screen to take, and the color of the car, * taking the settings and the player number. The track is generated * outside it. */ player_handler(int, track*); /* * Player's main loop. Returns true if the player continues to play, * and false if the game ends for him. Also does redraw the window. */ bool tick(int); /* * Knowing the player's key controls, this one does what it's named for. * Only sets the action to perform during next move. */ void parse_input(int); /* * These are used only in shared track races in order to catch player-player * collisions. */ void mark_position(void); void unmark_position(void); /* * Apart from deallocating standard structures, this one has also to * destroy the ncurses window it created, so it needs a separate destructor. */ //~player_handler(void); }; class game { int time; player_handler* players[MAX_PLAYERS]; bool alive[MAX_PLAYERS]; public: /* * Constructor does all the fancy things like initializing ncurses, * while destructor brings back normal tty behaviour. This is great, * as it doesn't force me to remember about deinitialization any * time I want to break the game. */ game(void); ~game(void); /* * This is the game's main loop action... * It returns true as long as game continues. */ bool tick(void); }; int main_menu (void); // This is a wrapper around printw, also accepts arbitrary number of arguments void message (char*, ...); int main (void) { bool keep_asking = true; settings.reset(); while(keep_asking) { switch(main_menu()) { case MENU_START: { game race; while(race.tick()) nanosleep(&settings.delay, NULL); break; } case MENU_OPTIONS: settings.editor(); break; case MENU_QUIT: keep_asking = false; } } // In C a "return 0;" would come here, but this is not C... } /* * This function opens a window with a message disregarding anything else that was * running. Good for displaying error messages, final results and so. Waits for an * ESC pressed before quiting. */ void message (char* format_string, ...) { va_list args; // Allocate a buffer for the message. char* final_string = new char[MESSAGE_LENGTH]; // Fetch the "..." arguments. va_start(args, format_string); // Transform all the arguments into a single string. vsprintf(final_string, format_string, args); // Finalize the work on the "..." arguments. va_end(args); // Get screen resolution. int screen_height, screen_width; getmaxyx(stdscr, screen_height, screen_width); // Create a window for the message, of it's size WINDOW* message_win = newwin(1, strlen(final_string), // and at the centre of the screen. screen_height/2, screen_width/2 - strlen(final_string)/2); // Print the message. wattron(message_win, COLOR_PAIR(RESULTS_COLORS)); wattron(message_win, A_BOLD); waddstr(message_win, final_string); wattroff(message_win, COLOR_PAIR(RESULTS_COLORS)); wattroff(message_win, A_BOLD); wrefresh(message_win); // Wait for an ESC. while(getch()!=KEY_ESC) nanosleep(&settings.delay, NULL); // Clean up after myself. delwin(message_win); } /* * This class is created solely for the cool trick used in game constructor/destructor. * It's struct just because it doesn't have anything private. * Watch it's ingenuity and simplicity! */ struct simple_curses { simple_curses(void) { // Initialize ncurses. This is done separately from initializing for // the game in order to have different options. initscr(); cbreak(); clear(); } ~simple_curses(void) { // Fall back to normal options nocbreak(); endwin(); } }; int main_menu (void) { // Whole trick is: constructor get's run here, destructor whenever leaving // the function. simple_curses enviroment; // Tell them what to do... printw("\t\tWelcome to ZRacer by lRem!\n"); printw("If you like this game look for more at http://lrem.net/\n\n"); printw("\t\t\tMAIN MENU:\n\n"); printw("q) Quit the game.\n"); printw("s) Start a new game.\n"); printw("o) Options.\n\n"); // And wait for them do it. char pressed = 0; for(;;) { printw("Choose any option: "); refresh(); pressed = getch(); switch(pressed) { case 'q': return MENU_QUIT; // No breaks - return already quits the switch. case 's': return MENU_START; case 'o': return MENU_OPTIONS; } // In case user missed the key, print the next request in next line. addch('\n'); } } void _settings::editor(void) { simple_curses enviroment; printw("\t\tSETTINGS EDITOR\n"); printw("q) Quit the editor\n"); printw("p) Set the number of players\n"); printw("l) Set the length of the racecourse\n"); printw("r) Set the chance to generate a rock\n"); printw("t) Set the chance to generate a turning\n"); printw("s) Set master delay\n"); printw("h) Set track sharing\n"); // This doesn't give nice results... //printw("w) Set the width of the racecourse\n"); char pressed = 0; for(;;) { printw("Choose any option: "); refresh(); pressed = getch(); switch(pressed) { case 'q': return; case 'p': _edit_players(); break; case 'l': _edit_length(); break; case 'r': _edit_rocks(); break; case 't': _edit_turns(); break; case 's': _edit_delay(); break; case 'h': _edit_sharing(); //case 'w': // _edit_width(); // break; } } } void _settings::_edit_players(void) { printw("\n\tSelect the number of players (1 - %d, currently %d):", MAX_PLAYERS, players); players = 0; // While players outside the possible range. for(; players<1 || MAX_PLAYERS<players;) { players = getch() - '0'; addch(' '); } addch('\n'); } void _settings::_edit_length(void) { printw("\n\tSet the length of the track (arbitrary, currently %d):", race_length); race_length = -1; // While players outside the possible range. for(; race_length<0;) { scanw("%d", &race_length); } } void _settings::_edit_width(void) { printw("\n\tSet the width of the track (narrower than terminal, 0 means max, currently %d):", race_width); race_width = -1; // While players outside the possible range. for(; race_width<0;) { scanw("%d", &race_width); } } void _settings::_edit_rocks(void) { printw("\n\tSet the chance of generating a rock (0-1, currently %lf):", rock_chance); rock_chance = -1; for(; rock_chance<0 || 1<rock_chance;) { scanw("%lf", &rock_chance); } } void _settings::_edit_turns(void) { printw("\n\tSet the chance of generating a turn (0-1, currently %lf):", turn_chance); turn_chance = -1; for(; turn_chance<0 || 1<turn_chance;) { scanw("%lf", &turn_chance); } } void _settings::_edit_delay(void) { printw("\n\tSet the master delay (positive, nanosceonds, currently %d):", delay.tv_nsec); delay.tv_nsec = -1; for(; delay.tv_nsec<0;) { scanw("%d", &delay.tv_nsec); } } void _settings::_edit_sharing(void) { printw("\n\tShould the track be Similar, sHared or Different for different palyers? "); char response; for(; response!='s' && response!='h' && response!='d';) { scanw("%c", &response); switch(tolower(response)) { case 's': similar_track = true; shared_track = false; break; case 'h': similar_track = true; shared_track = true; break; case 'd': similar_track = false; shared_track = false; break; } } } bool game::tick(void) { bool game_continues = false; time++; // For every key waiting in buffer... int pressed_key; while((pressed_key = getch()) != ERR) { if(pressed_key == KEY_ESC) // End the game. for(int i = 0; i<settings.players; i++) alive[i]=false; // By killing all players. // Pass the input to each player. for(int i = 0; i<settings.players; i++) players[i]->parse_input(pressed_key); } // Checking for player-player collisions is realized by marking each player's position // as an obstacle on the track. if(settings.shared_track) for(int i=0; i<settings.players; i++) if(alive[i]) players[i]->mark_position(); for(int i=0; i<settings.players; i++) if(alive[i]) if(settings.shared_track) { players[i]->unmark_position(); game_continues = (alive[i] = players[i]->tick(time)) || game_continues; players[i]->mark_position(); } else // If the player dies it sets his alive status to false. // If he lives, then there is a reason to continue the game. game_continues = (alive[i] = players[i]->tick(time)) || game_continues; if(settings.shared_track) for(int i=0; i<settings.players; i++) players[i]->unmark_position(); // Results. if(!game_continues) message("Game finished after %d turns.", time); return game_continues; } game::game (void) { // Initialize the RNG // We have already a variable called "time", and we need a function of the same name. // In order to circumvent this problem, we use the namespaces. srand(std::time(NULL)); // Initialize ncurses. initscr(); // Initialize colors (refuse to start without them). assert(has_colors()); start_color(); keypad(stdscr, TRUE); cbreak(); noecho(); nonl(); nodelay(stdscr, true); // Color palette. init_pair(COLOR_BLACK, COLOR_BLACK, COLOR_BLACK); init_pair(COLOR_RED, COLOR_RED, COLOR_BLACK); init_pair(COLOR_GREEN, COLOR_GREEN, COLOR_BLACK); init_pair(COLOR_YELLOW, COLOR_YELLOW, COLOR_BLACK); init_pair(COLOR_BLUE, COLOR_BLUE, COLOR_BLACK); init_pair(COLOR_MAGENTA, COLOR_MAGENTA, COLOR_BLACK); init_pair(COLOR_CYAN, COLOR_CYAN, COLOR_BLACK); init_pair(COLOR_WHITE, COLOR_WHITE, COLOR_BLACK); init_pair(RESULTS_COLORS, COLOR_YELLOW, COLOR_BLUE); // Adjust settings, if needed. if(settings.race_width == 0) { int y, x; getmaxyx(stdscr, y, x); settings.race_width = settings.vertical_split ? x/settings.players : x; } if(settings.minimal_width == 0) settings.minimal_width = (int)(MINIMAL_WIDTH); // Prepare players if(settings.shared_track) { track* course = new track(); for(int i = 0; i<settings.players; i++) players[i]=new player_handler(i, course); } else if(settings.similar_track) { track* course = new track(); for(int i = 0; i<settings.players; i++) players[i]=new player_handler(i, course); } else { for(int i = 0; i<settings.players; i++) players[i]=new player_handler(i, new track()); } for(int i = 0; i<settings.players; i++) alive[i]=true; // Let the moves begin. time = 0; } game::~game (void) { echo(); nl(); nocbreak(); nodelay(stdscr, false); endwin(); } track::track (void) { // Allocate the structures. circuit.resize(settings.race_length); for(vector<vector<char> >::iterator it = circuit.begin(); it!=circuit.end(); it++) it->resize(settings.race_width); // And create the course! int borders[2]; int borders_directions[2]={0,0}; // Initially make the road halfway between minimal and maximal possible. assert(settings.minimal_width <= settings.race_width); borders[0] = (settings.race_width - settings.minimal_width)/4; borders[1] = (settings.race_width*3 + settings.minimal_width)/4; // And generate the lines. for(int i=settings.race_length-1; 0<=i; i--) { // Put some background. for(int j=0; j<(int)circuit[i].size(); j++) circuit[i][j]=' '; // Distance meter. circuit[i][0]='0'+i%10; // Occasional rock on the track :> if(drand()<settings.rock_chance) circuit[i][rand()%settings.race_width]='*'; // Move the kerbs. borders[0]+=borders_directions[0]; borders[1]+=borders_directions[1]; // Draw the kerbs. switch(borders_directions[0]) {// Different chars, depending on the kerb direction. case 0: circuit[i][borders[0]]='|'; break; case 1: circuit[i][borders[0]]='/'; break; case -1: circuit[i][borders[0]]='\\'; } switch(borders_directions[1]) { case 0: circuit[i][borders[1]]='|'; break; case 1: circuit[i][borders[1]]='/'; break; case -1: circuit[i][borders[1]]='\\'; } // Turn the kerbs... int tries = 0; // This is in case it gets to narrow and no space at once (hangs). while( tries++<5 && (drand()<settings.turn_chance || // If RNG wants so, borders[0]+borders_directions[0] == 0 // or no space. )) borders_directions[0] = rand()%3-1; if(borders[1]-borders[0] < settings.minimal_width) // If to narrow, borders_directions[0] = -1; // Make it wider // A sanity check. if(borders[0]+borders_directions[0] == 0) borders_directions[0] = 0; // And the second one. tries = 0; while( tries++<5 && (drand()<settings.turn_chance || // If RNG wants so, borders[1]+borders_directions[1] == settings.race_width // or no space. )) borders_directions[1]=rand()%3-1; if(borders[1]-borders[0] < settings.minimal_width) borders_directions[1] = 1; if(borders[1]+borders_directions[1] == settings.race_width) borders_directions[1] = 0; } } void track::display(WINDOW* screen, int top_line) { // Get the geometry. int screen_width, screen_height; getmaxyx(screen, screen_height, screen_width); // For every visible line... for(int i = top_line; i<top_line+screen_height; i++) { // Move the cursor at it's beginning... // Position at screen centre. wmove(screen, i-top_line, (screen_width-settings.race_width)/2); // And print all the characters. for(int j=0; j<settings.race_width; j++) waddch(screen, circuit[i][j]); } } bool track::taken(int y, int x) { return circuit[y][x]!=' '; } void track::mark(int y, int x, car_image *car) { vector<pair<int, int> > dots = car->get_dots(); for(unsigned int i=0; i<dots.size(); i++) if(circuit[y+dots[i].first][x+dots[i].second] == ' ') circuit[y+dots[i].first][x+dots[i].second] = settings.character; } void track::unmark(int y, int x, car_image *car) { vector<pair<int, int> > dots = car->get_dots(); for(unsigned int i=0; i<dots.size(); i++) if(circuit[y+dots[i].first][x+dots[i].second] == settings.character) circuit[y+dots[i].first][x+dots[i].second] = ' '; } player_handler::player_handler(int position, track* racecourse) { // Set the sizes for the windows. // Height gets reused and thus is declared within class. int screen_width; getmaxyx(stdscr, screen_height, screen_width); int width = settings.vertical_split? screen_width/settings.players : screen_width; int height = settings.vertical_split? screen_height : screen_height/settings.players; // We can't set the track to be wider than the display. assert(settings.race_width<=width); // Prepare screen part. if(settings.vertical_split) { screen = newwin( height, width, // Take the corresponding vertical stripe. 0, width*(settings.players - position - 1) ); } else { screen = newwin( height, width, // Take the corresponding horizontal stripe. height*position, 0 ); } // Just copy this pointer. course = racecourse; // And create an image for yourself car = new car_image(); // Place the car at a reasonable place. y = settings.race_length - settings.car_size; if(settings.shared_track) x = (settings.race_width - (settings.car_size+1)*(settings.players-2*position))/ 2; else x = settings.race_width/2; // We want the car at the very bottom of the screen. top_line = settings.race_length - height; // If not set, the player actually could freeze for a while. last_move = -INF; // Copy the controls. memcpy(controls, settings.controls[position], 4*sizeof(int)); // And make sure player doesn't take off. command_y = command_x = 0; } /*player_handler::~player_handler(void) { // It's so simple... delwin(screen); }*/ // Just a simple switched assignment. void player_handler::parse_input(int pressed_key) { if(pressed_key == controls[0]) command_y=ACCELERATE; if(pressed_key == controls[1]) command_y=BRAKE; if(pressed_key == controls[2]) command_x=LEFT; if(pressed_key == controls[3]) command_x=RIGHT; } void player_handler::mark_position(void) { course->mark(y, x, car); } void player_handler::unmark_position(void) { course->unmark(y, x, car); } bool player_handler::tick(int time) { bool survive = true; // The higher the car on the screen, the faster it moves. if(last_move + (y-top_line)/settings.speed_base < time) { last_move=time; y--; // Watch to not segfault here. top_line=max(0, top_line-1); // Handle commands y+=command_y; x+=command_x; command_y = command_x = 0; // Make sure he doesn't escape from the screen. y = max(top_line, min(top_line + screen_height - settings.car_size, y)); if(y <= 0) // Plain win return false; course->display(screen, top_line); // Check for collisions. for(int i=y; i<y+settings.car_size; i++) for(int j=x; j<x+settings.car_size; j++) if(course->taken(i, j) && car->collision_check(i-y, j-x)) survive=false; if(survive) car->display(screen, y-top_line, x); else car->explode(screen, y-top_line, x); wrefresh(screen); } return survive; } car_image::car_image(void) { character = settings.character; color = COLOR_YELLOW; size = settings.car_size; // The coolest part - drawing the damned thing _clear(); // Original key points were (0,0), (1,4), (2,0), (3,4) and (4,0). // Now we just need to scale them and draw lines between. _line(0, 0, 1*(size-1)/4, 4*(size-1)/4); _line(1*(size-1)/4, 4*(size-1)/4, 2*(size-1)/4, 0); _line(2*(size-1)/4, 0, 3*(size-1)/4, 4*(size-1)/4); _line(3*(size-1)/4, 4*(size-1)/4, 4*(size-1)/4, 0); } void car_image::_clear(void) { // Simply clear the image for(int i=0; i<size; i++) for(int j=0; j<size; j++) storage[i][j]=false; } void car_image::display(WINDOW* screen, int y, int x) { // We don't need to store this anywhere, so it can be set ad-hoc. wattron(screen, COLOR_PAIR(color)); wattron(screen, A_BOLD); for(int i=0; i<size; i++) { for(int j=0; j<size; j++) if(storage[i][j]) mvwaddch(screen, y+i, x+j, character); } // Restore the normal color for everything else... wattroff(screen, COLOR_PAIR(color)); wattroff(screen, A_BOLD); } void car_image::explode(WINDOW* screen, int y, int x) {// Even in ASCII we can do cool explosions :> for(int i=0; i<size; i++) { for(int j=0; j<size; j++) if(rand()%2) { int color = rand()%7 + 1; wattron(screen, COLOR_PAIR(color)); mvwaddch(screen, y+i, x+j, '*'); wattroff(screen, COLOR_PAIR(color)); } } } bool car_image::collision_check(int y, int x) { return storage[y][x]; } vector<pair<int, int> > car_image::get_dots(void) { return dots; } /* * This function is the heart of the original task for which this program * was created. It is based on the simple formula, that for a line between * (y1, x1) and (y2, x2) and a given coordinate x, the y coordinate for a * point on a line is y1 + (y2-y1) / ( (x2-x1)/(x-x1) ). */ void car_image::_line(int y1, int x1, int y2, int x2) { // It can be given the other way round... if(x2 < x1) { // So just swap the points. int tmp = x1; x1 = x2; x2 = tmp; // And swap the other coordinate. tmp = y1; y1 = y2; y2 = tmp; } for(int i=x1; i<=x2; i++) { int y = y1 + (int)((float) (y2-y1)*(i-x1)/(x2-x1)+0.5); // +0.5 is in order to do real rounding, not just truncation. storage[y][i]=true; dots.push_back(make_pair(y, i)); } }