unified codebase

This commit is contained in:
Björn Esser 2014-12-03 16:14:30 +01:00
parent 82db92d4bf
commit 2efc3db045
5 changed files with 244 additions and 455 deletions

View File

@ -4,11 +4,11 @@ LIBS = -lcurses
all: 2048
2048: src/2048.c src/2048.h src/highscore_file.c
$(CC) $(CFLAGS) $(LDFLAGS) $(LIBS) src/2048.c src/highscore_file.c -o 2048
2048: src/2048.c
$(CC) $(CFLAGS) $(LDFLAGS) $(LIBS) src/2048.c -o 2048
2048nc: src/2048_no_curses.c src/2048.h src/highscore_file.c
$(CC) $(CFLAGS) $(LDFLAGS) src/2048_no_curses.c src/highscore_file.c -o 2048nc
2048nc: src/2048.c
$(CC) -DNO_CURSES=1 $(CFLAGS) $(LDFLAGS) src/2048.c -o 2048nc
clean:
rm -f 2048 2048nc

View File

@ -1,20 +1,69 @@
/*
* 2048.c - Main version that should be worked on.
* 2048.c - The game 2048 for your Linux terminal.
*
* Copyright (c) 2014 Tiehuis
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to
* deal in the Software without restriction, including without limitation the
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
* sell copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
* IN THE SOFTWARE.
*
* Things to do:
* - Finish cleaning up code.
* - Ask user if they would like to play again instead of exiting immediately.
* - Add an actual goal (what happens when we reach 2048?).
* - Keep track list of past scores, and store in a better fashion independent
* of cwd. Also, store in not in plaintext.
* of cwd. Also, store not in plaintext.
* */
#include <stdlib.h> /* for malloc */
#include <time.h> /* for time */
#include <unistd.h> /* for getopts */
#include <ncurses.h>
#include "2048.h"
#include <stdlib.h> /* for malloc */
#include <stdio.h> /* for printf, FILE */
#include <string.h> /* for strlen, strrchr */
#include <sys/stat.h> /* for mkdir */
#include <time.h> /* for time */
#include <unistd.h> /* for getopts */
#ifndef NO_CURSES
#include <ncurses.h> /* for ncurses terminal */
#else /* ifndef NO_CURSES */
#include <termios.h> /* for plain terminal */
#endif /* ifndef NO_CURSES */
/* Constants */
#define DATADIR_NAME "2048"
#define HIGHSCORE_FILE_NAME "highscore"
#define DEFAULT_GRID_SIZE 4
#define HISCORE_FILE get_highscore_file()
#define USAGE_STR\
"Usage:\n"\
" ./2048 [options]\n"\
"\n"\
"Controls:\n"\
" hjkl, wasd Movement\n"\
" q Quit\n"\
"\n"\
"Options:\n"\
" -s <size> Set the grid border length\n"\
" -b <rate> Set the block spawn rate\n"\
" -c Enables color support (ncurses version only)\n"\
" -C Disables color support (ncurses version only)\n"
/* Globals */
int **grid; /* grid pointer */
int grid_size; /* grid size */
int score; /* score */
@ -22,6 +71,70 @@ int score_last; /* Score for last move */
int score_high; /* Hiscore */
int printwidth; /* maximum length of any value in grid, for printing */
/* Typedefs */
typedef enum {
DIR_UP,
DIR_RIGHT,
DIR_DOWN,
DIR_LEFT
} dir_t;
/* Macros */
/* Repeat an expression y, x times */
#define ITER(x, expr)\
do {\
int i;\
for (i = 0; i < (x); i++){ expr;}\
} while (0)
/* Allocates a square pointer of array of arrays onto heap */
#define CALLOC2D(ptr, sz)\
do {\
int i;\
ptr = calloc((sz), sizeof(*ptr));\
for (i = 0; i < (sz); i++)\
ptr[i] = calloc((sz), sizeof(*ptr));\
} while (0)
/* Frees a square pointer of arrays to arrays */
#define FREE2D(ptr, sz)\
do {\
int i;\
for (i = 0; i < (sz); i++)\
free(ptr[i]);\
free(ptr);\
} while (0)
/* What occurs during a 'turn' of execution */
#define TURN(x) (gravitate(x) + merge(x) + gravitate(x))
/* Functions */
const char* get_highscore_file() {
static char buffer[4096];
if (getenv("XDG_DATA_HOME") != NULL) {
snprintf(buffer, sizeof(buffer), "%s/%s/%s", getenv("XDG_DATA_HOME"), DATADIR_NAME, HIGHSCORE_FILE_NAME);
} else if (getenv("HOME") != NULL) {
snprintf(buffer, sizeof(buffer), "%s/.local/share/%s/%s", getenv("HOME"), DATADIR_NAME, HIGHSCORE_FILE_NAME);
} else {
return HIGHSCORE_FILE_NAME;
}
// create hierarrchy of directories up to highscore file location
char* sep = strrchr(buffer, '/');
while (sep != NULL) {
*sep = '\0';
if (strlen(buffer) != 0)
mkdir(buffer, 0777);
char* tmpsep = sep;
sep = strrchr(buffer, '/');
*tmpsep = '/';
}
return buffer;
}
/* Merges adjacent squares of the same value together in a certain direction */
int merge(dir_t d)
{
@ -157,7 +270,8 @@ int gravitate(dir_t d)
}
/* Return the current highscore */
int get_score_high() {
int get_score_high()
{
int s = 0;
FILE *fd = fopen(HISCORE_FILE, "r");
if (fd == NULL)
@ -169,7 +283,8 @@ int get_score_high() {
}
/* saves hiscore, but only if playing on standard size grid */
void save_score_high() {
void save_score_high()
{
if (score > score_high && grid_size == 4) {
score_high = score;
FILE *fd = fopen(HISCORE_FILE, "w+");
@ -209,6 +324,8 @@ int flog2(unsigned int n)
return k;
}
#ifndef NO_CURSES
/* draws the grid and fills it with the current values */
/* colors just rotate around, works for now, can be confusing when you have some fairly high values on the board */
void draw_grid(WINDOW *gamewin)
@ -240,15 +357,48 @@ void draw_grid(WINDOW *gamewin)
wrefresh(gamewin);
}
#else /* ifndef NO_CURSES */
/* draws the grid and fills it with the current values */
void draw_grid()
{
printf("HISCORE: %d |", score_high);
printf("| SCORE: %d ", score);
if (score_last) printf("(+%d)", score_last);
printf("\n");
// alter this grid_size + 1 to match abitrary grid size
ITER(grid_size, printf("------"));
printf("-\n");
int i, j;
for (i = 0; i < grid_size; i++) {
printf("|");
for (j = 0; j < grid_size; j++) {
if (grid[i][j])
printf("%*d |", 4, grid[i][j]);
else
printf(" |");
}
printf("\n");
}
ITER(grid_size, printf("------"));
printf("-\n\n");
}
/* store the terminal settings and call this function on exit to restore */
struct termios sattr;
void reset_term()
{
tcsetattr(STDIN_FILENO, TCSANOW, &sattr);
}
#endif /* ifndef NO_CURSES */
/* entry point for the program */
/* parses options and stores the main game loop */
int main(int argc, char **argv)
{
/* init ncurses environment */
initscr();
cbreak();
noecho();
curs_set(FALSE);
/* init variables */
score = 0;
@ -257,13 +407,22 @@ int main(int argc, char **argv)
grid_size = DEFAULT_GRID_SIZE;
printwidth = DEFAULT_GRID_SIZE;
int n_blocks = 1;
int n_blocks = 1;
#ifndef NO_CURSES
/* init ncurses environment */
initscr();
cbreak();
noecho();
curs_set(FALSE);
int enable_color = has_colors();
#endif /* ifndef NO_CURSES */
/* parse options */
int c;
int enable_color = has_colors();
while ((c = getopt(argc, argv, "rcChs:b:")) != -1) {
switch (c) {
#ifndef NO_CURSES
/* Color support */
case 'c':
enable_color = 1;
@ -271,6 +430,7 @@ int main(int argc, char **argv)
case 'C':
enable_color = 0;
break;
#endif /* ifndef NO_CURSES */
// different board sizes
case 's':;
int optint = strtol(optarg, NULL, 10);
@ -282,7 +442,9 @@ int main(int argc, char **argv)
break;
// reset hiscores
case 'r':
#ifndef NO_CURSES
endwin();
#endif /* ifndef NO_CURSES */
printf("Are you sure you want to reset your highscores? (Y)es or (N)o\n");
int response;
if ((response = getchar()) == 'y' || response == 'Y') {
@ -292,12 +454,21 @@ int main(int argc, char **argv)
exit(EXIT_SUCCESS);
// help menu
case 'h':
#ifndef NO_CURSES
endwin();
#endif /* ifndef NO_CURSES */
printf(USAGE_STR);
exit(EXIT_SUCCESS);
}
}
/* Allocate memory once we actually know amount */
CALLOC2D(grid, grid_size);
#ifndef NO_CURSES
int width = grid_size * (printwidth + 2) + 1;
int height = grid_size * (printwidth + 2) + 3;
if (enable_color) {
if (!has_colors()) {
fprintf(stderr, "Terminal does not support color\n");
@ -313,64 +484,98 @@ int main(int argc, char **argv)
}
}
/* Allocate memory once we actually know amount */
CALLOC2D(grid, grid_size);
int width = grid_size * (printwidth + 2) + 1;
int height = grid_size * (printwidth + 2) + 3;
// might center in middle of screen
WINDOW *gamewin = newwin(height, width, 1, 1);
keypad(gamewin, TRUE);
#else /* ifndef NO_CURSES */
/* store term settings so we can restore on exit */
tcgetattr(STDIN_FILENO, &sattr);
atexit(reset_term);
/* alters terminal stdin to not echo and doesn't need \n before reading getchar */
struct termios tattr;
tcgetattr(STDIN_FILENO, &tattr);
tattr.c_lflag &= ~(ICANON | ECHO);
tcsetattr(STDOUT_FILENO, TCSANOW, &tattr);
#endif /* ifndef NO_CURSES */
/* random seed */
srand((unsigned int)time(NULL));
ITER(2, rand_block());
#ifndef NO_CURSES
draw_grid(gamewin);
#else /* ifndef NO_CURSES */
draw_grid();
#endif /* ifndef NO_CURSES */
int key, moved;
while (1) {
/* will goto this if we didn't get a valid keypress */
retry:;
retry:;
moved = 0;
score_last = 0;
#ifndef NO_CURSES
key = wgetch(gamewin);
#else /* ifndef NO_CURSES */
key = getchar();
#endif /* ifndef NO_CURSES */
/* should check if anything changed during merge and if not retry */
switch (key) {
case 'h':
case 'a':
#ifndef NO_CURSES
case KEY_LEFT:
#endif /* ifndef NO_CURSES */
moved = TURN(DIR_LEFT);
break;
case 'l':
case 'd':
#ifndef NO_CURSES
case KEY_RIGHT:
#endif /* ifndef NO_CURSES */
moved = TURN(DIR_RIGHT);
break;
case 'j':
case 's':
#ifndef NO_CURSES
case KEY_DOWN:
#endif /* ifndef NO_CURSES */
moved = TURN(DIR_DOWN);
break;
case 'k':
case 'w':
#ifndef NO_CURSES
case KEY_UP:
#endif /* ifndef NO_CURSES */
moved = TURN(DIR_UP);
break;
case 'q':
FREE2D(grid, grid_size);
#ifndef NO_CURSES
erase();
refresh();
endwin();
#endif /* ifndef NO_CURSES */
save_score_high();
exit(EXIT_SUCCESS);
default:
goto retry;
}
if (!moves_available()) {
#ifndef NO_CURSES
endwin();
#endif /* ifndef NO_CURSES */
printf("\n"
"YOU LOSE! - Your score was %d\n", score);
save_score_high();
@ -379,7 +584,11 @@ retry:;
if (moved) {
ITER(n_blocks, rand_block());
#ifndef NO_CURSES
draw_grid(gamewin);
#else /* ifndef NO_CURSES */
draw_grid();
#endif /* ifndef NO_CURSES */
}
}

View File

@ -1,65 +0,0 @@
#ifndef _2048_H_
#define _2048_H_
#include <stdlib.h>
#include <sys/stat.h>
#include <string.h>
typedef enum {
DIR_UP,
DIR_RIGHT,
DIR_DOWN,
DIR_LEFT
} dir_t;
#define DATADIR_NAME "2048"
#define HIGHSCORE_FILE_NAME "highscore"
const char* get_highscore_file();
// Repeat an expression y, x times */
#define ITER(x, expr)\
do {\
int i;\
for (i = 0; i < (x); i++){ expr;}\
} while (0)
/* Allocates a square pointer of array of arrays onto heap */
#define CALLOC2D(ptr, sz)\
do {\
int i;\
ptr = calloc((sz), sizeof(*ptr));\
for (i = 0; i < (sz); i++)\
ptr[i] = calloc((sz), sizeof(*ptr));\
} while (0)
/* Frees a square pointer of arrays to arrays */
#define FREE2D(ptr, sz)\
do {\
int i;\
for (i = 0; i < (sz); i++)\
free(ptr[i]);\
free(ptr);\
} while (0)
/* What occurs during a 'turn' of execution */
#define TURN(x) (gravitate(x) + merge(x) + gravitate(x))
/* Constants */
#define DEFAULT_GRID_SIZE 4
#define HISCORE_FILE get_highscore_file()
#define USAGE_STR\
"Usage:\n"\
" ./2048 [options]\n"\
"\n"\
"Controls:\n"\
" hjkl, wasd Movement\n"\
" q Quit\n"\
"\n"\
"Options:\n"\
" -s <size> Set the grid border length\n"\
" -b <rate> Set the block spawn rate\n"\
" -c Enables color support (ncurses version only)\n"\
" -C Disables color support (ncurses version only)\n"
#endif

View File

@ -1,326 +0,0 @@
/*
* 2048_no_curses.c - Non-curses version that can be updated at a later time
**/
#include <stdio.h> /* for printf */
#include <stdlib.h> /* for malloc */
#include <termios.h>/* */
#include <time.h> /* for time */
#include <unistd.h> /* for getopts */
#include "2048.h"
int **g; /* grid pointer */
int SZ; /* grid size */
int s; /* Current score */
int sl; /* Score for last turn */
int hs; /* Highscore */
/* Merges adjacent squares of the same value together in a certain direction */
int merge(int d)
{
int moved = 0;
if (d == DIR_LEFT) {
int i, j;
for (i = 0; i < SZ; i++) {
for (j = 0; j < SZ; j++) {
if (j + 1 < SZ && g[i][j] && g[i][j] == g[i][j + 1]) {
g[i][j] <<= 1;
sl += g[i][j];
s += g[i][j];
g[i][j++ + 1] = 0;
moved = 1;
}
}
}
}
else if (d == DIR_UP) {
int i, j;
for (i = 0; i < SZ; i++) {
for (j = 0; j < SZ; j++) {
if (j + 1 < SZ && g[j][i] && g[j][i] == g[j + 1][i]) {
g[j][i] <<= 1;
sl += g[j][i];
s += g[j][i];
g[j++ + 1][i] = 0;
moved = 1;
}
}
}
}
else if (d == DIR_RIGHT) {
int i, j;
for (i = SZ - 1; i >= 0; i--) {
for (j = SZ - 1; j >= 0; j--) {
if (j > 0 && g[i][j] && g[i][j] == g[i][j - 1]) {
g[i][j] <<= 1;
sl += g[i][j];
s += g[i][j];
g[i][j-- - 1] = 0;
moved = 1;
}
}
}
}
else if (d == DIR_DOWN) {
int i, j;
for (i = SZ - 1; i >= 0; i--) {
for (j = SZ - 1; j >= 0; j--) {
if (j > 0 && g[j][i] && g[j][i] == g[j - 1][i]) {
g[j][i] <<= 1;
sl += g[j][i];
s += g[j][i];
g[j-- - 1][i] = 0;
moved = 1;
}
}
}
}
return moved;
}
/* move all values in the grid to the edge given by the direction pressed */
/* would be nice to generalize this code a little so didn't need four loops */
int gravitate(int d)
{
int moved = 0;
if (d == DIR_LEFT) {
int i, j;
for (i = 0; i < SZ; i++) {
for (j = 0; j < SZ - 1; j++) {
if (g[i][j]) continue;
int st = 1;
while (j + st < SZ && !g[i][j + st]) st++;
if (j + st < SZ) {
g[i][j] = g[i][j + st];
g[i][j + st] = 0;
moved = 1;
}
}
}
}
else if (d == DIR_UP) {
int i, j;
for (i = 0; i < SZ; i++) {
for (j = 0; j < SZ - 1; j++) {
if (g[j][i]) continue;
int st = 1;
while (j + st < SZ && !g[j + st][i]) st++;
if (j + st < SZ) {
g[j][i] = g[j + st][i];
g[j + st][i] = 0;
moved = 1;
}
}
}
}
else if (d == DIR_RIGHT) {
int i, j;
for (i = 0; i < SZ; i++) {
for (j = SZ - 1; j > 0; j--) {
if (g[i][j]) continue;
int st = 1;
while (j - st >= 0 && !g[i][j - st]) st++;
if (j - st >= 0) {
g[i][j] = g[i][j - st];
g[i][j - st] = 0;
moved = 1;
}
}
}
}
else if (d == DIR_DOWN) {
int i, j;
for (i = 0; i < SZ; i++) {
for (j = SZ - 1; j > 0; j--) {
if (g[j][i]) continue;
int st = 1;
while (j - st >= 0 && !g[j - st][i]) st++;
if (j - st >= 0) {
g[j][i] = g[j - st][i];
g[j - st][i] = 0;
moved = 1;
}
}
}
}
return moved;
}
/* loads hiscore */
void load_score() {
FILE *fd = fopen(HISCORE_FILE, "r");
if (fd == NULL) fd = fopen(HISCORE_FILE, "w+");
if (fscanf(fd, "%d", &hs) == EOF) hs = 0;
fclose(fd);
}
/* saves hiscore, but only if playing on standard size grid */
void save_score() {
if (s > hs && SZ == 4) {
hs = s;
FILE *fd = fopen(HISCORE_FILE, "w+");
fprintf(fd, "%d", hs);
fclose(fd);
}
}
/* returns if there are any possible moves */
int moves_available()
{
int i, j;
for (i = 0; i < SZ; i++)
for (j = 0; j < SZ; j++)
if (!g[i][j] || ((i + 1 < SZ) && (g[i][j] == g[i + 1][j])) || ((j + 1 < SZ) && (g[i][j] == g[i][j + 1])))
return 1;
return 0;
}
/* places a random block onto the grid - either a 4, or a 2 with a ratio of 1:3 respectively */
/* do this in a smarter fashion */
void rand_block()
{
int x_p, y_p;
while (g[x_p = rand() % SZ][y_p = rand() % SZ]);
g[x_p][y_p] = (rand() & 3) ? 2 : 4;
}
/* draws the grid and fills it with the current values */
void draw_grid()
{
printf("HISCORE: %d |", hs);
printf("| SCORE: %d ", s);
if (sl) printf("(+%d)", sl);
printf("\n");
// alter this SZ + 1 to match abitrary grid size
ITER(SZ, printf("------"));
printf("-\n");
int i, j;
for (i = 0; i < SZ; i++) {
printf("|");
for (j = 0; j < SZ; j++) {
if (g[i][j])
printf("%*d |", 4, g[i][j]);
else
printf(" |");
}
printf("\n");
}
ITER(SZ, printf("------"));
printf("-\n\n");
}
/* store the terminal settings and call this function on exit to restore */
struct termios sattr;
void reset_term()
{
tcsetattr(STDIN_FILENO, TCSANOW, &sattr);
}
/* entry point for the program */
/* parses options and stores the main game loop */
int main(int argc, char **argv)
{
/* init variables */
hs = 0;
s = 0;
sl = 0;
SZ = 4;
CALLOC2D(g, SZ);
load_score();
int n_blocks = 1;
/* parse options */
int c;
while ((c = getopt(argc, argv, "rhs:b:")) != -1) {
switch (c) {
// different board sizes
case 's':
FREE2D(g, SZ);
int optint = atoi(optarg);
SZ = optint > 4 ? optint : 4;
CALLOC2D(g, SZ);
break;
// different block spawn rate
case 'b':
n_blocks = atoi(optarg);
break;
// reset hiscores
case 'r':
printf("Are you sure you want to reset your highscores? (Y)es or (N)o\n");
int response;
if ((response = getchar()) == 'y' || response == 'Y') {
FILE *fd = fopen(HISCORE_FILE, "w+");
fclose(fd);
}
exit(EXIT_SUCCESS);
case 'h':
printf(USAGE_STR);
exit(EXIT_SUCCESS);
}
}
/* random seed */
srand((unsigned int)time(NULL));
ITER(2, rand_block());
draw_grid();
/* store term settings so we can restore on exit */
tcgetattr(STDIN_FILENO, &sattr);
atexit(reset_term);
/* alters terminal stdin to not echo and doesn't need \n before reading getchar */
struct termios tattr;
tcgetattr(STDIN_FILENO, &tattr);
tattr.c_lflag &= ~(ICANON | ECHO);
tcsetattr(STDOUT_FILENO, TCSANOW, &tattr);
int key, moved;
while (1) {
/* will goto this if we didn't get a valid keypress */
retry:;
moved = 0;
key = getchar();
sl = 0;
/* should check if anything changed during merge and if not retry */
switch (key) {
case 'h':
case 'a':
moved = TURN(DIR_LEFT);
break;
case 'l':
case 'd':
moved = TURN(DIR_RIGHT);
break;
case 'j':
case 's':
moved = TURN(DIR_DOWN);
break;
case 'k':
case 'w':
moved = TURN(DIR_UP);
break;
case 'q':
save_score();
exit(EXIT_SUCCESS);
default:
goto retry;
}
if (!moves_available()) {
printf("\n"
"YOU LOSE! - Your score was %d\n", s);
save_score();
exit(EXIT_SUCCESS);
}
if (moved) {
ITER(n_blocks, rand_block());
draw_grid();
}
}
}

View File

@ -1,29 +0,0 @@
#include "2048.h"
#include <stdlib.h>
#include <sys/stat.h>
#include <string.h>
#include <stdio.h>
const char* get_highscore_file() {
static char buffer[4096];
if (getenv("XDG_DATA_HOME") != NULL) {
snprintf(buffer, sizeof(buffer), "%s/%s/%s", getenv("XDG_DATA_HOME"), DATADIR_NAME, HIGHSCORE_FILE_NAME);
} else if (getenv("HOME") != NULL) {
snprintf(buffer, sizeof(buffer), "%s/.local/share/%s/%s", getenv("HOME"), DATADIR_NAME, HIGHSCORE_FILE_NAME);
} else {
return HIGHSCORE_FILE_NAME;
}
// create hierarrchy of directories up to highscore file location
char* sep = strrchr(buffer, '/');
while (sep != NULL) {
*sep = '\0';
if (strlen(buffer) != 0)
mkdir(buffer, 0777);
char* tmpsep = sep;
sep = strrchr(buffer, '/');
*tmpsep = '/';
}
return buffer;
}