summaryrefslogtreecommitdiff
path: root/Userland
diff options
context:
space:
mode:
authorJamie Mansfield <jmansfield@cadixdev.org>2021-06-16 01:37:29 +0100
committerAndreas Kling <kling@serenityos.org>2021-06-24 10:32:53 +0200
commit3f8857cd21692488a638ce46a6e28bcfc940c817 (patch)
tree70b5a622be3e4fdaacecda8a9396c52442c3d591 /Userland
parentb7e806e15ef6b45fe662f3ca95e9856313021127 (diff)
downloadserenity-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.txt1
-rw-r--r--Userland/Games/Spider/CMakeLists.txt16
-rw-r--r--Userland/Games/Spider/Game.cpp346
-rw-r--r--Userland/Games/Spider/Game.h102
-rw-r--r--Userland/Games/Spider/Spider.gml17
-rw-r--r--Userland/Games/Spider/main.cpp190
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();
+}