5
\$\begingroup\$

"Wordle50" is a command-line-based adaptation of the popular Wordle daily word game. In this word-guessing game, players aim to decipher a randomly selected word with lengths varying from 5 to 8 characters, drawn from a predefined word list (text files). They are granted one extra guess compared to the length of the target word.

The program provides feedback using color-coded responses - green for correctly placed letters, yellow for correctly identified but misplaced letters, and red for entirely incorrect letters (the original game uses grey for entirely incorrect letters). Players continue their attempts until they accurately reveal the word or exhaust their permitted tries.

Review Goals:

General coding comments, style, potential undefined behavior, et cetera.

Code:

#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <stdbool.h>
#include <time.h>

/* Each of our text files contains 1000 words. */
#define LIST_SIZE       1000
#define FNAME_SIZE      6
#define PROMPT_SIZE     128
#define MAX_WORD_LENGTH 8
#define MIN_WORD_LENGTH 5

#define EXACT   2               /* Right letter; right place. */
#define CLOSE   1               /* Right letter; wrong place. */
#define WRONG   0               /* Wrong letter. */

/* ANSI color codes for boxed in letters.*/
#define GREEN   "\e[38;2;255;255;255;1m\e[48;2;106;170;100;1m"
#define YELLOW  "\e[38;2;255;255;255;1m\e[48;2;201;180;88;1m"
#define RED     "\e[38;2;255;255;255;1m\e[48;2;220;20;60;1m"
#define RESET   "\e[0;39m"

static char *get_guess(size_t wlen)
{
    char *guess = NULL;
    size_t glen = 0;
    ssize_t rc = 0;

    char prompt[PROMPT_SIZE];

    snprintf(prompt, PROMPT_SIZE, "Input a %zu-letter word: ", wlen);

    do {
        fputs(prompt, stdout);
        rc = getline(&guess, &glen, stdin);

        if (rc == -1) {
            break;
        }

        guess[strcspn(guess, "\n")] = '\0';
    } while (strlen(guess) != wlen);

    return guess;
}

static bool guess_exists(const char *restrict guess, size_t lsize, size_t wlen, const char list[lsize][wlen]) 
{
    for (size_t i = 0; i < lsize ; ++i) {
        if (!strcasecmp(guess, list[i])) {
            return true;
        }
    }

    return false;
}

static size_t sum_array(size_t size, const size_t arr[size])
{
    size_t sum = 0;

    for (size_t i = 0; i < size; ++i) {
        sum += arr[i];
    }

    return sum;
}

static size_t calculate_word_score(const char *restrict guess, size_t *restrict status,
                                   const char *restrict choice)
{
    size_t i;

    for (i = 0; guess[i]; ++i) {
        for (size_t j = 0; choice[j]; ++j) {
            if (choice[j] == guess[i]) {
                if (i == j) {
                    status[i] = EXACT;
                } else if (status[i] != EXACT) {
                    status[i] = CLOSE;
                }
            }
        }
    }

    return sum_array(i, status);
}

static void print_word(const char *restrict guess, size_t wlen,
                       const size_t *restrict status)
{
    for (size_t i = 0; i < wlen; ++i) {
        switch (status[i]) {
            case EXACT:
                printf(GREEN "%c" RESET, guess[i]);
                break;

            case CLOSE:
                printf(YELLOW "%c" RESET, guess[i]);
                break;

            default:
                printf(RED "%c" RESET, guess[i]);
                break;
        }
    }
    putchar('\n');
}

static inline bool validate_wlen(size_t wlen)
{
    return wlen >= MIN_WORD_LENGTH && wlen <= MAX_WORD_LENGTH;
}

static bool run_game(size_t lsize, size_t wlen, const char options[lsize][wlen + 1], const char *restrict choice)
{
    /* Allow one more guess than the length of the word. */
    const size_t guesses = wlen + 1;
    bool won = false;

    printf(GREEN "This is WORDLE50" RESET "\n"
           "You have %zu tries to guess the %zu-letter word I'm thinking of\n",
           guesses, wlen);

    for (size_t i = 0; i < guesses; ++i) {
        char *guess = get_guess(wlen);

        /* Check if guess is in the list. */
        while (!guess_exists(guess, LIST_SIZE, wlen + 1, options)) {
            puts("The word is not in list.");
            free(guess);
            guess = get_guess(wlen);
        }
        
        /* Array to hold guess status. */
        size_t status[wlen];

        /* Set all elements of status array initially to 0, aka WRONG. */
        for (size_t i = 0; i < wlen; ++i) {
            status[i] = 0;
        }

        size_t score = calculate_word_score(guess, status, choice);
        printf("Guess %zu: ", i + 1);
        print_word(guess, wlen, status);
        free(guess);
        
        /* If they guessed it exactly right, set terminate loop. */
        if (score == EXACT * wlen) {
            won = true;
            break;
        }
    }

    return won;
}

int main(int argc, char **argv)
{
    if (argc != 2 || !validate_wlen(strtol(argv[1], NULL, 10))) {
        fprintf(stderr, "Usage: %s <k> (5-8)\n", argv[0]);
        return EXIT_FAILURE;
    }

    /* Open correct file, each file has exactly LIST_SIZE words. */
    const size_t wlen = (size_t) strtol(argv[1], NULL, 10);
    char wl_filename[FNAME_SIZE];

    snprintf(wl_filename, FNAME_SIZE, "%zu.txt", wlen);

    FILE *const wlist = fopen(wl_filename, "r");

    if (!wlist) {
        perror("fopen()");
        return EXIT_FAILURE;
    }

    /* Load word file into an array of size LIST_SIZE. */
    char options[LIST_SIZE][wlen + 1];

    for (size_t i = 0; i < LIST_SIZE; ++i) {
        fscanf(wlist, "%s", options[i]);
    }
    
    /* Pseudorandomly select a word for this game. */
    srand(time(NULL));
    const char *const choice = options[rand() % LIST_SIZE];

    run_game(LIST_SIZE, wlen, options, choice) ? puts("You won!") : printf("The target word was: %s\n", choice);
    fclose(wlist);
    return EXIT_SUCCESS;
}

To build the game:

  • Clone this repository: Wordle50
  • cd into the repository.
  • Run:
    make wordle50
    
\$\endgroup\$
0

2 Answers 2

4
\$\begingroup\$

Lack of comments

At least <"Wordle50" is a command-line-based adaptation of ... the target word> deserves to be a code comment, perhaps as part of a command line option "-help".

Longer functions deserve a line or two describing the function goal.

Non-standard code

\e --> \033

getline(), strcasecmp(), ssize_t are not part of the C standard as of C17.

Data validation

I'd expect get_guess() to validate characters read, perhaps to letters.

Correct file?

fscanf(wlist, "%s", options[i]); stands on shaky ground as 1) return value not checked. 2) string size read may exceed options[i] size 3) if file longer than expected, no notice given.

I'd expect the number of words to be driven by the file and not a program constant here.

Consider a command line option to select the files' directory.

More informative prompt

When while (strlen(guess) != wlen) is true, consider a prompt to inform user of the invalidity of input.

Better random initialization

srand(time(NULL)); initializes to the same state if called in the same second as time_t usually has second resolution. Consider xor'ing in other to differentiate if code called multiple times in the same second - which may happen in automated usage.

// srand(time(NULL))
unsigned r = (unsigned) (time(NULL) ^ getpid_or_the_like());
srand(r);

For testing purposes, consider a command line option to override and assign r.

Close when done

fclose(wlist); deserves to be right after the for (size_t i = 0; i < LIST_SIZE; ++i) loop. No need to leave open while running the game.

Avoid magic numbers

As is, code has to update LIST_SIZE and a comment should a value other than 1000 get used.

// From
/* Each of our text files contains 1000 words. */
// To 
/* Each of our text files contains LIST_SIZE words. */

Auto format

Code looks manually formatted ( I could be wrong on this). Better to auto-format. Manually formatting is not productive.

IMO, the code width used is too great.

Define more locally

// size_t i;
// for (i = 0; guess[i]; ++i) {
for (size_t i = 0; guess[i]; ++i) {

// ssize_t rc = 0;
// ...
//    rc = getline(&guess, &glen, stdin);

    ssize_t rc = getline(&guess, &glen, stdin);

Pedantic: No NULL check

Code that calls get_guess() never checks for NULL. Perhaps exit code in get_guess() should allocation fail.

Pedantic: Check argc

argv[0] == NULL is possible.

//                                       NULL? 
fprintf(stderr, "Usage: %s <k> (5-8)\n", argv[0]);

Pedantic: Look for out of range input before changing due to type

validate_wlen(size_t wlen) --> validate_wlen(long wlen) as that is the type strtol() returns.

Minor: Unneeded restrict

Even if guess and status overlap, it is not a concern here as data is only read.

// static void print_word(const char *restrict guess, size_t wlen,
//                   const size_t *restrict status)
static void print_word(const char *guess, size_t wlen, const size_t *status)

With the restrict, code may optimize a tad better, but I do not see its value here.

Minor: Unnecessary dependence on Variable Length Array (VLA) support

VLA support is optional in C11 onward.

char options[LIST_SIZE][wlen + 1]; requires VLA support. Consider char options[LIST_SIZE][MAX_WORD_LENGTH + 1];

... and size_t status[wlen]; --> size_t status[MAX_WORD_LENGTH + 1];

\$\endgroup\$
3
\$\begingroup\$

I don't like the assumptions embedded here:

/* ANSI color codes for boxed in letters.*/
#define GREEN   "\e[38;2;255;255;255;1m\e[48;2;106;170;100;1m"
#define YELLOW  "\e[38;2;255;255;255;1m\e[48;2;201;180;88;1m"
#define RED     "\e[38;2;255;255;255;1m\e[48;2;220;20;60;1m"
#define RESET   "\e[0;39m"

Although terminals with ANSI control sequences are the most popular in new systems, others haven't gone away, and it's quite rude to start spewing control codes without even a cursory look at getenv("TERM").

And why are these macros, rather than char const *const (or char const[]) objects? Where they are used in printf() calls, it's easy to change the string concatenation to use a %s substitution instead (and it may make the resulting program slightly smaller, too).


These values appear related to each other:

#define EXACT   2               /* Right letter; right place. */
#define CLOSE   1               /* Right letter; wrong place. */
#define WRONG   0               /* Wrong letter. */

They would probably be better expressed using enum. That would help your compiler ensure that all cases are covered in switch, for example (once you remove the default: label).


This error message may get desynchronised from the code:

    fprintf(stderr, "Usage: %s <k> (5-8)\n", argv[0]);

It wouldn't be hard to substitute the actual min and max values from MIN_WORD_LENGTH and MAX_WORD_LENGTH (which again should be constants rather than macros).


This fragment suggests we need to create separate per-length wordlists in the user's working directory:

snprintf(wl_filename, FNAME_SIZE, "%zu.txt", wlen);

FILE *const wlist = fopen(wl_filename, "r");

It would be much easier for users if we could take an existing word list (on many systems we can find one in /usr/share/dict/words) and filter it when we use it. As well as being easier, this will reduce bloat.


Do not ever ignore the return value of scanf() functions. And never, ever trust inputs you don't completely control - %s conversion without a length limit is asking for trouble:

This code is simply dangerous:

for (size_t i = 0; i < LIST_SIZE; ++i) {
    fscanf(wlist, "%s", options[i]);
}
\$\endgroup\$
2
  • \$\begingroup\$ It would be much easier for users if we could take an existing word list (on many systems we can find one in /usr/share/dict/words) and filter it when we use it. ==> What might be the alternative for the ones that don't? \$\endgroup\$
    – Harith
    Oct 26, 2023 at 20:40
  • 1
    \$\begingroup\$ I would recommend reading an environment variable (or perhaps command-line option) to find the wordlist, with sensible default. That allows each user to choose which dictionary to use (particularly good on systems with users who speak different languages, or are learning new languages). It's also a natural way to implement different skill levels (my Debian system has several English dictionaries installed of different sizes). \$\endgroup\$ Oct 27, 2023 at 6:08

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.