diff options
author | Jamie Mansfield <jmansfield@cadixdev.org> | 2021-06-16 01:37:29 +0100 |
---|---|---|
committer | Andreas Kling <kling@serenityos.org> | 2021-06-24 10:32:53 +0200 |
commit | 3f8857cd21692488a638ce46a6e28bcfc940c817 (patch) | |
tree | 70b5a622be3e4fdaacecda8a9396c52442c3d591 /Userland | |
parent | b7e806e15ef6b45fe662f3ca95e9856313021127 (diff) | |
download | serenity-3f8857cd21692488a638ce46a6e28bcfc940c817.zip |
Games: Add Spider
Scoring is designed to mimic Microsoft's implementation - starting at
500, decreasing by 1 every move, and increasing by 100 for every full
stack.
Fixes GH-5319.
Diffstat (limited to 'Userland')
-rw-r--r-- | Userland/Games/CMakeLists.txt | 1 | ||||
-rw-r--r-- | Userland/Games/Spider/CMakeLists.txt | 16 | ||||
-rw-r--r-- | Userland/Games/Spider/Game.cpp | 346 | ||||
-rw-r--r-- | Userland/Games/Spider/Game.h | 102 | ||||
-rw-r--r-- | Userland/Games/Spider/Spider.gml | 17 | ||||
-rw-r--r-- | Userland/Games/Spider/main.cpp | 190 |
6 files changed, 672 insertions, 0 deletions
diff --git a/Userland/Games/CMakeLists.txt b/Userland/Games/CMakeLists.txt index 07e860670f..a74f979624 100644 --- a/Userland/Games/CMakeLists.txt +++ b/Userland/Games/CMakeLists.txt @@ -8,3 +8,4 @@ add_subdirectory(Minesweeper) add_subdirectory(Pong) add_subdirectory(Snake) add_subdirectory(Solitaire) +add_subdirectory(Spider) diff --git a/Userland/Games/Spider/CMakeLists.txt b/Userland/Games/Spider/CMakeLists.txt new file mode 100644 index 0000000000..29a5fd9e81 --- /dev/null +++ b/Userland/Games/Spider/CMakeLists.txt @@ -0,0 +1,16 @@ +serenity_component( + Spider + RECOMMENDED + TARGETS Spider +) + +compile_gml(Spider.gml SpiderGML.h spider_gml) + +set(SOURCES + Game.cpp + main.cpp + SpiderGML.h +) + +serenity_app(Spider ICON app-spider) +target_link_libraries(Spider LibCards LibGUI LibGfx LibCore) diff --git a/Userland/Games/Spider/Game.cpp b/Userland/Games/Spider/Game.cpp new file mode 100644 index 0000000000..4b003528ef --- /dev/null +++ b/Userland/Games/Spider/Game.cpp @@ -0,0 +1,346 @@ +/* + * Copyright (c) 2021, Jamie Mansfield <jmansfield@cadixdev.org> + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include "Game.h" +#include <LibGUI/Painter.h> +#include <LibGfx/Palette.h> + +REGISTER_WIDGET(Spider, Game); + +namespace Spider { + +static constexpr uint8_t new_game_animation_delay = 2; +static constexpr int s_timer_interval_ms = 1000 / 60; + +Game::Game() +{ + m_stacks.append(adopt_ref(*new CardStack({ 10, Game::height - Card::height - 10 }, CardStack::Type::Waste))); + m_stacks.append(adopt_ref(*new CardStack({ Game::width - Card::width - 10, Game::height - Card::height - 10 }, CardStack::Type::Stock))); + + for (int i = 0; i < 10; i++) { + m_stacks.append(adopt_ref(*new CardStack({ 10 + i * (Card::width + 10), 10 }, CardStack::Type::Normal))); + } +} + +Game::~Game() +{ +} + +void Game::setup(Mode mode) +{ + if (m_new_game_animation) + stop_timer(); + + m_mode = mode; + + if (on_game_end) + on_game_end(GameOverReason::NewGame, m_score); + + for (auto& stack : m_stacks) + stack.clear(); + + m_new_deck.clear(); + m_new_game_animation_pile = 0; + + m_score = 500; + update_score(0); + + for (int i = 0; i < Card::card_count; ++i) { + switch (m_mode) { + case Mode::SingleSuit: + for (int j = 0; j < 8; j++) { + m_new_deck.append(Card::construct(Card::Type::Spades, i)); + } + break; + case Mode::TwoSuit: + for (int j = 0; j < 4; j++) { + m_new_deck.append(Card::construct(Card::Type::Spades, i)); + m_new_deck.append(Card::construct(Card::Type::Hearts, i)); + } + break; + default: + VERIFY_NOT_REACHED(); + break; + } + } + + for (int x = 0; x < 3; x++) + for (uint8_t i = 0; i < Card::card_count * 8; ++i) + m_new_deck.append(m_new_deck.take(rand() % m_new_deck.size())); + + m_new_game_animation = true; + start_timer(s_timer_interval_ms); + update(); +} + +void Game::start_timer_if_necessary() +{ + if (on_game_start && m_waiting_for_new_game) { + on_game_start(); + m_waiting_for_new_game = false; + } +} + +void Game::update_score(int delta) +{ + m_score = max(static_cast<int>(m_score) + delta, 0); + + if (on_score_update) + on_score_update(m_score); +} + +void Game::draw_cards() +{ + // draw a single card from the stock for each pile + auto& stock_pile = stack(Stock); + if (stock_pile.is_empty()) + return; + + update_score(-1); + + for (auto pile : piles) { + auto& current_pile = stack(pile); + + auto card = stock_pile.pop(); + card->set_upside_down(false); + current_pile.push(card); + } + + detect_full_stacks(); + + update(); +} + +void Game::detect_full_stacks() +{ + auto& completed_stack = stack(Completed); + for (auto pile : piles) { + auto& current_pile = stack(pile); + + bool started = false; + uint8_t last_value; + Color color; + for (size_t i = current_pile.stack().size(); i > 0; i--) { + auto& card = current_pile.stack().at(i - 1); + if (card.is_upside_down()) + break; + + if (!started) { + if (card.value() != 0) { + break; + } + + started = true; + color = card.color(); + } else if (card.value() != last_value + 1 || card.color() != color) { + break; + } else if (card.value() == Card::card_count - 1) { + // we have a full set + for (size_t j = 0; j < Card::card_count; j++) { + completed_stack.push(current_pile.pop()); + } + + update_score(101); + } + + last_value = card.value(); + } + } + + detect_victory(); +} + +void Game::detect_victory() +{ + for (auto pile : piles) { + auto& current_pile = stack(pile); + + if (!current_pile.is_empty()) + return; + } + + if (on_game_end) + on_game_end(GameOverReason::Victory, m_score); +} + +void Game::paint_event(GUI::PaintEvent& event) +{ + static Gfx::Color s_background_color = palette().color(background_role()); + + GUI::Frame::paint_event(event); + + GUI::Painter painter(*this); + painter.add_clip_rect(frame_inner_rect()); + painter.add_clip_rect(event.rect()); + + if (m_new_game_animation) { + if (m_new_game_animation_delay < new_game_animation_delay) { + ++m_new_game_animation_delay; + } else { + m_new_game_animation_delay = 0; + auto& current_pile = stack(piles.at(m_new_game_animation_pile)); + + // for first 4 piles, draw 6 cards + // for last 6 piles, draw 5 cards + size_t cards_to_draw = m_new_game_animation_pile < 4 ? 6 : 5; + + if (current_pile.count() < (cards_to_draw - 1)) { + auto card = m_new_deck.take_last(); + card->set_upside_down(true); + current_pile.push(card); + } else { + current_pile.push(m_new_deck.take_last()); + ++m_new_game_animation_pile; + } + + if (m_new_game_animation_pile == piles.size()) { + VERIFY(m_new_deck.size() == 50); + while (!m_new_deck.is_empty()) + stack(Stock).push(m_new_deck.take_last()); + m_new_game_animation = false; + m_waiting_for_new_game = true; + stop_timer(); + } + } + } + + if (!m_focused_cards.is_empty()) { + for (auto& focused_card : m_focused_cards) + focused_card.clear(painter, s_background_color); + } + + for (auto& stack : m_stacks) { + stack.draw(painter, s_background_color); + } + + if (!m_focused_cards.is_empty()) { + for (auto& focused_card : m_focused_cards) { + focused_card.draw(painter); + focused_card.save_old_position(); + } + } + + if (!m_mouse_down) { + if (!m_focused_cards.is_empty()) { + for (auto& card : m_focused_cards) + card.set_moving(false); + m_focused_cards.clear(); + } + + if (m_focused_stack) { + m_focused_stack->set_focused(false); + m_focused_stack = nullptr; + } + } +} + +void Game::mousedown_event(GUI::MouseEvent& event) +{ + GUI::Frame::mousedown_event(event); + + if (m_new_game_animation) + return; + + auto click_location = event.position(); + for (auto& to_check : m_stacks) { + if (to_check.type() == CardStack::Type::Waste) + continue; + + if (to_check.bounding_box().contains(click_location)) { + if (to_check.type() == CardStack::Type::Stock) { + draw_cards(); + } else if (!to_check.is_empty()) { + auto& top_card = to_check.peek(); + + if (top_card.is_upside_down()) { + if (top_card.rect().contains(click_location)) { + top_card.set_upside_down(false); + start_timer_if_necessary(); + update(top_card.rect()); + } + } else if (m_focused_cards.is_empty()) { + to_check.add_all_grabbed_cards(click_location, m_focused_cards, Cards::CardStack::Same); + m_mouse_down_location = click_location; + if (m_focused_stack) + m_focused_stack->set_focused(false); + to_check.set_focused(true); + m_focused_stack = &to_check; + m_mouse_down = true; + start_timer_if_necessary(); + } + } + break; + } + } +} + +void Game::mouseup_event(GUI::MouseEvent& event) +{ + GUI::Frame::mouseup_event(event); + + if (!m_focused_stack || m_focused_cards.is_empty() || m_new_game_animation) + return; + + bool rebound = true; + for (auto& stack : m_stacks) { + if (stack.is_focused()) + continue; + + for (auto& focused_card : m_focused_cards) { + if (stack.bounding_box().intersects(focused_card.rect())) { + if (stack.is_allowed_to_push(m_focused_cards.at(0), m_focused_cards.size(), Cards::CardStack::Any)) { + for (auto& to_intersect : m_focused_cards) { + stack.push(to_intersect); + m_focused_stack->pop(); + } + + update_score(-1); + + detect_full_stacks(); + + rebound = false; + break; + } + } + } + } + + if (rebound) { + m_focused_stack->rebound_cards(); + } + + m_mouse_down = false; + + update(); +} + +void Game::mousemove_event(GUI::MouseEvent& event) +{ + GUI::Frame::mousemove_event(event); + + if (!m_mouse_down || m_new_game_animation) + return; + + auto click_location = event.position(); + int dx = click_location.dx_relative_to(m_mouse_down_location); + int dy = click_location.dy_relative_to(m_mouse_down_location); + + for (auto& to_intersect : m_focused_cards) { + to_intersect.rect().translate_by(dx, dy); + } + update(); + + m_mouse_down_location = click_location; +} + +void Game::timer_event(Core::TimerEvent&) +{ + if (m_new_game_animation) { + update(); + } +} + +} diff --git a/Userland/Games/Spider/Game.h b/Userland/Games/Spider/Game.h new file mode 100644 index 0000000000..94f48b43c9 --- /dev/null +++ b/Userland/Games/Spider/Game.h @@ -0,0 +1,102 @@ +/* + * Copyright (c) 2021, Jamie Mansfield <jmansfield@cadixdev.org> + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include <AK/Array.h> +#include <LibCards/CardStack.h> +#include <LibGUI/Frame.h> + +using Cards::Card; +using Cards::CardStack; + +namespace Spider { + +enum class Mode : u8 { + SingleSuit, + TwoSuit, + __Count +}; + +enum class GameOverReason { + Victory, + NewGame, +}; + +class Game final : public GUI::Frame { + C_OBJECT(Game) +public: + static constexpr int width = 10 + 10 * Card::width + 90 + 10; + static constexpr int height = 480; + + ~Game() override; + + Mode mode() const { return m_mode; } + void setup(Mode); + + Function<void(uint32_t)> on_score_update; + Function<void()> on_game_start; + Function<void(GameOverReason, uint32_t)> on_game_end; + +private: + Game(); + + enum StackLocation { + Completed, + Stock, + Pile1, + Pile2, + Pile3, + Pile4, + Pile5, + Pile6, + Pile7, + Pile8, + Pile9, + Pile10, + __Count + }; + static constexpr Array piles = { + Pile1, Pile2, Pile3, Pile4, Pile5, + Pile6, Pile7, Pile8, Pile9, Pile10 + }; + + void start_timer_if_necessary(); + void update_score(int delta); + void draw_cards(); + void detect_full_stacks(); + void detect_victory(); + + ALWAYS_INLINE CardStack& stack(StackLocation location) + { + return m_stacks[location]; + } + + void paint_event(GUI::PaintEvent&) override; + void mousedown_event(GUI::MouseEvent&) override; + void mouseup_event(GUI::MouseEvent&) override; + void mousemove_event(GUI::MouseEvent&) override; + void timer_event(Core::TimerEvent&) override; + + Mode m_mode { Mode::SingleSuit }; + + NonnullRefPtrVector<Card> m_focused_cards; + NonnullRefPtrVector<Card> m_new_deck; + NonnullRefPtrVector<CardStack> m_stacks; + CardStack* m_focused_stack { nullptr }; + Gfx::IntPoint m_mouse_down_location; + + bool m_mouse_down { false }; + + bool m_waiting_for_new_game { true }; + bool m_new_game_animation { false }; + uint8_t m_new_game_animation_delay { 0 }; + uint8_t m_new_game_animation_pile { 0 }; + + uint32_t m_score { 500 }; +}; + +} diff --git a/Userland/Games/Spider/Spider.gml b/Userland/Games/Spider/Spider.gml new file mode 100644 index 0000000000..09e29799c2 --- /dev/null +++ b/Userland/Games/Spider/Spider.gml @@ -0,0 +1,17 @@ +@GUI::Widget { + fill_with_background_color: true + + layout: @GUI::VerticalBoxLayout { + } + + @Spider::Game { + name: "game" + fill_with_background_color: true + background_color: "green" + } + + @GUI::Statusbar { + name: "statusbar" + label_count: 3 + } +} diff --git a/Userland/Games/Spider/main.cpp b/Userland/Games/Spider/main.cpp new file mode 100644 index 0000000000..1914ee4f23 --- /dev/null +++ b/Userland/Games/Spider/main.cpp @@ -0,0 +1,190 @@ +/* + * Copyright (c) 2021, Jamie Mansfield <jmansfield@cadixdev.org> + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include "Game.h" +#include <Games/Spider/SpiderGML.h> +#include <LibCore/ConfigFile.h> +#include <LibCore/Timer.h> +#include <LibGUI/Action.h> +#include <LibGUI/ActionGroup.h> +#include <LibGUI/Application.h> +#include <LibGUI/Icon.h> +#include <LibGUI/Menu.h> +#include <LibGUI/Menubar.h> +#include <LibGUI/MessageBox.h> +#include <LibGUI/Statusbar.h> +#include <LibGUI/Window.h> +#include <stdio.h> +#include <unistd.h> + +int main(int argc, char** argv) +{ + auto app = GUI::Application::construct(argc, argv); + auto app_icon = GUI::Icon::default_icon("app-spider"); + auto config = Core::ConfigFile::get_for_app("Spider"); + + if (pledge("stdio recvfd sendfd rpath wpath cpath", nullptr) < 0) { + perror("pledge"); + return 1; + } + + if (unveil("/res", "r") < 0) { + perror("unveil"); + return 1; + } + + if (unveil(config->filename().characters(), "crw") < 0) { + perror("unveil"); + return 1; + } + + if (unveil(nullptr, nullptr) < 0) { + perror("unveil"); + return 1; + } + + auto window = GUI::Window::construct(); + window->set_title("Spider"); + + auto mode = static_cast<Spider::Mode>(config->read_num_entry("Settings", "Mode", static_cast<int>(Spider::Mode::SingleSuit))); + + auto update_mode = [&](Spider::Mode new_mode) { + mode = new_mode; + config->write_num_entry("Settings", "Mode", static_cast<int>(mode)); + if (!config->sync()) + GUI::MessageBox::show(window, "Configuration could not be saved", "Error", GUI::MessageBox::Type::Error); + }; + + auto high_score = [&]() { + switch (mode) { + case Spider::Mode::SingleSuit: + return static_cast<u32>(config->read_num_entry("HighScores", "SingleSuit", 0)); + case Spider::Mode::TwoSuit: + return static_cast<u32>(config->read_num_entry("HighScores", "TwoSuit", 0)); + default: + VERIFY_NOT_REACHED(); + } + }; + + auto update_high_score = [&](u32 new_high_score) { + switch (mode) { + case Spider::Mode::SingleSuit: + config->write_num_entry("HighScores", "SingleSuit", static_cast<int>(new_high_score)); + break; + case Spider::Mode::TwoSuit: + config->write_num_entry("HighScores", "TwoSuit", static_cast<int>(new_high_score)); + break; + default: + VERIFY_NOT_REACHED(); + } + + if (!config->sync()) + GUI::MessageBox::show(window, "Configuration could not be saved", "Error", GUI::MessageBox::Type::Error); + }; + + if (mode >= Spider::Mode::__Count) + update_mode(Spider::Mode::SingleSuit); + + auto& widget = window->set_main_widget<GUI::Widget>(); + widget.load_from_gml(spider_gml); + + auto& game = *widget.find_descendant_of_type_named<Spider::Game>("game"); + game.set_focus(true); + + auto& statusbar = *widget.find_descendant_of_type_named<GUI::Statusbar>("statusbar"); + statusbar.set_text(0, "Score: 0"); + statusbar.set_text(1, String::formatted("High Score: {}", high_score())); + statusbar.set_text(2, "Time: 00:00:00"); + + app->on_action_enter = [&](GUI::Action& action) { + auto text = action.status_tip(); + if (text.is_empty()) + text = Gfx::parse_ampersand_string(action.text()); + statusbar.set_override_text(move(text)); + }; + + app->on_action_leave = [&](GUI::Action&) { + statusbar.set_override_text({}); + }; + + game.on_score_update = [&](uint32_t score) { + statusbar.set_text(0, String::formatted("Score: {}", score)); + }; + + uint64_t seconds_elapsed = 0; + + auto timer = Core::Timer::create_repeating(1000, [&]() { + ++seconds_elapsed; + + uint64_t hours = seconds_elapsed / 3600; + uint64_t minutes = (seconds_elapsed / 60) % 60; + uint64_t seconds = seconds_elapsed % 60; + + statusbar.set_text(2, String::formatted("Time: {:02}:{:02}:{:02}", hours, minutes, seconds)); + }); + + game.on_game_start = [&]() { + seconds_elapsed = 0; + timer->start(); + statusbar.set_text(2, "Time: 00:00:00"); + }; + game.on_game_end = [&](Spider::GameOverReason reason, uint32_t score) { + if (timer->is_active()) + timer->stop(); + + if (reason == Spider::GameOverReason::Victory) { + if (score > high_score()) { + update_high_score(score); + statusbar.set_text(1, String::formatted("High Score: {}", score)); + } + } + statusbar.set_text(2, "Timer starts after your first move"); + }; + + GUI::ActionGroup suit_actions; + suit_actions.set_exclusive(true); + + auto single_suit_action = GUI::Action::create_checkable("&Single Suit", [&](auto&) { + update_mode(Spider::Mode::SingleSuit); + statusbar.set_text(1, String::formatted("High Score: {}", high_score())); + game.setup(mode); + }); + single_suit_action->set_checked(mode == Spider::Mode::SingleSuit); + suit_actions.add_action(single_suit_action); + + auto two_suit_action = GUI::Action::create_checkable("&Two Suit", [&](auto&) { + update_mode(Spider::Mode::TwoSuit); + statusbar.set_text(1, String::formatted("High Score: {}", high_score())); + game.setup(mode); + }); + two_suit_action->set_checked(mode == Spider::Mode::TwoSuit); + suit_actions.add_action(two_suit_action); + + auto menubar = GUI::Menubar::construct(); + + auto& game_menu = menubar->add_menu("&Game"); + game_menu.add_action(GUI::Action::create("&New Game", { Mod_None, Key_F2 }, [&](auto&) { + game.setup(mode); + })); + game_menu.add_separator(); + game_menu.add_action(single_suit_action); + game_menu.add_action(two_suit_action); + game_menu.add_separator(); + game_menu.add_action(GUI::CommonActions::make_quit_action([&](auto&) { app->quit(); })); + + auto& help_menu = menubar->add_menu("&Help"); + help_menu.add_action(GUI::CommonActions::make_about_action("Spider", app_icon, window)); + + window->set_resizable(false); + window->resize(Spider::Game::width, Spider::Game::height + statusbar.max_height()); + window->set_menubar(move(menubar)); + window->set_icon(app_icon.bitmap_for_size(16)); + window->show(); + + game.setup(mode); + + return app->exec(); +} |