From 146aedc32cd90bbf7a6b6fa8646311114c0b86ac Mon Sep 17 00:00:00 2001 From: Andreas Kling Date: Fri, 26 Apr 2019 19:54:31 +0200 Subject: [PATCH] Minesweeper: Implement some feature requests. Someone was playing this game and suggested a number of improvements so here we go trying to address them: - Add "chording" support, where you can click a numbered square using both mouse buttons simultaneously to sweep all non-flagged adjacent squares. - Mis-flagged squares are now revealed as such on game over, with a special "bad flag" icon. - The game timer now shows tenths of seconds. It also doesn't start until you click the first square. - Add the three difficulty modes from the classic Windows version. --- Base/res/icons/minesweeper/badflag.png | Bin 0 -> 357 bytes Base/res/icons/minesweeper/face-bad.png | Bin 356 -> 332 bytes Games/Minesweeper/Field.cpp | 187 +++++++++++++++++++----- Games/Minesweeper/Field.h | 38 ++++- Games/Minesweeper/main.cpp | 28 ++++ 5 files changed, 213 insertions(+), 40 deletions(-) create mode 100644 Base/res/icons/minesweeper/badflag.png diff --git a/Base/res/icons/minesweeper/badflag.png b/Base/res/icons/minesweeper/badflag.png new file mode 100644 index 0000000000000000000000000000000000000000..d9ac0cf155292cbae046977ecd107bb432e24f81 GIT binary patch literal 357 zcmeAS@N?(olHy`uVBq!ia0y~yVBiN~4mJh`2J356y%-o6bv#`hLp07O+h}W5v@|w0 z9{lmOTAEFX(T87n7lY7`et%_VW@cvPT?~`B|FO5I8-4ij(b(kqE<=%R>(Wl_DxduD z|9W|zo*WsTyyg9dMhmSaySXJmz$ibaWwW^C48@#d%P#)-TK)QSd$`SR)f{ns|%X_DOZ?rw^}_K?Ji!Vg@BlZ>s~ z^yk}wt=5@o+c=Rk)_9Zngt=C+&hm$~dv+{Jo~>}dzEJGh;vb79b_SF@;C4}~k>Whc zB&^pP_ugL6gdv;Ddvfrmw1k8N12eOK{pT6nzPwl&8kA~L{Y^*dOK-2@taI~HUA_xF zjto4gynbiJ8c7KW2^%@PV>2Za+Rjv!yw|vs=2axha6Y-wxk*#)DFXuogQu&X%Q~lo FCIIj9kGuc? literal 0 HcmV?d00001 diff --git a/Base/res/icons/minesweeper/face-bad.png b/Base/res/icons/minesweeper/face-bad.png index 049ddc8e82537eaf4e6033ee08431303e55e92ee..4e4fc82a96f178048c8f862c3f3e785447ae8fbd 100644 GIT binary patch delta 305 zcmaFDbcShyO1-eBi(`n#@x9YFa%s8C2|D*e zI`_IMdl_qb{$KBTdkY8G=4-BH+hiTJ?|(jjzf6@VPk+b#x4A#%GJ_5WFnK)M z*P39*v7I6Mk&q6{A#H|gg~QF~7=BpPPnM`(7@H6kzS`lf{rvg`mrXB;dcU|G5$koz?6K%MaoWA^?*4)CD?P$ZT zbm&O!hn!Oj!#jVgp5C2Ss`V_Ux#v>r<9q5C^NwYd*KE0d>gxM-@3G( zk4JY{Bz7spGfdyXvbphte1qbIMcs9b7i#x-D#c~+RqWgK`ejP}kDoPSm#rij9G+$c zYPq~$`h43OlZg`H-Fe^29oBf4+pdfHUlo33eHKp)pZA6LdG`Hoo`D4o78xBIZr)$J z&rO{1z?{#mTW^%G#T?c!?bg%w*z6u%k-oj>Yhkd!fhi*Y*1LV3;J0f^Q%ZD4()P<< zEhdxY11IRM)}P=#XZ=EhX--qO*;ik@VfMQzLxiD0&_8D4hfq}u()z4*}Q$iB})d88T diff --git a/Games/Minesweeper/Field.cpp b/Games/Minesweeper/Field.cpp index e043e1d06e..81b825b850 100644 --- a/Games/Minesweeper/Field.cpp +++ b/Games/Minesweeper/Field.cpp @@ -26,27 +26,78 @@ public: } }; +class SquareLabel final : public GLabel { +public: + SquareLabel(Square& square, GWidget* parent) + : GLabel(parent) + , m_square(square) + { + } + + Function on_chord_click; + + virtual void mousedown_event(GMouseEvent& event) override + { + if (event.buttons() == (GMouseButton::Right | GMouseButton::Left)) { + if (event.button() == GMouseButton::Left || event.button() == GMouseButton::Right) { + m_chord = true; + m_square.field->set_chord_preview(m_square, true); + } + } + GLabel::mousedown_event(event); + } + + virtual void mousemove_event(GMouseEvent& event) override + { + if (m_chord) { + if (rect().contains(event.position())) { + m_square.field->set_chord_preview(m_square, true); + } else { + m_square.field->set_chord_preview(m_square, false); + } + } + GLabel::mousemove_event(event); + } + + virtual void mouseup_event(GMouseEvent& event) override + { + if (m_chord) { + if (event.button() == GMouseButton::Left || event.button() == GMouseButton::Right) { + if (rect().contains(event.position())) { + if (on_chord_click) + on_chord_click(); + } + m_chord = false; + } + } + m_square.field->set_chord_preview(m_square, m_chord); + GLabel::mouseup_event(event); + } + + Square& m_square; + bool m_chord { false }; +}; + Field::Field(GLabel& flag_label, GLabel& time_label, GButton& face_button, GWidget* parent) : GFrame(parent) , m_face_button(face_button) , m_flag_label(flag_label) , m_time_label(time_label) { - auto config = CConfigFile::get_for_app("Minesweeper"); - - m_mine_count = config->read_num_entry("Game", "MineCount", 10); - m_rows = config->read_num_entry("Game", "Rows", 9); - m_columns = config->read_num_entry("Game", "Columns", 9); - m_timer.on_timeout = [this] { - m_time_label.set_text(String::format("%u", ++m_seconds_elapsed)); + ++m_time_elapsed; + m_time_label.set_text(String::format("%u.%u", m_time_elapsed / 10, m_time_elapsed % 10)); }; - m_timer.set_interval(1000); + m_timer.set_interval(100); set_frame_thickness(2); set_frame_shape(FrameShape::Container); set_frame_shadow(FrameShadow::Sunken); m_mine_bitmap = GraphicsBitmap::load_from_file("/res/icons/minesweeper/mine.png"); m_flag_bitmap = GraphicsBitmap::load_from_file("/res/icons/minesweeper/flag.png"); + m_badflag_bitmap = GraphicsBitmap::load_from_file("/res/icons/minesweeper/badflag.png"); + m_default_face_bitmap = GraphicsBitmap::load_from_file("/res/icons/minesweeper/face-default.png"); + m_good_face_bitmap = GraphicsBitmap::load_from_file("/res/icons/minesweeper/face-good.png"); + m_bad_face_bitmap = GraphicsBitmap::load_from_file("/res/icons/minesweeper/face-bad.png"); for (int i = 0; i < 8; ++i) m_number_bitmap[i] = GraphicsBitmap::load_from_file(String::format("/res/icons/minesweeper/%u.png", i + 1)); @@ -66,50 +117,51 @@ void Field::set_face(Face face) { switch (face) { case Face::Default: - m_face_button.set_icon(GraphicsBitmap::load_from_file("/res/icons/minesweeper/face-default.png")); + m_face_button.set_icon(*m_default_face_bitmap); break; case Face::Good: - m_face_button.set_icon(GraphicsBitmap::load_from_file("/res/icons/minesweeper/face-good.png")); + m_face_button.set_icon(*m_good_face_bitmap); break; case Face::Bad: - m_face_button.set_icon(GraphicsBitmap::load_from_file("/res/icons/minesweeper/face-bad.png")); + m_face_button.set_icon(*m_bad_face_bitmap); break; } } template -void Field::for_each_neighbor_of(const Square& square, Callback callback) +void Square::for_each_neighbor(Callback callback) { - int r = square.row; - int c = square.column; + int r = row; + int c = column; if (r > 0) // Up - callback(this->square(r - 1, c)); + callback(field->square(r - 1, c)); if (c > 0) // Left - callback(this->square(r, c - 1)); - if (r < (m_rows - 1)) // Down - callback(this->square(r + 1, c)); - if (c < (m_columns - 1)) // Right - callback(this->square(r, c + 1)); + callback(field->square(r, c - 1)); + if (r < (field->m_rows - 1)) // Down + callback(field->square(r + 1, c)); + if (c < (field->m_columns - 1)) // Right + callback(field->square(r, c + 1)); if (r > 0 && c > 0) // UpLeft - callback(this->square(r - 1, c - 1)); - if (r > 0 && c < (m_columns - 1)) // UpRight - callback(this->square(r - 1, c + 1)); - if (r < (m_rows - 1) && c > 0) // DownLeft - callback(this->square(r + 1, c - 1)); - if (r < (m_rows - 1) && c < (m_columns - 1)) // DownRight - callback(this->square(r + 1, c + 1)); + callback(field->square(r - 1, c - 1)); + if (r > 0 && c < (field->m_columns - 1)) // UpRight + callback(field->square(r - 1, c + 1)); + if (r < (field->m_rows - 1) && c > 0) // DownLeft + callback(field->square(r + 1, c - 1)); + if (r < (field->m_rows - 1) && c < (field->m_columns - 1)) // DownRight + callback(field->square(r + 1, c + 1)); } void Field::reset() { - m_seconds_elapsed = 0; + m_time_elapsed = 0; m_time_label.set_text("0"); m_flags_left = m_mine_count; m_flag_label.set_text(String::format("%u", m_flags_left)); - m_timer.start(); + m_timer.stop(); set_greedy_for_hits(false); set_face(Face::Default); srand(time(nullptr)); + m_squares.clear(); m_squares.resize(rows() * columns()); HashTable mines; @@ -122,15 +174,17 @@ void Field::reset() int i = 0; for (int r = 0; r < rows(); ++r) { for (int c = 0; c < columns(); ++c) { + m_squares[i] = make(); Rect rect = { frame_thickness() + c * square_size(), frame_thickness() + r * square_size(), square_size(), square_size() }; auto& square = this->square(r, c); + square.field = this; square.row = r; square.column = c; square.has_mine = mines.contains(i); square.has_flag = false; square.is_swept = false; if (!square.label) - square.label = new GLabel(this); + square.label = new SquareLabel(square, this); square.label->set_relative_rect(rect); square.label->set_visible(false); square.label->set_icon(square.has_mine ? m_mine_bitmap : nullptr); @@ -138,6 +192,8 @@ void Field::reset() square.label->set_fill_with_background_color(false); if (!square.button) square.button = new SquareButton(this); + square.button->set_checkable(true); + square.button->set_checked(false); square.button->set_icon(nullptr); square.button->set_relative_rect(rect); square.button->set_visible(true); @@ -147,6 +203,9 @@ void Field::reset() square.button->on_right_click = [this, &square] { on_square_right_clicked(square); }; + square.label->on_chord_click = [this, &square] { + on_square_chorded(square); + }; ++i; } } @@ -154,7 +213,7 @@ void Field::reset() for (int c = 0; c < columns(); ++c) { auto& square = this->square(r, c); int number = 0; - for_each_neighbor_of(square, [&number] (auto& neighbor) { + square.for_each_neighbor([&number] (auto& neighbor) { number += neighbor.has_mine; }); square.number = number; @@ -173,7 +232,7 @@ void Field::reset() void Field::flood_fill(Square& square) { on_square_clicked(square); - for_each_neighbor_of(square, [this] (auto& neighbor) { + square.for_each_neighbor([this] (auto& neighbor) { if (!neighbor.is_swept && !neighbor.has_mine && neighbor.number == 0) flood_fill(neighbor); if (!neighbor.has_mine && neighbor.number) @@ -208,6 +267,8 @@ void Field::on_square_clicked(Square& square) return; if (square.has_flag) return; + if (!m_timer.is_active()) + m_timer.start(); update(); square.is_swept = true; square.button->set_visible(false); @@ -225,6 +286,26 @@ void Field::on_square_clicked(Square& square) win(); } +void Field::on_square_chorded(Square& square) +{ + if (!square.is_swept) + return; + if (!square.number) + return; + int adjacent_flags = 0; + square.for_each_neighbor([&] (auto& neighbor) { + if (neighbor.has_flag) + ++adjacent_flags; + }); + if (square.number != adjacent_flags) + return; + square.for_each_neighbor([&] (auto& neighbor) { + if (neighbor.has_flag) + return; + on_square_clicked(neighbor); + }); +} + void Field::on_square_right_clicked(Square& square) { if (square.is_swept) @@ -266,11 +347,51 @@ void Field::reveal_mines() for (int r = 0; r < rows(); ++r) { for (int c = 0; c < columns(); ++c) { auto& square = this->square(r, c); - if (square.has_mine) { + if (square.has_mine && !square.has_flag) { square.button->set_visible(false); square.label->set_visible(true); } + if (!square.has_mine && square.has_flag) { + square.button->set_icon(*m_badflag_bitmap); + square.button->set_visible(true); + square.label->set_visible(false); + } } } update(); } + +void Field::set_chord_preview(Square& square, bool chord_preview) +{ + if (m_chord_preview == chord_preview) + return; + m_chord_preview = chord_preview; + square.for_each_neighbor([&] (auto& neighbor) { + neighbor.button->set_checked(false); + if (!neighbor.has_flag) + neighbor.button->set_checked(chord_preview); + }); +} + +void Field::set_field_size(int rows, int columns, int mine_count) +{ + if (m_rows == rows && m_columns == columns && m_mine_count == mine_count) + return; + auto config = CConfigFile::get_for_app("Minesweeper"); + config->write_num_entry("Game", "MineCount", mine_count); + config->write_num_entry("Game", "Rows", rows); + config->write_num_entry("Game", "Columns", columns); + m_rows = rows; + m_columns = columns; + m_mine_count = mine_count; + reset(); + set_preferred_size({ frame_thickness() * 2 + m_columns * square_size(), frame_thickness() * 2 + m_rows * square_size() }); + if (on_size_changed) + on_size_changed(); +} + +Square::~Square() +{ + delete label; + delete button; +} diff --git a/Games/Minesweeper/Field.h b/Games/Minesweeper/Field.h index 2e91a24c37..6461e0f0a2 100644 --- a/Games/Minesweeper/Field.h +++ b/Games/Minesweeper/Field.h @@ -2,12 +2,21 @@ #include #include +#include -class SquareButton; +class Field; class GButton; class GLabel; +class SquareButton; +class SquareLabel; -struct Square { +class Square { + AK_MAKE_NONCOPYABLE(Square) +public: + Square() { } + ~Square(); + + Field* field { nullptr }; bool is_swept { false }; bool has_mine { false }; bool has_flag { false }; @@ -15,10 +24,14 @@ struct Square { int column { 0 }; int number { 0 }; SquareButton* button { nullptr }; - GLabel* label { nullptr }; + SquareLabel* label { nullptr }; + + template void for_each_neighbor(Callback); }; class Field final : public GFrame { + friend class Square; + friend class SquareLabel; public: Field(GLabel& flag_label, GLabel& time_label, GButton& face_button, GWidget* parent); virtual ~Field() override; @@ -28,19 +41,25 @@ public: int mine_count() const { return m_mine_count; } int square_size() const { return 15; } + void set_field_size(int rows, int columns, int mine_count); + void reset(); + Function on_size_changed; + private: virtual void paint_event(GPaintEvent&) override; void on_square_clicked(Square&); void on_square_right_clicked(Square&); + void on_square_chorded(Square&); void game_over(); void win(); void reveal_mines(); + void set_chord_preview(Square&, bool); - Square& square(int row, int column) { return m_squares[row * columns() + column]; } - const Square& square(int row, int column) const { return m_squares[row * columns() + column]; } + Square& square(int row, int column) { return *m_squares[row * columns() + column]; } + const Square& square(int row, int column) const { return *m_squares[row * columns() + column]; } void flood_fill(Square&); @@ -53,15 +72,20 @@ private: int m_columns { 9 }; int m_mine_count { 10 }; int m_unswept_empties { 0 }; - Vector m_squares; + Vector> m_squares; RetainPtr m_mine_bitmap; RetainPtr m_flag_bitmap; + RetainPtr m_badflag_bitmap; + RetainPtr m_default_face_bitmap; + RetainPtr m_good_face_bitmap; + RetainPtr m_bad_face_bitmap; RetainPtr m_number_bitmap[8]; GButton& m_face_button; GLabel& m_flag_label; GLabel& m_time_label; CTimer m_timer; - int m_seconds_elapsed { 0 }; + int m_time_elapsed { 0 }; int m_flags_left { 0 }; Face m_face { Face::Default }; + bool m_chord_preview { false }; }; diff --git a/Games/Minesweeper/main.cpp b/Games/Minesweeper/main.cpp index d1771b70ad..88c62da9a8 100644 --- a/Games/Minesweeper/main.cpp +++ b/Games/Minesweeper/main.cpp @@ -7,6 +7,7 @@ #include #include #include +#include int main(int argc, char** argv) { @@ -31,11 +32,28 @@ int main(int argc, char** argv) flag_icon_label->set_icon(GraphicsBitmap::load_from_file("/res/icons/minesweeper/flag.png")); auto* flag_label = new GLabel(container); auto* face_button = new GButton(container); + face_button->set_button_style(ButtonStyle::CoolBar); + face_button->set_size_policy(SizePolicy::Fixed, SizePolicy::Fill); + face_button->set_preferred_size({ 36, 0 }); auto* time_icon_label = new GLabel(container); time_icon_label->set_icon(GraphicsBitmap::load_from_file("/res/icons/minesweeper/timer.png")); auto* time_label = new GLabel(container); auto* field = new Field(*flag_label, *time_label, *face_button, widget); + field->on_size_changed = [&] { + auto size = field->preferred_size(); + size.set_height(size.height() + container->preferred_size().height()); + window->resize(size); + }; + + { + auto config = CConfigFile::get_for_app("Minesweeper"); + int mine_count = config->read_num_entry("Game", "MineCount", 10); + int rows = config->read_num_entry("Game", "Rows", 9); + int columns = config->read_num_entry("Game", "Columns", 9); + field->set_field_size(rows, columns, mine_count); + } + auto menubar = make(); auto app_menu = make("Minesweeper"); @@ -49,6 +67,16 @@ int main(int argc, char** argv) game_menu->add_action(GAction::create("New game", { Mod_None, Key_F2 }, [field] (const GAction&) { field->reset(); })); + game_menu->add_separator(); + game_menu->add_action(GAction::create("Beginner", { Mod_Ctrl, Key_B }, [field] (const GAction&) { + field->set_field_size(9, 9, 10); + })); + game_menu->add_action(GAction::create("Intermediate", { Mod_Ctrl, Key_I }, [field] (const GAction&) { + field->set_field_size(16, 16, 40); + })); + game_menu->add_action(GAction::create("Expert", { Mod_Ctrl, Key_E }, [field] (const GAction&) { + field->set_field_size(16, 30, 99); + })); menubar->add_menu(move(game_menu)); auto help_menu = make("Help");