Created
October 31, 2025 23:39
-
-
Save willkill07/0402675e3deb30f9356a5bf4b411ea90 to your computer and use it in GitHub Desktop.
My own Wordle Guess Program inspired by Dr. Chad Hogg -- Requires C++23
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #include <algorithm> | |
| #include <fstream> | |
| #include <iostream> | |
| #include <ranges> | |
| #include <random> | |
| #include <string> | |
| #include <string_view> | |
| #include <vector> | |
| constexpr int WORD_SIZE{5}; | |
| constexpr int NUM_LETTERS{26}; | |
| constexpr std::string_view DONE{"GGGGG"}; | |
| /// \brief Read all words from the file | |
| /// \return the string of all words | |
| /// | |
| /// The algorithm reads all words from the file "words.txt". | |
| /// The words are returned as a string. | |
| [[nodiscard]] auto read_all_words() -> std::string | |
| { | |
| std::ifstream file("words.txt"); | |
| return std::string{std::istreambuf_iterator<char>(file), {}}; | |
| } | |
| /// \brief Split words into a list of words of a given length | |
| /// \param words the string of words to split | |
| /// \param delimiter the delimiter to split on | |
| /// \param length the length of the words to split | |
| /// \return the list of words | |
| /// | |
| /// The algorithm splits the words into a list of words of a given length. | |
| /// The words are split on the delimiter and the words are filtered to only include words of the given length. | |
| /// The words are filtered to only include words that contain only lowercase letters. | |
| [[nodiscard]] auto split_words(std::string_view words, char delimiter, unsigned length) -> std::vector<std::string_view> | |
| { | |
| return words | |
| | std::views::split(delimiter) | |
| | std::views::filter([length](auto word) { return word.size() == length; }) | |
| | std::views::transform([](auto word) { return std::string_view(word.begin(), word.end()); }) | |
| | std::views::filter([](auto word) { return std::ranges::all_of(word, [](char c) { return 'a' <= c && c <= 'z'; }); }) | |
| | std::ranges::to<std::vector<std::string_view>>(); | |
| } | |
| /// \brief Filter words based on the guess and pattern | |
| /// \param guess the guess word | |
| /// \param words the list of words to filter | |
| /// \param pattern the pattern of the guess | |
| /// \return the filtered list of words | |
| /// | |
| /// The pattern is a string of 'G', 'Y', and 'W' characters, where: | |
| /// - 'G' means the letter is in the word and in the correct position | |
| /// - 'Y' means the letter is in the word but in the wrong position | |
| /// - 'W' means the letter is not in the word | |
| /// | |
| /// The algorithm is a simple brute force algorithm that checks all words against the guess and pattern. | |
| [[nodiscard]] auto filter_words(std::string_view guess, std::vector<std::string_view> words, std::string_view pattern) -> std::vector<std::string_view> | |
| { | |
| std::array<int, NUM_LETTERS> min_letter_counts; | |
| min_letter_counts.fill(0); | |
| for (auto [i, gch]: std::views::enumerate(guess)) { | |
| if (pattern[i] == 'G' || pattern[i] == 'Y') { | |
| ++min_letter_counts[gch - 'a']; | |
| } | |
| } | |
| std::vector<std::string_view> filtered; | |
| filtered.reserve(words.size()); | |
| for (const auto& word : words) { | |
| // Count all letter occurrences in candidate word using array | |
| std::array<int, NUM_LETTERS> word_letter_counts; | |
| word_letter_counts.fill(0); | |
| for (auto ch: word) { | |
| ++word_letter_counts[ch - 'a']; | |
| } | |
| // Validate all constraints using zip and all_of (with early exit) | |
| bool const valid = std::ranges::all_of( | |
| std::views::zip(guess, word, pattern), | |
| [&](const auto& triple) { | |
| if (const auto& [gch, wch, pch] = triple; pch == 'G') { | |
| // Green: exact position match required | |
| return wch == gch; | |
| } else if (pch == 'Y') { | |
| // Yellow: letter exists elsewhere, but not at this position | |
| return (wch != gch) && (word_letter_counts[gch - 'a'] > 0); | |
| } else if (pch == 'W') { | |
| // White: letter count cannot exceed minimum required (G+Y) | |
| return (wch != gch) && (word_letter_counts[gch - 'a'] == min_letter_counts[gch - 'a']); | |
| } | |
| return true; // Unknown pattern character, skip validation | |
| } | |
| ) && std::ranges::all_of( | |
| std::views::zip(word_letter_counts, min_letter_counts), | |
| [](const auto& counts) { | |
| const auto& [word_count, min_count] = counts; | |
| return word_count >= min_count; | |
| } | |
| ); | |
| if (valid) { | |
| filtered.push_back(word); | |
| } | |
| } | |
| return filtered; | |
| } | |
| /// \brief Guess the best word based on the letter frequency at each position | |
| /// \param words the list of words to guess from | |
| /// \return the best word | |
| /// | |
| /// The algorithm counts the frequency of each letter at each position in the words. | |
| /// The word with the highest score is the word with the highest sum of letter frequencies at each position. | |
| [[nodiscard]] auto guess_best_word(std::vector<std::string_view> words) -> std::string_view | |
| { | |
| std::array<std::array<int, NUM_LETTERS>, WORD_SIZE> letter_pos_counts; | |
| for (auto& row: letter_pos_counts) { | |
| row.fill(0); | |
| } | |
| for (const auto& word : words) { | |
| for (auto [i, ch]: std::views::enumerate(word)) { | |
| ++letter_pos_counts[i][ch - 'a']; | |
| } | |
| } | |
| auto score = [&letter_pos_counts](std::string_view word) -> int { | |
| return std::ranges::fold_left( | |
| std::views::enumerate(word), | |
| 0, | |
| [&letter_pos_counts](int acc, const auto& pair) { | |
| const auto& [i, ch] = pair; | |
| return acc + letter_pos_counts[i][ch - 'a']; | |
| } | |
| ); | |
| }; | |
| return *std::ranges::max_element(words, std::ranges::less{}, score); | |
| } | |
| auto main() -> int { | |
| std::minstd_rand rng{std::random_device{}()}; | |
| auto words = read_all_words(); | |
| auto word_list = split_words(words, '\n', WORD_SIZE); | |
| std::string pattern; | |
| while (true) { | |
| std::string_view guess = guess_best_word(word_list); | |
| std::cout << "You should guess: " << guess << std::endl; | |
| std::cout << "Enter a pattern: "; | |
| std::cin >> pattern; | |
| if (pattern == DONE) { | |
| std::cout << "You win!" << std::endl; | |
| break; | |
| } | |
| word_list = filter_words(guess, word_list, pattern); | |
| // Shuffle the word list to potentially randomize the next guess | |
| std::ranges::shuffle(word_list, rng); | |
| std::cout << word_list.size() << " words remaining" << std::endl; | |
| if (word_list.empty()) { | |
| std::cout << "Did you make a mistake?" << std::endl; | |
| break; | |
| } | |
| } | |
| return 0; | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment