Serenity Operating System
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}