ChessWidget.cpp 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730
  1. /*
  2. * Copyright (c) 2020-2022, the SerenityOS developers.
  3. *
  4. * SPDX-License-Identifier: BSD-2-Clause
  5. */
  6. #include "ChessWidget.h"
  7. #include "PromotionDialog.h"
  8. #include <AK/DeprecatedString.h>
  9. #include <AK/Random.h>
  10. #include <AK/String.h>
  11. #include <LibCore/DateTime.h>
  12. #include <LibCore/File.h>
  13. #include <LibGUI/MessageBox.h>
  14. #include <LibGUI/Painter.h>
  15. #include <LibGfx/AntiAliasingPainter.h>
  16. #include <LibGfx/Font/Font.h>
  17. #include <LibGfx/Font/FontDatabase.h>
  18. #include <LibGfx/Path.h>
  19. #include <unistd.h>
  20. ErrorOr<NonnullRefPtr<ChessWidget>> ChessWidget::try_create()
  21. {
  22. auto widget = TRY(AK::adopt_nonnull_ref_or_enomem(new (nothrow) ChessWidget));
  23. widget->set_piece_set("stelar7"sv);
  24. return widget;
  25. }
  26. void ChessWidget::paint_event(GUI::PaintEvent& event)
  27. {
  28. int const min_size = min(width(), height());
  29. int const widget_offset_x = (window()->width() - min_size) / 2;
  30. int const widget_offset_y = (window()->height() - min_size) / 2;
  31. GUI::Frame::paint_event(event);
  32. GUI::Painter painter(*this);
  33. painter.add_clip_rect(event.rect());
  34. painter.fill_rect(frame_inner_rect(), Color::Black);
  35. painter.translate(frame_thickness() + widget_offset_x, frame_thickness() + widget_offset_y);
  36. auto square_width = min_size / 8;
  37. auto square_height = min_size / 8;
  38. auto square_margin = square_width / 10;
  39. int coord_rank_file = (side() == Chess::Color::White) ? 0 : 7;
  40. Chess::Board& active_board = (m_playback ? board_playback() : board());
  41. auto& coordinate_font = Gfx::FontDatabase::default_font().bold_variant();
  42. Chess::Square::for_each([&](Chess::Square sq) {
  43. Gfx::IntRect tile_rect;
  44. if (side() == Chess::Color::White) {
  45. tile_rect = { sq.file * square_width, (7 - sq.rank) * square_height, square_width, square_height };
  46. } else {
  47. tile_rect = { (7 - sq.file) * square_width, sq.rank * square_height, square_width, square_height };
  48. }
  49. painter.fill_rect(tile_rect, (sq.is_light()) ? board_theme().light_square_color : board_theme().dark_square_color);
  50. if (active_board.last_move().has_value() && (active_board.last_move().value().to == sq || active_board.last_move().value().from == sq)) {
  51. painter.fill_rect(tile_rect, m_move_highlight_color);
  52. }
  53. if (m_coordinates) {
  54. auto coord = sq.to_algebraic();
  55. auto text_color = (sq.is_light()) ? board_theme().dark_square_color : board_theme().light_square_color;
  56. auto shrunken_rect = tile_rect;
  57. shrunken_rect.shrink(4, 4);
  58. if (sq.rank == coord_rank_file)
  59. painter.draw_text(shrunken_rect, coord.substring_view(0, 1), coordinate_font, Gfx::TextAlignment::BottomRight, text_color);
  60. if (sq.file == coord_rank_file)
  61. painter.draw_text(shrunken_rect, coord.substring_view(1, 1), coordinate_font, Gfx::TextAlignment::TopLeft, text_color);
  62. }
  63. for (auto& m : m_board_markings) {
  64. if (m.type() == BoardMarking::Type::Square && m.from == sq) {
  65. Gfx::Color color = m.secondary_color ? m_marking_secondary_color : (m.alternate_color ? m_marking_alternate_color : m_marking_primary_color);
  66. painter.fill_rect(tile_rect, color);
  67. }
  68. }
  69. if (!(m_dragging_piece && sq == m_moving_square)) {
  70. auto bmp = m_pieces.get(active_board.get_piece(sq));
  71. if (bmp.has_value()) {
  72. painter.draw_scaled_bitmap(tile_rect.shrunken(square_margin, square_margin, square_margin, square_margin), *bmp.value(), bmp.value()->rect(), 1.0f, Gfx::Painter::ScalingMode::BilinearBlend);
  73. }
  74. }
  75. return IterationDecision::Continue;
  76. });
  77. auto draw_arrow = [&painter](Gfx::FloatPoint A, Gfx::FloatPoint B, float w1, float w2, float h, Gfx::Color color) {
  78. float dx = B.x() - A.x();
  79. float dy = A.y() - B.y();
  80. float phi = atan2f(dy, dx);
  81. float hdx = h * cosf(phi);
  82. float hdy = h * sinf(phi);
  83. const auto cos_pi_2_phi = cosf(float { M_PI_2 } - phi);
  84. const auto sin_pi_2_phi = sinf(float { M_PI_2 } - phi);
  85. Gfx::FloatPoint A1(A.x() - (w1 / 2) * cos_pi_2_phi, A.y() - (w1 / 2) * sin_pi_2_phi);
  86. Gfx::FloatPoint B3(A.x() + (w1 / 2) * cos_pi_2_phi, A.y() + (w1 / 2) * sin_pi_2_phi);
  87. Gfx::FloatPoint A2(A1.x() + (dx - hdx), A1.y() - (dy - hdy));
  88. Gfx::FloatPoint B2(B3.x() + (dx - hdx), B3.y() - (dy - hdy));
  89. Gfx::FloatPoint A3(A2.x() - w2 * cos_pi_2_phi, A2.y() - w2 * sin_pi_2_phi);
  90. Gfx::FloatPoint B1(B2.x() + w2 * cos_pi_2_phi, B2.y() + w2 * sin_pi_2_phi);
  91. auto path = Gfx::Path();
  92. path.move_to(A);
  93. path.line_to(A1);
  94. path.line_to(A2);
  95. path.line_to(A3);
  96. path.line_to(B);
  97. path.line_to(B1);
  98. path.line_to(B2);
  99. path.line_to(B3);
  100. path.line_to(A);
  101. path.close();
  102. painter.fill_path(path, color, Gfx::Painter::WindingRule::EvenOdd);
  103. };
  104. for (auto& m : m_board_markings) {
  105. if (m.type() == BoardMarking::Type::Arrow) {
  106. Gfx::FloatPoint arrow_start;
  107. Gfx::FloatPoint arrow_end;
  108. if (side() == Chess::Color::White) {
  109. arrow_start = { m.from.file * square_width + square_width / 2.0f, (7 - m.from.rank) * square_height + square_height / 2.0f };
  110. arrow_end = { m.to.file * square_width + square_width / 2.0f, (7 - m.to.rank) * square_height + square_height / 2.0f };
  111. } else {
  112. arrow_start = { (7 - m.from.file) * square_width + square_width / 2.0f, m.from.rank * square_height + square_height / 2.0f };
  113. arrow_end = { (7 - m.to.file) * square_width + square_width / 2.0f, m.to.rank * square_height + square_height / 2.0f };
  114. }
  115. Gfx::Color color = m.secondary_color ? m_marking_secondary_color : (m.alternate_color ? m_marking_primary_color : m_marking_alternate_color);
  116. draw_arrow(arrow_start, arrow_end, square_width / 8.0f, square_width / 10.0f, square_height / 2.5f, color);
  117. }
  118. }
  119. if (m_dragging_piece) {
  120. if (m_show_available_moves) {
  121. Gfx::IntPoint move_point;
  122. Gfx::IntPoint point_offset = { square_width / 3, square_height / 3 };
  123. Gfx::IntSize rect_size = { square_width / 3, square_height / 3 };
  124. for (auto const& square : m_available_moves) {
  125. if (side() == Chess::Color::White) {
  126. move_point = { square.file * square_width, (7 - square.rank) * square_height };
  127. } else {
  128. move_point = { (7 - square.file) * square_width, square.rank * square_height };
  129. }
  130. Gfx::AntiAliasingPainter aa_painter { painter };
  131. aa_painter.fill_ellipse({ move_point + point_offset, rect_size }, Gfx::Color::LightGray);
  132. }
  133. }
  134. Gfx::IntRect origin_square;
  135. if (side() == Chess::Color::White) {
  136. origin_square = { m_moving_square.file * square_width, (7 - m_moving_square.rank) * square_height, square_width, square_height };
  137. } else {
  138. origin_square = { (7 - m_moving_square.file) * square_width, m_moving_square.rank * square_height, square_width, square_height };
  139. }
  140. painter.fill_rect(origin_square, m_move_highlight_color);
  141. auto bmp = m_pieces.get(active_board.get_piece(m_moving_square));
  142. if (bmp.has_value()) {
  143. auto center = m_drag_point - Gfx::IntPoint(square_width / 2, square_height / 2);
  144. painter.draw_scaled_bitmap({ center, { square_width, square_height } }, *bmp.value(), bmp.value()->rect(), 1.0f, Gfx::Painter::ScalingMode::BilinearBlend);
  145. }
  146. }
  147. }
  148. void ChessWidget::mousedown_event(GUI::MouseEvent& event)
  149. {
  150. int const min_size = min(width(), height());
  151. int const widget_offset_x = (window()->width() - min_size) / 2;
  152. int const widget_offset_y = (window()->height() - min_size) / 2;
  153. if (!frame_inner_rect().contains(event.position()))
  154. return;
  155. if (event.button() == GUI::MouseButton::Secondary) {
  156. if (m_dragging_piece) {
  157. m_dragging_piece = false;
  158. set_override_cursor(Gfx::StandardCursor::None);
  159. m_available_moves.clear();
  160. } else {
  161. m_current_marking.from = mouse_to_square(event);
  162. }
  163. return;
  164. }
  165. m_board_markings.clear();
  166. auto square = mouse_to_square(event);
  167. auto piece = board().get_piece(square);
  168. if (drag_enabled() && piece.color == board().turn() && !m_playback) {
  169. m_dragging_piece = true;
  170. set_override_cursor(Gfx::StandardCursor::Drag);
  171. m_drag_point = { event.position().x() - widget_offset_x, event.position().y() - widget_offset_y };
  172. m_moving_square = square;
  173. m_board.generate_moves([&](Chess::Move move) {
  174. if (move.from == m_moving_square) {
  175. m_available_moves.append(move.to);
  176. }
  177. return IterationDecision::Continue;
  178. });
  179. }
  180. update();
  181. }
  182. void ChessWidget::mouseup_event(GUI::MouseEvent& event)
  183. {
  184. if (!frame_inner_rect().contains(event.position()))
  185. return;
  186. if (event.button() == GUI::MouseButton::Secondary) {
  187. m_current_marking.secondary_color = event.shift();
  188. m_current_marking.alternate_color = event.ctrl();
  189. m_current_marking.to = mouse_to_square(event);
  190. auto match_index = m_board_markings.find_first_index(m_current_marking);
  191. if (match_index.has_value()) {
  192. m_board_markings.remove(match_index.value());
  193. update();
  194. return;
  195. }
  196. m_board_markings.append(m_current_marking);
  197. update();
  198. return;
  199. }
  200. if (!m_dragging_piece)
  201. return;
  202. m_dragging_piece = false;
  203. set_override_cursor(Gfx::StandardCursor::Hand);
  204. m_available_moves.clear();
  205. auto target_square = mouse_to_square(event);
  206. Chess::Move move = { m_moving_square, target_square };
  207. if (board().is_promotion_move(move)) {
  208. auto promotion_dialog = PromotionDialog::construct(*this);
  209. if (promotion_dialog->exec() == PromotionDialog::ExecResult::OK)
  210. move.promote_to = promotion_dialog->selected_piece();
  211. }
  212. if (board().apply_move(move)) {
  213. m_playback_move_number = board().moves().size();
  214. m_playback = false;
  215. m_board_playback = m_board;
  216. if (board().game_result() != Chess::Board::Result::NotFinished) {
  217. bool over = true;
  218. StringView msg;
  219. switch (board().game_result()) {
  220. case Chess::Board::Result::CheckMate:
  221. if (board().turn() == Chess::Color::White) {
  222. msg = "Black wins by Checkmate."sv;
  223. } else {
  224. msg = "White wins by Checkmate."sv;
  225. }
  226. break;
  227. case Chess::Board::Result::StaleMate:
  228. msg = "Draw by Stalemate."sv;
  229. break;
  230. case Chess::Board::Result::FiftyMoveRule:
  231. update();
  232. if (GUI::MessageBox::show(window(), "50 moves have elapsed without a capture. Claim Draw?"sv, "Claim Draw?"sv,
  233. GUI::MessageBox::Type::Information, GUI::MessageBox::InputType::YesNo)
  234. == GUI::Dialog::ExecResult::Yes) {
  235. msg = "Draw by 50 move rule."sv;
  236. } else {
  237. over = false;
  238. }
  239. break;
  240. case Chess::Board::Result::SeventyFiveMoveRule:
  241. msg = "Draw by 75 move rule."sv;
  242. break;
  243. case Chess::Board::Result::ThreeFoldRepetition:
  244. update();
  245. if (GUI::MessageBox::show(window(), "The same board state has repeated three times. Claim Draw?"sv, "Claim Draw?"sv,
  246. GUI::MessageBox::Type::Information, GUI::MessageBox::InputType::YesNo)
  247. == GUI::Dialog::ExecResult::Yes) {
  248. msg = "Draw by threefold repetition."sv;
  249. } else {
  250. over = false;
  251. }
  252. break;
  253. case Chess::Board::Result::FiveFoldRepetition:
  254. msg = "Draw by fivefold repetition."sv;
  255. break;
  256. case Chess::Board::Result::InsufficientMaterial:
  257. msg = "Draw by insufficient material."sv;
  258. break;
  259. default:
  260. VERIFY_NOT_REACHED();
  261. }
  262. if (over) {
  263. set_override_cursor(Gfx::StandardCursor::None);
  264. set_drag_enabled(false);
  265. update();
  266. GUI::MessageBox::show(window(), msg, "Game Over"sv, GUI::MessageBox::Type::Information);
  267. }
  268. } else {
  269. input_engine_move();
  270. }
  271. }
  272. update();
  273. }
  274. void ChessWidget::mousemove_event(GUI::MouseEvent& event)
  275. {
  276. int const min_size = min(width(), height());
  277. int const widget_offset_x = (window()->width() - min_size) / 2;
  278. int const widget_offset_y = (window()->height() - min_size) / 2;
  279. if (!frame_inner_rect().contains(event.position()))
  280. return;
  281. if (m_engine && board().turn() != side())
  282. return;
  283. if (!m_dragging_piece) {
  284. auto square = mouse_to_square(event);
  285. if (!square.in_bounds())
  286. return;
  287. auto piece = board().get_piece(square);
  288. if (piece.color == board().turn())
  289. set_override_cursor(Gfx::StandardCursor::Hand);
  290. else
  291. set_override_cursor(Gfx::StandardCursor::None);
  292. return;
  293. }
  294. m_drag_point = { event.position().x() - widget_offset_x, event.position().y() - widget_offset_y };
  295. update();
  296. }
  297. void ChessWidget::keydown_event(GUI::KeyEvent& event)
  298. {
  299. set_override_cursor(Gfx::StandardCursor::None);
  300. switch (event.key()) {
  301. case KeyCode::Key_Left:
  302. playback_move(PlaybackDirection::Backward);
  303. break;
  304. case KeyCode::Key_Right:
  305. playback_move(PlaybackDirection::Forward);
  306. break;
  307. case KeyCode::Key_Up:
  308. playback_move(PlaybackDirection::Last);
  309. break;
  310. case KeyCode::Key_Down:
  311. playback_move(PlaybackDirection::First);
  312. break;
  313. case KeyCode::Key_Home:
  314. playback_move(PlaybackDirection::First);
  315. break;
  316. case KeyCode::Key_End:
  317. playback_move(PlaybackDirection::Last);
  318. break;
  319. default:
  320. event.ignore();
  321. return;
  322. }
  323. update();
  324. }
  325. static constexpr StringView set_path = "/res/icons/chess/sets/"sv;
  326. static RefPtr<Gfx::Bitmap> get_piece(StringView set, StringView image)
  327. {
  328. StringBuilder builder;
  329. builder.append(set_path);
  330. builder.append(set);
  331. builder.append('/');
  332. builder.append(image);
  333. return Gfx::Bitmap::load_from_file(builder.to_deprecated_string()).release_value_but_fixme_should_propagate_errors();
  334. }
  335. void ChessWidget::set_piece_set(StringView set)
  336. {
  337. m_piece_set = set;
  338. m_pieces.set({ Chess::Color::White, Chess::Type::Pawn }, get_piece(set, "white-pawn.png"sv));
  339. m_pieces.set({ Chess::Color::Black, Chess::Type::Pawn }, get_piece(set, "black-pawn.png"sv));
  340. m_pieces.set({ Chess::Color::White, Chess::Type::Knight }, get_piece(set, "white-knight.png"sv));
  341. m_pieces.set({ Chess::Color::Black, Chess::Type::Knight }, get_piece(set, "black-knight.png"sv));
  342. m_pieces.set({ Chess::Color::White, Chess::Type::Bishop }, get_piece(set, "white-bishop.png"sv));
  343. m_pieces.set({ Chess::Color::Black, Chess::Type::Bishop }, get_piece(set, "black-bishop.png"sv));
  344. m_pieces.set({ Chess::Color::White, Chess::Type::Rook }, get_piece(set, "white-rook.png"sv));
  345. m_pieces.set({ Chess::Color::Black, Chess::Type::Rook }, get_piece(set, "black-rook.png"sv));
  346. m_pieces.set({ Chess::Color::White, Chess::Type::Queen }, get_piece(set, "white-queen.png"sv));
  347. m_pieces.set({ Chess::Color::Black, Chess::Type::Queen }, get_piece(set, "black-queen.png"sv));
  348. m_pieces.set({ Chess::Color::White, Chess::Type::King }, get_piece(set, "white-king.png"sv));
  349. m_pieces.set({ Chess::Color::Black, Chess::Type::King }, get_piece(set, "black-king.png"sv));
  350. }
  351. Chess::Square ChessWidget::mouse_to_square(GUI::MouseEvent& event) const
  352. {
  353. int const min_size = min(width(), height());
  354. int const widget_offset_x = (window()->width() - min_size) / 2;
  355. int const widget_offset_y = (window()->height() - min_size) / 2;
  356. int square_width = min_size / 8;
  357. int square_height = min_size / 8;
  358. if (side() == Chess::Color::White) {
  359. return { 7 - ((event.y() - widget_offset_y) / square_height), (event.x() - widget_offset_x) / square_width };
  360. } else {
  361. return { (event.y() - widget_offset_y) / square_height, 7 - ((event.x() - widget_offset_x) / square_width) };
  362. }
  363. }
  364. RefPtr<Gfx::Bitmap const> ChessWidget::get_piece_graphic(Chess::Piece const& piece) const
  365. {
  366. return m_pieces.get(piece).value();
  367. }
  368. void ChessWidget::reset()
  369. {
  370. m_board_markings.clear();
  371. m_playback = false;
  372. m_playback_move_number = 0;
  373. m_board_playback = Chess::Board();
  374. m_board = Chess::Board();
  375. m_side = (get_random<u32>() % 2) ? Chess::Color::White : Chess::Color::Black;
  376. m_drag_enabled = true;
  377. input_engine_move();
  378. update();
  379. }
  380. void ChessWidget::set_board_theme(StringView name)
  381. {
  382. // FIXME: Add some kind of themes.json
  383. // The following Colors have been taken from lichess.org, but i'm pretty sure they took them from chess.com.
  384. if (name == "Beige") {
  385. m_board_theme = { "Beige"sv, Color::from_rgb(0xb58863), Color::from_rgb(0xf0d9b5) };
  386. } else if (name == "Green") {
  387. m_board_theme = { "Green"sv, Color::from_rgb(0x86a666), Color::from_rgb(0xffffdd) };
  388. } else if (name == "Blue") {
  389. m_board_theme = { "Blue"sv, Color::from_rgb(0x8ca2ad), Color::from_rgb(0xdee3e6) };
  390. } else {
  391. set_board_theme("Beige"sv);
  392. }
  393. }
  394. bool ChessWidget::want_engine_move()
  395. {
  396. if (!m_engine)
  397. return false;
  398. if (board().turn() == side())
  399. return false;
  400. return true;
  401. }
  402. void ChessWidget::input_engine_move()
  403. {
  404. if (!want_engine_move())
  405. return;
  406. bool drag_was_enabled = drag_enabled();
  407. if (drag_was_enabled)
  408. set_drag_enabled(false);
  409. set_override_cursor(Gfx::StandardCursor::Wait);
  410. m_engine->get_best_move(board(), 4000, [this, drag_was_enabled](Chess::Move move) {
  411. set_override_cursor(Gfx::StandardCursor::None);
  412. if (!want_engine_move())
  413. return;
  414. set_drag_enabled(drag_was_enabled);
  415. VERIFY(board().apply_move(move));
  416. m_playback_move_number = m_board.moves().size();
  417. m_playback = false;
  418. m_board_markings.clear();
  419. update();
  420. });
  421. }
  422. void ChessWidget::playback_move(PlaybackDirection direction)
  423. {
  424. if (m_board.moves().is_empty())
  425. return;
  426. m_playback = true;
  427. m_board_markings.clear();
  428. switch (direction) {
  429. case PlaybackDirection::Backward:
  430. if (m_playback_move_number == 0)
  431. return;
  432. m_board_playback = Chess::Board();
  433. for (size_t i = 0; i < m_playback_move_number - 1; i++)
  434. m_board_playback.apply_move(m_board.moves().at(i));
  435. m_playback_move_number--;
  436. break;
  437. case PlaybackDirection::Forward:
  438. if (m_playback_move_number + 1 > m_board.moves().size()) {
  439. m_playback = false;
  440. return;
  441. }
  442. m_board_playback.apply_move(m_board.moves().at(m_playback_move_number++));
  443. if (m_playback_move_number == m_board.moves().size())
  444. m_playback = false;
  445. break;
  446. case PlaybackDirection::First:
  447. m_board_playback = Chess::Board();
  448. m_playback_move_number = 0;
  449. break;
  450. case PlaybackDirection::Last:
  451. while (m_playback) {
  452. playback_move(PlaybackDirection::Forward);
  453. }
  454. break;
  455. default:
  456. VERIFY_NOT_REACHED();
  457. }
  458. update();
  459. }
  460. DeprecatedString ChessWidget::get_fen() const
  461. {
  462. return m_playback ? m_board_playback.to_fen() : m_board.to_fen();
  463. }
  464. ErrorOr<void> ChessWidget::import_pgn(Core::File& file)
  465. {
  466. m_board = Chess::Board();
  467. ByteBuffer bytes = TRY(file.read_until_eof());
  468. StringView content = bytes;
  469. auto lines = content.lines();
  470. StringView line;
  471. size_t i = 0;
  472. // Tag Pair Section
  473. // FIXME: Parse these tags when they become relevant
  474. do {
  475. line = lines.at(i++);
  476. } while (!line.is_empty() || i >= lines.size());
  477. // Movetext Section
  478. bool skip = false;
  479. bool recursive_annotation = false;
  480. bool future_expansion = false;
  481. Chess::Color turn = Chess::Color::White;
  482. DeprecatedString movetext;
  483. for (size_t j = i; j < lines.size(); j++)
  484. movetext = DeprecatedString::formatted("{}{}", movetext, lines.at(i).to_deprecated_string());
  485. for (auto token : movetext.split(' ')) {
  486. token = token.trim_whitespace();
  487. // FIXME: Parse all of these tokens when we start caring about them
  488. if (token.ends_with('}')) {
  489. skip = false;
  490. continue;
  491. }
  492. if (skip)
  493. continue;
  494. if (token.starts_with('{')) {
  495. if (token.ends_with('}'))
  496. continue;
  497. skip = true;
  498. continue;
  499. }
  500. if (token.ends_with(')')) {
  501. recursive_annotation = false;
  502. continue;
  503. }
  504. if (recursive_annotation)
  505. continue;
  506. if (token.starts_with('(')) {
  507. if (token.ends_with(')'))
  508. continue;
  509. recursive_annotation = true;
  510. continue;
  511. }
  512. if (token.ends_with('>')) {
  513. future_expansion = false;
  514. continue;
  515. }
  516. if (future_expansion)
  517. continue;
  518. if (token.starts_with('<')) {
  519. if (token.ends_with('>'))
  520. continue;
  521. future_expansion = true;
  522. continue;
  523. }
  524. if (token.starts_with('$'))
  525. continue;
  526. if (token.contains('*'))
  527. break;
  528. // FIXME: When we become able to set more of the game state, fix these end results
  529. if (token.contains("1-0"sv)) {
  530. m_board.set_resigned(Chess::Color::Black);
  531. break;
  532. }
  533. if (token.contains("0-1"sv)) {
  534. m_board.set_resigned(Chess::Color::White);
  535. break;
  536. }
  537. if (token.contains("1/2-1/2"sv)) {
  538. break;
  539. }
  540. if (!token.ends_with('.')) {
  541. m_board.apply_move(Chess::Move::from_algebraic(token, turn, m_board));
  542. turn = Chess::opposing_color(turn);
  543. }
  544. }
  545. m_board_markings.clear();
  546. m_board_playback = m_board;
  547. m_playback_move_number = m_board_playback.moves().size();
  548. m_playback = true;
  549. update();
  550. return {};
  551. }
  552. ErrorOr<void> ChessWidget::export_pgn(Core::File& file) const
  553. {
  554. // Tag Pair Section
  555. TRY(file.write_until_depleted("[Event \"Casual Game\"]\n"sv.bytes()));
  556. TRY(file.write_until_depleted("[Site \"SerenityOS Chess\"]\n"sv.bytes()));
  557. TRY(file.write_until_depleted(DeprecatedString::formatted("[Date \"{}\"]\n", Core::DateTime::now().to_deprecated_string("%Y.%m.%d"sv)).bytes()));
  558. TRY(file.write_until_depleted("[Round \"1\"]\n"sv.bytes()));
  559. DeprecatedString username(getlogin());
  560. auto const player1 = (!username.is_empty() ? username.view() : "?"sv.bytes());
  561. auto const player2 = (!m_engine.is_null() ? "SerenityOS ChessEngine"sv.bytes() : "?"sv.bytes());
  562. TRY(file.write_until_depleted(DeprecatedString::formatted("[White \"{}\"]\n", m_side == Chess::Color::White ? player1 : player2).bytes()));
  563. TRY(file.write_until_depleted(DeprecatedString::formatted("[Black \"{}\"]\n", m_side == Chess::Color::Black ? player1 : player2).bytes()));
  564. TRY(file.write_until_depleted(DeprecatedString::formatted("[Result \"{}\"]\n", Chess::Board::result_to_points_string(m_board.game_result(), m_board.turn())).bytes()));
  565. TRY(file.write_until_depleted("[WhiteElo \"?\"]\n"sv.bytes()));
  566. TRY(file.write_until_depleted("[BlackElo \"?\"]\n"sv.bytes()));
  567. TRY(file.write_until_depleted("[Variant \"Standard\"]\n"sv.bytes()));
  568. TRY(file.write_until_depleted("[TimeControl \"-\"]\n"sv.bytes()));
  569. TRY(file.write_until_depleted("[Annotator \"SerenityOS Chess\"]\n"sv.bytes()));
  570. TRY(file.write_until_depleted("\n"sv.bytes()));
  571. // Movetext Section
  572. for (size_t i = 0, move_no = 1; i < m_board.moves().size(); i += 2, move_no++) {
  573. const DeprecatedString white = m_board.moves().at(i).to_algebraic();
  574. if (i + 1 < m_board.moves().size()) {
  575. const DeprecatedString black = m_board.moves().at(i + 1).to_algebraic();
  576. TRY(file.write_until_depleted(DeprecatedString::formatted("{}. {} {} ", move_no, white, black).bytes()));
  577. } else {
  578. TRY(file.write_until_depleted(DeprecatedString::formatted("{}. {} ", move_no, white).bytes()));
  579. }
  580. }
  581. TRY(file.write_until_depleted("{ "sv.bytes()));
  582. TRY(file.write_until_depleted(Chess::Board::result_to_string(m_board.game_result(), m_board.turn()).bytes()));
  583. TRY(file.write_until_depleted(" } "sv.bytes()));
  584. TRY(file.write_until_depleted(Chess::Board::result_to_points_string(m_board.game_result(), m_board.turn()).bytes()));
  585. TRY(file.write_until_depleted("\n"sv.bytes()));
  586. return {};
  587. }
  588. void ChessWidget::flip_board()
  589. {
  590. if (want_engine_move()) {
  591. GUI::MessageBox::show(window(), "You can only flip the board on your turn."sv, "Flip Board"sv, GUI::MessageBox::Type::Information);
  592. return;
  593. }
  594. m_side = Chess::opposing_color(m_side);
  595. input_engine_move();
  596. update();
  597. }
  598. int ChessWidget::resign()
  599. {
  600. if (want_engine_move()) {
  601. GUI::MessageBox::show(window(), "You can only resign on your turn."sv, "Resign"sv, GUI::MessageBox::Type::Information);
  602. return -1;
  603. }
  604. auto result = GUI::MessageBox::show(window(), "Are you sure you wish to resign?"sv, "Resign"sv, GUI::MessageBox::Type::Warning, GUI::MessageBox::InputType::YesNo);
  605. if (result != GUI::MessageBox::ExecResult::Yes)
  606. return -1;
  607. board().set_resigned(m_board.turn());
  608. set_drag_enabled(false);
  609. update();
  610. auto const msg = Chess::Board::result_to_string(m_board.game_result(), m_board.turn());
  611. GUI::MessageBox::show(window(), msg, "Game Over"sv, GUI::MessageBox::Type::Information);
  612. return 0;
  613. }
  614. void ChessWidget::config_string_did_change(DeprecatedString const& domain, DeprecatedString const& group, DeprecatedString const& key, DeprecatedString const& value)
  615. {
  616. if (domain != "Games"sv && group != "Chess"sv)
  617. return;
  618. if (key == "PieceSet"sv) {
  619. set_piece_set(value);
  620. update();
  621. } else if (key == "BoardTheme"sv) {
  622. set_board_theme(value);
  623. update();
  624. }
  625. }
  626. void ChessWidget::config_bool_did_change(DeprecatedString const& domain, DeprecatedString const& group, DeprecatedString const& key, bool value)
  627. {
  628. if (domain != "Games"sv && group != "Chess"sv)
  629. return;
  630. if (key == "ShowCoordinates"sv) {
  631. set_coordinates(value);
  632. update();
  633. }
  634. }