Created
June 30, 2018 18:01
-
-
Save chebert/b1dfc09e5bbac0030f96bd689a8ba724 to your computer and use it in GitHub Desktop.
Freecell in C using NCURSES
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
/* | |
map ,g :!gcc % -o %:r -lncurses && ./%:r <CR> | |
gcc freecell.c -o freecell -lncurses | |
./freecell | |
*/ | |
#include <assert.h> | |
#include <stdint.h> | |
#include <ncurses.h> | |
#include <sys/time.h> | |
#include <time.h> | |
#include <stdlib.h> | |
#include <unistd.h> | |
void* memcpy(void* d, void* s, size_t n); | |
void* memset(void* s, int c, size_t n); | |
char* strchr(const char* s, int c); | |
#define internal static | |
#define global_variable static | |
#define local_persist static | |
#define UNUSED(x) (void)(x); | |
#define MIN(x,y) ((x) < (y) ? (x) : (y)) | |
#define MAX(x,y) ((x) < (y) ? (y) : (x)) | |
#define ARRAY_LENGTH(arr)\ | |
(sizeof(arr) / sizeof(arr[0])) | |
#define for_array(index, arr)\ | |
for (index = 0; index < ARRAY_LENGTH(arr); ++index) | |
#define MS(x) (1000*(x)) | |
#define SECONDS(x) (MS(1000*(x))) | |
typedef int8_t int8; | |
typedef uint8_t uint8; | |
typedef int16_t int16; | |
typedef uint16_t uint16; | |
typedef int32_t int32; | |
typedef uint32_t uint32; | |
typedef int64_t int64; | |
typedef uint64_t uint64; | |
typedef float real32; | |
typedef double real64; | |
typedef int32_t bool32; | |
typedef char* c_str; | |
struct v2 { | |
int32 x, y; | |
}; | |
inline internal struct v2 | |
add(struct v2 a, struct v2 b) { | |
a.x += b.x; | |
a.y += b.y; | |
return a; | |
} | |
inline internal struct v2 | |
add2(struct v2 a, int x, int y) { | |
a.x += x; | |
a.y += y; | |
return a; | |
} | |
inline internal struct v2 | |
v2_both(int x) { | |
struct v2 a; | |
a.x = x; | |
a.y = x; | |
return a; | |
} | |
inline internal struct v2 | |
sub(struct v2 a, struct v2 b) { | |
a.x -= b.x; | |
a.y -= b.y; | |
return a; | |
} | |
inline internal bool32 | |
eql(struct v2* a, struct v2* b) { | |
return a->x == b->x && a->y == b->y; | |
} | |
inline internal struct v2 | |
scale(struct v2 a, int32 amt) { | |
a.x *= amt; | |
a.y *= amt; | |
return a; | |
} | |
inline internal int32 | |
floor_div(int32 num, int32 den) { | |
double fresult = (double)num / (double)den; | |
int32 result = (int32)fresult; | |
if (fresult < (double)result) { | |
--result; | |
} | |
return result; | |
} | |
inline internal struct v2 | |
div_scale(struct v2 a, int32 amt) { | |
a.x = floor_div(a.x, amt); | |
a.y = floor_div(a.y, amt); | |
return a; | |
} | |
uint64 | |
time_in_us() { | |
struct timeval t; | |
gettimeofday(&t, NULL); | |
return t.tv_sec * 1000000 + t.tv_usec; | |
} | |
#define SQUARE(x) ((x)*(x)) | |
#define DIST_SQUARE(x, y) (SQUARE(x) + SQUARE(y)) | |
enum { | |
BG_PAIR=1, | |
SLOT_PAIR, | |
RED_PAIR, | |
BLUE_PAIR, | |
DARK_RED_PAIR, | |
DARK_BLUE_PAIR, | |
NEXT_RED_PAIR, | |
NEXT_BLUE_PAIR, | |
NEXT_DARK_RED_PAIR, | |
NEXT_DARK_BLUE_PAIR, | |
TABLEAU_RED_PAIR, | |
TABLEAU_BLUE_PAIR, | |
NEXT_TABLEAU_RED_PAIR, | |
NEXT_TABLEAU_BLUE_PAIR, | |
RED_SLOT_PAIR, | |
BLUE_SLOT_PAIR, | |
SEL_SIZE_TEXT_PAIR, | |
CURSOR_PAIR, | |
COLOR_PICKER_PAIR, | |
NUM_COLOR_PAIRS, | |
}; | |
enum { | |
COLOR_BG, | |
COLOR_SLOT_FG, | |
COLOR_RED_BG, | |
COLOR_BLUE_BG, | |
COLOR_RED_HILIGHT_BG, | |
COLOR_BLUE_HILIGHT_BG, | |
COLOR_DARK_RED_FG, | |
COLOR_DARK_BLUE_FG, | |
COLOR_RED_FG, | |
COLOR_BLUE_FG, | |
COLOR_RED_TABLEAU_BG, | |
COLOR_BLUE_TABLEAU_BG, | |
COLOR_CURSOR, | |
COLOR_SEL_TEXT, | |
NUM_COLORS | |
}; | |
global_variable | |
int term_colors[NUM_COLORS] = { | |
#if DEBUG | |
#include "term_colors" | |
#else | |
251,246,255,245,225,104,124,22,160,19,241,236,232,241, | |
#endif | |
}; | |
global_variable | |
struct { | |
int color; | |
bool32 active; | |
int width; | |
} color_picker = { | |
.width = 20 | |
}; | |
internal void | |
initialize_color_pairs() { | |
init_pair(BG_PAIR, term_colors[COLOR_RED_FG], term_colors[COLOR_BG]); | |
init_pair(SLOT_PAIR, term_colors[COLOR_SLOT_FG], term_colors[COLOR_BG]); | |
init_pair(DARK_RED_PAIR, term_colors[COLOR_DARK_RED_FG], term_colors[COLOR_RED_BG]); | |
init_pair(DARK_BLUE_PAIR, term_colors[COLOR_DARK_BLUE_FG], term_colors[COLOR_BLUE_BG]); | |
init_pair(RED_PAIR, term_colors[COLOR_RED_FG], term_colors[COLOR_RED_BG]); | |
init_pair(BLUE_PAIR, term_colors[COLOR_BLUE_FG], term_colors[COLOR_BLUE_BG]); | |
init_pair(NEXT_RED_PAIR, term_colors[COLOR_RED_FG], term_colors[COLOR_RED_HILIGHT_BG]); | |
init_pair(NEXT_BLUE_PAIR, term_colors[COLOR_BLUE_FG], term_colors[COLOR_BLUE_HILIGHT_BG]); | |
init_pair(NEXT_DARK_RED_PAIR, term_colors[COLOR_DARK_RED_FG], term_colors[COLOR_RED_HILIGHT_BG]); | |
init_pair(NEXT_DARK_BLUE_PAIR, term_colors[COLOR_DARK_BLUE_FG], term_colors[COLOR_BLUE_HILIGHT_BG]); | |
init_pair(TABLEAU_RED_PAIR, term_colors[COLOR_RED_FG], term_colors[COLOR_RED_TABLEAU_BG]); | |
init_pair(TABLEAU_BLUE_PAIR, term_colors[COLOR_BLUE_FG], term_colors[COLOR_BLUE_TABLEAU_BG]); | |
init_pair(NEXT_TABLEAU_RED_PAIR, term_colors[COLOR_RED_FG], term_colors[COLOR_RED_TABLEAU_BG]); | |
init_pair(NEXT_TABLEAU_BLUE_PAIR, term_colors[COLOR_BLUE_FG], term_colors[COLOR_BLUE_TABLEAU_BG]); | |
init_pair(RED_SLOT_PAIR, term_colors[COLOR_RED_FG], term_colors[COLOR_BG]); | |
init_pair(BLUE_SLOT_PAIR, term_colors[COLOR_BLUE_FG], term_colors[COLOR_BG]); | |
init_pair(SEL_SIZE_TEXT_PAIR, term_colors[COLOR_SEL_TEXT], term_colors[COLOR_BG]); | |
init_pair(CURSOR_PAIR, term_colors[COLOR_CURSOR], term_colors[COLOR_BG]); | |
} | |
global_variable | |
char suits[] = { '@', '&', '#', '%'}; | |
global_variable | |
int max_y, max_x; | |
internal void initialize_curses() { | |
/* Initialize curses */ | |
initscr(); | |
cbreak(); | |
keypad(stdscr, TRUE); | |
noecho(); | |
curs_set(FALSE); | |
timeout(0); | |
getmaxyx(stdscr, max_y, max_x); | |
bool32 has_color_terminal = has_colors() && can_change_color(); | |
start_color(); | |
if (!has_color_terminal) { | |
mvprintw(0, 0, "Warning: Colors not enabled for this terminal.\nAuthors suggestion: try running in the unicode-rxvt terminal.\nPress any key to continue."); | |
mvprintw(0, 0, "%u colors available in this terminal. This game currently supports 256.", COLORS); | |
timeout(-1); | |
getch(); | |
timeout(0); | |
} else { | |
if (COLORS == 256) { | |
} else { | |
mvprintw(0, 0, "%u colors available in this terminal. This game currently supports 256.", COLORS); | |
timeout(-1); | |
getch(); | |
timeout(0); | |
} | |
initialize_color_pairs(); | |
bkgd(COLOR_PAIR(BG_PAIR)); | |
} | |
} | |
#define CARD_WIDTH 7 | |
#define CARD_HEIGHT 5 | |
const c_str card_template = | |
" A @ " | |
" _ _ _ " | |
" @ " | |
" " | |
" @ A "; | |
#define CARD_SLOT_WIDTH (CARD_WIDTH + 2) | |
#define CARD_SLOT_HEIGHT (CARD_HEIGHT + 2) | |
internal void | |
draw_card_slot(struct v2 pos, c_str text, int text_color) { | |
attron(COLOR_PAIR(SLOT_PAIR)); | |
int x, y; | |
for (y = 1; y < CARD_SLOT_HEIGHT-1; ++y) { | |
if (y % 2 == 0) { | |
mvaddch(pos.y + y, pos.x, '|'); | |
mvaddch(pos.y + y, pos.x + CARD_SLOT_WIDTH-1, '|'); | |
} | |
} | |
for (x = 1; x < CARD_SLOT_WIDTH-1; ++x) { | |
if (x % 2 == 0) { | |
mvaddch(pos.y, pos.x + x, '-'); | |
mvaddch(pos.y + CARD_SLOT_HEIGHT-1, pos.x + x, '-'); | |
} | |
} | |
mvaddch(pos.y, pos.x, '/'); | |
mvaddch(pos.y, pos.x + CARD_SLOT_WIDTH-1, '\\'); | |
mvaddch(pos.y + CARD_SLOT_HEIGHT-1, pos.x, '\\'); | |
mvaddch(pos.y + CARD_SLOT_HEIGHT-1, pos.x + CARD_SLOT_WIDTH-1, '/'); | |
attron(COLOR_PAIR(text_color)); | |
mvprintw(pos.y + CARD_SLOT_HEIGHT/2, pos.x + CARD_SLOT_WIDTH/2, "%s", text); | |
} | |
struct card { | |
enum suit { | |
HEART, | |
SPADE, | |
DIAMOND, | |
CLUB, | |
} suit; | |
int number; | |
}; | |
typedef int card_i_t; | |
#define NUM_CASCADES 8 | |
#define MAX_CASCADE_SIZE (7 + 13) | |
#define NUM_DECK_CARDS 52 | |
#define NUM_FREE_CELLS 4 | |
#define NUM_FOUNDATIONS 4 | |
#define EMPTY_CELL -1 | |
#define MAX_HISTORY_SIZE 4000 | |
#define NUM_BRANCHES 100 | |
global_variable | |
struct card cards[NUM_DECK_CARDS]; | |
global_variable | |
struct game { | |
struct state { | |
struct card_stack { | |
card_i_t cards[MAX_CASCADE_SIZE]; | |
int num_cards; | |
} cascades[NUM_CASCADES]; | |
card_i_t free_cells[NUM_FREE_CELLS]; | |
card_i_t foundations[NUM_FOUNDATIONS]; | |
bool32 is_top_row; | |
int origin_cursor_i; | |
} history[MAX_HISTORY_SIZE]; | |
int current_state; | |
struct card_stack sel_stack; | |
struct { | |
int src_i, dest_i; | |
} valid_cascade_moves[1000]; | |
int num_valid_cascade_moves; | |
enum stack_type { | |
FREE_CELL, | |
CASCADE, | |
FOUNDATION, | |
} origin_stack_type; | |
int num_valid; | |
bool32 is_valid; | |
int cursor_i, last_bottom_cursor_i; | |
int move_counter; | |
int undo_counter; | |
int restart_counter; | |
} new_game = { | |
.history = { | |
{ | |
.free_cells = { EMPTY_CELL, EMPTY_CELL, EMPTY_CELL, EMPTY_CELL }, | |
.foundations = { EMPTY_CELL, EMPTY_CELL, EMPTY_CELL, EMPTY_CELL }, | |
} | |
} | |
}; | |
internal card_i_t | |
top_card_stack(struct card_stack* cs) { | |
return cs->cards[cs->num_cards - 1]; | |
} | |
internal card_i_t | |
pop_card_stack(struct card_stack* cs) { | |
card_i_t top = top_card_stack(cs); | |
--cs->num_cards; | |
return top; | |
} | |
internal void | |
push_card_stack(struct card_stack* cs, card_i_t card_i) { | |
cs->cards[cs->num_cards++] = card_i; | |
} | |
internal bool32 | |
is_black_suit(enum suit suit) { | |
return suit == SPADE || suit == CLUB; | |
} | |
internal bool32 | |
eql_card(struct card a, struct card b) { | |
return (a.suit == b.suit && a.number == b.number); | |
} | |
internal struct card* | |
get_card(card_i_t card_i) { | |
if (card_i == EMPTY_CELL) { | |
return NULL; | |
} else { | |
return &cards[card_i]; | |
} | |
} | |
internal bool32 | |
can_place_on_foundation(struct card card, card_i_t* foundations) { | |
struct card next_card = { | |
.suit = card.suit, | |
}; | |
struct card* foundation_card = get_card(foundations[card.suit]); | |
if (foundation_card) { | |
next_card.number = foundation_card->number + 1; | |
} else { | |
next_card.number = 0; | |
} | |
return eql_card(next_card, card); | |
} | |
internal void | |
draw_card(struct v2 pos, struct card* card, card_i_t* foundations, bool32 is_part_of_tableau, bool32 is_on_top) { | |
if (!card) return; | |
bool32 blue_colors = is_black_suit(card->suit); | |
int suit_color_pair, dark_color_pair; | |
if (can_place_on_foundation(*card, foundations)) { | |
suit_color_pair = blue_colors ? NEXT_BLUE_PAIR : NEXT_RED_PAIR; | |
} else { | |
suit_color_pair = blue_colors ? BLUE_PAIR : RED_PAIR; | |
} | |
int x, y; | |
int card_number = card->number; | |
c_str card_drawing = card_template; | |
for (x = 0; x < CARD_WIDTH; ++x) { | |
for (y = 0; y < CARD_HEIGHT; ++y) { | |
char ch = card_drawing[x + y*CARD_WIDTH]; | |
attron(COLOR_PAIR(suit_color_pair)); | |
if (!is_on_top && ch == '_') { | |
if (is_part_of_tableau) { | |
mvaddch(y + pos.y, x + pos.x, '^'); | |
} else { | |
mvaddch(y + pos.y, x + pos.x, ch); | |
} | |
} else { | |
mvaddch(y + pos.y, x + pos.x, ' '); | |
} | |
} | |
} | |
for (x = 0; x < CARD_WIDTH; ++x) { | |
for (y = 0; y < CARD_HEIGHT; ++y) { | |
char ch = card_drawing[x + y*CARD_WIDTH]; | |
if (ch == '@') { | |
attron(COLOR_PAIR(suit_color_pair)); | |
mvaddch(y + pos.y, x + pos.x, suits[card->suit]); | |
} else if (ch == 'A') { | |
switch (card->number) { | |
case 0: | |
mvaddch(y + pos.y, x + pos.x, 'A'); | |
break; | |
case 10: | |
mvaddch(y + pos.y, x + pos.x, 'J'); | |
break; | |
case 11: | |
mvaddch(y + pos.y, x + pos.x, 'Q'); | |
break; | |
case 12: | |
mvaddch(y + pos.y, x + pos.x, 'K'); | |
break; | |
default: | |
mvprintw(y + pos.y, x + pos.x, "%d", card->number + 1); | |
break; | |
} | |
continue; | |
} | |
} | |
} | |
} | |
internal struct state* | |
game_state(struct game* game) { | |
return &game->history[game->current_state]; | |
} | |
internal void | |
save_game_state(struct game* game) { | |
memcpy(&(game->history[game->current_state + 1]), game_state(game), sizeof(struct state)); | |
++game->current_state; | |
} | |
internal void | |
get_num_tableau_moves_remaining(struct game* game); | |
internal void | |
shuffle_and_deal(struct game* game, uint32 seed) { | |
*game = new_game; | |
card_i_t deck[NUM_DECK_CARDS]; | |
{ | |
card_i_t card_i; | |
for (card_i = 0; card_i < NUM_DECK_CARDS; ++card_i) { | |
deck[card_i] = card_i; | |
} | |
} | |
/* NOTE: repeat if necessary */ | |
int repeat = 1; | |
while (repeat--) { | |
card_i_t card_i; | |
for (card_i = NUM_DECK_CARDS - 1; card_i >= 1; --card_i) { | |
card_i_t card_j = rand_r(&seed) % (card_i + 1); | |
card_i_t tmp_card_i = deck[card_j]; | |
deck[card_j] = deck[card_i]; | |
deck[card_i] = tmp_card_i; | |
} | |
} | |
/* NOTE: Deal cards into state.cascades, 7 7 7 7 6 6 6 6 */ | |
{ | |
int i; | |
for (i = 0; i < NUM_FREE_CELLS; ++i) { | |
game_state(game)->free_cells[i] = game_state(game)->foundations[i] = EMPTY_CELL; | |
} | |
int deck_i, cascade_i; | |
for (deck_i = 0; deck_i < NUM_DECK_CARDS; ) { | |
for (cascade_i = 0; cascade_i < NUM_CASCADES; ++cascade_i) { | |
struct card_stack* casc = &game_state(game)->cascades[cascade_i]; | |
push_card_stack(casc, deck[deck_i++]); | |
if (deck_i == NUM_DECK_CARDS) { | |
break; | |
} | |
} | |
} | |
} | |
save_game_state(game); | |
get_num_tableau_moves_remaining(game); | |
} | |
internal bool32 | |
is_valid_stacking(struct card* top_card, struct card* bottom_card) { | |
bool32 different_suits = is_black_suit(top_card->suit) != is_black_suit(bottom_card->suit); | |
return different_suits && (bottom_card->number == top_card->number - 1); | |
} | |
internal int | |
get_max_selection_size(struct game* game) { | |
int max_selection_size = 1; | |
int i; | |
for (i = 0 ; i < NUM_FREE_CELLS; ++i) { | |
if (game_state(game)->free_cells[i] == EMPTY_CELL) { | |
++max_selection_size; | |
} | |
} | |
for (i = 0 ; i < NUM_CASCADES; ++i) { | |
if (game_state(game)->cascades[i].num_cards == 0) { | |
max_selection_size *= 2; | |
} | |
} | |
return max_selection_size; | |
} | |
internal bool32 | |
update_selection_validity(struct game* game) { | |
bool32 is_valid_move = FALSE; | |
int max_selection_size = get_max_selection_size(game); | |
if (game_state(game)->is_top_row) { | |
/* Attempt to place card */ | |
if (game->cursor_i < NUM_FREE_CELLS) { | |
is_valid_move = game_state(game)->free_cells[game->cursor_i] == EMPTY_CELL; | |
} else { | |
card_i_t card_i = top_card_stack(&game->sel_stack); | |
/* NOTE: Check if valid move */ | |
int foundation_i = game->cursor_i - NUM_FREE_CELLS; | |
card_i_t foundation_card_i = game_state(game)->foundations[foundation_i]; | |
is_valid_move = | |
cards[card_i].suit == foundation_i && | |
can_place_on_foundation(cards[card_i], game_state(game)->foundations); | |
} | |
game->num_valid = 1; | |
} else { | |
if (game_state(game)->cascades[game->cursor_i].num_cards) { | |
int i; | |
for (i = 0; i < game->sel_stack.num_cards; ++i) { | |
struct card* scard = get_card(game->sel_stack.cards[i]); | |
struct card* top_card = get_card(top_card_stack(&game_state(game)->cascades[game->cursor_i])); | |
is_valid_move = is_valid_stacking(top_card, scard); | |
if (is_valid_move) { | |
game->num_valid = game->sel_stack.num_cards - i; | |
break; | |
} | |
} | |
} else { | |
/* NOTE: Empty slots are always valid. */ | |
/* Selection Size decreases by half when moving to an empty table pos. */ | |
is_valid_move = TRUE; | |
game->num_valid = MIN(max_selection_size / 2, game->sel_stack.num_cards); | |
} | |
{ | |
bool32 is_cancelling_move = game->origin_stack_type == CASCADE && game_state(game)->origin_cursor_i == game->cursor_i; | |
if (is_cancelling_move) { | |
game->num_valid = game->sel_stack.num_cards; | |
is_cancelling_move = TRUE; | |
is_valid_move = TRUE; | |
} | |
} | |
} | |
game->is_valid = is_valid_move; | |
} | |
internal bool32 | |
check_is_cursor_valid(struct game* game) { | |
bool32 is_cursor_valid = TRUE; | |
/* NOTE: Only go to filled slots when selection is empty */ | |
if (!game->sel_stack.num_cards) { | |
if (game_state(game)->is_top_row) { | |
if (game->cursor_i < 4) { | |
is_cursor_valid = game_state(game)->free_cells[game->cursor_i] != EMPTY_CELL; | |
} else { | |
is_cursor_valid = game_state(game)->foundations[game->cursor_i - 4] != EMPTY_CELL; | |
} | |
} else { | |
is_cursor_valid = game_state(game)->cascades[game->cursor_i].num_cards; | |
} | |
} else { | |
update_selection_validity(game); | |
is_cursor_valid = game->is_valid; | |
} | |
return is_cursor_valid; | |
} | |
internal struct v2 | |
draw_card_stack(struct v2 pos, struct card_stack* stack, int start_card_i, card_i_t* foundations) { | |
card_i_t card_i; | |
int start_of_tableau; | |
/*TODO: merge. search is_valid_stacking */ | |
for (start_of_tableau = stack->num_cards - 1; start_of_tableau > 0; --start_of_tableau) { | |
if (!(is_valid_stacking(get_card(stack->cards[start_of_tableau - 1]), get_card(stack->cards[start_of_tableau])))) { | |
break; | |
} | |
} | |
for (card_i = start_card_i; card_i < stack->num_cards; ++card_i) { | |
draw_card(pos, get_card(stack->cards[card_i]), foundations, card_i >= start_of_tableau, card_i == stack->num_cards - 1); | |
int big_size = 13; | |
if (stack->num_cards >= big_size && card_i < big_size) { | |
pos.y += 1; | |
} else { | |
pos.y += 2; | |
} | |
} | |
return pos; | |
} | |
internal void | |
draw_cursor_and_selection(struct game* game, struct v2 card_pos) { | |
int max_selection_size = get_max_selection_size(game); | |
card_pos.y += CARD_HEIGHT - 1; | |
attron(COLOR_PAIR(CURSOR_PAIR)); | |
mvprintw(card_pos.y, card_pos.x + CARD_WIDTH / 2 - 1, "/\\"); | |
mvprintw(card_pos.y+1, card_pos.x + CARD_WIDTH / 2 - 2, "/ \\"); | |
attron(COLOR_PAIR(SEL_SIZE_TEXT_PAIR)); | |
mvprintw(++card_pos.y, card_pos.x + CARD_WIDTH / 2 - 1, "%02d", MIN(max_selection_size, 13)); | |
/* Draw Grabbed card */ | |
if (game->sel_stack.num_cards) { | |
++card_pos.y; | |
card_pos = draw_card_stack( | |
card_pos, | |
&game->sel_stack, | |
game->sel_stack.num_cards - game->num_valid, | |
game_state(game)->foundations); | |
} | |
} | |
internal void | |
cancel_remaining_move(struct game* game) { | |
int i; | |
/* NOTE: Cancel remaining move */ | |
if (game->sel_stack.num_cards) { | |
int temp_cursor = game->cursor_i; | |
game->cursor_i = game_state(game)->origin_cursor_i; | |
/* NOTE: Updates the num_valid */ | |
update_selection_validity(game); | |
for (i = 0; i < game->sel_stack.num_cards; ++i) { | |
push_card_stack(&game_state(game)->cascades[game->cursor_i], game->sel_stack.cards[i]); | |
} | |
game->sel_stack.num_cards = 0; | |
game->cursor_i = temp_cursor; | |
} | |
} | |
internal void | |
grab_cards_from_cascade(struct game* game) { | |
game->origin_stack_type = CASCADE; | |
/* Grab cards from cascade */ | |
int max_selection_size = get_max_selection_size(game); | |
struct card_stack* casc = &game_state(game)->cascades[game->cursor_i]; | |
int start_i; | |
for (start_i = casc->num_cards - 1; start_i > 0; --start_i) { | |
if (!(is_valid_stacking(get_card(casc->cards[start_i - 1]), get_card(casc->cards[start_i])) && --max_selection_size)) { | |
break; | |
} | |
} | |
int i; | |
for (i = start_i; i < casc->num_cards; ++i) { | |
push_card_stack(&game->sel_stack, casc->cards[i]); | |
} | |
game->num_valid = game->sel_stack.num_cards; | |
casc->num_cards = start_i; | |
} | |
internal void | |
get_num_tableau_moves_remaining(struct game* game) { | |
cancel_remaining_move(game); | |
int original_cursor_i = game->cursor_i; | |
int was_top_row = game_state(game)->is_top_row; | |
int cascade_i, other_i; | |
game->num_valid_cascade_moves = 0; | |
game_state(game)->is_top_row = FALSE; | |
for (cascade_i = 0; cascade_i < NUM_CASCADES; ++cascade_i) { | |
game_state(game)->origin_cursor_i = game->cursor_i = cascade_i; | |
grab_cards_from_cascade(game); | |
for (other_i = 0; other_i < NUM_CASCADES; ++other_i) { | |
if (cascade_i != other_i) { | |
/* If can move selection onto other */ | |
game->cursor_i = other_i; | |
update_selection_validity(game); | |
if (game->is_valid) { | |
game->valid_cascade_moves[game->num_valid_cascade_moves].src_i = cascade_i; | |
game->valid_cascade_moves[game->num_valid_cascade_moves].dest_i = other_i; | |
++game->num_valid_cascade_moves; | |
} | |
} | |
} | |
cancel_remaining_move(game); | |
} | |
game->cursor_i = original_cursor_i; | |
game_state(game)->is_top_row = was_top_row; | |
} | |
/* TODO: | |
* | |
* animate | |
* : cards slide into/out of cursor selection | |
* : cards/cursor slide to next choice | |
* animation for when the game over | |
* | |
* BUG: when picking up from freecell extra cards will sometimes appear (visual | |
* bug only). | |
* | |
* don't send card if opposite lower card is not on foundation (always safe send)? | |
* | |
*/ | |
int main(int argc, char** argv) { | |
UNUSED(argc); | |
UNUSED(argv); | |
initialize_curses(); | |
struct game game; | |
bool32 show_info = FALSE; | |
uint64 last_update = time_in_us(), frame_time_us = 16667; | |
uint32 frame_timer = 0; | |
bool32 running = TRUE; | |
{ | |
int i; | |
int num_card_numbers = 13; | |
for (i = 0; i < NUM_DECK_CARDS; ++i) { | |
cards[i].suit = i / (NUM_DECK_CARDS / 4); | |
cards[i].number = (i % num_card_numbers); | |
} | |
#if 0 | |
FILE* fid = fopen("term_colors", "r"); | |
for (i = 0; i < NUM_COLORS; ++i) { | |
fscanf(fid, "%d,", &term_colors[i]); | |
} | |
fclose(fid); | |
initialize_color_pairs(); | |
#endif | |
} | |
shuffle_and_deal(&game, time(NULL)); | |
uint64 start_time = time_in_us(); | |
while (running) { | |
/* Get Input */ | |
int ch; | |
while ((ch = getch()) != -1) { | |
switch (ch) { | |
case 'q': case 'Q': { | |
running = FALSE; | |
#if DEBUG | |
FILE* fid = fopen("term_colors", "w"); | |
int i; | |
for (i = 0; i < NUM_COLORS; ++i) { | |
fprintf(fid, "%d,", term_colors[i]); | |
} | |
fclose(fid); | |
#endif | |
} break; | |
case 'j': case 'J': | |
color_picker.active = !color_picker.active; | |
break; | |
} | |
if (color_picker.active) { | |
switch (ch) { | |
case '+': | |
color_picker.color++; | |
if (color_picker.color == NUM_COLORS) | |
color_picker.color = 0; | |
break; | |
case '-': | |
color_picker.color--; | |
if (color_picker.color == -1) | |
color_picker.color = NUM_COLORS - 1; | |
break; | |
case KEY_LEFT: | |
term_colors[color_picker.color] -= 1; | |
if (term_colors[color_picker.color] < 0) | |
term_colors[color_picker.color] = 255; | |
initialize_color_pairs(); | |
break; | |
case KEY_RIGHT: | |
term_colors[color_picker.color] += 1; | |
if (term_colors[color_picker.color] > 255) | |
term_colors[color_picker.color] = 0; | |
initialize_color_pairs(); | |
break; | |
case KEY_UP: | |
term_colors[color_picker.color] -= color_picker.width; | |
if (term_colors[color_picker.color] < 0) | |
term_colors[color_picker.color] += 255; | |
initialize_color_pairs(); | |
break; | |
case KEY_DOWN: | |
term_colors[color_picker.color] += color_picker.width; | |
if (term_colors[color_picker.color] > 255) | |
term_colors[color_picker.color] -= 255; | |
initialize_color_pairs(); | |
break; | |
} | |
} else { | |
switch (ch) { | |
case '\n': | |
show_info = !show_info; | |
break; | |
case 'n': case 'N': | |
/* NOTE: New game */ | |
shuffle_and_deal(&game, time(NULL)); | |
break; | |
case 'c': case 'C': break; | |
case 's': case 'S': { | |
/* Send all possible cards to the state.foundations */ | |
cancel_remaining_move(&game); | |
save_game_state(&game); | |
bool32 sent_any = FALSE; | |
while (TRUE) { | |
bool32 sent_one = FALSE; | |
int i; | |
for (i = 0; i < NUM_CASCADES; ++i) { | |
if (game_state(&game)->cascades[i].num_cards) { | |
struct card_stack* casc = &game_state(&game)->cascades[i]; | |
struct card* card = get_card(top_card_stack(casc)); | |
if (can_place_on_foundation(*card, game_state(&game)->foundations)) { | |
sent_one = TRUE; | |
++game.move_counter; | |
game_state(&game)->foundations[card->suit] = pop_card_stack(casc); | |
} | |
} | |
if (i < NUM_FREE_CELLS && game_state(&game)->free_cells[i] != EMPTY_CELL) { | |
card_i_t card_i = game_state(&game)->free_cells[i]; | |
struct card* card = get_card(card_i); | |
if (can_place_on_foundation(*card, game_state(&game)->foundations)) { | |
sent_one = TRUE; | |
++game.move_counter; | |
game_state(&game)->foundations[card->suit] = card_i; | |
game_state(&game)->free_cells[i] = EMPTY_CELL; | |
} | |
} | |
} | |
if (!sent_one) { | |
break; | |
} else { | |
sent_any = TRUE; | |
} | |
} | |
if (!sent_any) { | |
--game.current_state; | |
} else { | |
/* TODO: move cursor to first valid position on bottom */ | |
} | |
get_num_tableau_moves_remaining(&game); | |
} break; | |
case 'z': case 'Z': { | |
/* NOTE: undo */ | |
cancel_remaining_move(&game); | |
if (game.current_state) { | |
--game.current_state; | |
++game.undo_counter; | |
get_num_tableau_moves_remaining(&game); | |
} | |
} break; | |
case 'r': case 'R': { | |
/* NOTE: restart game */ | |
cancel_remaining_move(&game); | |
if (game.current_state) { | |
++game.restart_counter; | |
game.current_state = 0; | |
get_num_tableau_moves_remaining(&game); | |
} | |
} break; | |
case KEY_DOWN: | |
case KEY_UP: { | |
int old_cursor_i = game.cursor_i; | |
int tries; | |
game_state(&game)->is_top_row = !game_state(&game)->is_top_row; | |
if (game_state(&game)->is_top_row) { | |
game.last_bottom_cursor_i = game.cursor_i; | |
if (game.sel_stack.num_cards) { | |
/* NOTE: Prefer the game_state(&game)->foundations for the top. */ | |
game.cursor_i = 4; | |
} else { | |
game.cursor_i = 0; | |
} | |
} else { | |
game.cursor_i = game.last_bottom_cursor_i; | |
} | |
for (tries = 0; tries < NUM_CASCADES; ++tries) { | |
if (check_is_cursor_valid(&game)) { | |
break; | |
} else if (++game.cursor_i > NUM_CASCADES - 1) { | |
game.cursor_i = 0; | |
} | |
} | |
if (tries == NUM_CASCADES) { | |
/* NOTE: Nothing in the other row is a valid game.selection */ | |
game_state(&game)->is_top_row = !game_state(&game)->is_top_row; | |
game.cursor_i = old_cursor_i; | |
} | |
} break; | |
case KEY_RIGHT: | |
case KEY_LEFT: { | |
int tries = NUM_CASCADES; | |
while (tries--) { | |
if (ch == KEY_LEFT && --game.cursor_i < 0) { | |
game.cursor_i = NUM_CASCADES - 1; | |
} else if (ch == KEY_RIGHT && ++game.cursor_i > NUM_CASCADES - 1) { | |
game.cursor_i = 0; | |
} | |
if (check_is_cursor_valid(&game)) { | |
break; | |
} | |
} | |
} break; | |
case ' ': { | |
int foundation_i = game.cursor_i - NUM_FREE_CELLS; | |
if (!game.sel_stack.num_cards) { | |
save_game_state(&game); | |
game_state(&game)->origin_cursor_i = game.cursor_i; | |
if (game_state(&game)->is_top_row) { | |
if (game.cursor_i < NUM_FREE_CELLS) { | |
/* Grab card from free_cell */ | |
game.sel_stack.cards[0] = game_state(&game)->free_cells[game.cursor_i]; | |
game.origin_stack_type = FREE_CELL; | |
game_state(&game)->free_cells[game.cursor_i] = EMPTY_CELL; | |
} else { | |
game.sel_stack.cards[0] = game_state(&game)->foundations[foundation_i]; | |
game.origin_stack_type = FOUNDATION; | |
card_i_t* card_ip = &game_state(&game)->foundations[foundation_i]; | |
struct card* card = get_card(*card_ip); | |
if (card) { | |
if (card->number == 0) { | |
*card_ip = EMPTY_CELL; | |
} else { | |
--(*card_ip); | |
} | |
} | |
} | |
game.sel_stack.num_cards = 1; | |
} else { | |
grab_cards_from_cascade(&game); | |
} | |
} else { | |
update_selection_validity(&game); | |
bool32 is_valid_move = game.is_valid; | |
enum stack_type stack_type; | |
if (game_state(&game)->is_top_row) { | |
/* Attempt to place card */ | |
if (game.cursor_i < NUM_FREE_CELLS) { | |
stack_type = FREE_CELL; | |
if (is_valid_move) { | |
game_state(&game)->free_cells[game.cursor_i] = pop_card_stack(&game.sel_stack); | |
cancel_remaining_move(&game); | |
} else { | |
assert(!"Invalid move to free cell"); | |
cancel_remaining_move(&game); | |
} | |
} else { | |
stack_type = FOUNDATION; | |
if (is_valid_move) { | |
game_state(&game)->foundations[foundation_i] = pop_card_stack(&game.sel_stack); | |
cancel_remaining_move(&game); | |
game_state(&game)->is_top_row = FALSE; | |
game.cursor_i = game.last_bottom_cursor_i; | |
} else { | |
assert(!"Invalid move to foundation"); | |
cancel_remaining_move(&game); | |
} | |
} | |
} else { | |
stack_type = CASCADE; | |
/* TODO: Should be able to place less than num_valid cards. choose | |
* this number somehow */ | |
if (is_valid_move) { | |
int i; | |
for (i = game.sel_stack.num_cards - game.num_valid; i < game.sel_stack.num_cards; ++i) { | |
struct card_stack* casc = &game_state(&game)->cascades[game.cursor_i]; | |
push_card_stack(casc, game.sel_stack.cards[i]); | |
} | |
game.sel_stack.num_cards -= game.num_valid; | |
cancel_remaining_move(&game); | |
} else { | |
assert(!"Invalid move to cascade"); | |
cancel_remaining_move(&game); | |
} | |
} | |
/* Remove non-moves from history */ | |
if (game_state(&game)->origin_cursor_i == game.cursor_i && game.origin_stack_type == stack_type) { | |
--game.current_state; | |
} else { | |
++game.move_counter; | |
} | |
get_num_tableau_moves_remaining(&game); | |
} | |
} break; | |
default: break; | |
} | |
} | |
} | |
if (running) { | |
frame_timer += time_in_us() - last_update; | |
last_update = time_in_us(); | |
if (frame_timer >= frame_time_us) { | |
erase(); | |
int info_text_row = 0; | |
int info_text_col = 3; | |
attron(COLOR_PAIR(SEL_SIZE_TEXT_PAIR)); | |
if (show_info) { | |
mvprintw(info_text_row, info_text_col, "Moves: %d", game.move_counter); | |
mvprintw(info_text_row, info_text_col + 24, "Undos: %d", game.undo_counter); | |
mvprintw(info_text_row++, info_text_col + 45, "Restarts: %d", game.restart_counter); | |
uint64 elapsed_us = time_in_us() - start_time; | |
uint32 seconds = elapsed_us / 1000000; | |
uint32 minutes = seconds / 60; | |
seconds -= minutes * 60; | |
mvprintw(info_text_row++, info_text_col, "Time: %d:%d", minutes, seconds); | |
mvprintw(info_text_row++, 1, "^ Enter %d Tableau Move(s) Available", game.num_valid_cascade_moves); | |
} else { | |
mvprintw(info_text_row++, 1, "V Enter %d Tableau Move(s) Available", game.num_valid_cascade_moves); | |
} | |
{ | |
struct v2 top_left = { 3, info_text_row }; | |
struct v2 free_cells_pos = top_left; | |
int padding = 1; | |
{ | |
struct v2 cascade_pos = add2(top_left, 4, CARD_HEIGHT + 2); | |
int cascade_i; | |
for (cascade_i = 0; cascade_i < NUM_CASCADES; ++cascade_i) { | |
draw_card_slot(cascade_pos, "x2", SEL_SIZE_TEXT_PAIR); | |
struct card_stack* casc = &game_state(&game)->cascades[cascade_i]; | |
struct v2 card_pos = | |
draw_card_stack( | |
add(cascade_pos, v2_both(1)), | |
casc, | |
0, | |
game_state(&game)->foundations); | |
/* Draw cursor */ | |
if (!game_state(&game)->is_top_row && game.cursor_i == cascade_i) { | |
draw_cursor_and_selection(&game, card_pos); | |
} else if (game.sel_stack.num_cards > game.num_valid && game_state(&game)->origin_cursor_i == cascade_i) { | |
/* Draw Invalid selection */ | |
card_pos.y += CARD_HEIGHT; | |
struct card_stack invalid_cards = { | |
.num_cards = game.sel_stack.num_cards - game.num_valid, | |
}; | |
memcpy(invalid_cards.cards, game.sel_stack.cards, sizeof(invalid_cards.cards)); | |
card_pos = draw_card_stack(card_pos, &invalid_cards, 0, game_state(&game)->foundations); | |
} | |
cascade_pos.x += CARD_SLOT_WIDTH + padding; | |
} | |
} | |
{ | |
int free_cell_i; | |
for (free_cell_i = 0; free_cell_i < NUM_FREE_CELLS; ++free_cell_i) { | |
draw_card_slot(free_cells_pos, "+1", SEL_SIZE_TEXT_PAIR); | |
struct v2 card_pos = add(free_cells_pos, v2_both(1)); | |
draw_card(card_pos, get_card(game_state(&game)->free_cells[free_cell_i]), game_state(&game)->foundations, FALSE, TRUE); | |
free_cells_pos.x += CARD_SLOT_WIDTH + padding; | |
if (game_state(&game)->is_top_row && game.cursor_i == free_cell_i) { | |
if (game_state(&game)->free_cells[free_cell_i] != EMPTY_CELL) { | |
++card_pos.y; | |
} | |
draw_cursor_and_selection(&game, card_pos); | |
} | |
} | |
} | |
{ | |
int foundation_i; | |
struct v2 foundations_pos = add2(free_cells_pos, CARD_SLOT_WIDTH + 1, 0); | |
for (foundation_i = 0; foundation_i < NUM_FOUNDATIONS; ++foundation_i) { | |
char suit_str[] = { suits[foundation_i], 0 }; | |
draw_card_slot(foundations_pos, suit_str, is_black_suit(foundation_i) ? BLUE_SLOT_PAIR : RED_SLOT_PAIR); | |
struct v2 card_pos = add(foundations_pos, v2_both(1)); | |
draw_card(card_pos, get_card(game_state(&game)->foundations[foundation_i]), game_state(&game)->foundations, FALSE, TRUE); | |
if (game_state(&game)->is_top_row && game.cursor_i - NUM_FREE_CELLS == foundation_i) { | |
if (game_state(&game)->foundations[foundation_i] != EMPTY_CELL) { | |
++card_pos.y; | |
} | |
draw_cursor_and_selection(&game, card_pos); | |
} | |
foundations_pos.x += CARD_SLOT_WIDTH + padding; | |
} | |
} | |
} | |
/* Draws the currently color pair. */ | |
if (color_picker.active) { | |
init_pair(COLOR_PICKER_PAIR, COLOR_WHITE, term_colors[color_picker.color]); | |
attron(COLOR_PAIR(COLOR_PICKER_PAIR)); | |
mvprintw(0, 0, " "); | |
bool32 top_half = term_colors[color_picker.color] < 128; | |
int i; | |
for (i = 0; i < 128; ++i) { | |
int color_i = top_half ? i : i + 128; | |
init_pair(NUM_COLOR_PAIRS + i, COLOR_BLACK, color_i); | |
color_set(NUM_COLOR_PAIRS + i, 0); | |
int row = i / color_picker.width + 1, col = (i%color_picker.width)*3; | |
mvprintw(row, col, " "); | |
if (term_colors[color_picker.color] == color_i) { | |
mvprintw(row, col, "%d", color_i); | |
} | |
} | |
} | |
attron(COLOR_PAIR(SEL_SIZE_TEXT_PAIR)); | |
if (show_info) { | |
mvprintw(max_y - 1, 3, "Move Cursor: Arrows Grab/Place: Space Undo: Z Restart: R New Game: N Send All: S Quit: Q"); | |
} | |
refresh(); | |
frame_timer = 0; | |
} | |
usleep(1); | |
} | |
} | |
endwin(); | |
return 0; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment