388 lines
11 KiB
C
388 lines
11 KiB
C
/*
|
|
* 2048.c - Main version that should be worked on.
|
|
*
|
|
* 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.
|
|
* */
|
|
|
|
#include <stdlib.h> /* for malloc */
|
|
#include <time.h> /* for time */
|
|
#include <unistd.h> /* for getopts */
|
|
#include <ncurses.h>
|
|
#include "2048.h"
|
|
|
|
int **grid; /* grid pointer */
|
|
int grid_size; /* grid size */
|
|
int score; /* score */
|
|
int score_last; /* Score for last move */
|
|
int score_high; /* Hiscore */
|
|
int printwidth; /* maximum length of any value in grid, for printing */
|
|
|
|
/* Merges adjacent squares of the same value together in a certain direction */
|
|
int merge(dir_t d)
|
|
{
|
|
int moved = 0;
|
|
int i, j;
|
|
|
|
if (d == DIR_LEFT) {
|
|
/* Move from upper left, across rows until second to second-last elem each row */
|
|
for (i = 0; i < grid_size; i++) {
|
|
for (j = 0; j < grid_size - 1; j++) {
|
|
if (grid[i][j] == grid[i][j + 1]) {
|
|
grid[i][j] <<= 1;
|
|
grid[i][j + 1] = 0;
|
|
score_last += grid[i][j];
|
|
score += grid[i][j];
|
|
moved = 1;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else if (d == DIR_UP) {
|
|
/* Move from upper left, across rows until final row which is skipped */
|
|
for (i = 0; i < grid_size - 1; i++) {
|
|
for (j = 0; j < grid_size; j++) {
|
|
if (grid[i][j] == grid[i + 1][j]) {
|
|
grid[i][j] <<= 1;
|
|
grid[i + 1][j] = 0;
|
|
score_last += grid[i][j];
|
|
score += grid[i][j];
|
|
moved = 1;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else if (d == DIR_RIGHT) {
|
|
/* Move from lower right, backwards across rows until first elem each row */
|
|
for (i = grid_size - 1; i >= 0; i--) {
|
|
for (j = grid_size - 1; j > 0; j--) {
|
|
if (grid[i][j] == grid[i][j - 1]) {
|
|
grid[i][j] <<= 1;
|
|
grid[i][j - 1] = 0;
|
|
score_last += grid[i][j];
|
|
score += grid[i][j];
|
|
moved = 1;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else if (d == DIR_DOWN) {
|
|
/* Move from lower right, across rows until first row which is skipped */
|
|
for (i = grid_size - 1; i > 0; i--) {
|
|
for (j = grid_size - 1; j >= 0; j--) {
|
|
if (grid[i][j] == grid[i - 1][j]) {
|
|
grid[i][j] <<= 1;
|
|
grid[i - 1][j] = 0;
|
|
score_last += grid[i][j];
|
|
score += grid[i][j];
|
|
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 */
|
|
/* if animations are wanted, then need to alter this so it moves each square one at a time */
|
|
int gravitate(dir_t d)
|
|
{
|
|
int moved = 0;
|
|
int i, j;
|
|
|
|
if (d == DIR_LEFT) {
|
|
for (i = 0; i < grid_size; i++) {
|
|
for (j = 0; j < grid_size - 1; j++) {
|
|
if (grid[i][j]) continue;
|
|
int st = 1;
|
|
while (j + st < grid_size && !grid[i][j + st]) st++;
|
|
if (j + st < grid_size) {
|
|
grid[i][j] = grid[i][j + st];
|
|
grid[i][j + st] = 0;
|
|
moved = 1;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else if (d == DIR_UP) {
|
|
for (i = 0; i < grid_size; i++) {
|
|
for (j = 0; j < grid_size - 1; j++) {
|
|
if (grid[j][i]) continue;
|
|
int st = 1;
|
|
while (j + st < grid_size && !grid[j + st][i]) st++;
|
|
if (j + st < grid_size) {
|
|
grid[j][i] = grid[j + st][i];
|
|
grid[j + st][i] = 0;
|
|
moved = 1;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else if (d == DIR_RIGHT) {
|
|
for (i = 0; i < grid_size; i++) {
|
|
for (j = grid_size - 1; j > 0; j--) {
|
|
if (grid[i][j]) continue;
|
|
int st = 1;
|
|
while (j - st >= 0 && !grid[i][j - st]) st++;
|
|
if (j - st >= 0) {
|
|
grid[i][j] = grid[i][j - st];
|
|
grid[i][j - st] = 0;
|
|
moved = 1;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else if (d == DIR_DOWN) {
|
|
for (i = 0; i < grid_size; i++) {
|
|
for (j = grid_size - 1; j > 0; j--) {
|
|
if (grid[j][i]) continue;
|
|
int st = 1;
|
|
while (j - st >= 0 && !grid[j - st][i]) st++;
|
|
if (j - st >= 0) {
|
|
grid[j][i] = grid[j - st][i];
|
|
grid[j - st][i] = 0;
|
|
moved = 1;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return moved;
|
|
}
|
|
|
|
/* Return the current highscore */
|
|
int get_score_high() {
|
|
int s = 0;
|
|
FILE *fd = fopen(HISCORE_FILE, "r");
|
|
if (fd == NULL)
|
|
fd = fopen(HISCORE_FILE, "w+");
|
|
|
|
if (fscanf(fd, "%d", &s) == 1) {};
|
|
fclose(fd);
|
|
return s;
|
|
}
|
|
|
|
/* saves hiscore, but only if playing on standard size grid */
|
|
void save_score_high() {
|
|
if (score > score_high && grid_size == 4) {
|
|
score_high = score;
|
|
FILE *fd = fopen(HISCORE_FILE, "w+");
|
|
fprintf(fd, "%d", score_high);
|
|
fclose(fd);
|
|
}
|
|
}
|
|
|
|
/* returns if there are any possible moves */
|
|
int moves_available()
|
|
{
|
|
int i, j;
|
|
for (i = 0; i < grid_size; i++)
|
|
for (j = 0; j < grid_size; j++)
|
|
if (!grid[i][j]
|
|
|| ((i + 1 < grid_size) && (grid[i][j] == grid[i + 1][j]))
|
|
|| ((j + 1 < grid_size) && (grid[i][j] == grid[i][j + 1])))
|
|
return 1;
|
|
return 0;
|
|
}
|
|
|
|
/* places a random block onto the grid - either a 4, or a 2 with a chance of 1:3 respectively */
|
|
/* could do this in a much smarter fashion by finding which spaces are free */
|
|
void rand_block()
|
|
{
|
|
int x, y;
|
|
while (grid[x = rand() % grid_size][y = rand() % grid_size]);
|
|
grid[x][y] = (rand() & 3) ? 2 : 4;
|
|
}
|
|
|
|
/* quick floor log2(n) */
|
|
int flog2(unsigned int n)
|
|
{
|
|
int k = 0;
|
|
while (n)
|
|
k++, n >>= 1;
|
|
return k;
|
|
}
|
|
|
|
/* 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)
|
|
{
|
|
// mvwprintw will sometimes have a useless arg, this is warned, but doesn't affect the program
|
|
char *scr = score_last ? "SCORE: %d (+%d)\n" : "SCORE: %d\n";
|
|
mvwprintw(gamewin, 0, 0, scr, score, score_last);
|
|
mvwprintw(gamewin, 1, 0, "HISCR: %d\n", score_high);
|
|
|
|
ITER(grid_size*(printwidth + 2) + 1, waddch(gamewin, '-'));
|
|
int i, j, xps = 0, yps = 3;
|
|
for (i = 0; i < grid_size; i++, xps = 0, yps++) {
|
|
mvwprintw(gamewin, yps, xps++, "|");
|
|
for (j = 0; j < grid_size; j++) {
|
|
if (grid[i][j]) {
|
|
wattron(gamewin, COLOR_PAIR(flog2(grid[i][j]) % 7));
|
|
mvwprintw(gamewin, yps, xps, "%*d", printwidth, grid[i][j]);
|
|
wattroff(gamewin, COLOR_PAIR(flog2(grid[i][j]) % 7));
|
|
mvwprintw(gamewin, yps, xps + printwidth, " |");
|
|
}
|
|
else {
|
|
ITER(printwidth + 1, waddch(gamewin, ' '));
|
|
waddch(gamewin, '|');
|
|
}
|
|
xps += (printwidth + 2);
|
|
}
|
|
}
|
|
ITER(grid_size*(printwidth + 2) + 1, waddch(gamewin, '-'));
|
|
wrefresh(gamewin);
|
|
}
|
|
|
|
/* 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;
|
|
score_last = 0;
|
|
score_high = get_score_high();
|
|
grid_size = DEFAULT_GRID_SIZE;
|
|
printwidth = DEFAULT_GRID_SIZE;
|
|
|
|
int n_blocks = 1;
|
|
|
|
/* parse options */
|
|
int c;
|
|
int enable_color = has_colors();
|
|
while ((c = getopt(argc, argv, "rcChs:b:")) != -1) {
|
|
switch (c) {
|
|
/* Color support */
|
|
case 'c':
|
|
enable_color = 1;
|
|
break;
|
|
case 'C':
|
|
enable_color = 0;
|
|
break;
|
|
// different board sizes
|
|
case 's':;
|
|
int optint = strtol(optarg, NULL, 10);
|
|
grid_size = optint > DEFAULT_GRID_SIZE ? optint : DEFAULT_GRID_SIZE;
|
|
break;
|
|
// different block spawn rate
|
|
case 'b':
|
|
n_blocks = strtol(optarg, NULL, 10);
|
|
break;
|
|
// reset hiscores
|
|
case 'r':
|
|
endwin();
|
|
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);
|
|
// help menu
|
|
case 'h':
|
|
endwin();
|
|
printf(USAGE_STR);
|
|
exit(EXIT_SUCCESS);
|
|
}
|
|
}
|
|
|
|
if (enable_color) {
|
|
if (!has_colors()) {
|
|
fprintf(stderr, "Terminal does not support color\n");
|
|
} else {
|
|
start_color();
|
|
init_pair(0, 1, 0);
|
|
init_pair(1, 2, 0);
|
|
init_pair(2, 3, 0);
|
|
init_pair(3, 4, 0);
|
|
init_pair(4, 5, 0);
|
|
init_pair(5, 6, 0);
|
|
init_pair(6, 7, 0);
|
|
}
|
|
}
|
|
|
|
/* 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);
|
|
|
|
/* random seed */
|
|
srand((unsigned int)time(NULL));
|
|
ITER(2, rand_block());
|
|
draw_grid(gamewin);
|
|
|
|
int key, moved;
|
|
while (1) {
|
|
/* will goto this if we didn't get a valid keypress */
|
|
retry:;
|
|
moved = 0;
|
|
score_last = 0;
|
|
key = wgetch(gamewin);
|
|
|
|
/* should check if anything changed during merge and if not retry */
|
|
switch (key) {
|
|
case 'h':
|
|
case 'a':
|
|
case KEY_LEFT:
|
|
moved = TURN(DIR_LEFT);
|
|
break;
|
|
case 'l':
|
|
case 'd':
|
|
case KEY_RIGHT:
|
|
moved = TURN(DIR_RIGHT);
|
|
break;
|
|
case 'j':
|
|
case 's':
|
|
case KEY_DOWN:
|
|
moved = TURN(DIR_DOWN);
|
|
break;
|
|
case 'k':
|
|
case 'w':
|
|
case KEY_UP:
|
|
moved = TURN(DIR_UP);
|
|
break;
|
|
case 'q':
|
|
FREE2D(grid, grid_size);
|
|
erase();
|
|
refresh();
|
|
endwin();
|
|
save_score_high();
|
|
exit(EXIT_SUCCESS);
|
|
default:
|
|
goto retry;
|
|
}
|
|
|
|
if (!moves_available()) {
|
|
endwin();
|
|
printf("\n"
|
|
"YOU LOSE! - Your score was %d\n", score);
|
|
save_score_high();
|
|
exit(EXIT_SUCCESS);
|
|
}
|
|
|
|
if (moved) {
|
|
ITER(n_blocks, rand_block());
|
|
draw_grid(gamewin);
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|