From 6590157cc50ccc6eca566fa0f4bc6314facc880d Mon Sep 17 00:00:00 2001 From: Gregory Gauthier Date: Mon, 6 Oct 2025 14:58:25 +0100 Subject: [PATCH] refactored for modularity --- CMakeLists.txt | 28 ++++- cordle.c => _orig_cordle. | 0 include/game.h | 39 +++++++ include/ui.h | 22 ++++ include/words.h | 11 ++ src/game.c | 115 ++++++++++++++++++ src/main.c | 237 ++++++++++++++++++++++++++++++++++++++ src/ui.c | 156 +++++++++++++++++++++++++ src/words.c | 83 +++++++++++++ 9 files changed, 686 insertions(+), 5 deletions(-) rename cordle.c => _orig_cordle. (100%) create mode 100644 include/game.h create mode 100644 include/ui.h create mode 100644 include/words.h create mode 100644 src/game.c create mode 100644 src/main.c create mode 100644 src/ui.c create mode 100644 src/words.c diff --git a/CMakeLists.txt b/CMakeLists.txt index 68d1851..a1b61c7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,13 +1,31 @@ -cmake_minimum_required(VERSION 4.0) +cmake_minimum_required(VERSION 3.10) project(cordle C) set(CMAKE_C_STANDARD 90) +set(CMAKE_C_STANDARD_REQUIRED ON) -# Find the curses library +# Find ncurses library find_package(Curses REQUIRED) -add_executable(cordle cordle.c) +# Include directories +include_directories(include) +include_directories(${CURSES_INCLUDE_DIR}) -# Link against the curses library +# Source files +set(SOURCES + src/main.c + src/game.c + src/words.c + src/ui.c +) + +# Create executable +add_executable(cordle ${SOURCES}) + +# Link ncurses target_link_libraries(cordle ${CURSES_LIBRARIES}) -target_include_directories(cordle PRIVATE ${CURSES_INCLUDE_DIRS}) + +# Set compiler flags for C90 compliance +if(CMAKE_C_COMPILER_ID MATCHES "GNU|Clang") + target_compile_options(cordle PRIVATE -Wall -Wextra -pedantic) +endif() diff --git a/cordle.c b/_orig_cordle. similarity index 100% rename from cordle.c rename to _orig_cordle. diff --git a/include/game.h b/include/game.h new file mode 100644 index 0000000..ca18eae --- /dev/null +++ b/include/game.h @@ -0,0 +1,39 @@ + +/* game.h - Game logic and state */ +#ifndef GAME_H +#define GAME_H + +#define MAX_FILENAME 256 +#define MAX_MESSAGE 256 + +#define MAX_WORDS 10000 +#define WORD_LENGTH 5 +#define MAX_GUESSES 6 + +/* Letter status constants */ +#define STATUS_UNUSED 0 +#define STATUS_ABSENT 1 +#define STATUS_PRESENT 2 +#define STATUS_CORRECT 3 + +/* Game state structure */ +typedef struct { + char words[MAX_WORDS][WORD_LENGTH + 1]; + int word_count; + char target_word[WORD_LENGTH + 1]; + char guesses[MAX_GUESSES][WORD_LENGTH + 1]; + int guess_colors[MAX_GUESSES][WORD_LENGTH]; + int guess_count; + char current_guess[WORD_LENGTH + 1]; + int current_guess_length; + int game_over; + int won; + int letter_status[26]; +} GameState; + +/* Function declarations */ +void init_game(GameState* game); +int make_guess(GameState* game, const char* guess); +void check_guess(GameState* game, const char* guess, int* colors); + +#endif /* GAME_H */ diff --git a/include/ui.h b/include/ui.h new file mode 100644 index 0000000..9129e83 --- /dev/null +++ b/include/ui.h @@ -0,0 +1,22 @@ +/* ui.h - User interface functions */ +#ifndef UI_H +#define UI_H + +#include +#include "game.h" + +/* Color pair constants */ +#define COLOR_DEFAULT 1 +#define COLOR_CORRECT 2 +#define COLOR_PRESENT 3 +#define COLOR_ABSENT 4 +#define COLOR_WWHITE 7 +#define COLOR_UNUSED 6 + +void draw_title(WINDOW* win, int y, const char* difficulty); +void draw_board(WINDOW* win, GameState* game, int y); +void draw_keyboard(WINDOW* win, GameState* game, int y); +void draw_message(WINDOW* win, const char* message, int y, int color_pair); +void draw_instructions(WINDOW* win, int y); + +#endif /* UI_H */ diff --git a/include/words.h b/include/words.h new file mode 100644 index 0000000..f84dc2f --- /dev/null +++ b/include/words.h @@ -0,0 +1,11 @@ +/* words.h - Word list management */ +#ifndef WORDS_H +#define WORDS_H + +#include "game.h" + +int load_words(GameState* game, const char* filename); +int is_valid_word(GameState* game, const char* word); +void to_upper(char* str); + +#endif /* WORDS_H */ diff --git a/src/game.c b/src/game.c new file mode 100644 index 0000000..052f648 --- /dev/null +++ b/src/game.c @@ -0,0 +1,115 @@ +// +// Created by Gregory Gauthier on 06/10/2025. +// +/* game.c - Game logic implementation */ +#include "../include/game.h" +#include "../include/ui.h" +#include +#include + +/* Initialize game state */ +void init_game(GameState* game) { + int i, j; + + game->word_count = 0; + game->target_word[0] = '\0'; + game->guess_count = 0; + game->current_guess[0] = '\0'; + game->current_guess_length = 0; + game->game_over = 0; + game->won = 0; + + /* Initialize letter status to unused */ + for (i = 0; i < 26; i++) { + game->letter_status[i] = STATUS_UNUSED; + } + + /* Clear guesses */ + for (i = 0; i < MAX_GUESSES; i++) { + game->guesses[i][0] = '\0'; + for (j = 0; j < WORD_LENGTH; j++) { + game->guess_colors[i][j] = COLOR_DEFAULT; + } + } +} + +/* Check guess against target word */ +void check_guess(GameState* game, const char* guess, int* colors) { + char target_copy[WORD_LENGTH + 1]; + char guess_copy[WORD_LENGTH + 1]; + int i, j; + int target_used[WORD_LENGTH] = {0}; + + strcpy(target_copy, game->target_word); + strcpy(guess_copy, guess); + + /* Ensure uppercase */ + for (i = 0; i < WORD_LENGTH; i++) { + guess_copy[i] = toupper(guess_copy[i]); + } + + /* Initialize all as absent */ + for (i = 0; i < WORD_LENGTH; i++) { + colors[i] = COLOR_ABSENT; + } + + /* First pass: mark correct positions */ + for (i = 0; i < WORD_LENGTH; i++) { + if (guess_copy[i] == target_copy[i]) { + colors[i] = COLOR_CORRECT; + target_used[i] = 1; + game->letter_status[guess_copy[i] - 'A'] = STATUS_CORRECT; + } + } + + /* Second pass: mark present but wrong position */ + for (i = 0; i < WORD_LENGTH; i++) { + if (colors[i] == COLOR_ABSENT) { + for (j = 0; j < WORD_LENGTH; j++) { + if (!target_used[j] && guess_copy[i] == target_copy[j]) { + colors[i] = COLOR_PRESENT; + target_used[j] = 1; + if (game->letter_status[guess_copy[i] - 'A'] != STATUS_CORRECT) { + game->letter_status[guess_copy[i] - 'A'] = STATUS_PRESENT; + } + break; + } + } + /* If still absent, mark letter as not in word */ + if (colors[i] == COLOR_ABSENT) { + if (game->letter_status[guess_copy[i] - 'A'] == STATUS_UNUSED) { + game->letter_status[guess_copy[i] - 'A'] = STATUS_ABSENT; + } + } + } + } +} + +/* Process a guess */ +int make_guess(GameState* game, const char* guess) { + char upper_guess[WORD_LENGTH + 1]; + int i; + + if (strlen(guess) != WORD_LENGTH) { + return 0; + } + + strcpy(upper_guess, guess); + for (i = 0; i < WORD_LENGTH; i++) { + upper_guess[i] = toupper(upper_guess[i]); + } + + strcpy(game->guesses[game->guess_count], upper_guess); + check_guess(game, guess, game->guess_colors[game->guess_count]); + game->guess_count++; + + if (strcmp(upper_guess, game->target_word) == 0) { + game->game_over = 1; + game->won = 1; + } else if (game->guess_count >= MAX_GUESSES) { + game->game_over = 1; + game->won = 0; + } + + return 1; +} \ No newline at end of file diff --git a/src/main.c b/src/main.c new file mode 100644 index 0000000..4398add --- /dev/null +++ b/src/main.c @@ -0,0 +1,237 @@ +// +// Created by Gregory Gauthier on 06/10/2025. +// + +/* main.c - Entry point and main game loop */ +/* Compile with: gcc -std=c90 -o cordle src/*.c -Iinclude -lncurses */ + +#include +#include +#include +#include +#include +#include "../include/game.h" +#include "../include/words.h" +#include "../include/ui.h" + +/* Parse command line arguments */ +void parse_arguments(int argc, char* argv[], char* filename, char* difficulty) { + int i; + + /* Default values */ + strcpy(filename, "pyrdle_words_easy.txt"); + strcpy(difficulty, "EASY"); + + for (i = 1; i < argc; i++) { + if (strcmp(argv[i], "--easy") == 0) { + strcpy(filename, "pyrdle_words_easy.txt"); + strcpy(difficulty, "EASY"); + } else if (strcmp(argv[i], "--medium") == 0) { + strcpy(filename, "pyrdle_words_medium.txt"); + strcpy(difficulty, "MEDIUM"); + } else if (strcmp(argv[i], "--hard") == 0) { + strcpy(filename, "pyrdle_words_hard.txt"); + strcpy(difficulty, "HARD"); + } else if (strcmp(argv[i], "--techy") == 0) { + strcpy(filename, "pyrdle_words_techy.txt"); + strcpy(difficulty, "TECHNICAL"); + } else if (strcmp(argv[i], "--literary") == 0) { + strcpy(filename, "pyrdle_words_literary.txt"); + strcpy(difficulty, "LITERARY"); + } else if (strcmp(argv[i], "--cultural") == 0) { + strcpy(filename, "pyrdle_words_cultural.txt"); + strcpy(difficulty, "CULTURAL"); + } else if (strcmp(argv[i], "--full") == 0) { + strcpy(filename, "pyrdle_words_full.txt"); + strcpy(difficulty, "FULL"); + } else if (strcmp(argv[i], "--wordlist") == 0 && i + 1 < argc) { + strcpy(filename, argv[i + 1]); + strcpy(difficulty, "CUSTOM"); + i++; /* Skip next argument */ + } + } +} + +/* Main game loop */ +int main_game_loop(int argc, char* argv[]) { + WINDOW* stdscr; + GameState game; + char filename[MAX_FILENAME]; + char difficulty[32]; + char message[MAX_MESSAGE] = ""; + int message_color = COLOR_DEFAULT; + int height, width, y_pos; + int key; + char temp_guess[WORD_LENGTH + 1]; + char win_message[MAX_MESSAGE]; + + /* Get command line arguments */ + parse_arguments(argc, argv, filename, difficulty); + + /* Initialize ncurses */ + stdscr = initscr(); + curs_set(0); /* Hide cursor */ + clear(); + + /* Initialize colors */ + start_color(); + init_pair(COLOR_DEFAULT, COLOR_WWHITE, COLOR_BLACK); + init_pair(COLOR_CORRECT, COLOR_BLACK, COLOR_GREEN); + init_pair(COLOR_PRESENT, COLOR_BLACK, COLOR_YELLOW); + init_pair(COLOR_ABSENT, COLOR_WWHITE, COLOR_RED); + init_pair(COLOR_WWHITE, COLOR_WWHITE, COLOR_BLACK); + init_pair(COLOR_UNUSED, COLOR_WWHITE, COLOR_BLACK); + + /* Initialize game */ + init_game(&game); + + if (!load_words(&game, filename)) { + /* Try fallback */ + if (strcmp(filename, "pyrdle_words.txt") != 0 && + load_words(&game, "pyrdle_words.txt")) { + strcpy(difficulty, "DEFAULT"); + } else { + mvaddstr(0, 0, "Error: Could not load word list"); + mvaddstr(1, 0, "Press any key to exit..."); + getch(); + endwin(); + return 1; + } + } + + /* Main game loop */ + while (1) { + clear(); + getmaxyx(stdscr, height, width); + + y_pos = 1; + + /* Draw components */ + draw_title(stdscr, y_pos, difficulty); + y_pos += 2; + + draw_board(stdscr, &game, y_pos); + y_pos += MAX_GUESSES * 2 + 1; + + draw_keyboard(stdscr, &game, y_pos); + y_pos += 4; + + /* Draw message if exists */ + if (strlen(message) > 0) { + draw_message(stdscr, message, y_pos, message_color); + y_pos += 2; + } + + /* Draw instructions */ + if (y_pos + 6 < height) { + draw_instructions(stdscr, y_pos); + } + + /* Handle game over */ + if (game.game_over) { + y_pos = (height > 20) ? height - 3 : y_pos; + + if (game.won) { + sprintf(win_message, "Congratulations! You won in %d guesses!", + game.guess_count); + draw_message(stdscr, win_message, y_pos, COLOR_CORRECT); + } else { + sprintf(win_message, "Game Over! The word was: %s", + game.target_word); + draw_message(stdscr, win_message, y_pos, COLOR_ABSENT); + } + + draw_message(stdscr, "Press 'N' for new game or 'Q' to quit", + y_pos + 1, COLOR_DEFAULT); + + refresh(); + key = getch(); + + if (key == 'n' || key == 'N') { + /* Start new game */ + init_game(&game); + load_words(&game, filename); + strcpy(message, ""); + continue; + } else if (key == 'q' || key == 'Q' || key == 27) { + break; + } + continue; + } + + refresh(); + + /* Get input */ + key = getch(); + + /* Handle input */ + if (key == 27) { /* ESC */ + break; + } else if (key == '\n' || key == '\r' || key == KEY_ENTER) { + if (game.current_guess_length == WORD_LENGTH) { + strcpy(temp_guess, game.current_guess); + if (make_guess(&game, temp_guess)) { + game.current_guess[0] = '\0'; + game.current_guess_length = 0; + strcpy(message, ""); + } else { + /* Check if word is valid */ + if (!is_valid_word(&game, temp_guess)) { + strcpy(message, "Not in word list!"); + message_color = COLOR_ABSENT; + } + } + } else { + sprintf(message, "Word must be %d letters!", WORD_LENGTH); + message_color = COLOR_ABSENT; + } + } else if (key == KEY_BACKSPACE || key == 127 || key == '\b') { + if (game.current_guess_length > 0) { + game.current_guess_length--; + game.current_guess[game.current_guess_length] = '\0'; + strcpy(message, ""); + } + } else if ((key >= 'a' && key <= 'z') || (key >= 'A' && key <= 'Z')) { + if (game.current_guess_length < WORD_LENGTH) { + game.current_guess[game.current_guess_length] = toupper(key); + game.current_guess_length++; + game.current_guess[game.current_guess_length] = '\0'; + strcpy(message, ""); + } + } + } + + endwin(); + return 0; +} + +/* Main entry point */ +int main(int argc, char* argv[]) { + int i; + + /* Handle help */ + for (i = 1; i < argc; i++) { + if (strcmp(argv[i], "--help") == 0 || strcmp(argv[i], "-h") == 0) { + printf("cordle - C90 Wordle Game\n\n"); + printf("Usage: %s [OPTIONS]\n\n", argv[0]); + printf("Difficulty Levels:\n"); + printf(" --easy Common everyday words (default)\n"); + printf(" --medium Standard vocabulary\n"); + printf(" --hard Challenging words\n\n"); + printf("Word Categories:\n"); + printf(" --techy Technical and scientific terms\n"); + printf(" --literary Literary and archaic terms\n"); + printf(" --cultural Cultural and international terms\n"); + printf(" --full Complete dictionary\n\n"); + printf("Custom:\n"); + printf(" --wordlist FILE Use custom word list\n\n"); + printf("Examples:\n"); + printf(" %s --easy\n", argv[0]); + printf(" %s --techy\n", argv[0]); + printf(" %s --wordlist mywords.txt\n", argv[0]); + return 0; + } + } + + return main_game_loop(argc, argv); +} \ No newline at end of file diff --git a/src/ui.c b/src/ui.c new file mode 100644 index 0000000..f6db75d --- /dev/null +++ b/src/ui.c @@ -0,0 +1,156 @@ +// +// Created by Gregory Gauthier on 06/10/2025. +// +/* ui.c - User interface implementation */ +#include "../include/ui.h" +#include +#include + +/* Keyboard layout */ +static const char* keyboard_rows[3] = { + "QWERTYUIOP", + "ASDFGHJKL", + "ZXCVBNM" +}; + +/* Draw game title */ +void draw_title(WINDOW* win, int y, const char* difficulty) { + char title[256]; + int height, width, x; + + sprintf(title, "CORDLE - The C90 Wordle Game [%s]", difficulty); + + getmaxyx(win, height, width); + x = (width - strlen(title)) / 2; + + wattron(win, A_BOLD); + mvwaddstr(win, y, x, title); + wattroff(win, A_BOLD); +} + +/* Draw the game board */ +void draw_board(WINDOW* win, GameState* game, int y) { + int height, width, board_x; + int row, col, x_pos, y_pos; + char cell[4]; + + getmaxyx(win, height, width); + board_x = (width - (WORD_LENGTH * 4 - 1)) / 2; + + for (row = 0; row < MAX_GUESSES; row++) { + y_pos = y + row * 2; + + if (row < game->guess_count) { + /* Draw completed guess */ + for (col = 0; col < WORD_LENGTH; col++) { + x_pos = board_x + col * 4; + sprintf(cell, "[%c]", game->guesses[row][col]); + + wattron(win, COLOR_PAIR(game->guess_colors[row][col])); + mvwaddstr(win, y_pos, x_pos, cell); + wattroff(win, COLOR_PAIR(game->guess_colors[row][col])); + } + } else if (row == game->guess_count && !game->game_over) { + /* Draw current guess */ + for (col = 0; col < WORD_LENGTH; col++) { + x_pos = board_x + col * 4; + if (col < game->current_guess_length) { + sprintf(cell, "[%c]", game->current_guess[col]); + } else { + strcpy(cell, "[ ]"); + } + + wattron(win, COLOR_PAIR(COLOR_WWHITE)); + mvwaddstr(win, y_pos, x_pos, cell); + wattroff(win, COLOR_PAIR(COLOR_WWHITE)); + } + } else { + /* Draw empty slots */ + for (col = 0; col < WORD_LENGTH; col++) { + x_pos = board_x + col * 4; + mvwaddstr(win, y_pos, x_pos, "[ ]"); + } + } + } +} + +/* Draw visual keyboard */ +void draw_keyboard(WINDOW* win, GameState* game, int y) { + int height, width; + int row_idx, x, y_pos, i; + const char* row; + char letter; + int status, color; + + getmaxyx(win, height, width); + + for (row_idx = 0; row_idx < 3; row_idx++) { + row = keyboard_rows[row_idx]; + x = (width - strlen(row) * 2 + 1) / 2; + y_pos = y + row_idx; + + /* Add indentation for keyboard layout */ + if (row_idx == 1) x += 1; + else if (row_idx == 2) x += 3; + + for (i = 0; row[i]; i++) { + letter = row[i]; + status = game->letter_status[letter - 'A']; + + switch (status) { + case STATUS_CORRECT: color = COLOR_CORRECT; break; + case STATUS_PRESENT: color = COLOR_PRESENT; break; + case STATUS_ABSENT: color = COLOR_ABSENT; break; + default: color = COLOR_UNUSED; break; + } + + if (color == COLOR_UNUSED) { + wattron(win, A_DIM); + mvwaddch(win, y_pos, x, letter); + wattroff(win, A_DIM); + } else { + wattron(win, COLOR_PAIR(color)); + mvwaddch(win, y_pos, x, letter); + wattroff(win, COLOR_PAIR(color)); + } + x += 2; + } + } +} + +/* Draw centered message */ +void draw_message(WINDOW* win, const char* message, int y, int color_pair) { + int height, width, x; + + getmaxyx(win, height, width); + x = (width - strlen(message)) / 2; + + wattron(win, COLOR_PAIR(color_pair)); + mvwaddstr(win, y, x, message); + wattroff(win, COLOR_PAIR(color_pair)); +} + +/* Draw game instructions */ +void draw_instructions(WINDOW* win, int y) { + const char* instructions[] = { + "Guess the 5-letter word in 6 tries!", + "", + "Colors: GREEN=Correct, YELLOW=Wrong position, RED=Not in word", + "", + "Type letters: A-Z", + "Enter: Submit guess", + "Backspace: Delete letter", + "Ctrl+C or ESC: Quit game" + }; + int num_instructions = 8; + int height, width, x, i; + + getmaxyx(win, height, width); + + for (i = 0; i < num_instructions; i++) { + if (y + i < height - 1) { + x = (width - strlen(instructions[i])) / 2; + mvwaddstr(win, y + i, x, instructions[i]); + } + } +} \ No newline at end of file diff --git a/src/words.c b/src/words.c new file mode 100644 index 0000000..a7f4ec6 --- /dev/null +++ b/src/words.c @@ -0,0 +1,83 @@ +// +// Created by Gregory Gauthier on 06/10/2025. +// +/* words.c - Word list management implementation */ +#include "../include/words.h" +#include +#include +#include +#include +#include + +/* Convert string to uppercase */ +void to_upper(char* str) { + int i; + for (i = 0; str[i]; i++) { + str[i] = toupper(str[i]); + } +} + +/* Load words from file */ +int load_words(GameState* game, const char* filename) { + FILE* file; + char filepath[MAX_FILENAME]; + char line[32]; + char word[WORD_LENGTH + 1]; + int len; + + /* Construct file path */ + sprintf(filepath, "wordlists/%s", filename); + + file = fopen(filepath, "r"); + if (!file) { + /* Try without wordlists/ prefix */ + file = fopen(filename, "r"); + if (!file) { + return 0; + } + } + + game->word_count = 0; + while (fgets(line, sizeof(line), file) && game->word_count < MAX_WORDS) { + /* Remove newline and whitespace */ + len = strlen(line); + while (len > 0 && (line[len-1] == '\n' || line[len-1] == '\r' || line[len-1] == ' ')) { + line[--len] = '\0'; + } + + if (len == WORD_LENGTH) { + strcpy(word, line); + to_upper(word); + strcpy(game->words[game->word_count], word); + game->word_count++; + } + } + + fclose(file); + + if (game->word_count == 0) { + return 0; + } + + /* Select random target word */ + srand((unsigned int)time(NULL)); + strcpy(game->target_word, game->words[rand() % game->word_count]); + + return 1; +} + +/* Check if word exists in word list */ +int is_valid_word(GameState* game, const char* word) { + int i; + char upper_word[WORD_LENGTH + 1]; + + strcpy(upper_word, word); + to_upper(upper_word); + + for (i = 0; i < game->word_count; i++) { + if (strcmp(game->words[i], upper_word) == 0) { + return 1; + } + } + return 0; +}