1
0
mirror of https://github.com/SerenityOS/serenity synced 2024-07-09 06:40:45 +00:00

Games: Add MasterWord

A simple wordle clone.
This commit is contained in:
Joe Petrus 2022-02-11 12:21:42 -05:00 committed by Brian Gianforcaro
parent d56544aca0
commit 011b6df63a
10 changed files with 2590 additions and 0 deletions

View File

@ -0,0 +1,4 @@
[App]
Name=MasterWord
Executable=/bin/MasterWord
Category=Games

Binary file not shown.

After

Width:  |  Height:  |  Size: 198 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 582 B

2091
Base/res/words.txt Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,15 @@
## Name
![Icon](/res/icons/16x16/app-masterword.png) MasterWord
[Open](file:///bin/MasterWord)
## Synopsis
```**sh
$ MasterWord
```
## Description
Try to guess a secret word. As you guess, letters that occur but are in the wrong spot will turn yellow and letters that are in the correct spot will turn green.

View File

@ -4,6 +4,7 @@ add_subdirectory(Chess)
add_subdirectory(FlappyBug)
add_subdirectory(GameOfLife)
add_subdirectory(Hearts)
add_subdirectory(MasterWord)
add_subdirectory(Minesweeper)
add_subdirectory(Pong)
add_subdirectory(Snake)

View File

@ -0,0 +1,13 @@
serenity_component(
MasterWord
RECOMMENDED
TARGETS MasterWord
)
set(SOURCES
main.cpp
WordGame.cpp
)
serenity_app(MasterWord ICON app-masterword)
target_link_libraries(MasterWord LibGUI LibConfig LibMain LibDesktop)

View File

@ -0,0 +1,259 @@
/*
* Copyright (c) 2022, Joe Petrus <joe@petrus.io>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include "WordGame.h"
#include <AK/QuickSort.h>
#include <AK/Random.h>
#include <LibConfig/Client.h>
#include <LibCore/File.h>
#include <LibGUI/Application.h>
#include <LibGUI/MessageBox.h>
#include <LibGUI/Painter.h>
#include <LibGfx/Font.h>
#include <LibGfx/FontDatabase.h>
#include <LibGfx/Palette.h>
// TODO: Add stats
WordGame::WordGame()
{
read_words();
m_num_letters = Config::read_i32("MasterWord", "", "word_length", 5);
m_max_guesses = Config::read_i32("MasterWord", "", "max_guesses", 6);
reset();
pick_font();
}
void WordGame::reset()
{
m_current_guess = {};
m_guesses.clear();
auto maybe_word = WordGame::random_word(m_num_letters);
if (maybe_word.has_value())
m_current_word = maybe_word.value();
else {
GUI::MessageBox::show(window(), String::formatted("Could not get a random {} letter word. Defaulting to 5.", m_num_letters), "MasterWord");
if (m_num_letters != 5) {
m_num_letters = 5;
reset();
}
}
update();
}
void WordGame::pick_font()
{
String best_font_name;
auto best_font_size = -1;
auto& font_database = Gfx::FontDatabase::the();
font_database.for_each_font([&](Gfx::Font const& font) {
if (font.family() != "Liza" || font.weight() != 700)
return;
auto size = font.glyph_height();
if (size * 2 <= m_letter_height && size > best_font_size) {
best_font_name = font.qualified_name();
best_font_size = size;
}
});
auto font = font_database.get_by_name(best_font_name);
set_font(font);
}
void WordGame::resize_event(GUI::ResizeEvent&)
{
pick_font();
update();
}
void WordGame::keydown_event(GUI::KeyEvent& event)
{
// If we can still add a letter and the key was alpha
if (m_current_guess.length() < m_num_letters && is_ascii_alpha(event.code_point())) {
m_current_guess = String::formatted("{}{}", m_current_guess, event.text().to_uppercase());
}
// If backspace pressed and already have some letters entered
else if (event.key() == KeyCode::Key_Backspace && m_current_guess.length() > 0) {
m_current_guess = m_current_guess.substring(0, m_current_guess.length() - 1);
}
// If enough letters and return pressed
else if (m_current_guess.length() == m_num_letters && event.key() == KeyCode::Key_Return) {
add_guess(m_current_guess);
auto won = m_current_guess == m_current_word;
m_current_guess = {};
if (won) {
GUI::MessageBox::show(window(), "You win!", "MasterWord");
reset();
} else if (m_guesses.size() == m_max_guesses) {
GUI::MessageBox::show(window(), String::formatted("You lose!\nThe word was {}", m_current_word), "MasterWord");
reset();
}
}
update();
}
void WordGame::paint_event(GUI::PaintEvent& event)
{
GUI::Frame::paint_event(event);
GUI::Painter painter(*this);
painter.add_clip_rect(frame_inner_rect());
painter.add_clip_rect(event.rect());
painter.fill_rect(event.rect(), m_background_color);
for (size_t guess_index = 0; guess_index < m_max_guesses; ++guess_index) {
for (size_t letter_index = 0; letter_index < m_num_letters; ++letter_index) {
auto this_rect = letter_rect(guess_index, letter_index);
if (guess_index < m_guesses.size()) {
switch (m_guesses[guess_index].letter_states.at(letter_index)) {
case Correct:
painter.fill_rect(this_rect, m_right_letter_right_spot_color);
break;
case WrongSpot:
painter.fill_rect(this_rect, m_right_letter_wrong_spot_color);
break;
case Incorrect:
painter.fill_rect(this_rect, m_wrong_letter_color);
break;
}
painter.draw_text(this_rect, m_guesses[guess_index].text.substring_view(letter_index, 1), font(), Gfx::TextAlignment::Center, m_text_color);
} else if (guess_index == m_guesses.size()) {
if (letter_index < m_current_guess.length())
painter.draw_text(this_rect, m_current_guess.substring_view(letter_index, 1), font(), Gfx::TextAlignment::Center, m_text_color);
}
painter.draw_rect(this_rect, m_border_color);
}
}
}
Gfx::IntRect WordGame::letter_rect(size_t guess_number, size_t letter_number) const
{
auto letter_left = m_outer_margin + letter_number * m_letter_width + m_letter_spacing * letter_number;
auto letter_top = m_outer_margin + guess_number * m_letter_height + m_letter_spacing * guess_number;
return Gfx::IntRect(int(letter_left), int(letter_top), m_letter_width, m_letter_height);
}
void WordGame::read_words()
{
m_words.clear();
auto response = Core::File::open("/res/words.txt", Core::OpenMode::ReadOnly);
if (response.is_error()) {
GUI::MessageBox::show(nullptr, "Could not read /res/words.txt.\nPlease ensure this file exists and restart MasterWord.", "MasterWord");
exit(0);
}
auto words_file = response.value();
while (!words_file->eof()) {
auto current_word = words_file->read_line();
if (!current_word.starts_with('#') and current_word.length() > 0)
m_words.ensure(current_word.length()).append(current_word);
}
}
Optional<String> WordGame::random_word(size_t length)
{
auto words_for_length = m_words.get(length);
if (words_for_length.has_value()) {
auto i = get_random_uniform(words_for_length->size());
return words_for_length->at(i).to_uppercase();
}
return {};
}
size_t WordGame::shortest_word()
{
auto available_lengths = m_words.keys();
AK::quick_sort(available_lengths);
return available_lengths.first();
}
size_t WordGame::longest_word()
{
auto available_lengths = m_words.keys();
AK::quick_sort(available_lengths);
return available_lengths.last();
}
void WordGame::set_use_system_theme(bool b)
{
if (b) {
auto theme = palette();
m_right_letter_wrong_spot_color = Color::from_rgb(0xb59f3b);
m_right_letter_right_spot_color = Color::from_rgb(0x538d4e);
m_border_color = Color::Black;
m_wrong_letter_color = theme.window();
m_background_color = theme.window();
m_text_color = theme.accent();
} else {
m_right_letter_wrong_spot_color = Color::from_rgb(0xb59f3b);
m_right_letter_right_spot_color = Color::from_rgb(0x538d4e);
m_border_color = Color::from_rgb(0x3a3a3c);
m_wrong_letter_color = m_border_color;
m_background_color = Color::from_rgb(0x121213);
m_text_color = Color::White;
}
update();
}
void WordGame::set_word_length(size_t length)
{
m_num_letters = length;
reset();
}
void WordGame::set_max_guesses(size_t max_guesses)
{
m_max_guesses = max_guesses;
reset();
}
Gfx::IntSize WordGame::game_size() const
{
auto w = 2 * m_outer_margin + m_num_letters * m_letter_width + (m_num_letters - 1) * m_letter_spacing;
auto h = 2 * m_outer_margin + m_max_guesses * m_letter_height + (m_max_guesses - 1) * m_letter_spacing;
return Gfx::IntSize(w, h);
}
void WordGame::add_guess(AK::StringView guess)
{
AK::Vector<LetterState> letter_states;
auto number_correct_for_letter = [this, &guess](StringView letter) -> size_t {
VERIFY(m_current_word.length() == guess.length());
auto correct_count = 0;
for (size_t i = 0; i < m_current_word.length(); ++i) {
if (m_current_word.substring_view(i, 1) == letter && guess.substring_view(i, 1) == letter)
++correct_count;
}
return correct_count;
};
for (size_t letter_index = 0; letter_index < m_num_letters; ++letter_index) {
auto guess_letter = guess.substring_view(letter_index, 1);
if (m_current_word[letter_index] == guess_letter[0])
letter_states.append(Correct);
else if (m_current_word.contains(guess_letter)) {
auto occurrences_in_word = m_current_word.count(guess_letter);
auto occurrences_in_guess_already_counted = guess.substring_view(0, letter_index).count(guess_letter);
auto correct = number_correct_for_letter(guess_letter);
if (occurrences_in_word > correct && occurrences_in_guess_already_counted < occurrences_in_word)
letter_states.append(WrongSpot);
else
letter_states.append(Incorrect);
} else
letter_states.append(Incorrect);
}
m_guesses.append({ guess, letter_states });
update();
}

View File

@ -0,0 +1,73 @@
/*
* Copyright (c) 2022, Joe Petrus <joe@petrus.io>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/String.h>
#include <AK/Vector.h>
#include <LibGUI/Frame.h>
#include <LibGfx/Rect.h>
class WordGame : public GUI::Frame {
C_OBJECT(WordGame);
public:
virtual ~WordGame() override = default;
void reset();
void set_use_system_theme(bool b);
void set_word_length(size_t length);
void set_max_guesses(size_t max_guesses);
Gfx::IntSize game_size() const;
Optional<String> random_word(size_t length);
size_t shortest_word();
size_t longest_word();
void add_guess(AK::StringView guess);
private:
WordGame();
void read_words();
virtual void paint_event(GUI::PaintEvent&) override;
virtual void keydown_event(GUI::KeyEvent&) override;
virtual void resize_event(GUI::ResizeEvent&) override;
void pick_font();
Gfx::IntRect letter_rect(size_t guess_number, size_t letter_number) const;
size_t m_max_guesses { 6 };
size_t m_num_letters { 5 };
static constexpr int m_letter_width { 40 };
static constexpr int m_letter_spacing { 5 };
static constexpr int m_outer_margin { 20 };
static constexpr int m_letter_height { 60 };
Color m_right_letter_wrong_spot_color { Color::from_rgb(0xb59f3b) };
Color m_right_letter_right_spot_color { Color::from_rgb(0x538d4e) };
Color m_border_color { Color::from_rgb(0x3a3a3c) };
Color m_wrong_letter_color { m_border_color };
Color m_background_color { Color::from_rgb(0x121213) };
Color m_text_color { Color::White };
enum LetterState {
Correct,
WrongSpot,
Incorrect
};
struct Guess {
AK::String text;
AK::Vector<LetterState> letter_states;
};
AK::Vector<Guess> m_guesses;
AK::String m_current_guess;
AK::String m_current_word;
HashMap<size_t, AK::Vector<String>> m_words;
};

View File

@ -0,0 +1,134 @@
/*
* Copyright (c) 2022, Joe Petrus <joe@petrus.io>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include "WordGame.h"
#include <AK/URL.h>
#include <LibConfig/Client.h>
#include <LibCore/System.h>
#include <LibDesktop/Launcher.h>
#include <LibGUI/Action.h>
#include <LibGUI/ActionGroup.h>
#include <LibGUI/Application.h>
#include <LibGUI/Icon.h>
#include <LibGUI/InputBox.h>
#include <LibGUI/Menubar.h>
#include <LibGUI/MessageBox.h>
#include <LibGUI/Window.h>
#include <LibMain/Main.h>
ErrorOr<int> serenity_main(Main::Arguments arguments)
{
TRY(Core::System::pledge("stdio rpath recvfd sendfd unix"));
auto app = TRY(GUI::Application::try_create(arguments));
Config::pledge_domain("MasterWord");
TRY(Desktop::Launcher::add_allowed_handler_with_only_specific_urls("/bin/Help", { URL::create_with_file_protocol("/usr/share/man/man6/MasterWord.md") }));
TRY(Desktop::Launcher::seal_allowlist());
TRY(Core::System::pledge("stdio rpath recvfd sendfd"));
TRY(Core::System::unveil("/res", "r"));
TRY(Core::System::unveil("/tmp/portal/launch", "rw"));
TRY(Core::System::unveil(nullptr, nullptr));
auto app_icon = TRY(GUI::Icon::try_create_default_icon("app-masterword"));
auto window = TRY(GUI::Window::try_create());
window->set_double_buffering_enabled(false);
window->set_title("MasterWord");
window->set_resizable(false);
auto game = TRY(window->try_set_main_widget<WordGame>());
auto use_system_theme = Config::read_bool("MasterWord", "", "use_system_theme", false);
game->set_use_system_theme(use_system_theme);
auto shortest_word = game->shortest_word();
auto longest_word = game->longest_word();
window->resize(game->game_size());
auto game_menu = TRY(window->try_add_menu("&Game"));
TRY(game_menu->try_add_action(GUI::Action::create("&New Game", { Mod_None, Key_F2 }, [&](auto&) {
game->reset();
})));
TRY(game_menu->try_add_action(GUI::Action::create("Set &Word Length", [&](auto&) {
auto word_length = Config::read_i32("MasterWord", "", "word_length", 5);
auto word_length_string = String::number(word_length);
if (GUI::InputBox::show(window, word_length_string, "Word length:", "MasterWord") == GUI::InputBox::ExecOK && !word_length_string.is_empty()) {
auto maybe_word_length = word_length_string.template to_uint();
if (!maybe_word_length.has_value() || maybe_word_length.value() < shortest_word || maybe_word_length.value() > longest_word) {
GUI::MessageBox::show(window, String::formatted("Please enter a number between {} and {}.", shortest_word, longest_word), "MasterWord");
return;
}
word_length = maybe_word_length.value();
Config::write_i32("MasterWord", "", "word_length", word_length);
game->set_word_length(word_length);
window->resize(game->game_size());
}
})));
TRY(game_menu->try_add_action(GUI::Action::create("Set &Number Of Guesses", [&](auto&) {
auto max_guesses = Config::read_i32("MasterWord", "", "max_guesses", 5);
auto max_guesses_string = String::number(max_guesses);
if (GUI::InputBox::show(window, max_guesses_string, "Maximum number of guesses:", "MasterWord") == GUI::InputBox::ExecOK && !max_guesses_string.is_empty()) {
auto maybe_max_guesses = max_guesses_string.template to_uint();
if (!maybe_max_guesses.has_value() || maybe_max_guesses.value() < 1 || maybe_max_guesses.value() > 20) {
GUI::MessageBox::show(window, "Please enter a number between 1 and 20.", "MasterWord");
return;
}
max_guesses = maybe_max_guesses.value();
Config::write_i32("MasterWord", "", "max_guesses", max_guesses);
game->set_max_guesses(max_guesses);
window->resize(game->game_size());
}
})));
TRY(game_menu->try_add_separator());
TRY(game_menu->try_add_action(GUI::CommonActions::make_quit_action([](auto&) {
GUI::Application::the()->quit();
})));
auto theme_menu = TRY(window->try_add_menu("&Theme"));
auto system_theme_action = GUI::Action::create("&System", [&](auto&) {
game->set_use_system_theme(true);
Config::write_bool("MasterWord", "", "use_system_theme", true);
});
system_theme_action->set_checkable(true);
system_theme_action->set_checked(use_system_theme);
TRY(theme_menu->try_add_action(system_theme_action));
auto wordle_theme_action = GUI::Action::create("&Wordle", [&](auto&) {
game->set_use_system_theme(false);
Config::write_bool("MasterWord", "", "use_system_theme", false);
});
wordle_theme_action->set_checkable(true);
wordle_theme_action->set_checked(!use_system_theme);
TRY(theme_menu->try_add_action(wordle_theme_action));
GUI::ActionGroup theme_actions;
theme_actions.set_exclusive(true);
theme_actions.set_unchecking_allowed(false);
theme_actions.add_action(system_theme_action);
theme_actions.add_action(wordle_theme_action);
auto help_menu = TRY(window->try_add_menu("&Help"));
TRY(help_menu->try_add_action(GUI::CommonActions::make_help_action([](auto&) {
Desktop::Launcher::open(URL::create_with_file_protocol("/usr/share/man/man6/MasterWord.md"), "/bin/Help");
})));
TRY(help_menu->try_add_action(GUI::CommonActions::make_about_action("MasterWord", app_icon, window)));
window->show();
window->set_icon(app_icon.bitmap_for_size(16));
return app->exec();
}