Field.cpp 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505
  1. #include "Field.h"
  2. #include <AK/HashTable.h>
  3. #include <AK/Queue.h>
  4. #include <LibCore/CConfigFile.h>
  5. #include <LibGUI/GButton.h>
  6. #include <LibGUI/GLabel.h>
  7. #include <LibGUI/GPainter.h>
  8. #include <time.h>
  9. #include <unistd.h>
  10. class SquareButton final : public GButton {
  11. public:
  12. SquareButton(GWidget* parent)
  13. : GButton(parent)
  14. {
  15. }
  16. Function<void()> on_right_click;
  17. Function<void()> on_middle_click;
  18. virtual void mousedown_event(GMouseEvent& event) override
  19. {
  20. if (event.button() == GMouseButton::Right) {
  21. if (on_right_click)
  22. on_right_click();
  23. }
  24. if (event.button() == GMouseButton::Middle) {
  25. if (on_middle_click)
  26. on_middle_click();
  27. }
  28. GButton::mousedown_event(event);
  29. }
  30. };
  31. class SquareLabel final : public GLabel {
  32. public:
  33. SquareLabel(Square& square, GWidget* parent)
  34. : GLabel(parent)
  35. , m_square(square)
  36. {
  37. }
  38. Function<void()> on_chord_click;
  39. virtual void mousedown_event(GMouseEvent& event) override
  40. {
  41. if (event.button() == GMouseButton::Right || event.button() == GMouseButton::Left) {
  42. if (event.buttons() == (GMouseButton::Right | GMouseButton::Left) ||
  43. m_square.field->is_single_chording()) {
  44. m_chord = true;
  45. m_square.field->set_chord_preview(m_square, true);
  46. }
  47. }
  48. if (event.button() == GMouseButton::Middle) {
  49. m_square.field->for_each_square([](auto& square) {
  50. if (square.is_considering) {
  51. square.is_considering = false;
  52. square.button->set_icon(nullptr);
  53. }
  54. });
  55. }
  56. GLabel::mousedown_event(event);
  57. }
  58. virtual void mousemove_event(GMouseEvent& event) override
  59. {
  60. if (m_chord) {
  61. if (rect().contains(event.position())) {
  62. m_square.field->set_chord_preview(m_square, true);
  63. } else {
  64. m_square.field->set_chord_preview(m_square, false);
  65. }
  66. }
  67. GLabel::mousemove_event(event);
  68. }
  69. virtual void mouseup_event(GMouseEvent& event) override
  70. {
  71. if (m_chord) {
  72. if (event.button() == GMouseButton::Left || event.button() == GMouseButton::Right) {
  73. if (rect().contains(event.position())) {
  74. if (on_chord_click)
  75. on_chord_click();
  76. }
  77. m_chord = false;
  78. }
  79. }
  80. m_square.field->set_chord_preview(m_square, m_chord);
  81. GLabel::mouseup_event(event);
  82. }
  83. Square& m_square;
  84. bool m_chord { false };
  85. };
  86. Field::Field(GLabel& flag_label, GLabel& time_label, GButton& face_button, GWidget* parent, Function<void(Size)> on_size_changed)
  87. : GFrame(parent)
  88. , m_face_button(face_button)
  89. , m_flag_label(flag_label)
  90. , m_time_label(time_label)
  91. , m_on_size_changed(move(on_size_changed))
  92. {
  93. srand(time(nullptr));
  94. m_timer = CTimer::construct();
  95. m_timer->on_timeout = [this] {
  96. ++m_time_elapsed;
  97. m_time_label.set_text(String::format("%u.%u", m_time_elapsed / 10, m_time_elapsed % 10));
  98. };
  99. m_timer->set_interval(100);
  100. set_frame_thickness(2);
  101. set_frame_shape(FrameShape::Container);
  102. set_frame_shadow(FrameShadow::Sunken);
  103. m_mine_bitmap = GraphicsBitmap::load_from_file("/res/icons/minesweeper/mine.png");
  104. m_flag_bitmap = GraphicsBitmap::load_from_file("/res/icons/minesweeper/flag.png");
  105. m_badflag_bitmap = GraphicsBitmap::load_from_file("/res/icons/minesweeper/badflag.png");
  106. m_consider_bitmap = GraphicsBitmap::load_from_file("/res/icons/minesweeper/consider.png");
  107. m_default_face_bitmap = GraphicsBitmap::load_from_file("/res/icons/minesweeper/face-default.png");
  108. m_good_face_bitmap = GraphicsBitmap::load_from_file("/res/icons/minesweeper/face-good.png");
  109. m_bad_face_bitmap = GraphicsBitmap::load_from_file("/res/icons/minesweeper/face-bad.png");
  110. for (int i = 0; i < 8; ++i)
  111. m_number_bitmap[i] = GraphicsBitmap::load_from_file(String::format("/res/icons/minesweeper/%u.png", i + 1));
  112. set_fill_with_background_color(true);
  113. set_background_color(Color::WarmGray);
  114. reset();
  115. m_face_button.on_click = [this](auto&) { reset(); };
  116. set_face(Face::Default);
  117. {
  118. auto config = CConfigFile::get_for_app("Minesweeper");
  119. bool single_chording = config->read_num_entry("Minesweeper", "SingleChording", false);
  120. int mine_count = config->read_num_entry("Game", "MineCount", 10);
  121. int rows = config->read_num_entry("Game", "Rows", 9);
  122. int columns = config->read_num_entry("Game", "Columns", 9);
  123. set_field_size(rows, columns, mine_count);
  124. set_single_chording(single_chording);
  125. }
  126. }
  127. Field::~Field()
  128. {
  129. }
  130. void Field::set_face(Face face)
  131. {
  132. switch (face) {
  133. case Face::Default:
  134. m_face_button.set_icon(*m_default_face_bitmap);
  135. break;
  136. case Face::Good:
  137. m_face_button.set_icon(*m_good_face_bitmap);
  138. break;
  139. case Face::Bad:
  140. m_face_button.set_icon(*m_bad_face_bitmap);
  141. break;
  142. }
  143. }
  144. template<typename Callback>
  145. void Square::for_each_neighbor(Callback callback)
  146. {
  147. int r = row;
  148. int c = column;
  149. if (r > 0) // Up
  150. callback(field->square(r - 1, c));
  151. if (c > 0) // Left
  152. callback(field->square(r, c - 1));
  153. if (r < (field->m_rows - 1)) // Down
  154. callback(field->square(r + 1, c));
  155. if (c < (field->m_columns - 1)) // Right
  156. callback(field->square(r, c + 1));
  157. if (r > 0 && c > 0) // UpLeft
  158. callback(field->square(r - 1, c - 1));
  159. if (r > 0 && c < (field->m_columns - 1)) // UpRight
  160. callback(field->square(r - 1, c + 1));
  161. if (r < (field->m_rows - 1) && c > 0) // DownLeft
  162. callback(field->square(r + 1, c - 1));
  163. if (r < (field->m_rows - 1) && c < (field->m_columns - 1)) // DownRight
  164. callback(field->square(r + 1, c + 1));
  165. }
  166. void Field::reset()
  167. {
  168. m_first_click = true;
  169. set_updates_enabled(false);
  170. m_time_elapsed = 0;
  171. m_time_label.set_text("0");
  172. m_flags_left = m_mine_count;
  173. m_flag_label.set_text(String::number(m_flags_left));
  174. m_timer->stop();
  175. set_greedy_for_hits(false);
  176. set_face(Face::Default);
  177. m_squares.resize(max(m_squares.size(), rows() * columns()));
  178. for (int i = rows() * columns(); i < m_squares.size(); ++i) {
  179. auto& square = m_squares[i];
  180. square->button->set_visible(false);
  181. square->label->set_visible(false);
  182. }
  183. HashTable<int> mines;
  184. while (mines.size() != m_mine_count) {
  185. int location = rand() % (rows() * columns());
  186. if (!mines.contains(location))
  187. mines.set(location);
  188. }
  189. int i = 0;
  190. for (int r = 0; r < rows(); ++r) {
  191. for (int c = 0; c < columns(); ++c) {
  192. if (!m_squares[i])
  193. m_squares[i] = make<Square>();
  194. Rect rect = { frame_thickness() + c * square_size(), frame_thickness() + r * square_size(), square_size(), square_size() };
  195. auto& square = this->square(r, c);
  196. square.field = this;
  197. square.row = r;
  198. square.column = c;
  199. square.has_mine = mines.contains(i);
  200. square.has_flag = false;
  201. square.is_considering = false;
  202. square.is_swept = false;
  203. if (!square.label) {
  204. square.label = new SquareLabel(square, this);
  205. square.label->set_background_color(Color::from_rgb(0xff4040));
  206. }
  207. square.label->set_fill_with_background_color(false);
  208. square.label->set_relative_rect(rect);
  209. square.label->set_visible(false);
  210. square.label->set_icon(square.has_mine ? m_mine_bitmap : nullptr);
  211. if (!square.button) {
  212. square.button = new SquareButton(this);
  213. square.button->on_click = [this, &square](GButton&) {
  214. on_square_clicked(square);
  215. };
  216. square.button->on_right_click = [this, &square] {
  217. on_square_right_clicked(square);
  218. };
  219. square.button->on_middle_click = [this, &square] {
  220. on_square_middle_clicked(square);
  221. };
  222. square.label->on_chord_click = [this, &square] {
  223. on_square_chorded(square);
  224. };
  225. }
  226. square.button->set_checked(false);
  227. square.button->set_icon(nullptr);
  228. square.button->set_relative_rect(rect);
  229. square.button->set_visible(true);
  230. ++i;
  231. }
  232. }
  233. for (int r = 0; r < rows(); ++r) {
  234. for (int c = 0; c < columns(); ++c) {
  235. auto& square = this->square(r, c);
  236. int number = 0;
  237. square.for_each_neighbor([&number](auto& neighbor) {
  238. number += neighbor.has_mine;
  239. });
  240. square.number = number;
  241. if (square.has_mine)
  242. continue;
  243. if (square.number)
  244. square.label->set_icon(m_number_bitmap[square.number - 1]);
  245. }
  246. }
  247. m_unswept_empties = rows() * columns() - m_mine_count;
  248. set_updates_enabled(true);
  249. }
  250. void Field::flood_fill(Square& square)
  251. {
  252. Queue<Square*> queue;
  253. queue.enqueue(&square);
  254. while (!queue.is_empty()) {
  255. Square* s = queue.dequeue();
  256. s->for_each_neighbor([this, &queue](Square& neighbor) {
  257. if (!neighbor.is_swept && !neighbor.has_mine && neighbor.number == 0) {
  258. on_square_clicked_impl(neighbor, false);
  259. queue.enqueue(&neighbor);
  260. }
  261. if (!neighbor.has_mine && neighbor.number)
  262. on_square_clicked_impl(neighbor, false);
  263. });
  264. }
  265. }
  266. void Field::paint_event(GPaintEvent& event)
  267. {
  268. GFrame::paint_event(event);
  269. GPainter painter(*this);
  270. painter.add_clip_rect(event.rect());
  271. auto inner_rect = frame_inner_rect();
  272. painter.add_clip_rect(inner_rect);
  273. for (int y = inner_rect.top() - 1; y <= inner_rect.bottom(); y += square_size()) {
  274. Point a { inner_rect.left(), y };
  275. Point b { inner_rect.right(), y };
  276. painter.draw_line(a, b, Color::MidGray);
  277. }
  278. for (int x = frame_inner_rect().left() - 1; x <= frame_inner_rect().right(); x += square_size()) {
  279. Point a { x, inner_rect.top() };
  280. Point b { x, inner_rect.bottom() };
  281. painter.draw_line(a, b, Color::MidGray);
  282. }
  283. }
  284. void Field::on_square_clicked_impl(Square& square, bool should_flood_fill)
  285. {
  286. if (m_first_click) {
  287. while (square.has_mine) {
  288. reset();
  289. }
  290. }
  291. m_first_click = false;
  292. if (square.is_swept)
  293. return;
  294. if (square.has_flag)
  295. return;
  296. if (square.is_considering)
  297. return;
  298. if (!m_timer->is_active())
  299. m_timer->start();
  300. update();
  301. square.is_swept = true;
  302. square.button->set_visible(false);
  303. square.label->set_visible(true);
  304. if (square.has_mine) {
  305. square.label->set_fill_with_background_color(true);
  306. game_over();
  307. return;
  308. }
  309. --m_unswept_empties;
  310. if (should_flood_fill && square.number == 0)
  311. flood_fill(square);
  312. if (!m_unswept_empties)
  313. win();
  314. }
  315. void Field::on_square_clicked(Square& square)
  316. {
  317. on_square_clicked_impl(square, true);
  318. }
  319. void Field::on_square_chorded(Square& square)
  320. {
  321. if (!square.is_swept)
  322. return;
  323. if (!square.number)
  324. return;
  325. int adjacent_flags = 0;
  326. square.for_each_neighbor([&](auto& neighbor) {
  327. if (neighbor.has_flag)
  328. ++adjacent_flags;
  329. });
  330. if (square.number != adjacent_flags)
  331. return;
  332. square.for_each_neighbor([&](auto& neighbor) {
  333. if (neighbor.has_flag)
  334. return;
  335. on_square_clicked(neighbor);
  336. });
  337. }
  338. void Field::on_square_right_clicked(Square& square)
  339. {
  340. if (square.is_swept)
  341. return;
  342. if (!square.has_flag && !m_flags_left)
  343. return;
  344. set_flag(square, !square.has_flag);
  345. }
  346. void Field::set_flag(Square& square, bool flag)
  347. {
  348. ASSERT(!square.is_swept);
  349. if (square.has_flag == flag)
  350. return;
  351. square.is_considering = false;
  352. if (!flag) {
  353. ++m_flags_left;
  354. } else {
  355. ASSERT(m_flags_left);
  356. --m_flags_left;
  357. }
  358. square.has_flag = flag;
  359. m_flag_label.set_text(String::number(m_flags_left));
  360. square.button->set_icon(square.has_flag ? m_flag_bitmap : nullptr);
  361. square.button->update();
  362. }
  363. void Field::on_square_middle_clicked(Square& square)
  364. {
  365. if (square.is_swept)
  366. return;
  367. if (square.has_flag) {
  368. ++m_flags_left;
  369. square.has_flag = false;
  370. m_flag_label.set_text(String::number(m_flags_left));
  371. }
  372. square.is_considering = !square.is_considering;
  373. square.button->set_icon(square.is_considering ? m_consider_bitmap : nullptr);
  374. square.button->update();
  375. }
  376. void Field::win()
  377. {
  378. m_timer->stop();
  379. set_greedy_for_hits(true);
  380. set_face(Face::Good);
  381. for_each_square([&](auto& square) {
  382. if (!square.has_flag && square.has_mine)
  383. set_flag(square, true);
  384. });
  385. reveal_mines();
  386. }
  387. void Field::game_over()
  388. {
  389. m_timer->stop();
  390. set_greedy_for_hits(true);
  391. set_face(Face::Bad);
  392. reveal_mines();
  393. }
  394. void Field::reveal_mines()
  395. {
  396. for (int r = 0; r < rows(); ++r) {
  397. for (int c = 0; c < columns(); ++c) {
  398. auto& square = this->square(r, c);
  399. if (square.has_mine && !square.has_flag) {
  400. square.button->set_visible(false);
  401. square.label->set_visible(true);
  402. }
  403. if (!square.has_mine && square.has_flag) {
  404. square.button->set_icon(*m_badflag_bitmap);
  405. square.button->set_visible(true);
  406. square.label->set_visible(false);
  407. }
  408. }
  409. }
  410. update();
  411. }
  412. void Field::set_chord_preview(Square& square, bool chord_preview)
  413. {
  414. if (m_chord_preview == chord_preview)
  415. return;
  416. m_chord_preview = chord_preview;
  417. square.for_each_neighbor([&](auto& neighbor) {
  418. neighbor.button->set_checked(false);
  419. if (!neighbor.has_flag && !neighbor.is_considering)
  420. neighbor.button->set_checked(chord_preview);
  421. });
  422. }
  423. void Field::set_field_size(int rows, int columns, int mine_count)
  424. {
  425. if (m_rows == rows && m_columns == columns && m_mine_count == mine_count)
  426. return;
  427. {
  428. auto config = CConfigFile::get_for_app("Minesweeper");
  429. config->write_num_entry("Game", "MineCount", mine_count);
  430. config->write_num_entry("Game", "Rows", rows);
  431. config->write_num_entry("Game", "Columns", columns);
  432. }
  433. m_rows = rows;
  434. m_columns = columns;
  435. m_mine_count = mine_count;
  436. set_preferred_size(frame_thickness() * 2 + m_columns * square_size(), frame_thickness() * 2 + m_rows * square_size());
  437. reset();
  438. m_on_size_changed(preferred_size());
  439. }
  440. void Field::set_single_chording(bool enabled) {
  441. auto config = CConfigFile::get_for_app("Minesweeper");
  442. m_single_chording = enabled;
  443. config->write_bool_entry("Minesweeper", "SingleChording", m_single_chording);
  444. }
  445. Square::~Square()
  446. {
  447. delete label;
  448. delete button;
  449. }
  450. template<typename Callback>
  451. void Field::for_each_square(Callback callback)
  452. {
  453. for (int i = 0; i < rows() * columns(); ++i)
  454. callback(*m_squares[i]);
  455. }