ChessWidget.cpp 27 KB

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