/*
* simple ncurses minesweeper (Daniel Jones daniel@danieljon.es)
*
* this program is free software: you can redistribute it and/or modify
* it under the terms of the gnu general public license as published by
* the free software foundation, either version 3 of the license, or
* (at your option) any later version.
*
* this program is distributed in the hope that it will be useful,
* but without any warranty; without even the implied warranty of
* merchantability or fitness for a particular purpose. see the
* gnu general public license for more details.
*
* you should have received a copy of the gnu general public license
* along with this program. if not, see .
*/
#include
#include
#include
#include
#include
#include
#define WIDTH 15
#define HEIGHT 15
#define MINECOUNT 31
#define TILEGAP 2
#define DOWN 0
#define UP 1
#define LEFT 2
#define RIGHT 3
enum DEMO_ACTION_TYPE
{
NONE = 0,
GOUP,
GODOWN,
GOLEFT,
GORIGHT,
FLAG,
REVEAL,
QUIT,
};
struct demo_header
{
int width;
int height;
int mine_count;
};
struct demo_mine
{
int x;
int y;
};
struct demo_action
{
double action_pre_delay;
enum DEMO_ACTION_TYPE type;
int start_x;
int start_y;
};
struct action_node
{
struct demo_action *action;
struct action_node *next;
} *action_head = NULL;
enum STATE
{
HIDDEN = 1 << 0,
MINE = 1 << 1,
FLAGGED = 1 << 2,
};
struct game
{
int width;
int height;
int minecount;
int action_count;
int is_demo;
int is_recording;
char demo_filename[512];
} game;
struct tile
{
enum STATE state;
int x, y;
int neighbormines;
} *board = NULL;
struct cursor
{
int x;
int y;
} cursor = {0};
WINDOW *window;
int exitgame = 0;
void draw();
int canmove(int dir);
int generateboard();
void drawboard();
int reveal(int x, int y);
void revealmines();
struct tile *getneighbors(struct tile *tile, struct tile **neighbors);
struct tile *gettileat(int x, int y);
int checkwin();
enum DEMO_ACTION_TYPE input();
void free_action_list();
struct action_node *generate_action_node(double delay, enum DEMO_ACTION_TYPE type, int x, int y);
int append_action_node(struct action_node *node);
void save_demo();
int load_demo();
struct action_node *play_demo_action(struct action_node *current_action);
struct tile *getneighbors(struct tile *tile, struct tile **neighbors)
{
int badup = 0, baddown = 0, badleft = 0, badright = 0;
if (tile->x-1<0) badleft = 1;
if (tile->x+1>game.width-1) badright = 1;
if (tile->y-1<0) badup = 1;
if (tile->y+1>game.height-1) baddown = 1;
if (!badleft && !badup) neighbors[0] = gettileat(tile->x-1, tile->y-1);
if (!badup) neighbors[1] = gettileat(tile->x, tile->y-1);
if (!badright && !badup) neighbors[2] = gettileat(tile->x+1, tile->y-1);
if (!badleft) neighbors[3] = gettileat(tile->x-1, tile->y);
if (!badright) neighbors[4] = gettileat(tile->x+1, tile->y);
if (!badleft && !baddown) neighbors[5] = gettileat(tile->x-1, tile->y+1);
if (!baddown) neighbors[6] = gettileat(tile->x, tile->y+1);
if (!badright && !baddown) neighbors[7] = gettileat(tile->x+1, tile->y+1);
return *neighbors;
}
int
checkwin()
{
int allowedmines = game.minecount;
int safetiles = (game.height * game.width) - game.minecount;
int correctflags = 0;
int correcttiles = 0;
for (int y = 0; y < game.height; y++)
{
for (int x = 0; x < game.width; x++)
{
struct tile *tile = gettileat(x, y);
if (tile->state & MINE && tile->state & FLAGGED)
correctflags++;
else if (!(tile->state & MINE) && !(tile->state & HIDDEN))
correcttiles++;
}
}
return (correctflags == allowedmines) || (correcttiles == safetiles);
}
int
generateboard()
{
srand(time(NULL));
if (!game.is_demo)
{
board = malloc(sizeof (struct tile) * (game.width * game.height));
/* place mines */
int mx, my;
for (int x = 0; x < game.minecount; x++)
{
place_mine:
mx = rand() % game.width;
my = rand() % game.height;
/*ensure our tile is not already a mine */
if (gettileat(mx, my)->state & MINE)
goto place_mine;
struct tile *tile = gettileat(mx, my);
tile->state |= MINE;
}
}
else
{
if(!load_demo())
{
puts("cannot generate board");
return 0;
}
}
/* create window here because if we're playing a demo we need the width/height */
window = newwin(game.height+TILEGAP, (game.width*TILEGAP)+1, 1, 8);
/* figure out neighbors */
for (int y = 0; y < game.height; y++)
{
for (int x = 0; x < game.width; x++)
{
struct tile *tile = gettileat(x, y);
tile->x = x;
tile->y = y;
tile->state |= HIDDEN;
struct tile *neighbors[8] = {NULL};
getneighbors(tile, neighbors);
tile->neighbormines = 0;
for (int i = 0; i < 8; i++)
if (neighbors[i] != NULL && neighbors[i]->state & MINE)
tile->neighbormines += 1;
}
}
return 1;
}
struct tile *
gettileat(int x, int y)
{
if (x < 0 || x > game.width-1 || y < 0 || y > game.height-1 || board == NULL)
return NULL;
/* the board is single dimensional, so we map it as 2d */
return &board[game.width*x+y];
}
int
reveal(int x, int y)
{
struct tile *tile = gettileat(x, y);
tile->state &= ~HIDDEN;
if (tile->state & MINE)
return 1;
if (tile->neighbormines == 0)
{
tile->state &= ~HIDDEN;
struct tile *neighbors[8] = {NULL};
getneighbors(tile, neighbors);
for (int nc = 0; nc < 8; nc++)
{
struct tile *neighbor = neighbors[nc];
if (neighbor != NULL && !(neighbor->state & MINE) && neighbor->state & HIDDEN)
{
neighbor->state &= ~HIDDEN;
if (neighbor->neighbormines == 0)
{
reveal(neighbor->x, neighbor->y);
}
}
}
}
return 0;
}
void
revealmines()
{ for (int y = 0; y < game.height; y++)
{
for (int x = 0; x < game.width; x++)
{
if (gettileat(x, y)->state & MINE)
reveal(x, y);
}
}
}
int
canmove(int dir)
{
/* check if cursor inside game region */
if (dir == LEFT && cursor.x <= 0) return 0;
else if (dir == RIGHT && cursor.x >= game.width-1) return 0;
else if (dir == UP && cursor.y <= 0) return 0;
else if (dir == DOWN && cursor.y >= game.height-1) return 0;
return 1;
}
void
draw()
{
wclear(window);
box(window, 0, 0);
if (!exitgame)
{
mvprintw(game.height+3, 0, "The aim of the game is to reveal all non-mine tiles or flag every mine tile");
mvprintw(game.height+5, 0, "hjkl/wasd to move cursor\nspace to reveal tile\nf to flag tile");
}
else
{
clear();
}
for (int x = 0; x < game.width; x++)
{
for (int y = 0; y < game.height; y++)
{
struct tile *tile = gettileat(x, y);
char neighbormines = (char)tile->neighbormines+'0';
if (neighbormines == '0')
neighbormines = ' ';
if (tile->state & FLAGGED)
mvwprintw(window, y+1, (x*TILEGAP)+1, "F");
else if (tile->state & HIDDEN)
mvwprintw(window, y+1, (x*TILEGAP)+1, ".");
else
mvwprintw(window, y+1, (x*TILEGAP)+1, "%c", (tile->state & MINE) ? 'M' : neighbormines);
}
}
wmove(window, cursor.y+1, (cursor.x*TILEGAP)+1);
refresh();
wrefresh(window);
}
enum DEMO_ACTION_TYPE
input()
{
int ch = getch(); /* blocking */
enum DEMO_ACTION_TYPE type = NONE;
struct tile *tile = gettileat(cursor.x, cursor.y);
switch(ch)
{
case 'k':
case 'w':
if (canmove(UP))
{
type = GOUP;
cursor.y--;
}
break;
case 'j':
case 's':
if (canmove(DOWN))
{
type = GODOWN;
cursor.y++;
}
break;
case 'h':
case 'a':
if (canmove(LEFT))
{
type= GOLEFT;
cursor.x--;
}
break;
case 'l':
case 'd':
if (canmove(RIGHT))
{
type = GORIGHT;
cursor.x++;
}
break;
case 'f':
{
if (tile && tile->state & HIDDEN)
{
type = FLAG;
tile->state ^= FLAGGED;
draw();
}
break;
}
case ' ':
if (tile && !(tile->state & FLAGGED))
{
type = REVEAL;
exitgame = reveal(cursor.x, cursor.y);
}
break;
case 'q':
type = QUIT;
exitgame = 1;
break;
default:
break;
}
return type;
}
void
free_action_list()
{
struct action_node *node;
while (action_head)
{
node = action_head;
action_head = action_head->next;
free(node->action);
free(node);
}
}
struct action_node *generate_action_node(double delay, enum DEMO_ACTION_TYPE type, int x, int y)
{
struct action_node *node = malloc(sizeof(struct action_node));
struct demo_action *action = malloc(sizeof(struct demo_action));
if (!node || !action)
return NULL;
action->action_pre_delay = delay;
action->start_x = x;
action->start_y = y;
action->type = type;
node->action = action;
node->next = NULL;
return node;
}
int append_action_node(struct action_node *node)
{
struct action_node **finger = &action_head;
while (*finger)
{
finger = &(*finger)->next;
}
node->next = *finger;
*finger = node;
return 1;
}
void print_nodes()
{
struct action_node *finger = action_head;
while (finger)
{
if (finger->action)
{
printf("%d,%d\n", finger->action->start_x, finger->action->start_y);
}
finger = finger->next;
}
}
void
save_demo()
{
printf("saving demo to %s..\n", game.demo_filename);
/* open demo file */
FILE *demo = fopen(game.demo_filename, "wb");
if (!demo)
{
puts("cannot open demo file");
return;
}
/* generate header and write */
struct demo_header header;
header.width = game.width;
header.height = game.height;
header.mine_count = game.minecount;
fwrite(&header, sizeof(struct demo_header), 1, demo);
/* generate mines and write */
struct demo_mine demo_mines[game.minecount];
/* loop over every tile and save it if it's a mine */
int i = 0;
for (int x = 0; x < game.width; x++)
{
for (int y = 0; y < game.height; y++)
{
struct tile *tile = gettileat(x, y);
if (tile->state & MINE)
{
demo_mines[i].x = tile->x;
demo_mines[i].y = tile->y;
fwrite(&demo_mines[i], sizeof(struct demo_mine), 1, demo);
i++;
}
}
}
/* walk action list and write it to file */
fwrite(&game.action_count, sizeof game.action_count, 1, demo);
/* skip first node */
if (!action_head->next)
{
puts("cannot write demo file, no actions exist..");
fclose(demo);
return;
}
struct action_node *finger = action_head->next;
int x = 0;
while (finger)
{
struct demo_action *action = finger->action;
fwrite(action, sizeof(struct demo_action), 1, demo);
finger = finger->next;
x++;
}
printf("saved 0x%x nodes\n", x);
fclose(demo);
}
int
load_demo()
{
printf("reading demo %s..\n", game.demo_filename);
FILE *demo = fopen(game.demo_filename, "rb");
if (!demo)
{
puts("unable to read demo..");
return 0;
}
/* read header and set data */
struct demo_header header;
fread(&header, sizeof(struct demo_header), 1, demo);
game.width = header.width;
game.height = header.height;
game.minecount = header.mine_count;
/* malloc board here because we need the header information from the demo */
board = malloc(sizeof (struct tile) * (game.width * game.height));
/* read and set mine data */
struct demo_mine demo_mine;
for (int mc = 0; mc < game.minecount; mc++)
{
fread(&demo_mine, sizeof(struct demo_mine), 1, demo);
/* set tile as mine */
struct tile *tile = gettileat(demo_mine.x, demo_mine.y);
if (!tile)
{
puts("demo corrupt");
fclose(demo);
return 0;
}
tile->state |= MINE;
}
/* read move data and add action to the list */
int action_count;
fread(&action_count, sizeof action_count, 1, demo);
for (int ac = 0; ac < action_count; ac++)
{
struct demo_action action;
fread(&action, sizeof(struct demo_action), 1, demo);
struct action_node *move = generate_action_node(action.action_pre_delay, action.type, action.start_x, action.start_y);
append_action_node(move);
}
fclose(demo);
return 1;
}
struct action_node *play_demo_action(struct action_node *current_action)
{
struct demo_action *action = current_action->action;
usleep(action->action_pre_delay);
struct tile *tile = gettileat(action->start_x, action->start_y);
if (!tile)
return NULL;
switch (action->type)
{
case GOUP:
if (canmove(UP))
{
cursor.y--;
}
break;
case GODOWN:
if (canmove(DOWN))
{
cursor.y++;
}
break;
case GOLEFT:
if (canmove(LEFT))
{
cursor.x--;
}
break;
case GORIGHT:
if (canmove(RIGHT))
{
cursor.x++;
}
break;
case FLAG:
{
if (tile && tile->state & HIDDEN)
{
tile->state ^= FLAGGED;
draw();
}
break;
}
case REVEAL:
if (tile && !(tile->state & FLAGGED))
{
exitgame = reveal(action->start_x, action->start_y);
}
break;
case QUIT:
exitgame = 1;
break;
case NONE:
default:
break;
}
return current_action->next;
}
int
main(int argc, char **argv)
{
game.is_demo = 0;
game.is_recording = 0;
if ((argc > 1 && argc != 3))
{
printf("usage: %s [-record save.dem | -play load.dem]\n", argv[0]);
goto safe_exit;
}
if (argc == 3)
{
if (strcmp(argv[1], "-record") == 0)
{
game.is_recording = 1;
strncpy(game.demo_filename, argv[2], 511);
}
else if (strcmp(argv[1], "-play") == 0)
{
game.is_demo = 1;
strncpy(game.demo_filename, argv[2], 511);
}
else
{
printf("usage: %s [-record save.dem | -play load.dem]\n", argv[0]);
goto safe_exit;
}
}
initscr();
noecho();
game.width = WIDTH;
game.height = HEIGHT;
game.minecount = MINECOUNT;
game.action_count = 0;
action_head = malloc(sizeof(struct action_node));
action_head->action = NULL;
action_head->next = NULL;
if (!action_head)
goto safe_exit;
if (!generateboard())
goto safe_exit;
struct timespec begin, end;
double move_us;
struct action_node *current_action = action_head->next;
while(!exitgame)
{
draw();
if (game.is_demo)
{
if (current_action)
{
current_action = play_demo_action(current_action);
}
else
exitgame = 1;
}
else
{
clock_gettime(CLOCK_MONOTONIC, &begin);
enum DEMO_ACTION_TYPE type = input();
clock_gettime(CLOCK_MONOTONIC, &end);
move_us = (end.tv_sec - begin.tv_sec) * 1000.0;
move_us += (end.tv_nsec - begin.tv_nsec) / 1000000.0;
move_us *= 1000;
struct action_node *move = generate_action_node(move_us, type, cursor.x, cursor.y);
append_action_node(move);
game.action_count++;
//printf("%.3f us elapsed\n", move_us);
}
if (checkwin())
{
exitgame = 1;
revealmines();
draw();
mvprintw(game.height+3, 0, "you won");
break;
}
else if (exitgame)
{
revealmines();
draw();
mvprintw(game.height+3, 0, "you lost");
break;
}
}
mvprintw(game.height+4, 0, "press any key to exit..");
flushinp();
getch();
safe_exit:
delwin(window);
endwin();
if (game.is_recording)
save_demo();
free(board);
free_action_list();
return 0;
}