8
\$\begingroup\$

I've been teaching myself C for the last few months. As a learning exercise, I set out to write an ncurses implementation of the classic card game Uno, mostly because it was was one of the simpler card games I could think of.

Since my emphasis in writing this was primarily learning concepts and algorithms, the interface is pretty minimal, and there aren't a lot of bells and whistles beyond what's needed to play the game. Nonetheless, I've tried to implement the rules and gameplay dynamics of the card game as closely as possible, so things like saying "Uno" and challenging Draw Four Wilds are included. For the most part, my goal was to make the game as lightweight, efficient, and airtight as I could.

In the header I define a Card type which contains values representing the number/symbol and color of that card, and then an array of Card constants for every card in the deck. Most of the program, though, works through Handcard structs, which contain a pointer to a Card constant as well as a pointer to another Handcard. This way, each player's hand, as well as discard and draw piles, are singly linked lists, to which memory is dynamically allocated and deallocated every time a card is drawn or played.

For the most part, I'm happy with how the game came out. To my knowledge all the memory leaks have been closed, and most of the bugs have been stamped out too. It's not perfect: everything is written to the standard ncurses window, so updating different parts of the user interface is a little clumsy. Also, I couldn't find an ncurses equivalent for fgets, so when entering players' names and backspacing the cursor behaves a little funny (though it still deletes characters properly.)

My main questions basically revolve around whether my implementation follows best practices, and whether I could have implemented certain things better, in particular the data structures for organizing cards, and the layered gameplay loops. I've also not gotten around to writing a function for sorting the cards in players' hands by color and symbol, which isn't essential but would be ideal as well as good practice.

With all that said, here is the code as well as a download link, complete with a primitive makefile. You will need the ncurses library installed to actually play this. Give it a look and let me know what suggestions y'all have.

uno.h

/* HEADER FOR THE UNO GAME
 * The deck, typedefs, constants, function prototypes, #defines etc
 * are all in here */

#define DECKSIZE 108
#define INITHAND 7      /* initial hand dealt (7 in standard Uno rules) */
#define CARDTOP 6
#define CARDLINES 7
#define CARDLEFT 2

/* Constants for each of the card values, which also correspond to their
 * char values to be displayed */
enum Number { ZERO = 48, ONE, TWO, THREE, FOUR, FIVE, SIX, SEVEN, EIGHT, NINE,
       REVERSE = 82, SKIP, DRAWTWO = 68, WILD = 87, PLUSWILD = 43 };

/* Color constants
 * DEF is the standard dialog color pair
 * UNO is used for the Uno tag in the player stats
 * WHITE is for Wilds, and a couple other things */
enum Color { RED = 1, BLUE, GREEN, YELLOW, WHITE, DEF, UNO };

typedef struct {
    char num;
    int col;
} Card;

/* Yeah, I probably could've just made cardname a string and
 * concatenated the parts together, but this seemed more fun :3 */
struct Cardname {
    union {
        char *s;
        char c;
    } num;
    char *col;
};

/* players' hands
 * Each element contains a pointer to a card,
 * as well as a link to the next card in the player's hand. */
struct Handcard {
    Card const *card;
    struct Handcard *link;
};

/* Card constants */
const Card deck[] = {
    { ZERO, RED },
    { ONE, RED },
    { ONE, RED },
    { TWO, RED },
    { TWO, RED },
    { THREE, RED },
    { THREE, RED },
    { FOUR, RED },
    { FOUR, RED },
    { FIVE, RED },
    { FIVE, RED },
    { SIX, RED },
    { SIX, RED },
    { SEVEN, RED },
    { SEVEN, RED },
    { EIGHT, RED },
    { EIGHT, RED },
    { NINE, RED },
    { NINE, RED },
    { REVERSE, RED },
    { REVERSE, RED },
    { SKIP, RED },
    { SKIP, RED },
    { DRAWTWO, RED },
    { DRAWTWO, RED },
    { ZERO, BLUE },
    { ONE, BLUE },
    { ONE, BLUE },
    { TWO, BLUE },
    { TWO, BLUE },
    { THREE, BLUE },
    { THREE, BLUE },
    { FOUR, BLUE },
    { FOUR, BLUE },
    { FIVE, BLUE },
    { FIVE, BLUE },
    { SIX, BLUE },
    { SIX, BLUE },
    { SEVEN, BLUE },
    { SEVEN, BLUE },
    { EIGHT, BLUE },
    { EIGHT, BLUE },
    { NINE, BLUE },
    { NINE, BLUE },
    { REVERSE, BLUE },
    { REVERSE, BLUE },
    { SKIP, BLUE },
    { SKIP, BLUE },
    { DRAWTWO, BLUE },
    { DRAWTWO, BLUE },
    { ZERO, GREEN },
    { ONE, GREEN },
    { ONE, GREEN },
    { TWO, GREEN },
    { TWO, GREEN },
    { THREE, GREEN },
    { THREE, GREEN },
    { FOUR, GREEN },
    { FOUR, GREEN },
    { FIVE, GREEN },
    { FIVE, GREEN },
    { SIX, GREEN },
    { SIX, GREEN },
    { SEVEN, GREEN },
    { SEVEN, GREEN },
    { EIGHT, GREEN },
    { EIGHT, GREEN },
    { NINE, GREEN },
    { NINE, GREEN },
    { REVERSE, GREEN },
    { REVERSE, GREEN },
    { SKIP, GREEN },
    { SKIP, GREEN },
    { DRAWTWO, GREEN },
    { DRAWTWO, GREEN },
    { ZERO, YELLOW },
    { ONE, YELLOW },
    { ONE, YELLOW },
    { TWO, YELLOW },
    { TWO, YELLOW },
    { THREE, YELLOW },
    { THREE, YELLOW },
    { FOUR, YELLOW },
    { FOUR, YELLOW },
    { FIVE, YELLOW },
    { FIVE, YELLOW },
    { SIX, YELLOW },
    { SIX, YELLOW },
    { SEVEN, YELLOW },
    { SEVEN, YELLOW },
    { EIGHT, YELLOW },
    { EIGHT, YELLOW },
    { NINE, YELLOW },
    { NINE, YELLOW },
    { REVERSE, YELLOW },
    { REVERSE, YELLOW },
    { SKIP, YELLOW },
    { SKIP, YELLOW },
    { DRAWTWO, YELLOW },
    { DRAWTWO, YELLOW },
    { WILD, WHITE },
    { WILD, WHITE },
    { WILD, WHITE },
    { WILD, WHITE },
    { PLUSWILD, WHITE },
    { PLUSWILD, WHITE },
    { PLUSWILD, WHITE },
    { PLUSWILD, WHITE }
};

void clean(struct Handcard *);
void shuffle(struct Handcard *);
void showhand(struct Handcard *);
int length(struct Handcard *);
struct Cardname getcardname(struct Cardname, Card const *);
int draw(struct Handcard **pl, struct Handcard **draw, struct Handcard **discard);

uno.c

#include <ncurses.h>
#include <stdlib.h>
#include <ctype.h>
#include <string.h>
#include <time.h>       /* only needed in shuffle */

#include "uno.h"

int main(int argc, char *argv[])
{
    int cursy, cursx;
    int maxy, maxx;
    int i, j;
    int prev;       /* used in +4 Wild challenges */
    int lastcol;    /* used in +4 Wild challenges */
    int plrs;       /* total number of players */
    int dir;        /* direction of gameplay, for use with reverse cards.   *
                 * 1 if "clockwise", -1 if "counterclockwise"           */
    int color;      /* Current color in play */
    int key;        /* input character
                 * Needs to be int, to make room for arrow keys */
    char turn;      /* becomes 0 when player turn ends */
    int drawx;      /* If a draw X card is played, how screwed next player is */
    char gtype;     /* Quick or full game */
    int won;        /* Once player wins full game, will be set to their index */
    int round;      /* Round (for full games) */
    int multdraw;   /* player draws until they have a playable card */
    int noskip;     /* Draw Two and Draw Four Wild don't skip */
    int winscore;   /* Score needed to win the game */

    char *cp = NULL;    /* char pointer (for player name dialog) */

    struct Handcard *pile = NULL;       /* head of draw pile (linked list) */
    struct Handcard *discard = NULL;    /* head of discard pile (linked list) */

    struct Handcard *plcard = NULL; /* selected card in player's hand */
    struct Handcard *p = NULL;      /* the generic Handcard pointer */

    struct Cardname cardname;

    /* Defaults for parameters */
    multdraw = 0;
    noskip = 0;
    winscore = 500;
    /* Read in command line parameters */
    while(--argc > 0 && (*++argv)[0] == '-') {
            switch(*++argv[0]) {
            case 'm':
                multdraw = 1;
                break;
            case 'd':
                noskip = 1;
                break;
            case 's':
                if((*++argv[0]) != '=') {
                    printf("Error: Format should be -s=score\n");
                    return 1;
                }
                winscore = 0;
                while((*++argv[0]))
                    if(!isdigit(*argv[0])) {
                        printf("Error: Numerical parameter needed\n");
                        return 1;
                    } else
                        winscore = 10*winscore + *argv[0] - '0';
                break;
            case 'h': case 'H':
                printf("UNO usage\n"
                    "-d\t\tDraw Two and Draw Four Wilds don't skip\n"
                    "-h, -H\t\tThis help reference\n"
                    "-m\t\tPlayers draw until they pull a playable card\n"
                    "-s=score\tSet winning score for full games (Default = 500)"
                    "\n\n"
                    "For UNO rules, visit www.unorules.com\n");
                return 0;
            default:
                printf("Illegal parameter %c\n", *argv[0]);
                return 1;
            }
    }

    /* NCURSES MODE INITIATES HERE.  TRY TO KEEP DECLARATIONS
     * ABOVE THIS LINE PLS */
    initscr();
    start_color();
    cbreak();
    noecho();
    keypad(stdscr, TRUE);   /* for arrow keys and stuff */

    getmaxyx(stdscr, maxy, maxx);

    /* quit if terminal screen not big enough */
    if(maxy < 24 || maxx < 80) {
        endwin();
        printf("ERROR: Screen too small! Aborting...\n\n");
        return 1;
    }

    /* Color pair initializations */
    init_pair(RED, COLOR_BLACK, COLOR_RED);
    init_pair(BLUE, COLOR_WHITE, COLOR_BLUE);
    init_pair(GREEN, COLOR_BLACK, COLOR_GREEN);
    init_pair(YELLOW, COLOR_BLACK, COLOR_YELLOW);
    init_pair(WHITE, COLOR_BLACK, COLOR_WHITE);     /* for wilds */
    init_pair(DEF, COLOR_WHITE, COLOR_BLACK);       /* for text displays */
    init_pair(UNO, COLOR_RED, COLOR_BLACK);     /* for UNO tag */

    /* MAYBE SOME KIND OF SPLASH SCREEN? */

    /* Quick or full game? */
    mvprintw(0,0, "<Q>uick or <F>ull game?");
    clrtoeol();
    curs_set(1);
    while((gtype=toupper(getch())) != 'Q' && gtype != 'F')
        ;

    /* get number of players */
    echo();
    do {
        mvprintw(0,0, "How many players (2-10)? ");
        clrtoeol();
        scanw("%d", &plrs);
    } while(plrs < 2 || plrs > 10);
    noecho();

    /* Player array declarations */
    struct Handcard *pl[plrs];      /* heads of players' hands (linked lists) */
    char *plname[plrs];             /* players' names */
    int plscore[plrs];              /* Players' scores */
    int uno[plrs];                  /* tracks who has "UNO"'d */


    /* get players' names, set scores to 0 */
    for(i=0; i<plrs; i++) {
        plscore[i] = 0;
        plname[i] = (char *) calloc(13, sizeof(char));
        cp = plname[i];
        mvprintw(i, 0, "Player %d, enter your name (12 chars max): ", i+1);
        getyx(stdscr, cursy, cursx);
        while((key=getch()) != '\n') {
            if(strlen(plname[i]) < 12 && (isalpha(key) || isdigit(key)
            || ispunct(key) || key == ' ')) {
                *cp++ = key;
                mvaddch(cursy, cursx++, key);
            }
            if((key == KEY_BACKSPACE || key == KEY_DC || key == 127)
            && strlen(plname[i])) {
                *--cp = '\0';
                mvaddch(cursy, --cursx, ' ');
            }
        }
        key = *cp = '\0';
        attron(A_BOLD);
        mvprintw(i,0, "%s", plname[i]); clrtoeol();
        attroff(A_BOLD);
        refresh();
    }
    mvprintw(i+1,0, "Let's go!");
    getch();
    curs_set(0);

    round = 1;
    won = -1;

    /* GAMEPLAY LOOP BEGINS HERE
     * This loop will run only once in a quick game.  In a full game,
     * it loops until any player reaches 500 points */
    while(won < 0) {

        curs_set(0);

        /* Allocate memory for players' hands, set uno's to 0 */
        for(i=0; i<plrs; i++) {
            pl[i] = NULL;
            uno[i] = 0;
        }

        /* initialize pile to sorted deck */
        pile = (struct Handcard *) malloc(sizeof(struct Handcard));
        p = pile;
        for(i=0; i<DECKSIZE; i++) {
            p->card = &deck[i];
            if(i < DECKSIZE-1)
                p->link = (struct Handcard *) malloc(sizeof(struct Handcard));
            else
                p->link = NULL;
            p = p->link;
        }

        shuffle(pile);  /* Shuffle the deck */

        /* deal the initial hand to each player */
        for(j=0; j < INITHAND; j++)
            for(i=0; i<plrs; i++) {
                if(pl[i] == NULL) {
                    pl[i] = (struct Handcard *) malloc(sizeof(struct Handcard));
                    pl[i]->card = pile->card;
                    pl[i]->link = NULL;
                } else {
                    for(plcard = pl[i]; plcard->link != NULL; plcard = plcard->link)
                        ;
                    plcard->link = (struct Handcard *) malloc(sizeof(struct Handcard));
                    plcard = plcard->link;
                    plcard->card = pile->card;
                    plcard->link = NULL;
                }
                p = pile;           /* Remove the card from the head of the */
                pile = pile->link;  /* pile                                 */
                free(p);            /*                                      */
            }

        /* Turn over top card */
        do {
            if(discard != NULL)
                if(discard->card->col == WHITE) {
                    p = (struct Handcard *) malloc(sizeof(struct Handcard));
                    p->card = discard->card;
                    p->link = pile;
                    pile = p;
                    free(discard);
                    discard = NULL;
                    shuffle(pile);
                }
            discard = (struct Handcard *) malloc(sizeof(struct Handcard));
            discard->card = pile->card;
            discard->link = NULL;
            p = pile;
            pile = pile->link;
            free(p);
        } while(discard->card->col == WHITE);

        i = -1; /* dealer */
        dir = 1;
        drawx = 0;
        lastcol = color = discard->card->col;

        /* If top card is Draw Two or Reverse, respectively */
        if(discard->card->num == DRAWTWO && !drawx)
            drawx = 2;
        else if(discard->card->num == REVERSE)
            dir = -dir;

        if(gtype == 'F') {
            clear();
            mvprintw(maxy/2, maxx/2, "ROUND %d", round);
            getch();
        }

        /* PLAYER TURN STARTS HERE */
        do {
            turn = 1; /* needed for stupid input loop below */

            clear();
            curs_set(0);

            /* Advance to next player, includes handling SKIPs */
            i += dir + dir*(discard->card->num == SKIP && key != 'D')
              + dir*(discard->card->num == REVERSE && plrs == 2 && key != 'D');
            if(i >= plrs)   /* ensure i is always a valid index */
                i -= plrs;
            else if(i < 0)
                i += plrs;

            mvprintw(maxy/2, maxx/2-strlen(plname[i]), 
                         "%s, get ready!", plname[i]);
            getch();
            move(maxy/2,0); clrtoeol();

            cursy = CARDTOP;        /* where card selection arrow begins */
            cursx = CARDLEFT+2;     /* each turn                         */

            plcard = pl[i]; /* Set player card pointer to head of hand */

            /* Show player's name in ll corner */
            attron(COLOR_PAIR(WHITE) | A_BOLD);
            move(maxy-1, 0);
            if(gtype == 'F')
                printw("Round %d - ", round);
            printw("%s - <D>raw, <P>lay, <U>no, <Q>uit. "
                   "Select with up/down arrows", plname[i]);
            clrtoeol();

            /* Show discard pile */
            attron(COLOR_PAIR(DEF));
            attroff(A_BOLD);
            mvprintw(maxy/2, maxx*3/8-10, "DISCARD ");
            attron(COLOR_PAIR(color) | A_BOLD);
            if(discard->card->num >= ZERO && discard->card->num <= NINE)
                attroff(A_BOLD);
            addch(discard->card->num);

            /* Display all player stats */
            attron(COLOR_PAIR(DEF) | A_BOLD);
            mvprintw(6,maxx/2, "PLAYER\tCARDS\tSCORE");
            for(j=0; j<plrs; j++) {
                attron(COLOR_PAIR(DEF));
                if(i == j) {
                    attron(A_BOLD);
                    if(plrs > 2) { /* indicate direction of gameplay */
                        if(dir > 0)
                            mvaddch(j+8,maxx/2 - 2, 'v');
                        else if(dir < 0)
                            mvaddch(j+8,maxx/2 - 2, '^');
                    }
                } else
                    attroff(A_BOLD);
                mvprintw(j+8,maxx/2, "%-12s %00d\t%000d",
                         plname[j], length(pl[j]), plscore[j]);
                if(uno[j]) {
                    attron(COLOR_PAIR(UNO) | A_BOLD);
                    printw("\tU");       /* mark if player has UNO'd */
                } else
                    printw("\t ");
            }

            /* Player forced to draw cards */
            if(drawx) {
                showhand(pl[i]);

                /* For challenging +4 wilds */
                if(discard->card->num == PLUSWILD) {
                    curs_set(1);
                    mvprintw(0,0, "%s has played a Wild Draw Four! "
                                  "Do you challenge? (Y/N)", plname[prev]);
                    clrtoeol();
                    while((key=tolower(getch())) != 'y' && key != 'n')
                        ;

                    /* Player has challenged */
                    if(key == 'y') {
                        for(p = pl[prev]; p != NULL; p = p->link)
                            if(p->card->num == discard->link->card->num
                            || p->card->col == discard->link->card->col
                            || (discard->link->card->col == WHITE 
                                && p->card->col == lastcol)
                            || p->card->num == WILD) {
                                mvprintw(0,0,
                                        "%s played a Wild Draw Four illegally! "
                                         "They will be dealt four cards.",
                                         plname[prev]);
                                clrtoeol();
                                getch();
                                for(j=0; j<4; j++) {
                                    drawx = 0;
                                    if(!draw(&pl[prev], &pile, &discard)) {
                                        mvprintw(0,0, "Oops! Not enough cards"
                                                      " in the deck.");
                                        clrtoeol();
                                        curs_set(1);
                                        getch();
                                        curs_set(0);
                                        break;
                                    }
                                }
                                mvprintw(prev+8,maxx/2, "%-12s %00d\t%000d",
                                         plname[prev], length(pl[prev]),
                                         plscore[prev]);
                                uno[prev] = 0;
                                printw("\t ");
                                break;
                            }
                        if(p == NULL) {
                            mvprintw(0,0, "Your challenge failed!");
                            clrtoeol();
                            getch();
                            drawx = 6;
                        }
                    }
                }

                if(drawx) {
                    curs_set(1);
                    mvprintw(0,0, "You must draw %d cards.", drawx);
                    clrtoeol();
                    getch();
                    curs_set(0);
                    do {
                        if(pile == NULL && length(discard) == 1) {
                            mvprintw(0,0, "It's your lucky day...not enough "
                                          "cards in the pile!");
                            curs_set(1);
                            clrtoeol();
                            getch();
                            curs_set(0);
                            drawx = 0;
                            break;
                        }
                        draw(&pl[i], &pile, &discard);
                    } while(--drawx);

                    uno[i] = 0;
                    attron(A_BOLD);
                    mvprintw(i+8,maxx/2, "%-12s %00d\t%000d",
                             plname[i], length(pl[i]), plscore[i]);
                    printw("\t ");
                    attroff(A_BOLD);

                    /* "Draw X" cards skip unless in noskip mode */
                    if(!noskip && (discard->card->num == DRAWTWO
                                || discard->card->num == PLUSWILD)) {
                        showhand(pl[i]);
                        curs_set(1);
                        mvprintw(0,0, "Proceed to next player's turn.");
                        clrtoeol();
                        getch();
                        curs_set(0);
                        continue;
                    }
                }
            }

            /* input loop. It exits once I set "turn" to 0, which
             * only happens on a break in the middle of the switch
             * block below. Otherwise I use continue to skip the
             * "turn = 0" line. A little hacky, but then it used
             * to be a goto label here... */
            do {
                if(length(pl[i]) > 1)
                    uno[i] = 0;

                curs_set(0);
                move(0,0); clrtoeol();
                showhand(pl[i]);
                cardname = getcardname(cardname, plcard->card);

                /* Show discard pile again, if we just shuffled */
                mvprintw(maxy/2, maxx*3/8-10, "DISCARD ");
                attron(COLOR_PAIR(color) | A_BOLD);
                if(discard->card->num >= ZERO && discard->card->num <= NINE)
                    attroff(A_BOLD);
                addch(discard->card->num);

                /* Draw cursor next to selected card */
                attron(COLOR_PAIR(DEF) | A_BOLD);
                mvaddch(cursy,cursx, '<');
                attroff(A_BOLD);

                /* Show name of selected card */
                mvprintw(0,0, "Selected: %s", cardname.col);
                if(plcard->card->num >= ZERO && plcard->card->num <= NINE)
                    addch(cardname.num.c);
                else
                    printw("%s", cardname.num.s);
                clrtoeol();

            /* Wait for player command */
                switch(key=toupper(getch()))
                {
                case KEY_UP:
                    if(plcard != pl[i]) {
                        p = pl[i];
                        mvaddch(cursy,cursx, ' ');
                        cursy -= 2;
                        if(cursy < CARDTOP) {
                            cursy = CARDTOP + CARDLINES*2;
                            cursx -= 4;
                        }
                        while(p->link != plcard)
                            p = p->link;
                        plcard = p;
                    } else {
                        mvaddch(cursy,cursx, ' ');
                        for(p = pl[i]; p->link != NULL; p = p->link) {
                            cursy += 2;
                            if(cursy > CARDTOP + CARDLINES*2) {
                                cursy = CARDTOP;
                                cursx += 4;
                            }
                        }
                        plcard = p;
                    }
                    continue;
                case KEY_DOWN:
                    if(plcard->link != NULL) {
                        plcard = plcard->link;
                        mvaddch(cursy,cursx, ' ');
                        cursy += 2;
                        if(cursy > CARDTOP + CARDLINES*2) {
                            cursy = CARDTOP;
                            cursx += 4;
                        }
                    } else {
                        plcard = pl[i];
                        mvaddch(cursy,cursx, ' ');
                        cursy = CARDTOP;
                        cursx = CARDLEFT+2;
                    }
                    continue;
                case 'D':                           /* Draw a card */
                    for(p = pl[i]; p != NULL; p = p->link)
                        if(p->card->num == discard->card->num 
                        || p->card->col == color
                        || p->card->col == WHITE) {
                            mvprintw(0,0, "You have playable cards! "
                                          "Draw anyway?");
                            clrtoeol();
                            curs_set(1);
                            if(tolower(getch()) != 'y')
                                break;      /* exit loop, card won't draw */
                            else {
                                p = NULL;   /* exit loop anyway, card will draw */
                                break;
                            }
                        }

                    /* call draw, and update # of player's cards on display */
                    if(p == NULL) {
                        if(!draw(&pl[i], &pile, &discard)) {
                            mvprintw(0,0, "No more cards to draw. Proceed "
                                          "to next player.");
                            clrtoeol();
                            curs_set(1);
                            getch();
                            curs_set(0);
                            break;
                        };
                        uno[i] = 0;
                        attron(A_BOLD);
                        mvprintw(i+8,maxx/2, "%-12s %00d\t%000d", 
                                 plname[i], length(pl[i]), plscore[i]);
                        printw("\t ");
                        attroff(A_BOLD);

                        /* if multdraw, then player can keep drawing indefinitely.
                         * if not, player only gets one chance to draw per turn */
                        if(multdraw)
                            continue;
                        else {
                            showhand(pl[i]);
                            for(p = pl[i]; p->link != NULL; p = p->link)
                                ;
                            if(p->card->num == discard->card->num
                            || p->card->col == color
                            || p->card->col == WHITE) {
                                mvprintw(0,0, "You drew a playable card! Play it? "
                                  "(Y/N%s", (length(pl[i]) == 2) ? "/U)" : ")");
                                clrtoeol();
                                curs_set(1);
                                while((key=tolower(getch())) != 'y' && key != 'n'
                                      && !(key == 'u' && length(pl[i]) == 2))
                                    ;
                                if(key == 'n') {
                                    break;      /* to next player */
                                } else {
                                    if(key == 'u')
                                        uno[i] = 1;
                                    plcard = p; /* ...and then go to case 'P' */
                                }
                            } else {
                                mvprintw(0,0, "Not a playable card. "
                                              "Proceed to next player.");
                                curs_set(1);
                                clrtoeol();
                                getch();
                                break;
                            }
                        }
                    } else
                        continue;
                case 'P': case 'U':                 /* Play a card */
                    if(key == 'U') {
                        for(j=0; j<plrs; j++)
                            if(length(pl[j]) == 1 && !uno[j] && j != i) {
                                attron(COLOR_PAIR(DEF));
                                attroff(A_BOLD);
                                curs_set(1);
                                mvprintw(0,0, "%s didn't say UNO!"
                                       " They must draw two cards.", plname[j]);
                                clrtoeol();
                                getch();
                                curs_set(0);
                                drawx = 2;
                                do
                                    if(!draw(&pl[j], &pile, &discard)) {
                                        mvprintw(0,0, "Oops! Not enough cards"
                                                      " in the deck.");
                                        clrtoeol();
                                        curs_set(1);
                                        getch();
                                        curs_set(0);
                                        drawx = 0;
                                        break;
                                    }
                                while(--drawx);
                                mvprintw(j+8,maxx/2, "%-12s %00d\t%000d",
                                        plname[j], length(pl[j]), plscore[j]);
                                break;
                            }
                        if(j == plrs && length(pl[i]) > 2) {
                            curs_set(1);
                            mvprintw(0,0, "Nobody forgot to say UNO!");
                            clrtoeol();
                            getch();
                        }
                        curs_set(0);
                        if(length(pl[i]) > 2)
                            continue;
                        else if(length(pl[i]) == 1) {
                            uno[i] = 1;
                            mvprintw(i+8,maxx/2, "%-12s %00d\t%000d",
                                plname[i], length(pl[i]), plscore[i]);
                            attron(COLOR_PAIR(UNO) | A_BOLD);
                            printw("\tU");
                            attron(COLOR_PAIR(DEF));
                            attroff(A_BOLD);
                            continue;
                        }
                    }

                /* Player chose a playable card */
                    if(plcard->card->num == discard->card->num 
                    || plcard->card->col == color
                    || plcard->card->col == WHITE) {
                        /* place on discard pile */
                        p = (struct Handcard *) malloc(sizeof(struct Handcard));
                        p->card = plcard->card;
                        p->link = discard;
                        discard = p;

                        /* ...and remove from player's hand */
                        if(plcard->link != NULL) {
                            plcard->card = plcard->link->card;
                            p = plcard->link->link;
                            free(plcard->link);
                            plcard->link = p;
                        } else {    /* card at end of hand */
                            if(pl[i] == plcard) {   /* Last card! */
                                free(pl[i]);
                                pl[i] = NULL;
                                break;
                            }
                            for(p=pl[i]; p->link != plcard; p = p->link)
                                ;
                            p->link = NULL;
                            free(plcard);
                        }

                        /* "UNO!" */
                        if(length(pl[i]) == 1 && key == 'U') {
                            uno[i] = 1;
                            beep();
                        }

                    /* Choose color if wild; easy to set if not */
                        if(discard->card->col != WHITE)
                            color = discard->card->col;
                        else {
                            lastcol = color;
                            color = 0;
                            curs_set(1);
                            while(color == 0) {
                                mvprintw(0,0, "Call a color (<R>ed, <B>lue, "
                                              "<G>reen, <Y>ellow): ");
                                clrtoeol();
                                switch(key = tolower(getch())) {
                                case 'r':
                                    color = RED;
                                    break;
                                case 'b':
                                    color = BLUE;
                                    break;
                                case 'g':
                                    color = GREEN;
                                    break;
                                case 'y':
                                    color = YELLOW;
                                    break;
                                default:
                                    mvprintw(0,0, "Not a valid color!");
                                    clrtoeol();
                                    getch();
                                    break;
                                }
                            }
                        }
                    /* More card actions (skip behavior handled 
                     * at start of turn) */
                        switch(discard->card->num) {
                        case REVERSE:
                            dir = -dir;
                            break;
                        case DRAWTWO:
                            drawx = 2;
                            break;
                        case PLUSWILD:
                            drawx = 4;
                            prev = i;
                            break;
                        }

                    } else {
                        mvprintw(0,0, "You can't play that card!");
                        curs_set(1);
                        clrtoeol();
                        getch();
                        curs_set(0);
                        continue;
                    }
                    break;
                case 'Q':                               /* Quit game */
                    mvprintw(0,0, "Really quit?");
                    curs_set(1);
                    clrtoeol();
                    if((key=tolower(getch())) == 'y') {
                        endwin();
                        clean(discard);
                        clean(pile);
                        for(i=0; i<plrs; i++) {
                            clean(pl[i]);
                            free(plname[i]);
                        }
                        printf("Buhbye now :3\n");
                        return 0;
                    } else
                        continue;
                default:
                    continue;
                }
                turn = 0; /* only reached on break, not continue */

            } while(turn);

        } while(length(pl[i]) > 0);     /* round ends when a 
                                         * player has no cards */

        /* Won round (or game, in quick mode) */
        clear();

        /* tallies score (for full game) */
        if(gtype == 'F') {
            attron(COLOR_PAIR(UNO) | A_BOLD);
            mvprintw(maxy/2, maxx/2, "%s won round %d", plname[i], round);
            getch();
            for(j=0; j<plrs; j++)
                if(i != j)
                    for(p = pl[j]; p != NULL; p = p->link)
                        if(p->card->num >= ZERO && p->card->num <= NINE)
                            plscore[i] += p->card->num - ZERO;
                        else if(p->card-> num == WILD 
                                || p->card->num == PLUSWILD)
                            plscore[i] += 50;
                        else
                            plscore[i] += 20;
            if(plscore[i] >= winscore)
                won = i;
        } else
            won = i;

        /* display end-of-round scores in full game */
        if(gtype == 'F') {
            clear();
            attron(COLOR_PAIR(DEF) | A_BOLD);
            mvprintw(0,0, "ROUND %d", round);
            mvprintw(1,0, "PLAYER\t\tSCORE");
            attroff(A_BOLD);
            for(j=0; j<plrs; j++)
                mvprintw(j+3,0, "%-12s\t%000d", plname[j], plscore[j]);
            attron(A_BOLD);
            printw("\n\nWin at %d", winscore);
            getch();
        }

        /* free memory allocated for this round */
        clean(pile);
        clean(discard);
        for(j=0; j<plrs; j++)
            clean(pl[j]);

        round++;
    }

    /* WINNING SCREEN */

    clear();
    attron(COLOR_PAIR(YELLOW) | A_BOLD);
    mvprintw(maxy/2, maxx/2-strlen(plname[won]), "%s is the winner!", plname[won]);
    getch();

    endwin();

    printf("Congrats, %s! gg and see y'all next time...\n\n", plname[won]);

    for(i=0; i<plrs; i++)
        free(plname[i]);

    return 0;

}

/****           -FUNCTIONS-         ****/

/* clean *
 * Frees up a linked list from tail to head end, then sets head to NULL */
void clean(struct Handcard *head)
{
    if(head == NULL) /* don't try cleaning a null list */
        return;

    struct Handcard *p;
    while(head->link != NULL) {
        for(p=head; p->link->link != NULL; p = p->link)
            ;
        free(p->link);
        p->link = NULL;
    }
    free(head);
    head = NULL;
}

/* shuffle *
 * Shuffle whatever pile has been passed to it, starting at head */
void shuffle(struct Handcard *head)
{
    int swap, i;
    struct Handcard *p, *swapcard;
    Card const *temp;

    if(head == NULL)
        return;

    srand(time(NULL));  /* Seed the random number generator each
                         * time we shuffle */

    for(p = head; p != NULL; p = p->link) {
        swap = (rand() % (length(head)));   /* index of swap card */
        temp = p->card;
        for(i=0, swapcard=head; i<swap; i++)    /* walk to swapcard */
            swapcard = swapcard->link;
        p->card = swapcard->card;               /* exchange     */
        swapcard->card = temp;                  /* addresses ;3 */
    }
}

/* showhand *
 * Display hand one card at a time, starting from head */
void showhand(struct Handcard *head)
{
    int y, x;

    if(head == NULL)
        return;

    y = CARDTOP;
    x = CARDLEFT;
    struct Handcard *p;
    for(p = head; p != NULL; p = p->link) {
        attron(COLOR_PAIR(p->card->col) | A_BOLD);
        if(p->card->num >= ZERO && p->card->num <= NINE)
            attroff(A_BOLD);
        mvaddch(y, x, p->card->num);
        y += 2;
        if(y > CARDTOP + CARDLINES*2) {
            x += 4;
            y = CARDTOP;
        }
    }
    attron(COLOR_PAIR(DEF)); attroff(A_BOLD);
}

/* length *
 * Get the length of the hand passed to it */
int length(struct Handcard *head)
{
    struct Handcard *current;
    int count = 0;

    if(head == NULL)
        return 0;
    for(current = head; current != NULL; current = current->link)
        count++;

    return count;
}

/* getcardname *
 * Gets the name of the card as strings */
struct Cardname getcardname(struct Cardname cardname, Card const *card)
{
    /* set color string */
    switch(card->col) {
    case RED:
        cardname.col = "Red ";
        break;
    case BLUE:
        cardname.col = "Blue ";
        break;
    case GREEN:
        cardname.col = "Green ";
        break;
    case YELLOW:
        cardname.col = "Yellow ";
        break;
    default:                /* for wilds */
        cardname.col = "";
        break;
    }

    /* ...and now name string */
    switch(card->num) {
    case REVERSE:
        cardname.num.s = "Reverse";
        break;
    case SKIP:
        cardname.num.s = "Skip";
        break;
    case DRAWTWO:
        cardname.num.s = "Draw Two";
        break;
    case WILD:
        cardname.num.s = "Wild Card";
        break;
    case PLUSWILD:
        cardname.num.s = "Draw Four Wild";
        break;
    default:
        cardname.num.c = card->num;
        break;
    }

    return cardname;
}

/* draw *
 * A more complicated function. Draws a card from the draw pile into
 * the player's hand, and also handles reshuffling deck if draw pile is empty.
 * Returns 1 if a card is successfully drawn, 0 if not */
int draw(struct Handcard **pl, struct Handcard **pile, struct Handcard **discard)
{
    struct Handcard *p, *d, *temp;

    /* No more cards in pile, need to shuffle */
    if(*pile == NULL) {

        /* If discard is also just one card, leave function without drawing
         * or shuffling */
        if((*discard)->link == NULL)
            return 0;

        curs_set(1);
        attron(COLOR_PAIR(DEF)); attroff(A_BOLD);
        mvprintw(0,0, "No more cards to draw. "
                      "Shuffling the discard pile first...");
        clrtoeol();
        getch();
        curs_set(0);

        /* Reassign discard pile (minus top card)
         * to draw pile before shuffling */
        *pile = (struct Handcard *) malloc(sizeof(struct Handcard));
        for(d = (*discard)->link, p = *pile; d != NULL; p = p->link) {
            p->card = d->card;
            if(d->link != NULL)
                p->link = (struct Handcard *) malloc(sizeof(struct Handcard));
            else
                p->link = NULL;
            temp = d;
            d = d->link;
            free(temp);
        }
        (*discard)->link = NULL;
        shuffle(*pile);
    }

    /* Go to end of player's hand */
    for(p = *pl; p->link != NULL; p = p->link)
        ;
    p->link = (struct Handcard *) malloc(sizeof(struct Handcard));
    p = p->link;
    p->card = (*pile)->card;
    p->link = NULL;

    p = *pile;
    if((*pile)->link == NULL) { /* No more cards in the draw pile */
        free(*pile);
        *pile = NULL;
    } else {
        *pile = (*pile)->link;
        free(p);
    }

    return 1;
}
\$\endgroup\$

2 Answers 2

5
\$\begingroup\$
  1. Your shuffling-algorithms is broken: See The Danger of Naïveté for the explanation, and Fisher-Yates shuffle on wikipedia for a working replacement.

    Aside from that, you really should only initialize the random-generator once, at the start of the program.
    (For debuggability, you might want to add an option to specify the seed on the command-line.)

  2. For no obvious reason, you have separated definition and initialization of multdraw, noskip and winscore in main...

  3. You could use getopt for parsing the command-line. As-is, it looks complicated enough to extract into its own (local) function.

  4. Actually, you should really break it up in more functions. If they are only used in the same TU, mark them static. And if you define them before you use them, you don't even need any forward-declaration.

  5. Your handling of cards is far too complicated.
    A deck consists of:

    • Wild card, and Wild card draw four, each four times.

      The wild-cards are special in that they don't actually have a color. Whoever plays one decides which color follows. Best to model that by letting them assume that color.

    • 0 once per color.
    • skip, reverse, draw two, 1 to 9, each twice per color.

    All cards of same color and symbol are indistinguishable, their identities are immaterial.
    So, let's encode them:

    • We have four colors, put that in the low bits.
    • And we have 15 symbols, declaring symbol 0 invalid makes 16.

    6 bits comfortably fit into a char...
    And now we can allocate a single buffer for each hand/deck/pile at the start, and use the first byte as a count. Much easier and faster.

  6. You really should test whether scanw and other input-functions actually succeed in getting the asked number of inputs, not only whether those inputs are in the range you accept...

  7. sizeof(char) is defined as 1, also Don't cast the result of malloc/calloc, and avoid useless zeroing of buffers.

  8. return 0; is implicit in main since C99.

  9. Your handling of single-linked-lists can be improved. Not that you should use any single-linked-list in that program, a counted or 0-terminated string is far superior...

    void clean(struct Handcard* head) { // Was quadratic. Now optimal
        for(struct Handcard* next; head; head = next) {
            next = head->link;
            free(head);
        }
    }
    
  10. I wonder why you added the superfluous null-check in length...

  11. Regarding struct Cardname, I earnestly suggest you not use a union but always two strings.

\$\endgroup\$
6
  • \$\begingroup\$ A weakness to clean(struct Handcard* head) is that although head is free'd, the calling code's head is not affected. By using clean2(struct Handcard** head), the calling code's clean2(&head) can also result in that head == NULL. \$\endgroup\$
    – chux
    Commented Nov 4, 2015 at 18:14
  • \$\begingroup\$ @chux: That's not a weakness, that's a strength. See stackoverflow.com/questions/33374483/… (Though the link is about Visual-C++ and using C++ not not C, it applies.) \$\endgroup\$ Commented Nov 4, 2015 at 18:20
  • \$\begingroup\$ @Deduplicator I really like the idea of using bit fields to store the cards. Not being well-practiced (yet) in using bit masking myself, would you suggest I use the first two bits for the colors (meaning no flag for "no color"), and the rest for the symbols? \$\endgroup\$
    – Earth2
    Commented Nov 4, 2015 at 19:05
  • \$\begingroup\$ Well, you get the most useful encoding if the colors are in the lowest bits (and I would use manual encoding, probably with a helper for getting the ui-color (black+4), one for getting the card-color (4), and one for getting the symbol). See, then we have: 0..3 invalid 4..11 wild cards 12..23 other special cards 24..63 plain old cards \$\endgroup\$ Commented Nov 4, 2015 at 19:12
  • \$\begingroup\$ In that case, I'd recommend OP's code to go from clean(pile); clean(discard); to clean(pile); pile = NULL; clean(discard); discard = NULL; following the C paradigm that free(ptr); ptr = NULL; ... free(ptr); should not error. \$\endgroup\$
    – chux
    Commented Nov 4, 2015 at 19:19
4
\$\begingroup\$

Split main into functions

Right now your program is 80% contained in one function, main(). This makes the code difficult to read, and is not a good practice. You should be splitting your code into functions, such as:

  • Inputting game parameters
  • Initializing game state
  • Displaying the cards on the screen
  • Displaying messages on the screen
  • Receiving and validating user commands
  • Handling each user command
  • Checking for end of game

Use typedefs

You should typedef your structs such as:

typedef struct HandCard HandCard;

so that you can use HandCard instead of struct HandCard everywhere.

Using arrays instead of lists

Your cards are currently stored as linked lists. This unnecessarily complicates your code. For example, adding a card to the end of a list requires you to traverse the linked list in a loop to find the tail. If you had used arrays instead of lists, your code would look a lot cleaner. Since the maximum size of the deck and each hand is bounded by DECKSIZE, you can make each array that size. Then you wouldn't need to malloc and free all over the place. You could make a Hand struct that looked like this:

typedef struct Hand {
    HandCard cards[DECKSIZE];
    int numCards;
} Hand;

and have code that looked like this:

void addCardToHand(Hand *hand, const HandCard *card)
{
    hand->cards[hand->numCards++] = *card;
}

HandCard *getTopCardOfHand(Hand *hand)
{
    if (hand->size == 0)
        return NULL;
    return &hand->cards[hand->numCards-1];
}

void removeTopCardFromHand(Hand *hand)
{
    if (hand->size == 0)
        return;
    hand->size--;
}

A goal to strive for

One goal you can think about is how you could structure your code in such a way that all of the input and drawing code is separated from the main game logic. The idea behind this that if you wanted to port this game to another platform other than ncurses, you could keep all of the game logic code and just replace the gui code with a new implementation.

You probably don't want to bother doing this, because it would be a lot of work for no real benefit (unless you were actually planning to port the game to another platform). But when you write your next game, you could keep this goal in mind and see if you can keep your different components in separate modules such that certain modules such as the gui module could easily be rewritten to support other platforms.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.