Skip to content

Instantly share code, notes, and snippets.

@chebert
Created June 30, 2018 18:01
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save chebert/b1dfc09e5bbac0030f96bd689a8ba724 to your computer and use it in GitHub Desktop.
Save chebert/b1dfc09e5bbac0030f96bd689a8ba724 to your computer and use it in GitHub Desktop.
Freecell in C using NCURSES
/*
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