diff --git a/Base/res/icons/minesweeper/badflag.png b/Base/res/icons/minesweeper/badflag.png new file mode 100644 index 00000000000..d9ac0cf1552 Binary files /dev/null and b/Base/res/icons/minesweeper/badflag.png differ diff --git a/Base/res/icons/minesweeper/face-bad.png b/Base/res/icons/minesweeper/face-bad.png index 049ddc8e825..4e4fc82a96f 100644 Binary files a/Base/res/icons/minesweeper/face-bad.png and b/Base/res/icons/minesweeper/face-bad.png differ diff --git a/Games/Minesweeper/Field.cpp b/Games/Minesweeper/Field.cpp index e043e1d06e9..81b825b8500 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 2e91a24c370..6461e0f0a2b 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 d1771b70ad5..88c62da9a88 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");