Skip to content

Instantly share code, notes, and snippets.

@michd
Created May 20, 2019 08:05
Show Gist options
  • Save michd/f700de2c7502210900156f1eaabcb07c to your computer and use it in GitHub Desktop.
Save michd/f700de2c7502210900156f1eaabcb07c to your computer and use it in GitHub Desktop.
ncurses-snake (cobbled together in a couple of hours)
#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();
}
}
#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