refactored for modularity

This commit is contained in:
Gregory Gauthier 2025-10-06 14:58:25 +01:00
parent cdd51fdf5c
commit 6590157cc5
9 changed files with 686 additions and 5 deletions

View File

@ -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()

39
include/game.h Normal file
View File

@ -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 */

22
include/ui.h Normal file
View File

@ -0,0 +1,22 @@
/* ui.h - User interface functions */
#ifndef UI_H
#define UI_H
#include <curses.h>
#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 */

11
include/words.h Normal file
View File

@ -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 */

115
src/game.c Normal file
View File

@ -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 <string.h>
#include <ctype.h>
/* 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;
}

237
src/main.c Normal file
View File

@ -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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <curses.h>
#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);
}

156
src/ui.c Normal file
View File

@ -0,0 +1,156 @@
//
// Created by Gregory Gauthier on 06/10/2025.
//
/* ui.c - User interface implementation */
#include "../include/ui.h"
#include <string.h>
#include <stdio.h>
/* 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]);
}
}
}

83
src/words.c Normal file
View File

@ -0,0 +1,83 @@
//
// Created by Gregory Gauthier on 06/10/2025.
//
/* words.c - Word list management implementation */
#include "../include/words.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <ctype.h>
/* 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;
}