Serenity Operating System
at master 324 lines 11 kB view raw
1/* 2 * Copyright (c) 2022, Joe Petrus <joe@petrus.io> 3 * 4 * SPDX-License-Identifier: BSD-2-Clause 5 */ 6 7#include "WordGame.h" 8#include <AK/QuickSort.h> 9#include <AK/Random.h> 10#include <AK/StringView.h> 11#include <LibConfig/Client.h> 12#include <LibCore/Timer.h> 13#include <LibGUI/Application.h> 14#include <LibGUI/MessageBox.h> 15#include <LibGUI/Painter.h> 16#include <LibGUI/Widget.h> 17#include <LibGfx/Font/Font.h> 18#include <LibGfx/Font/FontDatabase.h> 19#include <LibGfx/Palette.h> 20 21REGISTER_WIDGET(MasterWord, WordGame) 22 23// TODO: Add stats 24namespace MasterWord { 25 26WordGame::WordGame() 27 : m_clear_message_timer(Core::Timer::create_single_shot(5000, [this] { clear_message(); }).release_value_but_fixme_should_propagate_errors()) 28{ 29 read_words(); 30 m_num_letters = Config::read_i32("MasterWord"sv, ""sv, "word_length"sv, 5); 31 m_max_guesses = Config::read_i32("MasterWord"sv, ""sv, "max_guesses"sv, 6); 32 m_check_guesses = Config::read_bool("MasterWord"sv, ""sv, "check_guesses_in_dictionary"sv, false); 33 reset(); 34 pick_font(); 35} 36 37void WordGame::reset() 38{ 39 m_current_guess = {}; 40 m_guesses.clear(); 41 auto maybe_word = WordGame::random_word(m_num_letters); 42 if (maybe_word.has_value()) 43 m_current_word = maybe_word.value(); 44 else { 45 GUI::MessageBox::show(window(), DeprecatedString::formatted("Could not get a random {} letter word. Defaulting to 5.", m_num_letters), "MasterWord"sv); 46 if (m_num_letters != 5) { 47 m_num_letters = 5; 48 reset(); 49 } 50 } 51 clear_message(); 52 update(); 53} 54 55void WordGame::pick_font() 56{ 57 DeprecatedString best_font_name; 58 auto best_font_size = -1; 59 auto& font_database = Gfx::FontDatabase::the(); 60 font_database.for_each_font([&](Gfx::Font const& font) { 61 if (font.family() != "Liza" || font.weight() != 700) 62 return; 63 auto size = font.pixel_size_rounded_up(); 64 if (size * 2 <= m_letter_height && size > best_font_size) { 65 best_font_name = font.qualified_name(); 66 best_font_size = size; 67 } 68 }); 69 70 auto font = font_database.get_by_name(best_font_name); 71 set_font(font); 72} 73 74void WordGame::resize_event(GUI::ResizeEvent&) 75{ 76 pick_font(); 77 update(); 78} 79 80void WordGame::keydown_event(GUI::KeyEvent& event) 81{ 82 // If we can still add a letter and the key was alpha 83 if (m_current_guess.length() < m_num_letters && is_ascii_alpha(event.code_point())) { 84 m_current_guess = DeprecatedString::formatted("{}{}", m_current_guess, event.text().to_uppercase()); 85 m_last_word_invalid = false; 86 } 87 // If backspace pressed and already have some letters entered 88 else if (event.key() == KeyCode::Key_Backspace && m_current_guess.length() > 0) { 89 m_current_guess = m_current_guess.substring(0, m_current_guess.length() - 1); 90 m_last_word_invalid = false; 91 } 92 // If return pressed 93 else if (event.key() == KeyCode::Key_Return) { 94 if (m_current_guess.length() < m_num_letters) { 95 show_message("Not enough letters"sv); 96 m_last_word_invalid = true; 97 } else if (!is_in_dictionary(m_current_guess)) { 98 show_message("Not in dictionary"sv); 99 m_last_word_invalid = true; 100 } else { 101 m_last_word_invalid = false; 102 clear_message(); 103 104 add_guess(m_current_guess); 105 auto won = m_current_guess == m_current_word; 106 m_current_guess = {}; 107 if (won) { 108 GUI::MessageBox::show(window(), "You win!"sv, "MasterWord"sv); 109 reset(); 110 } else if (m_guesses.size() == m_max_guesses) { 111 GUI::MessageBox::show(window(), DeprecatedString::formatted("You lose!\nThe word was {}", m_current_word), "MasterWord"sv); 112 reset(); 113 } 114 } 115 } else { 116 event.ignore(); 117 } 118 119 update(); 120} 121 122void WordGame::paint_event(GUI::PaintEvent& event) 123{ 124 GUI::Frame::paint_event(event); 125 GUI::Painter painter(*this); 126 painter.add_clip_rect(frame_inner_rect()); 127 painter.add_clip_rect(event.rect()); 128 painter.fill_rect(event.rect(), m_background_color); 129 130 for (size_t guess_index = 0; guess_index < m_max_guesses; ++guess_index) { 131 for (size_t letter_index = 0; letter_index < m_num_letters; ++letter_index) { 132 auto this_rect = letter_rect(guess_index, letter_index); 133 134 if (guess_index < m_guesses.size()) { 135 136 switch (m_guesses[guess_index].letter_states.at(letter_index)) { 137 case Correct: 138 painter.fill_rect(this_rect, m_right_letter_right_spot_color); 139 break; 140 case WrongSpot: 141 painter.fill_rect(this_rect, m_right_letter_wrong_spot_color); 142 break; 143 case Incorrect: 144 painter.fill_rect(this_rect, m_wrong_letter_color); 145 break; 146 } 147 148 painter.draw_text(this_rect, m_guesses[guess_index].text.substring_view(letter_index, 1), font(), Gfx::TextAlignment::Center, m_text_color); 149 } else if (guess_index == m_guesses.size()) { 150 if (letter_index < m_current_guess.length()) 151 painter.draw_text(this_rect, m_current_guess.substring_view(letter_index, 1), font(), Gfx::TextAlignment::Center, m_text_color); 152 if (m_last_word_invalid) { 153 painter.fill_rect(this_rect, m_word_not_in_dict_color); 154 } 155 } 156 157 painter.draw_rect(this_rect, m_border_color); 158 } 159 } 160} 161 162Gfx::IntRect WordGame::letter_rect(size_t guess_number, size_t letter_number) const 163{ 164 auto letter_left = m_outer_margin + letter_number * m_letter_width + m_letter_spacing * letter_number; 165 auto letter_top = m_outer_margin + guess_number * m_letter_height + m_letter_spacing * guess_number; 166 return Gfx::IntRect(int(letter_left), int(letter_top), m_letter_width, m_letter_height); 167} 168 169bool WordGame::is_in_dictionary(AK::StringView guess) 170{ 171 return !m_check_guesses || !m_words.ensure(guess.length()).find(guess).is_end(); 172} 173 174void WordGame::read_words() 175{ 176 m_words.clear(); 177 178 auto try_load_words = [&]() -> ErrorOr<void> { 179 auto response = TRY(Core::File::open("/res/words.txt"sv, Core::File::OpenMode::Read)); 180 auto words_file = TRY(Core::BufferedFile::create(move(response))); 181 Array<u8, 128> buffer; 182 183 while (!words_file->is_eof()) { 184 auto current_word = TRY(words_file->read_line(buffer)); 185 if (!current_word.starts_with('#') and current_word.length() > 0) 186 m_words.ensure(current_word.length()).append(current_word.to_uppercase_string()); 187 } 188 189 return {}; 190 }; 191 192 if (try_load_words().is_error()) { 193 GUI::MessageBox::show(nullptr, "Could not read /res/words.txt.\nPlease ensure this file exists and restart MasterWord."sv, "MasterWord"sv); 194 exit(0); 195 } 196} 197 198Optional<DeprecatedString> WordGame::random_word(size_t length) 199{ 200 auto words_for_length = m_words.get(length); 201 if (words_for_length.has_value()) { 202 auto i = get_random_uniform(words_for_length->size()); 203 return words_for_length->at(i); 204 } 205 206 return {}; 207} 208 209size_t WordGame::shortest_word() 210{ 211 auto available_lengths = m_words.keys(); 212 AK::quick_sort(available_lengths); 213 return available_lengths.first(); 214} 215 216size_t WordGame::longest_word() 217{ 218 auto available_lengths = m_words.keys(); 219 AK::quick_sort(available_lengths); 220 return available_lengths.last(); 221} 222 223void WordGame::set_use_system_theme(bool b) 224{ 225 if (b) { 226 auto theme = palette(); 227 m_right_letter_wrong_spot_color = Color::from_rgb(0xb59f3b); 228 m_right_letter_right_spot_color = Color::from_rgb(0x538d4e); 229 m_border_color = Color::Black; 230 m_wrong_letter_color = theme.window(); 231 m_background_color = theme.window(); 232 m_text_color = theme.accent(); 233 } else { 234 m_right_letter_wrong_spot_color = Color::from_rgb(0xb59f3b); 235 m_right_letter_right_spot_color = Color::from_rgb(0x538d4e); 236 m_border_color = Color::from_rgb(0x3a3a3c); 237 m_wrong_letter_color = m_border_color; 238 m_background_color = Color::from_rgb(0x121213); 239 m_text_color = Color::White; 240 } 241 242 update(); 243} 244 245void WordGame::set_word_length(size_t length) 246{ 247 m_num_letters = length; 248 reset(); 249} 250 251void WordGame::set_max_guesses(size_t max_guesses) 252{ 253 m_max_guesses = max_guesses; 254 reset(); 255} 256 257void WordGame::set_check_guesses_in_dictionary(bool b) 258{ 259 m_check_guesses = b; 260 update(); 261} 262 263bool WordGame::is_checking_guesses() const 264{ 265 return m_check_guesses; 266} 267 268Gfx::IntSize WordGame::game_size() const 269{ 270 auto w = 2 * m_outer_margin + m_num_letters * m_letter_width + (m_num_letters - 1) * m_letter_spacing; 271 auto h = 2 * m_outer_margin + m_max_guesses * m_letter_height + (m_max_guesses - 1) * m_letter_spacing; 272 return Gfx::IntSize(w, h); 273} 274 275void WordGame::add_guess(AK::StringView guess) 276{ 277 AK::Vector<LetterState> letter_states; 278 279 auto number_correct_for_letter = [this, &guess](StringView letter) -> size_t { 280 VERIFY(m_current_word.length() == guess.length()); 281 auto correct_count = 0; 282 for (size_t i = 0; i < m_current_word.length(); ++i) { 283 if (m_current_word.substring_view(i, 1) == letter && guess.substring_view(i, 1) == letter) 284 ++correct_count; 285 } 286 return correct_count; 287 }; 288 289 for (size_t letter_index = 0; letter_index < m_num_letters; ++letter_index) { 290 auto guess_letter = guess.substring_view(letter_index, 1); 291 292 if (m_current_word[letter_index] == guess_letter[0]) 293 letter_states.append(Correct); 294 else if (m_current_word.contains(guess_letter)) { 295 auto occurrences_in_word = m_current_word.count(guess_letter); 296 auto occurrences_in_guess_already_counted = guess.substring_view(0, letter_index).count(guess_letter); 297 auto correct = number_correct_for_letter(guess_letter); 298 if (occurrences_in_word > correct && occurrences_in_guess_already_counted < occurrences_in_word) 299 letter_states.append(WrongSpot); 300 else 301 letter_states.append(Incorrect); 302 } else 303 letter_states.append(Incorrect); 304 } 305 306 m_guesses.append({ guess, letter_states }); 307 update(); 308} 309 310void WordGame::show_message(StringView message) const 311{ 312 m_clear_message_timer->restart(); 313 if (on_message) 314 on_message(message); 315} 316 317void WordGame::clear_message() const 318{ 319 m_clear_message_timer->stop(); 320 if (on_message) 321 on_message({}); 322} 323 324}