Created
May 20, 2019 08:05
-
-
Save michd/f700de2c7502210900156f1eaabcb07c to your computer and use it in GitHub Desktop.
ncurses-snake (cobbled together in a couple of hours)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#include <ncurses.h> | |
#include <string.h> | |
#include <stdlib.h> | |
#include <time.h> | |
#include "snake.h" | |
#define SNAKE_BOX_W 32 | |
#define SNAKE_BOX_H 16 | |
#define SNAKE_LOCATIONS (SNAKE_BOX_W * SNAKE_BOX_H) | |
#define INITIAL_SNAKE_LENGTH 5 | |
#define SNAKE_BODY_CHAR '0' | |
#define SNAKE_FOOD_CHAR '*' | |
#define ALLOW_WRAPPING FALSE | |
// Playing field, 2 for borders | |
#define COLS_REQUIRED (SNAKE_BOX_W + 2) | |
// Playing field, 2 for borders, 1 for title, 1 for status line | |
#define ROWS_REQUIRED (SNAKE_BOX_H + 2 + 1 + 1) | |
coord boxCoord; | |
WINDOW *fieldWindow; | |
int score = 0; | |
Direction dir; | |
SnakeSegment * headSegment; | |
SnakeSegment * tailSegment; | |
coord foodPos; | |
int main() { | |
initscr(); | |
if (!haveRequiredSpace()) { | |
endwin(); | |
printf( | |
"Sorry, you need a terminal of at least %d colums and %d rows to play.", | |
COLS_REQUIRED, | |
ROWS_REQUIRED); | |
return 1; | |
} | |
// Don't show the cursor | |
curs_set(0); | |
// Don't output characters that have been input | |
noecho(); | |
setupField(); | |
showStartPrompt(); | |
endwin(); | |
return 0; | |
} | |
bool haveRequiredSpace() { | |
return (COLS_REQUIRED <= COLS && ROWS_REQUIRED <= LINES); | |
} | |
void setupField() { | |
refresh(); | |
boxCoord.y = (LINES - SNAKE_BOX_H) / 2; | |
boxCoord.x = (COLS - SNAKE_BOX_W) / 2; | |
fieldWindow = newwin( | |
SNAKE_BOX_H + 2, | |
SNAKE_BOX_W + 2, | |
boxCoord.y, | |
boxCoord.x); | |
box(fieldWindow, 0, 0); | |
wrefresh(fieldWindow); | |
attron(A_BOLD); | |
center(boxCoord.y - 1, "SNAKE"); | |
attroff(A_BOLD); | |
refresh(); | |
} | |
void center(int row, char* text) { | |
int len, offset; | |
len = strlen(text); | |
offset = (COLS - strlen(text)) / 2; | |
mvaddstr(row, offset, text); | |
} | |
void writeScore(int score) { | |
int len = snprintf(NULL, 0, "Score: %d", score); | |
int offset = boxCoord.x + SNAKE_BOX_W + 2 - len; | |
int line = boxCoord.y + SNAKE_BOX_H + 2; | |
move(line, 0); | |
deleteln(); | |
mvprintw(boxCoord.y + SNAKE_BOX_H + 2, offset, "Score: %d", score); | |
refresh(); | |
} | |
void showStartPrompt() { | |
center(boxCoord.y + SNAKE_BOX_H / 2, "Press any key to play."); | |
center(boxCoord.y + SNAKE_BOX_H / 2 + 2, "Press escape to exit."); | |
center(boxCoord.y + SNAKE_BOX_H / 2 + 4, "Move with WASD / arrow keys."); | |
char input = getch(); | |
switch (input) { | |
case 27: | |
quit(false); | |
return; | |
default: | |
startGame(); | |
break; | |
} | |
} | |
bool quit(bool prompt) { | |
// TODO: actually prompt y/n for quitting | |
endwin(); | |
return true; | |
} | |
void startGame() { | |
werase(fieldWindow); | |
box(fieldWindow, 0, 0); | |
wrefresh(fieldWindow); | |
dir = Right; | |
score = 0; | |
writeScore(score); | |
initSnake(); | |
SnakeSegment * seg = headSegment; | |
while (seg != NULL) { | |
drawSnakeSegment(seg->pos); | |
seg = seg->behind; | |
} | |
createNewFood(); | |
drawFood(foodPos); | |
wrefresh(fieldWindow); | |
gameLoop(); | |
} | |
SnakeSegment * newSnakeSegment() { | |
SnakeSegment * segment = (SnakeSegment *)malloc(sizeof(SnakeSegment)); | |
segment->ahead = NULL; | |
segment->behind = NULL; | |
return segment; | |
} | |
// Shuffles linked list items arround, adding or moving around. | |
// Note that this function does not alter any coordinates; | |
// Calculating the coordinates of the new head segment and drawing it | |
// is handled elsewhere (TODO add function names in comment) | |
void prepAdvanceSnake(bool grow) { | |
if (grow) { | |
// Add a new segment at the front | |
SnakeSegment * newHead = newSnakeSegment(); | |
newHead->behind = headSegment; | |
headSegment->ahead = newHead; | |
headSegment = newHead; | |
} else { | |
// Not growing: The tail segment will/has been be cleared, so let's | |
// repurpose that segment to be the new head. | |
SnakeSegment * newHead = tailSegment; | |
tailSegment = tailSegment->ahead; | |
tailSegment->behind = NULL; | |
// Move the tail segment to be the front. | |
newHead->ahead = NULL; | |
newHead->behind = headSegment; | |
headSegment->ahead = newHead; | |
headSegment = newHead; | |
} | |
} | |
// Frees the memory allocated for snake segments. | |
void deleteSnakeMemory() { | |
SnakeSegment * segment = headSegment; | |
while (segment != NULL) { | |
SnakeSegment * next = segment->behind; | |
free(segment); | |
segment = next; | |
} | |
} | |
// Sets up the snake to start the game with. | |
void initSnake() { | |
coord headCoord; | |
headCoord.y = SNAKE_BOX_H / 2 - 1; | |
headCoord.x = ((SNAKE_BOX_W - INITIAL_SNAKE_LENGTH) / 3) + INITIAL_SNAKE_LENGTH; | |
headSegment = newSnakeSegment(); | |
headSegment->pos = headCoord; | |
int segsLeft = INITIAL_SNAKE_LENGTH - 1; | |
SnakeSegment * seg = headSegment; | |
while (segsLeft--) { | |
seg->behind = newSnakeSegment(); | |
seg->behind->ahead = seg; | |
seg = seg->behind; | |
seg->pos.y = headCoord.y; | |
seg->pos.x = seg->ahead->pos.x - 1; | |
} | |
tailSegment = seg; | |
} | |
void drawSnakeSegment(coord pos) { | |
mvwaddch(fieldWindow, pos.y + 1, pos.x + 1, SNAKE_BODY_CHAR); | |
} | |
void drawFood(coord pos) { | |
mvwaddch(fieldWindow, pos.y + 1, pos.x + 1, SNAKE_FOOD_CHAR); | |
} | |
void clearSnakeSegment(coord pos) { | |
mvwaddch(fieldWindow, pos.y + 1, pos.x + 1, ' '); | |
} | |
// Non-trivial new food generation, pick a random location that is not on a | |
// position that currently has snake. | |
void createNewFood() { | |
int snakeLength = score - INITIAL_SNAKE_LENGTH; | |
coord potentialSpots[SNAKE_LOCATIONS - snakeLength]; | |
coord takenSpots[snakeLength]; | |
SnakeSegment * seg = headSegment; | |
int i = 0; | |
// Stick the existing segments into an array for easy access | |
while (seg != NULL) { | |
takenSpots[i++] = seg->pos; | |
seg = seg->behind; | |
} | |
i = 0; | |
for (int x = 0; x < SNAKE_BOX_W; x++) { | |
for (int y = 0; y < SNAKE_BOX_H; y++) { | |
// Check if these coords exist in takenSpots | |
bool taken = false; | |
for (int j = 0; j < snakeLength; j++) { | |
if (takenSpots[j].x == x && takenSpots[j].y == y) { | |
taken = true; | |
break; | |
} | |
} | |
if (taken) continue; | |
coord newCoord; | |
newCoord.x = x; | |
newCoord.y = y; | |
potentialSpots[i++] = newCoord; | |
} | |
} | |
// Seed randomg number generator | |
// TODO: move to initalization somewhere | |
time_t t; | |
srand((unsigned) time(&t)); | |
// Get a random index in the potential spots list | |
size_t randIndex = rand() % (SNAKE_LOCATIONS - snakeLength); | |
foodPos = potentialSpots[randIndex]; | |
} | |
void gameLoop() { | |
// Don't block on getch() | |
nodelay(stdscr, TRUE); | |
char input = '\0'; | |
while (input != 27) { // Escape key | |
napms(250); | |
input = getch(); | |
switch (input) { | |
case 'w': | |
case 'W': | |
dir = Up; | |
break; | |
case 'a': | |
case 'A': | |
dir = Left; | |
break; | |
case 's': | |
case 'S': | |
dir = Down; | |
break; | |
case 'd': | |
case 'D': | |
dir = Right; | |
break; | |
} | |
if (!advanceSnake()) { | |
nodelay(stdscr, FALSE); | |
gameOver(); | |
} | |
} | |
// Start blocking on getch again | |
nodelay(stdscr, FALSE); | |
} | |
bool advanceSnake() { | |
coord nextPos; | |
nextPos.x = headSegment->pos.x; | |
nextPos.y = headSegment->pos.y; | |
switch(dir) { | |
case Right: | |
nextPos.x++; | |
break; | |
case Left: | |
nextPos.x--; | |
break; | |
case Up: | |
nextPos.y--; | |
break; | |
case Down: | |
nextPos.y++; | |
break; | |
} | |
if (isSnakeBody(nextPos)) return false; | |
if (isWall(nextPos)) { | |
if (!ALLOW_WRAPPING) return false; | |
switch (dir) { | |
case Right: | |
nextPos.x = 0; | |
break; | |
case Left: | |
nextPos.x = SNAKE_BOX_W - 1; | |
break; | |
case Up: | |
nextPos.y = SNAKE_BOX_H - 1; | |
break; | |
case Down: | |
nextPos.y = 0; | |
break; | |
} | |
if (isSnakeBody(nextPos)) return false; | |
} | |
bool eating = nextPos.x == foodPos.x && nextPos.y == foodPos.y; | |
if (!eating) { | |
clearSnakeSegment(tailSegment->pos); | |
} | |
prepAdvanceSnake(eating); | |
headSegment->pos = nextPos; | |
drawSnakeSegment(headSegment->pos); | |
if (eating) { | |
score++; | |
createNewFood(); | |
drawFood(foodPos); | |
writeScore(score); | |
} | |
wrefresh(fieldWindow); | |
refresh(); | |
return true; | |
} | |
bool isWall(coord pos) { | |
return pos.x == -1 | |
|| pos.y == -1 | |
|| pos.x == SNAKE_BOX_W | |
|| pos.y == SNAKE_BOX_H; | |
} | |
bool isSnakeBody(coord pos) { | |
SnakeSegment * seg = headSegment; | |
while (seg != NULL) { | |
if (seg->pos.x == pos.x && seg->pos.y == pos.y) return true; | |
seg = seg->behind; | |
} | |
return false; | |
} | |
void gameOver() { | |
setupField(); | |
attron(A_BLINK | A_BOLD); | |
center(boxCoord.y + SNAKE_BOX_H / 2, "Game over!"); | |
attroff(A_BLINK | A_BOLD); | |
center(boxCoord.y + SNAKE_BOX_H / 2 + 2, "Press escape to exit."); | |
center(boxCoord.y + SNAKE_BOX_H / 2 + 3, "Or any key to play again."); | |
char input = getch(); | |
if (input == 27) { | |
quit(false); | |
} else { | |
startGame(); | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#ifndef _SNAKE_H_ | |
#define _SNAKE_H_ | |
typedef struct { int y; int x; } coord; | |
typedef struct snake_segment_t { | |
coord pos; | |
struct snake_segment_t * ahead; | |
struct snake_segment_t * behind; | |
} SnakeSegment; | |
typedef enum { | |
Up, | |
Down, | |
Left, | |
Right | |
} Direction; | |
bool haveRequiredSpace(); | |
void setupField(); | |
void center(int, char *); | |
void writeScore(int); | |
void showStartPrompt(); | |
bool quit(bool); | |
void startGame(); | |
SnakeSegment * newSnakeSegment(); | |
void prepAdvanceSnake(bool); | |
void deleteSnakeMemory(); | |
void initSnake(); | |
void drawSnakeSegment(coord); | |
void clearSnakeSegment(coord); | |
void createNewFood(); | |
void drawFood(coord); | |
void gameLoop(); | |
bool advanceSnake(); | |
bool isWall(coord); | |
bool isSnakeBody(coord); | |
void gameOver(); | |
#endif |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment