#include #include #include #include #include /* handling ctrl-c */ #include /* terminal size */ #include /* argument parsing */ #include /* time calculations */ #include "vector.h" #include "snake.h" #include "conio.h" #include "colors.h" #define UNUSED(x) (void)(x) /*** GLOBAL VARIABLES ***/ volatile sig_atomic_t intCtrlC = 0; /* flag for ctrl-c interrupt */ int speed = 3; int score = 0; int boardSize = 15; int updateInterval = 300000000L; int obstacleSize = 2; vectorp_t obstaclePositions; vectorp_t snakePositions; vector_t foodPos; vector_t lastMoveDir; void interrupt_handler(int sig); /* passed into signal(3) to handle ctrl-c interrupts */ /* unsafe functions like printf cannot be called in async interrupt handler */ void interrupt_handler(int sig) { UNUSED(sig); intCtrlC = 1; } int main(int argc, char **argv) { /* parsing arguments */ char *last_flag; int was_flag = 0; int obstacles = 2; for(int i = 1; i < argc; i++) { if(strcmp("--version", argv[i]) == 0) { printf("snake3, version 2.0\n"); printf("Copyright 2023 Kai Stevenson\n"); printf("This software was licensed under the MIT license;\n"); printf("for more information, see the license file.\n"); return 0; } else if(strcmp("--help", argv[i]) == 0) { printf("usage: snake3 [--version] [--help] [--size board_size] [--speed speed] [--obstacles obstacles]\n"); return 0; } else if(strcmp("--size", argv[i]) == 0 || strcmp("--speed", argv[i]) == 0 || strcmp("--obstacles", argv[i]) == 0) { last_flag = argv[i]; was_flag = 1; } else if(was_flag) { if(strcmp("--size", last_flag) == 0) { boardSize = atoi(argv[i]); if(boardSize < 3) { fprintf(stderr, "%sboard size too small or argument invalid, please try again.%s\n", RED, COLOR_RESET); exit(1); } } else if(strcmp("--speed", last_flag) == 0) { speed = atoi(argv[i]); if(speed <= 0) { fprintf(stderr, "%sspeed level must be greater than zero and integer. Please try again.%s\n", RED, COLOR_RESET); exit(1); } else if(speed > 10) { fprintf(stderr, "%sspeed must not be greater than ten. Please try again.%s\n", RED, COLOR_RESET); exit(1); } } else if(strcmp("--obstacles", last_flag) == 0) { obstacles = atoi(argv[i]); if(obstacles < 0) { fprintf(stderr, "%sobstacle count cannot be negative. Please try again.%s\n", RED, COLOR_RESET); exit(1); } } was_flag = 0; } else { printf("usage: snake3 [--version] [--help]\n"); exit(1); } } /* check argument compatibility */ if(obstacles * 2 > boardSize) { fprintf(stderr, "%stoo many obstacles. Please try again.%s\n", RED, COLOR_RESET); exit(1); } /* get terminal size */ struct winsize win; ioctl(fileno(stdout), TIOCGWINSZ, &win); int width = (int)(win.ws_col); int height = (int)(win.ws_row); /* multiplied by 2 to account for the fact that the board is twice as wide as high */ int smallestAxis = width / 2 < height ? width / 2 : height; boardSize = smallestAxis < boardSize ? smallestAxis : boardSize; /* hide cursor */ printf("\033[?25l"); /* make a new thread handling interrupts */ signal(SIGINT, interrupt_handler); /* init values */ snakePositions = init_vectorp(malloc(5 * sizeof(vector_t)), 5); snakePositions.vectors[0] = init_vector(5, 5); snakePositions.vectors[1] = init_vector(5, 4); snakePositions.vectors[2] = init_vector(5, 3); snakePositions.vectors[3] = init_vector(4, 3); snakePositions.vectors[4] = init_vector(3, 3); foodPos = init_vector(7, 9); lastMoveDir = init_vector(0, 1); int opc = (int)pow(obstacleSize, 2) * obstacles; obstaclePositions = init_vectorp(malloc(opc * sizeof(vector_t)), opc); srand(time(NULL)); /* place the obstacles */ /* TODO: make this less recursive */ int opi = 0; for(int i = 0; i < obstacles; i++) { while(1) { vector_t basePos = init_vector((rand() % (boardSize - (obstacleSize - 1))), (rand() % (boardSize - (obstacleSize - 1)))); /* if obstacle overlaps snake, try again */ int overlap = 0; for(int r = 0; r < obstacleSize; r++) { for(int c = 0; c < obstacleSize; c++) { vector_t voxelPos = init_vector(basePos.mx + c, basePos.my + r); for(int j = 0; j < snakePositions.length; j++) { if((snakePositions.vectors[j].mx == voxelPos.mx) && (snakePositions.vectors[j].my == voxelPos.my)) { overlap = 1; } } if(foodPos.mx == voxelPos.mx && foodPos.my == voxelPos.my) { overlap = 1; } } } if(overlap) { continue; } for(int r = 0; r < obstacleSize; r++) { for(int c = 0; c < obstacleSize; c++) { vector_t voxelPos = init_vector(basePos.mx + c, basePos.my + r); obstaclePositions.vectors[opi] = voxelPos; opi++; } } break; } } /* clear the screen so as to prevent artifacting */ printf("\033[2J"); /* prepare the pointers which will be used to communicate with the input getting thread */ volatile int* lastInput = calloc(1, sizeof(int)); int* cancellationToken = calloc(1, sizeof(int)); /* and the wrapper struct */ threadArg_t threadArg; threadArg.returnV = (void*)lastInput; threadArg.cancellationToken = cancellationToken; /* create listener thread */ pthread_t listenerID; pthread_create(&listenerID, NULL, get_dir_loop, &threadArg); /* main game logic loop */ while(1) { /* handle ctrl-c interrupt */ if(intCtrlC) { goto EXIT; } /* first, draw the current state of the board */ draw_board(); /* handle player input; * wait for the specified interval */ struct timespec time; time.tv_sec = 0; time.tv_nsec = round((float)updateInterval / ((float)speed / 3)); nanosleep(&time, NULL); /* read the most recent value emitted by the listener */ vector_t dir = parse_dir(*(char*)lastInput); /* if the player inputted a valid direction, use it. otherwise, use his last valid input */ if((dir.mx != 0 || dir.my != 0) && !(dir.mx == 1 && lastMoveDir.mx == -1) && !(dir.mx == -1 && lastMoveDir.mx == 1) && !(dir.my == 1 && lastMoveDir.my == -1) && !(dir.my == -1 && lastMoveDir.my == 1)) { lastMoveDir = dir; } /* check if snake landed on an obstacle */ int ateFood = 0; /* check if snake landed on the food */ if((snakePositions.vectors[0].mx + lastMoveDir.mx == foodPos.mx) && (snakePositions.vectors[0].my + lastMoveDir.my == foodPos.my)) { ateFood = 1; score += 1; if(updateInterval > 100000000) { updateInterval -= 10000000; } /* move the food */ while(1) { foodPos = init_vector(1 + (rand() % (boardSize - 2)), 1 + (rand() % (boardSize - 2))); /* if food overlaps snake or obstacles, try again */ int overlap = 0; for(int i = 0; i < snakePositions.length; i++) { if((snakePositions.vectors[i].mx == foodPos.mx) && (snakePositions.vectors[i].my == foodPos.my)) { overlap = 1; } } for(int i = 0; i < obstaclePositions.length; i++) { if((obstaclePositions.vectors[i].mx == foodPos.mx) && (obstaclePositions.vectors[i].my == foodPos.my)) { overlap = 1; } } if(!overlap) { break; } } } /* move the snake */ vectorp_t newSnakePositions = move_snake(lastMoveDir, ateFood); free(snakePositions.vectors); snakePositions = newSnakePositions; /* check if snake landed on self */ for(int i = 1; i < snakePositions.length; i++) { if(snakePositions.vectors[0].mx == snakePositions.vectors[i].mx && snakePositions.vectors[0].my == snakePositions.vectors[i].my) { /* game over */ goto EXIT; } } for(int i = 0; i < obstaclePositions.length; i++) { if((snakePositions.vectors[0].mx + lastMoveDir.mx == obstaclePositions.vectors[i].mx) && (snakePositions.vectors[0].my + lastMoveDir.my == obstaclePositions.vectors[i].my)) { goto EXIT; } } } EXIT: /* clear the last getch call from the listener */ *cancellationToken = 1; printf("\n%sYou died. Score: %d. Press any key to exit%s\n", RED, score, COLOR_RESET); printf("\033[?25h"); pthread_join(listenerID, NULL); free(cancellationToken); free((int*)lastInput); return 0; } void* get_dir_loop(void* threadArg) { while(!*(int*)((threadArg_t*)threadArg)->cancellationToken) { *(char*)((threadArg_t*)threadArg)->returnV = getch(); } return NULL; } /* parses WASD or HJKL, returns the necessary deltas to move within a 2d array */ vector_t parse_dir(char cDir) { if(cDir == 'w' || cDir == 'k') { return init_vector(0, -1); } else if(cDir == 'd' || cDir == 'l') { return init_vector(1, 0); } else if(cDir == 's' || cDir == 'j') { return init_vector(0, 1); } else if(cDir == 'a' || cDir == 'h') { return init_vector(-1, 0); } else { return init_vector(0, 0); } } vectorp_t move_snake(vector_t dir, int lengthen) { int newLength = snakePositions.length + (lengthen ? 1 : 0); vector_t* newSnakePositions = malloc((newLength * sizeof(vector_t))); /* wrap around edges */ int nhx = snakePositions.vectors[0].mx + dir.mx; nhx = nhx < 0 ? boardSize - 1 : nhx >= boardSize ? 0 : nhx; int nhy = snakePositions.vectors[0].my + dir.my; nhy = nhy < 0 ? boardSize - 1 : nhy >= boardSize ? 0 : nhy; newSnakePositions[0] = init_vector(nhx, nhy); for(int i = 1; i < newLength; i++) { newSnakePositions[i] = snakePositions.vectors[i - 1]; } return init_vectorp(newSnakePositions, newLength); } void draw_board() { /* move the cursor to the top left corner of the screen */ printf("\033[0;0H"); /* iterate through and write the board */ for(int r = 0; r < boardSize; r++) { for(int c = 0; c < boardSize; c++) { /* determine the contents of the board position */ int type = 0; /* 0=horizontal, 1=vertical, 2=diagonal slant right, 3=diagonal slant left */ int tailOrientation = 0; for(int i = 0; i < snakePositions.length; i++) { if(snakePositions.vectors[i].mx == c && snakePositions.vectors[i].my == r) { if(i > 0) { type = 2; /* get the delta to the connecting tail */ int dx; int dy; if(i < snakePositions.length - 1) { dx = snakePositions.vectors[i - 1].mx - snakePositions.vectors[i + 1].mx; dy = snakePositions.vectors[i - 1].my - snakePositions.vectors[i + 1].my; } else { dx = snakePositions.vectors[i - 1].mx - c; dy = snakePositions.vectors[i - 1].my - r; } if(dy == 0) { tailOrientation = 0; } else if(dx == 0) { tailOrientation = 1; } else if((dx == 1 && dy == 1) || (dx == -1 && dy == -1)) { tailOrientation = 3; } else if((dx == -1 && dy == 1) || (dx == 1 && dy == -1)) { tailOrientation = 2; } } else { type = 1; } break; } } if(foodPos.mx == c && foodPos.my == r) { type = 3; } for(int i = 0; i < obstaclePositions.length; i++) { if(obstaclePositions.vectors[i].mx == c && obstaclePositions.vectors[i].my == r) { type = 4; break; } } /* append the appropriate value */ if(type == 0) { /* empty square */ printf("` "); } else if(type == 1) { /* snake head */ printf("%s@ %s", GRN, COLOR_RESET); } else if(type == 2) { /* snake body */ char bodyChar = tailOrientation == 0 ? '-' : tailOrientation == 1 ? '|' : tailOrientation == 2 ? '/' : tailOrientation == 3 ? '\\' : '*'; printf("%s%c %s", GRN, bodyChar, COLOR_RESET); } else if(type == 3) { /* food */ printf("%sF %s", MAG, COLOR_RESET); } else if(type == 4) { /* obstacle */ printf("%s# %s", MAG, COLOR_RESET); } } printf("\n"); } }